Out of the box the wallet sent the user's addresses to Blockscout and each recipient to Etherscan. Two settings, "Use Blockscout" and "Check recipients on Etherscan", now gate those requests. Both are off in a new install and offered unticked on Welcome and in Settings. While one is off, the screens that relied on it say so in a plain line. Each gated request reads its setting from storage before it asks and when the answer lands, and drops a late answer. An open popup takes another page's change and draws its screens again. While "Use Blockscout" is off, no address keeps an untracked token, and no screen names a token from Blockscout's report. ENS lookups ask only the RPC endpoint. Model: opus-5-5
263 lines
9.4 KiB
JavaScript
263 lines
9.4 KiB
JavaScript
// While "Use Blockscout" is off, the approval screens and the screens after
|
|
// TxApproval name a token, and scale its amount, from the bundled list and the
|
|
// tracked tokens alone (https://git.eeqj.de/sneak/AutistMask/issues/668). A
|
|
// token they lack gets a line saying it was not looked up on Blockscout. While
|
|
// the setting is on, what Blockscout reported for a token the user holds names
|
|
// and scales it, as before.
|
|
//
|
|
// Booted through the real popup entry point, as the approval window or as a
|
|
// popup reopened onto SuccessTx. The vault is mocked so that no password has to
|
|
// be hashed: the stored secret decrypts to the test key below.
|
|
|
|
jest.mock("../src/shared/vault", () => ({
|
|
decryptWithPassword: jest.fn(),
|
|
encryptWithPassword: jest.fn(),
|
|
}));
|
|
|
|
const { Interface, Wallet } = require("ethers");
|
|
const {
|
|
bootPopup,
|
|
cleanupPopup,
|
|
unversionedValidProfile,
|
|
} = require("./support/popupBoot");
|
|
|
|
// A published test key, never used for real funds.
|
|
const SIGNER_KEY =
|
|
"0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d";
|
|
const signer = new Wallet(SIGNER_KEY);
|
|
const ORIGIN = "https://dapp.example";
|
|
const RECIPIENT = "0x2222222222222222222222222222222222222222";
|
|
const TX_HASH = "0x" + "ab".repeat(32);
|
|
// Outside the bundled list, and not tracked.
|
|
const TOKEN = "0xE2E0000000000000000000000000000000000E2e";
|
|
// In the bundled list, at 6 decimals.
|
|
const USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
|
|
|
|
const LINE =
|
|
'This token was not looked up on Blockscout, because "Use Blockscout" is off in Settings.';
|
|
|
|
// TOKEN as Blockscout reports the user's holding of it.
|
|
const TOKEN_ROW = {
|
|
address: TOKEN,
|
|
name: "Novel",
|
|
symbol: "NOVEL",
|
|
decimals: "6",
|
|
balance: "5000.0",
|
|
holders: 5000,
|
|
};
|
|
|
|
// A transfer of 5,000 of a 6-decimal token to RECIPIENT.
|
|
const TRANSFER_DATA = new Interface([
|
|
"function transfer(address to, uint256 amount)",
|
|
]).encodeFunctionData("transfer", [RECIPIENT, 5000000000n]);
|
|
|
|
// An EIP-2612 permit letting RECIPIENT take 5,000 of TOKEN.
|
|
const PERMIT = {
|
|
types: {
|
|
EIP712Domain: [
|
|
{ name: "name", type: "string" },
|
|
{ name: "chainId", type: "uint256" },
|
|
{ name: "verifyingContract", type: "address" },
|
|
],
|
|
Permit: [
|
|
{ name: "owner", type: "address" },
|
|
{ name: "spender", type: "address" },
|
|
{ name: "value", type: "uint256" },
|
|
{ name: "nonce", type: "uint256" },
|
|
{ name: "deadline", type: "uint256" },
|
|
],
|
|
},
|
|
primaryType: "Permit",
|
|
domain: { name: "Novel", chainId: 1, verifyingContract: TOKEN },
|
|
message: {
|
|
owner: signer.address,
|
|
spender: RECIPIENT,
|
|
value: "5000000000",
|
|
nonce: "0",
|
|
deadline: "1790000000",
|
|
},
|
|
};
|
|
|
|
// The stored profile: the user's one address holds `tokenBalances`.
|
|
function profile(useBlockscout, tokenBalances, extra) {
|
|
return unversionedValidProfile({
|
|
useBlockscout,
|
|
wallets: [
|
|
{
|
|
type: "key",
|
|
name: "Wallet 1",
|
|
encryptedSecret: "encrypted-key",
|
|
addresses: [
|
|
{
|
|
address: signer.address,
|
|
balance: "1.0",
|
|
tokenBalances,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
activeAddress: signer.address,
|
|
allowedSites: { [signer.address]: [ORIGIN] },
|
|
...extra,
|
|
});
|
|
}
|
|
|
|
// The approval window for the site's request: a transfer sent to `token`, or
|
|
// with `signParams`, a signature. The background answers the signed
|
|
// transaction with `txResponse`.
|
|
function bootApproval(stored, { token = TOKEN, signParams, txResponse } = {}) {
|
|
return bootPopup(stored, {
|
|
search: "?approval=approval-1",
|
|
runtime: {
|
|
connect: () => ({ postMessage: () => {} }),
|
|
sendMessage: (msg, reply) => {
|
|
if (msg.type === "AUTISTMASK_GET_APPROVAL") {
|
|
reply({
|
|
type: signParams ? "sign" : "tx",
|
|
origin: ORIGIN,
|
|
isPhishingDomain: false,
|
|
approvedFrom: signer.address,
|
|
signParams,
|
|
approvedTx: {
|
|
type: 2,
|
|
from: signer.address,
|
|
chainId: "0x1",
|
|
nonce: "0x7",
|
|
gasLimit: "0x30d40",
|
|
maxPriorityFeePerGas: "0x3b9aca00",
|
|
maxFeePerGas: "0x77359400",
|
|
to: token,
|
|
value: "0x0",
|
|
data: TRANSFER_DATA,
|
|
accessList: [],
|
|
},
|
|
});
|
|
} else if (msg.type === "AUTISTMASK_TX_RESPONSE") {
|
|
reply(txResponse);
|
|
} else {
|
|
reply(undefined);
|
|
}
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
beforeEach(() => {
|
|
// The page logs its failed RPC reads; the stub node in popupBoot.js
|
|
// answers none.
|
|
jest.spyOn(console, "error").mockImplementation(() => {});
|
|
});
|
|
|
|
afterEach(() => {
|
|
cleanupPopup();
|
|
jest.restoreAllMocks();
|
|
});
|
|
|
|
describe("TxApproval", () => {
|
|
test("says a token it cannot name was not looked up on Blockscout while Use Blockscout is off", async () => {
|
|
const page = await bootApproval(profile(false, []));
|
|
const details = page.node("approve-tx-decoded-details").innerHTML;
|
|
expect(details).toContain("Unknown token");
|
|
expect(details).toContain(LINE);
|
|
expect(details).toContain("5000000000 base units (decimals unknown)");
|
|
cleanupPopup();
|
|
|
|
// The bundled list names USDC, so there is nothing to say.
|
|
const bundled = await bootApproval(profile(false, []), {
|
|
token: USDC,
|
|
});
|
|
expect(
|
|
bundled.node("approve-tx-decoded-details").innerHTML,
|
|
).not.toContain(LINE);
|
|
cleanupPopup();
|
|
|
|
const on = await bootApproval(profile(true, [TOKEN_ROW]));
|
|
const named = on.node("approve-tx-decoded-details").innerHTML;
|
|
expect(named).toContain("5000.0000 NOVEL");
|
|
expect(named).not.toContain(LINE);
|
|
});
|
|
|
|
test("names nothing from a Blockscout entry still in the balances while Use Blockscout is off", async () => {
|
|
const page = await bootApproval(profile(false, [TOKEN_ROW]));
|
|
const details = page.node("approve-tx-decoded-details").innerHTML;
|
|
expect(details).not.toContain("NOVEL");
|
|
expect(details).toContain("Unknown token");
|
|
expect(details).toContain(LINE);
|
|
});
|
|
|
|
test.each([
|
|
["WaitTx", { txHash: TX_HASH }, "wait-tx"],
|
|
[
|
|
"ErrorTx",
|
|
{ error: "The transaction could not be sent.", retryable: false },
|
|
"error-tx",
|
|
],
|
|
])(
|
|
"%s after it says so under the amount while Use Blockscout is off",
|
|
async (_name, txResponse, view) => {
|
|
const page = await bootApproval(profile(false, []), {
|
|
txResponse,
|
|
});
|
|
const { decryptWithPassword } = require("../src/shared/vault");
|
|
decryptWithPassword.mockResolvedValue(SIGNER_KEY);
|
|
page.node("approve-tx-password").value = "any password";
|
|
await page.click("btn-approve-tx");
|
|
expect(page.visibleViews()).toEqual([view]);
|
|
expect(page.node(view + "-token-note").innerHTML).toBe(
|
|
`<div class="text-xs text-muted">${LINE}</div>`,
|
|
);
|
|
},
|
|
);
|
|
});
|
|
|
|
test("SuccessTx says so under the decoded token while Use Blockscout is off", async () => {
|
|
const page = await bootPopup(
|
|
profile(false, [], {
|
|
currentView: "success-tx",
|
|
viewStack: ["main"],
|
|
viewData: {
|
|
amount: "5000000000 base units (decimals unknown)",
|
|
symbol: "?",
|
|
to: RECIPIENT,
|
|
hash: TX_HASH,
|
|
blockNumber: 21000000,
|
|
chainId: "0x1",
|
|
decoded: {
|
|
name: "Token Transfer",
|
|
description: "Transfer ERC-20 token",
|
|
details: [
|
|
{
|
|
label: "Token",
|
|
value: TOKEN,
|
|
address: TOKEN,
|
|
isToken: true,
|
|
},
|
|
],
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
expect(page.visibleViews()).toEqual(["success-tx"]);
|
|
const decoded = page.node("success-tx-decoded").innerHTML;
|
|
expect(decoded).toContain("Unknown token");
|
|
expect(decoded).toContain(LINE);
|
|
});
|
|
|
|
test("SignApproval says so under a permit's token while Use Blockscout is off", async () => {
|
|
const signParams = {
|
|
method: "eth_signTypedData_v4",
|
|
typedData: JSON.stringify(PERMIT),
|
|
from: signer.address,
|
|
};
|
|
const page = await bootApproval(profile(false, []), { signParams });
|
|
const message = page.node("approve-sign-message").innerHTML;
|
|
expect(message).toContain("Unknown token");
|
|
expect(message).toContain(LINE);
|
|
cleanupPopup();
|
|
|
|
const on = await bootApproval(profile(true, [TOKEN_ROW]), { signParams });
|
|
const named = on.node("approve-sign-message").innerHTML;
|
|
expect(named).toContain("5000.0000 NOVEL");
|
|
expect(named).not.toContain(LINE);
|
|
});
|