Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe5b9609ea |
@@ -2124,12 +2124,7 @@ view would leave a wallet one click from deletion.
|
||||
blocklist)
|
||||
- Site origin (bold, scheme and port included) + "wants you to sign a
|
||||
message"
|
||||
- Warning box, shown for `eth_sign`: "This site used the older eth_sign
|
||||
method. The data will be signed as a personal message." An `eth_sign`
|
||||
request names the address first and the data second. Its data is signed
|
||||
exactly as a `personal_sign` message is, as an EIP-191 personal message,
|
||||
which is what the Ethereum JSON-RPC specification defines `eth_sign` to
|
||||
return.
|
||||
- Danger warning box (shown for `eth_sign`, which signs a raw hash)
|
||||
- Type: "Personal message" or "Typed data (EIP-712)"
|
||||
- From: color dot + full address + etherscan link
|
||||
- Message: for `personal_sign` and `eth_sign`, the text the message's bytes
|
||||
|
||||
@@ -984,8 +984,9 @@ async function handleRpc(method, params, origin) {
|
||||
|
||||
if (method === "eth_sign") {
|
||||
signParams.dangerWarning =
|
||||
"This site used the older eth_sign method. " +
|
||||
"The data will be signed as a personal message.";
|
||||
"\u26a0\ufe0f DANGER: This site is requesting to sign a raw hash. " +
|
||||
"This can be used to sign transactions that drain your funds. " +
|
||||
"Only proceed if you fully understand what you are signing.";
|
||||
}
|
||||
|
||||
if (findPendingApproval(origin, "sign")) {
|
||||
|
||||
+8
-12
@@ -7,10 +7,9 @@
|
||||
<link rel="stylesheet" href="styles.css" />
|
||||
</head>
|
||||
<!-- Chrome gives extension pages a stylesheet of its own that sets the
|
||||
font and its size on body, and a Tailwind class beats it only when
|
||||
marked important: hence font-mono! and text-sm! rather than
|
||||
font-mono and text-sm. -->
|
||||
<body class="bg-bg text-fg font-mono! text-sm!">
|
||||
font on body, and a Tailwind class beats it only when marked
|
||||
important: hence font-mono! rather than font-mono. -->
|
||||
<body class="bg-bg text-fg font-mono! text-sm">
|
||||
<div id="app" class="p-2 pr-5 overflow-x-hidden">
|
||||
<!-- ============ GLOBAL TITLE BAR ============ -->
|
||||
<div
|
||||
@@ -88,14 +87,11 @@
|
||||
|
||||
<!-- Mnemonic form section -->
|
||||
<div id="add-wallet-section-mnemonic">
|
||||
<!-- The die button shares the instruction's row: on a row
|
||||
of its own it pushes Import below the fold at
|
||||
360x600. -->
|
||||
<div class="mb-2 flex justify-between items-start gap-2">
|
||||
<p>
|
||||
Enter your 12 or 24 word recovery phrase below, or
|
||||
click the button to roll the die for a new one.
|
||||
</p>
|
||||
<p class="mb-2">
|
||||
Enter your 12 or 24 word recovery phrase below, or click
|
||||
the button to roll the die for a new one.
|
||||
</p>
|
||||
<div class="mb-1 flex justify-end">
|
||||
<button
|
||||
id="btn-generate-phrase"
|
||||
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer text-xs"
|
||||
|
||||
@@ -721,7 +721,7 @@ function showSignApproval(details) {
|
||||
"This message is plain text, not hex, so it cannot be signed.";
|
||||
}
|
||||
|
||||
// The warning the background attaches to an eth_sign request.
|
||||
// Display danger warning for eth_sign (raw hash signing)
|
||||
const warningEl = $("approve-sign-danger-warning");
|
||||
if (warningEl) {
|
||||
if (sp.dangerWarning) {
|
||||
|
||||
@@ -183,7 +183,7 @@ function render() {
|
||||
if (el) el.classList.add("hidden");
|
||||
}
|
||||
|
||||
loadFullTxDetails(tx.hash, tx.to, tx.chainId);
|
||||
loadFullTxDetails(tx.hash, tx.chainId);
|
||||
|
||||
const isoStr = isoDate(tx.timestamp);
|
||||
$("tx-detail-time").innerHTML =
|
||||
@@ -291,7 +291,7 @@ function populateOnChainDetails(txData, chainId) {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadFullTxDetails(txHash, toAddress, chainId) {
|
||||
async function loadFullTxDetails(txHash, chainId) {
|
||||
const section = $("tx-detail-calldata-section");
|
||||
const actionEl = $("tx-detail-calldata-action");
|
||||
const detailsEl = $("tx-detail-calldata-details");
|
||||
@@ -313,7 +313,10 @@ async function loadFullTxDetails(txHash, toAddress, chainId) {
|
||||
const inputData = txData.raw_input || txData.input || null;
|
||||
if (!inputData || inputData === "0x") return;
|
||||
|
||||
const decoded = decodeCalldata(inputData, toAddress || "");
|
||||
// Decoded against the address the transaction was sent to, as the
|
||||
// explorer reports it, not the history entry's `to`: for a token
|
||||
// transfer that is the transfer's recipient, not the token contract.
|
||||
const decoded = decodeCalldata(inputData, txData.to?.hash || "");
|
||||
if (decoded) {
|
||||
// Render decoded calldata matching approval view style
|
||||
actionEl.textContent = decoded.name;
|
||||
|
||||
@@ -1551,53 +1551,6 @@ describe("what the approval is verified against", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// eth_sign names the address first and the data second, the reverse of
|
||||
// personal_sign, and is answered as the JSON-RPC specification defines it:
|
||||
// with an EIP-191 personal message signature over the data, the same
|
||||
// signature personal_sign returns
|
||||
// (https://git.eeqj.de/sneak/AutistMask/issues/409).
|
||||
describe("eth_sign", () => {
|
||||
test("is read address first, and returns a personal message signature over the data", async () => {
|
||||
const bg = loadBackground();
|
||||
const page = bg.send(
|
||||
{
|
||||
type: "AUTISTMASK_RPC",
|
||||
method: "eth_sign",
|
||||
params: [signer.address, MESSAGE],
|
||||
},
|
||||
{ origin: ORIGIN },
|
||||
);
|
||||
await settle();
|
||||
const id = approvalIdIn(bg.created.map((c) => c.url));
|
||||
|
||||
const details = bg.send(
|
||||
{ type: "AUTISTMASK_GET_APPROVAL", id },
|
||||
{ url: bg.fromPopup.url },
|
||||
);
|
||||
const { signParams } = details.sendResponse.mock.calls[0][0];
|
||||
expect(signParams).toMatchObject({
|
||||
method: "eth_sign",
|
||||
message: MESSAGE,
|
||||
from: signer.address,
|
||||
});
|
||||
expect(signParams.dangerWarning).toBe(
|
||||
"This site used the older eth_sign method. " +
|
||||
"The data will be signed as a personal message.",
|
||||
);
|
||||
|
||||
const signature = await signer.signMessage(
|
||||
Buffer.from(MESSAGE.slice(2), "hex"),
|
||||
);
|
||||
bg.send(
|
||||
{ type: "AUTISTMASK_SIGN_RESPONSE", id, approved: true, signature },
|
||||
{ url: bg.fromPopup.url },
|
||||
);
|
||||
await settle();
|
||||
|
||||
expect(page.sendResponse).toHaveBeenCalledWith({ result: signature });
|
||||
});
|
||||
});
|
||||
|
||||
// The interlock must not cost the retry the approval exists to allow.
|
||||
describe("the interlock releases a failed attempt", () => {
|
||||
test("a retryable failure before the broadcast leaves the approval usable", async () => {
|
||||
|
||||
@@ -128,18 +128,6 @@ step("the popup is drawn in the monospace font it declares", async (env) => {
|
||||
);
|
||||
});
|
||||
|
||||
// The same check as the Chrome suite's (#456), so both browsers are held to
|
||||
// the same size.
|
||||
step("the popup's text is drawn at the 14px text-sm declares", async (env) => {
|
||||
const size = await env.driver.execute(
|
||||
"return getComputedStyle(document.body).fontSize;",
|
||||
);
|
||||
assert(
|
||||
size === "14px",
|
||||
"the popup's text is drawn at " + size + ", not at the 14px of text-sm",
|
||||
);
|
||||
});
|
||||
|
||||
// The recovery screen (#361): the Chrome suite's four cases, run before any
|
||||
// wallet exists for the same reason. With no wallet nothing saves on a timer,
|
||||
// so no save can write a good record over the unreadable one. The last of them
|
||||
|
||||
@@ -113,18 +113,6 @@ test("the popup is drawn in the monospace font it declares (#418)", async (env)
|
||||
);
|
||||
});
|
||||
|
||||
// The same stylesheet sets the size on body to 75%, which would draw the
|
||||
// popup at 12px.
|
||||
test("the popup's text is drawn at the 14px text-sm declares (#456)", async (env) => {
|
||||
const size = await env.page.evaluate(
|
||||
() => getComputedStyle(document.body).fontSize,
|
||||
);
|
||||
assert(
|
||||
size === "14px",
|
||||
"the popup's text is drawn at " + size + ", not at the 14px of text-sm",
|
||||
);
|
||||
});
|
||||
|
||||
// The empirical half of #182. The manifest change is only a claim about
|
||||
// what the CSP permits; this is the observation. Two things have to hold
|
||||
// together, and the run covers both: the popup realm compiles WASM (here),
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
// The decoded details on the transaction detail screen of a token transfer
|
||||
// (https://git.eeqj.de/sneak/AutistMask/issues/515).
|
||||
//
|
||||
// A history row for a token transfer holds the transfer's recipient as its
|
||||
// `to`, not the contract the transaction was sent to. The screen used to decode
|
||||
// the transaction's data against that `to`, so its Token line named the
|
||||
// recipient, and the amount read "decimals unknown" for a token the wallet
|
||||
// tracks. It decodes against the address the explorer reports the transaction
|
||||
// was sent to.
|
||||
//
|
||||
// The row is the one the real history parser makes from the explorer's answer,
|
||||
// opened through transactionDetail.show(), which is what a click on the row
|
||||
// calls. Driven against a minimal DOM stub in the shape
|
||||
// tests/contractCreation.test.js uses.
|
||||
|
||||
const { Interface } = require("ethers");
|
||||
|
||||
jest.mock("../src/shared/log", () => ({
|
||||
log: {
|
||||
debugf: () => {},
|
||||
infof: () => {},
|
||||
warnf: () => {},
|
||||
errorf: () => {},
|
||||
},
|
||||
debugFetch: async (url) => ({
|
||||
ok: true,
|
||||
json: async () => mockExplorerAnswer(url),
|
||||
}),
|
||||
setRuntimeDebug: () => {},
|
||||
isDebug: () => false,
|
||||
}));
|
||||
|
||||
globalThis.chrome = {
|
||||
storage: { local: { get: async () => ({}), set: async () => {} } },
|
||||
};
|
||||
|
||||
const { state } = require("../src/shared/state");
|
||||
const { fetchRecentTransactions } = require("../src/shared/transactions");
|
||||
const transactionDetail = require("../src/popup/views/transactionDetail");
|
||||
|
||||
const EXPLORER = "https://explorer.example/api/v2";
|
||||
const HOLDER = "0x0000000000000000000000000000000000000a11";
|
||||
const RECIPIENT = "0x711e691962E5c054b78071C789A0C8CE657377aD";
|
||||
// A token the wallet tracks and the bundled token list does not name.
|
||||
const TOKEN = "0x74de5d4FCbf63E00296fd95d33236B9794016631";
|
||||
const TX_HASH =
|
||||
"0x85215772ed26ea8b39c2b3b18779030487efbe0b5fd7e882592b2f62b837be84";
|
||||
|
||||
// HOLDER sends 1234.5 of the 6-decimal token to RECIPIENT.
|
||||
const TRANSFER_DATA = new Interface([
|
||||
"function transfer(address to, uint256 amount)",
|
||||
]).encodeFunctionData("transfer", [RECIPIENT, 1234500000n]);
|
||||
|
||||
// The explorer's answers: the transfer in HOLDER's token transfers, and the
|
||||
// transaction itself, sent to the token contract.
|
||||
function mockExplorerAnswer(url) {
|
||||
if (
|
||||
url ===
|
||||
EXPLORER + "/addresses/" + HOLDER + "/token-transfers?type=ERC-20"
|
||||
) {
|
||||
return {
|
||||
items: [
|
||||
{
|
||||
transaction_hash: TX_HASH,
|
||||
block_number: 100,
|
||||
timestamp: "2026-10-01T00:00:00Z",
|
||||
from: { hash: HOLDER },
|
||||
to: { hash: RECIPIENT },
|
||||
total: { value: "1234500000", decimals: "6" },
|
||||
token: {
|
||||
address_hash: TOKEN,
|
||||
symbol: "TDOL",
|
||||
holders_count: "5000",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
if (url === EXPLORER + "/transactions/" + TX_HASH) {
|
||||
return { to: { hash: TOKEN }, raw_input: TRANSFER_DATA };
|
||||
}
|
||||
return { items: [] };
|
||||
}
|
||||
|
||||
function makeElement(id) {
|
||||
const classes = new Set();
|
||||
const el = {
|
||||
id,
|
||||
textContent: "",
|
||||
value: "",
|
||||
innerHTML: "",
|
||||
style: {},
|
||||
dataset: {},
|
||||
classList: {
|
||||
add: (...names) => names.forEach((n) => classes.add(n)),
|
||||
remove: (...names) => names.forEach((n) => classes.delete(n)),
|
||||
contains: (n) => classes.has(n),
|
||||
toggle: () => false,
|
||||
},
|
||||
addEventListener: () => {},
|
||||
querySelectorAll: () => [],
|
||||
appendChild: () => {},
|
||||
};
|
||||
// Views reach for .parentElement to hide whole sections.
|
||||
Object.defineProperty(el, "parentElement", {
|
||||
get: () => node(id + "-parent"),
|
||||
});
|
||||
return el;
|
||||
}
|
||||
|
||||
function makeDocument() {
|
||||
const els = new Map();
|
||||
return {
|
||||
getElementById(id) {
|
||||
// The debug banner is created on demand by helpers.js; absent
|
||||
// is the state a non-debug, non-testnet popup is in.
|
||||
if (id === "debug-banner") return null;
|
||||
if (!els.has(id)) els.set(id, makeElement(id));
|
||||
return els.get(id);
|
||||
},
|
||||
createElement: () => makeElement("created"),
|
||||
body: { prepend: () => {} },
|
||||
};
|
||||
}
|
||||
|
||||
function node(id) {
|
||||
return globalThis.document.getElementById(id);
|
||||
}
|
||||
|
||||
// One line of the decoded details: its label and what is drawn under it.
|
||||
function decodedLine(label) {
|
||||
return node("tx-detail-calldata-details")
|
||||
.innerHTML.split('<div class="mb-2">')
|
||||
.find((line) =>
|
||||
line.startsWith(`<div class="text-muted">${label}</div>`),
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
globalThis.document = makeDocument();
|
||||
globalThis.window = { location: { search: "" } };
|
||||
state.blockscoutUrl = EXPLORER;
|
||||
state.wallets = [
|
||||
{
|
||||
name: "Main",
|
||||
type: "key",
|
||||
addresses: [{ address: HOLDER, balance: "0.0000" }],
|
||||
},
|
||||
];
|
||||
state.trackedTokens = [{ address: TOKEN, symbol: "TDOL", decimals: 6 }];
|
||||
state.viewData = {};
|
||||
state.viewStack = [];
|
||||
state.currentView = null;
|
||||
});
|
||||
|
||||
test("a token transfer's decoded details name the token contract and the amount at its scale", async () => {
|
||||
const [row] = await fetchRecentTransactions(HOLDER, EXPLORER, 1);
|
||||
expect(row.to).toBe(RECIPIENT);
|
||||
expect(row.contractAddress).toBe(TOKEN.toLowerCase());
|
||||
|
||||
transactionDetail.show(row);
|
||||
// The decoded details are drawn once the explorer has answered.
|
||||
for (let i = 0; i < 10; i++) await new Promise((r) => setTimeout(r, 0));
|
||||
|
||||
expect(node("tx-detail-calldata-action").textContent).toBe(
|
||||
"Token Transfer",
|
||||
);
|
||||
const token = decodedLine("Token");
|
||||
expect(token).toContain('<div class="font-bold">TDOL</div>');
|
||||
expect(token).toContain(TOKEN);
|
||||
expect(token).not.toContain(RECIPIENT);
|
||||
expect(decodedLine("Recipient")).toContain(RECIPIENT);
|
||||
expect(decodedLine("Amount")).toContain(
|
||||
'<div class="font-bold">1234.5000 TDOL</div>',
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user