Token auto-discovery, tx history, balance polling, EIP-6963, UI overhaul
All checks were successful
check / check (push) Successful in 14s

Major changes:
- Fetch token balances and tx history from Blockscout API (configurable)
- Remove manual token discovery (discoverTokens) in favor of Blockscout
- HD address gap scanning on mnemonic import
- Duplicate mnemonic detection on wallet add
- EIP-6963 multi-wallet discovery + selectedAddress updates in inpage
- Two-tier balance refresh: 10s while popup open, 60s background
- Fix $0.00 flash before prices load (return null when no prices)
- No-layout-shift: min-height on total value element
- Aligned balance columns (42ch address width, consistent USD column)
- All errors use flash messages instead of off-screen error divs
- Settings gear in global title bar, add-wallet moved to settings pane
- Settings wells with light grey background, configurable Blockscout URL
- Consistent "< Back" buttons top-left on all views
- Address titles (Address 1.1, 1.2, etc.) on main and detail views
- Send view shows current balance of selected asset
- Clickable affordance policy added to README
- Shortened mnemonic backup warning
- Fix broken background script constant imports
This commit is contained in:
2026-02-26 02:13:39 +07:00
parent 2b2137716c
commit 3bd2b58543
27 changed files with 978 additions and 420 deletions

108
src/shared/transactions.js Normal file
View File

@@ -0,0 +1,108 @@
// Transaction history fetching via Blockscout v2 API.
// Fetches normal transactions and ERC-20 token transfers,
// merges them, and returns the most recent entries.
const { formatEther, formatUnits } = require("ethers");
const { log } = require("./log");
function formatTxValue(val) {
const parts = val.split(".");
if (parts.length === 1) return val;
const dec = parts[1].slice(0, 6).replace(/0+$/, "") || "0";
return parts[0] + "." + dec;
}
function parseTx(tx, addrLower) {
const from = tx.from?.hash || "";
const to = tx.to?.hash || "";
return {
hash: tx.hash,
blockNumber: tx.block_number,
timestamp: Math.floor(new Date(tx.timestamp).getTime() / 1000),
from: from,
to: to,
value: formatTxValue(formatEther(tx.value || "0")),
symbol: "ETH",
direction: from.toLowerCase() === addrLower ? "sent" : "received",
isError: tx.status !== "ok",
};
}
function parseTokenTransfer(tt, addrLower) {
const from = tt.from?.hash || "";
const to = tt.to?.hash || "";
const decimals = parseInt(tt.total?.decimals || "18", 10);
const rawValue = tt.total?.value || "0";
return {
hash: tt.transaction_hash,
blockNumber: tt.block_number,
timestamp: Math.floor(new Date(tt.timestamp).getTime() / 1000),
from: from,
to: to,
value: formatTxValue(formatUnits(rawValue, decimals)),
symbol: tt.token?.symbol || "?",
direction: from.toLowerCase() === addrLower ? "sent" : "received",
isError: false,
};
}
async function fetchRecentTransactions(address, blockscoutUrl, count = 25) {
log.debugf("fetchRecentTransactions", address);
const addrLower = address.toLowerCase();
const [txResp, ttResp] = await Promise.all([
fetch(
blockscoutUrl +
"/addresses/" +
address +
"/transactions?limit=" +
count,
),
fetch(
blockscoutUrl +
"/addresses/" +
address +
"/token-transfers?limit=" +
count +
"&type=ERC-20",
),
]);
if (!txResp.ok) {
log.errorf(
"blockscout transactions:",
txResp.status,
txResp.statusText,
);
}
if (!ttResp.ok) {
log.errorf(
"blockscout token-transfers:",
ttResp.status,
ttResp.statusText,
);
}
const txJson = txResp.ok ? await txResp.json() : {};
const ttJson = ttResp.ok ? await ttResp.json() : {};
const txs = [];
for (const tx of txJson.items || []) {
txs.push(parseTx(tx, addrLower));
}
// Deduplicate: skip token transfers whose tx hash is already in the list
const seenHashes = new Set(txs.map((t) => t.hash));
for (const tt of ttJson.items || []) {
if (seenHashes.has(tt.transaction_hash)) continue;
txs.push(parseTokenTransfer(tt, addrLower));
}
txs.sort((a, b) => b.blockNumber - a.blockNumber);
const result = txs.slice(0, count);
log.debugf("fetchRecentTransactions done, count:", result.length);
return result;
}
module.exports = { fetchRecentTransactions };