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 (implementation); fable-5-1 (prose)
309 lines
11 KiB
JavaScript
309 lines
11 KiB
JavaScript
// While "Use Blockscout" is off, the approval screens, and the screens that
|
|
// follow TxApproval, take a token's name and scale from the bundled list and
|
|
// the tracked tokens alone (https://git.eeqj.de/sneak/AutistMask/issues/668).
|
|
// A token missing from both gets a line under it. The line says Blockscout's
|
|
// report of the user's balances is not used to name the token. While the
|
|
// setting is on, Blockscout's report of the user's holding is a source too, as
|
|
// before.
|
|
//
|
|
// Booted through the real popup entry point, as the approval window or as a
|
|
// popup reopened onto SuccessTx, WaitTx or ErrorTx. 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 =
|
|
'Blockscout\'s report of your balances is not used to name this token, 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. The request is a transfer sent to
|
|
// `token`, or a signature request when `signParams` is given. 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("has the line under a token it cannot name 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.each([
|
|
[
|
|
"wait-tx",
|
|
{
|
|
pendingWait: {
|
|
hash: TX_HASH,
|
|
txInfo: {
|
|
to: RECIPIENT,
|
|
amount: "1",
|
|
token: 42,
|
|
chainId: "0x1",
|
|
},
|
|
broadcastTime: Date.now(),
|
|
},
|
|
},
|
|
],
|
|
[
|
|
"error-tx",
|
|
{
|
|
amount: "1",
|
|
symbol: "?",
|
|
token: 42,
|
|
to: RECIPIENT,
|
|
hash: TX_HASH,
|
|
message: "The transaction failed.",
|
|
chainId: "0x1",
|
|
},
|
|
],
|
|
])(
|
|
"a popup reopened onto %s shows no line for a stored token that is not text",
|
|
async (view, viewData) => {
|
|
const page = await bootPopup(
|
|
profile(false, [], {
|
|
currentView: view,
|
|
viewStack: ["main"],
|
|
viewData,
|
|
}),
|
|
);
|
|
expect(page.pageErrors).toEqual([]);
|
|
expect(page.visibleViews()).toEqual([view]);
|
|
expect(page.node(view + "-token-note").innerHTML).toBe("");
|
|
},
|
|
);
|
|
|
|
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);
|
|
});
|