Refactor popup into shared modules, wire up real ERC-20 tokens
All checks were successful
check / check (push) Successful in 13s

Split popup/index.js (784 lines) into focused modules:
- shared/state.js: state management, storage persistence
- shared/wallet.js: mnemonic gen, HD derivation, signing
- shared/prices.js: price cache (5min TTL), USD formatting,
  value aggregation (address → wallet → total)
- shared/balances.js: ETH + ERC-20 balance cache (60s TTL),
  ENS lookup, token contract metadata lookup
- shared/vault.js: unchanged (libsodium encryption)
- shared/tokens.js: unchanged (token list + CoinDesk client)
- popup/index.js: view switching and event wiring only

Token tracking is now app-wide: trackedTokens stored in state,
balances fetched for all tracked tokens across all addresses.
Add Token now calls the real contract to read name/symbol/decimals.
Total portfolio value shown in 2x type on Home screen.
This commit is contained in:
2026-02-25 18:48:44 +07:00
parent 2a8c051377
commit f50a2a0389
5 changed files with 494 additions and 333 deletions

View File

@@ -1,21 +1,37 @@
// AutistMask popup UI — view management and event wiring
const {
Mnemonic,
HDNodeWallet,
Wallet,
JsonRpcProvider,
formatEther,
parseEther,
} = require("ethers");
const { TOKENS, getTopTokenPrices } = require("../shared/tokens");
const { encryptWithPassword, decryptWithPassword } = require("../shared/vault");
// AutistMask popup UI — view switching and event wiring only.
// All business logic lives in src/shared/*.
const { parseEther } = require("ethers");
const QRCode = require("qrcode");
const DEBUG = true;
const DEBUG_MNEMONIC =
"cube evolve unfold result inch risk jealous skill hotel bulb night wreck";
const BIP44_ETH_BASE = "m/44'/60'/0'/0";
const { TOKENS } = require("../shared/tokens");
const {
state,
saveState,
loadState,
currentAddress,
} = require("../shared/state");
const {
DEBUG,
generateMnemonic,
deriveAddressFromXpub,
hdWalletFromMnemonic,
addressFromPrivateKey,
getSignerForAddress,
isValidMnemonic,
} = require("../shared/wallet");
const {
refreshPrices,
formatUsd,
getAddressValueUsd,
getTotalValueUsd,
} = require("../shared/prices");
const {
refreshBalances,
lookupTokenInfo,
invalidateBalanceCache,
getProvider,
} = require("../shared/balances");
const { encryptWithPassword, decryptWithPassword } = require("../shared/vault");
const VIEWS = [
"welcome",
@@ -30,6 +46,21 @@ const VIEWS = [
"approve",
];
// -- DOM helpers --
function $(id) {
return document.getElementById(id);
}
function showError(id, msg) {
const el = $(id);
el.textContent = msg;
el.classList.remove("hidden");
}
function hideError(id) {
$(id).classList.add("hidden");
}
function showView(name) {
for (const v of VIEWS) {
const el = document.getElementById(`view-${v}`);
@@ -45,228 +76,63 @@ function showView(name) {
}
}
// Browser-agnostic storage API
const storageApi =
typeof browser !== "undefined"
? browser.storage.local
: chrome.storage.local;
// Persisted state (unencrypted, public data only):
// wallets[]: { type, name, xpub (for hd), addresses: [{ address, balance, tokens }], nextIndex }
// Mnemonic/private key will be stored encrypted separately (not yet implemented).
const DEFAULT_STATE = {
hasWallet: false,
wallets: [],
rpcUrl: "https://eth.llamarpc.com",
};
const state = {
...DEFAULT_STATE,
selectedWallet: null,
selectedAddress: null,
};
async function saveState() {
const persisted = {
hasWallet: state.hasWallet,
wallets: state.wallets,
rpcUrl: state.rpcUrl,
};
await storageApi.set({ autistmask: persisted });
}
async function loadState() {
const result = await storageApi.get("autistmask");
if (result.autistmask) {
const saved = result.autistmask;
state.hasWallet = saved.hasWallet;
state.wallets = saved.wallets || [];
state.rpcUrl = saved.rpcUrl || DEFAULT_STATE.rpcUrl;
}
}
// -- helpers --
function $(id) {
return document.getElementById(id);
}
function showError(id, msg) {
const el = $(id);
el.textContent = msg;
el.classList.remove("hidden");
}
function hideError(id) {
$(id).classList.add("hidden");
}
function generateMnemonic() {
if (DEBUG) return DEBUG_MNEMONIC;
const m = Mnemonic.fromEntropy(
globalThis.crypto.getRandomValues(new Uint8Array(16)),
);
return m.phrase;
}
// Derive an Ethereum address at index from an xpub string
function deriveAddressFromXpub(xpub, index) {
const node = HDNodeWallet.fromExtendedKey(xpub);
const child = node.deriveChild(index);
return child.address;
}
// Create an HD wallet from a mnemonic: returns { xpub, firstAddress }
function hdWalletFromMnemonic(mnemonic) {
const node = HDNodeWallet.fromPhrase(mnemonic, "", BIP44_ETH_BASE);
const xpub = node.neuter().extendedKey;
const firstAddress = node.deriveChild(0).address;
return { xpub, firstAddress };
}
// Get address from a private key
function addressFromPrivateKey(key) {
const w = new Wallet(key);
return w.address;
}
// -- caching layer --
const PRICE_CACHE_TTL = 300000; // 5 minutes
const BALANCE_CACHE_TTL = 60000; // 60 seconds
const cache = {
prices: { data: {}, fetchedAt: 0 },
balances: { fetchedAt: 0 },
};
// { "ETH": 1234.56, "LINK": 8.60, ... }
const prices = {};
async function refreshPrices() {
const now = Date.now();
if (now - cache.prices.fetchedAt < PRICE_CACHE_TTL) return;
try {
const fetched = await getTopTokenPrices(25);
Object.assign(prices, fetched);
cache.prices.fetchedAt = now;
} catch (e) {
// prices stay stale on error
}
}
function formatUsd(amount) {
if (amount === null || amount === undefined || isNaN(amount)) return "";
if (amount === 0) return "$0.00";
if (amount < 0.01) return "< $0.01";
return (
"$" +
amount.toLocaleString("en-US", {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})
);
}
// Get an ethers Wallet (signer) for the currently selected address.
// Requires the decrypted secret (mnemonic or private key).
function getSignerForCurrentAddress(decryptedSecret) {
const wallet = state.wallets[state.selectedWallet];
const addrIndex = state.selectedAddress;
if (wallet.type === "hd") {
const node = HDNodeWallet.fromPhrase(
decryptedSecret,
"",
BIP44_ETH_BASE,
);
return node.deriveChild(addrIndex);
} else {
return new Wallet(decryptedSecret);
}
}
// -- balance fetching --
function getProvider() {
return new JsonRpcProvider(state.rpcUrl);
}
function formatBalance(wei) {
const eth = formatEther(wei);
// Show up to 6 decimal places, trim trailing zeros
const parts = eth.split(".");
if (parts.length === 1) return eth + ".0";
const dec = parts[1].slice(0, 6).replace(/0+$/, "") || "0";
return parts[0] + "." + dec;
}
async function refreshBalances() {
const now = Date.now();
if (now - cache.balances.fetchedAt < BALANCE_CACHE_TTL) return;
const provider = getProvider();
const updates = [];
for (const wallet of state.wallets) {
for (const addr of wallet.addresses) {
updates.push(
provider
.getBalance(addr.address)
.then((bal) => {
addr.balance = formatBalance(bal);
})
.catch(() => {}),
);
updates.push(
provider
.lookupAddress(addr.address)
.then((name) => {
addr.ensName = name || null;
})
.catch(() => {
addr.ensName = null;
}),
);
}
}
await Promise.all(updates);
cache.balances.fetchedAt = now;
await saveState();
}
function getAddressValueUsd(addr) {
let total = 0;
const ethBal = parseFloat(addr.balance || "0");
if (prices.ETH) {
total += ethBal * prices.ETH;
}
for (const token of addr.tokens || []) {
const tokenBal = parseFloat(token.balance || "0");
if (tokenBal > 0 && prices[token.symbol]) {
total += tokenBal * prices[token.symbol];
}
}
return total;
}
function getWalletValueUsd(wallet) {
let total = 0;
for (const addr of wallet.addresses) {
total += getAddressValueUsd(addr);
}
return total;
}
function getTotalValueUsd() {
let total = 0;
for (const wallet of state.wallets) {
total += getWalletValueUsd(wallet);
}
return total;
}
// -- rendering --
function renderTotalValue() {
const el = $("total-value");
if (!el) return;
el.textContent = formatUsd(getTotalValueUsd());
el.textContent = formatUsd(getTotalValueUsd(state.wallets));
}
function renderTokenList(addr) {
const list = $("token-list");
const balances = addr.tokenBalances || [];
if (balances.length === 0 && state.trackedTokens.length === 0) {
list.innerHTML =
'<div class="text-muted text-xs py-1">No tokens added yet. Use "+ Add" to track a token.</div>';
return;
}
list.innerHTML = balances
.map(
(t) =>
`<div class="py-1 border-b border-border-light flex justify-between">` +
`<span>${t.symbol}</span>` +
`<span>${t.balance || "0"}</span>` +
`</div>`,
)
.join("");
}
function renderSendTokenSelect(addr) {
const sel = $("send-token");
sel.innerHTML = '<option value="ETH">ETH</option>';
for (const t of addr.tokenBalances || []) {
const opt = document.createElement("option");
opt.value = t.address;
opt.textContent = t.symbol;
sel.appendChild(opt);
}
}
function showAddressDetail() {
const wallet = state.wallets[state.selectedWallet];
const addr = wallet.addresses[state.selectedAddress];
$("address-title").textContent = wallet.name;
$("address-full").textContent = addr.address;
$("address-copied-msg").textContent = "";
$("address-eth-balance").textContent = addr.balance;
$("address-usd-value").textContent = formatUsd(getAddressValueUsd(addr));
const ensEl = $("address-ens");
if (addr.ensName) {
ensEl.textContent = addr.ensName;
ensEl.classList.remove("hidden");
} else {
ensEl.classList.add("hidden");
}
renderTokenList(addr);
renderSendTokenSelect(addr);
showView("address");
}
// -- render wallet list on main view --
function renderWalletList() {
const container = $("wallet-list");
if (state.wallets.length === 0) {
@@ -294,8 +160,7 @@ function renderWalletList() {
html += `<div class="text-xs break-all">${addr.address}</div>`;
html += `<div class="flex justify-between items-center">`;
html += `<span class="text-xs">${addr.balance} ETH</span>`;
const addrUsd = getAddressValueUsd(addr);
html += `<span class="text-xs text-muted">${formatUsd(addrUsd)}</span>`;
html += `<span class="text-xs text-muted">${formatUsd(getAddressValueUsd(addr))}</span>`;
html += `</div>`;
html += `</div>`;
});
@@ -324,7 +189,7 @@ function renderWalletList() {
wallet.addresses.push({
address: newAddr,
balance: "0.0000",
tokens: [],
tokenBalances: [],
});
wallet.nextIndex++;
await saveState();
@@ -335,62 +200,6 @@ function renderWalletList() {
renderTotalValue();
}
function showAddressDetail() {
const wallet = state.wallets[state.selectedWallet];
const addr = wallet.addresses[state.selectedAddress];
$("address-title").textContent = wallet.name;
$("address-full").textContent = addr.address;
$("address-copied-msg").textContent = "";
$("address-eth-balance").textContent = addr.balance;
$("address-usd-value").textContent = formatUsd(getAddressValueUsd(addr));
const ensEl = $("address-ens");
if (addr.ensName) {
ensEl.textContent = addr.ensName;
ensEl.classList.remove("hidden");
} else {
ensEl.classList.add("hidden");
}
updateTokenList(addr);
updateSendTokenSelect(addr);
showView("address");
}
function updateTokenList(addr) {
const list = $("token-list");
if (addr.tokens.length === 0) {
list.innerHTML =
'<div class="text-muted text-xs py-1">No tokens added yet. Use "+ Add" to track a token.</div>';
return;
}
list.innerHTML = addr.tokens
.map(
(t) =>
`<div class="py-1 border-b border-border-light flex justify-between">` +
`<span>${t.symbol}</span>` +
`<span>${t.balance || "0"}</span>` +
`</div>`,
)
.join("");
}
function updateSendTokenSelect(addr) {
const sel = $("send-token");
sel.innerHTML = '<option value="ETH">ETH</option>';
addr.tokens.forEach((t) => {
const opt = document.createElement("option");
opt.value = t.contractAddress;
opt.textContent = t.symbol;
sel.appendChild(opt);
});
}
function currentAddress() {
if (state.selectedWallet === null || state.selectedAddress === null) {
return null;
}
return state.wallets[state.selectedWallet].addresses[state.selectedAddress];
}
async function addWalletAndGoToMain(wallet) {
state.wallets.push(wallet);
state.hasWallet = true;
@@ -401,6 +210,8 @@ async function addWalletAndGoToMain(wallet) {
function showAddWalletView() {
$("wallet-mnemonic").value = "";
$("add-wallet-password").value = "";
$("add-wallet-password-confirm").value = "";
$("add-wallet-phrase-warning").classList.add("hidden");
hideError("add-wallet-error");
showView("add-wallet");
@@ -408,6 +219,8 @@ function showAddWalletView() {
function showImportKeyView() {
$("import-private-key").value = "";
$("import-key-password").value = "";
$("import-key-password-confirm").value = "";
hideError("import-key-error");
showView("import-key");
}
@@ -421,6 +234,15 @@ function backFromWalletAdd() {
}
}
async function doRefreshAndRender() {
await Promise.all([
refreshPrices(),
refreshBalances(state.wallets, state.trackedTokens, state.rpcUrl),
]);
await saveState();
renderWalletList();
}
// -- init --
async function init() {
if (DEBUG) {
@@ -439,18 +261,15 @@ async function init() {
} else {
renderWalletList();
showView("main");
// Fetch prices and balances in parallel, re-render as each completes
refreshPrices().then(() => renderWalletList());
refreshBalances().then(() => renderWalletList());
doRefreshAndRender();
}
// -- Welcome --
$("btn-welcome-add").addEventListener("click", showAddWalletView);
// -- Add wallet (unified create/import) --
// -- Add wallet --
$("btn-generate-phrase").addEventListener("click", () => {
const phrase = generateMnemonic();
$("wallet-mnemonic").value = phrase;
$("wallet-mnemonic").value = generateMnemonic();
$("add-wallet-phrase-warning").classList.remove("hidden");
});
@@ -473,7 +292,7 @@ async function init() {
);
return;
}
if (!Mnemonic.isValidMnemonic(mnemonic)) {
if (!isValidMnemonic(mnemonic)) {
showError(
"add-wallet-error",
"Invalid recovery phrase. Please check for typos.",
@@ -498,7 +317,6 @@ async function init() {
return;
}
hideError("add-wallet-error");
const encrypted = await encryptWithPassword(mnemonic, pw);
const { xpub, firstAddress } = hdWalletFromMnemonic(mnemonic);
const walletNum = state.wallets.length + 1;
@@ -509,7 +327,7 @@ async function init() {
encryptedSecret: encrypted,
nextIndex: 1,
addresses: [
{ address: firstAddress, balance: "0.0000", tokens: [] },
{ address: firstAddress, balance: "0.0000", tokenBalances: [] },
],
});
});
@@ -555,7 +373,9 @@ async function init() {
type: "key",
name: "Wallet " + walletNum,
encryptedSecret: encrypted,
addresses: [{ address: addr, balance: "0.0000", tokens: [] }],
addresses: [
{ address: addr, balance: "0.0000", tokenBalances: [] },
],
});
});
@@ -642,21 +462,19 @@ async function init() {
$("send-status").classList.remove("hidden");
return;
}
// Resolve ENS name if it looks like one
let resolvedTo = to;
if (to.includes(".") && !to.startsWith("0x")) {
const statusEl = $("send-status");
statusEl.textContent = "Resolving " + to + "...";
statusEl.classList.remove("hidden");
try {
const provider = getProvider();
const provider = getProvider(state.rpcUrl);
const resolved = await provider.resolveName(to);
if (!resolved) {
showError("send-status", "Could not resolve " + to);
return;
}
resolvedTo = resolved;
statusEl.textContent = to + " = " + resolvedTo;
} catch (e) {
showError("send-status", "Failed to resolve ENS name.");
return;
@@ -684,8 +502,12 @@ async function init() {
}
statusEl.textContent = "Sending...";
try {
const signer = getSignerForCurrentAddress(decryptedSecret);
const provider = getProvider();
const signer = getSignerForAddress(
wallet,
state.selectedAddress,
decryptedSecret,
);
const provider = getProvider(state.rpcUrl);
const connectedSigner = signer.connect(provider);
const tx = await connectedSigner.sendTransaction({
to: resolvedTo,
@@ -698,28 +520,22 @@ async function init() {
receipt.blockNumber +
". Tx: " +
receipt.hash;
// Refresh balance after send
refreshBalances().then(() => renderWalletList());
invalidateBalanceCache();
doRefreshAndRender();
} catch (e) {
statusEl.textContent = "Failed: " + (e.shortMessage || e.message);
}
});
$("btn-send-back").addEventListener("click", () => {
showAddressDetail();
});
$("btn-send-back").addEventListener("click", () => showAddressDetail());
// -- Receive --
$("btn-receive-copy").addEventListener("click", () => {
const addr = $("receive-address").textContent;
if (addr) {
navigator.clipboard.writeText(addr);
}
if (addr) navigator.clipboard.writeText(addr);
});
$("btn-receive-back").addEventListener("click", () => {
showAddressDetail();
});
$("btn-receive-back").addEventListener("click", () => showAddressDetail());
// -- Add Token --
$("btn-add-token-confirm").addEventListener("click", async () => {
@@ -731,24 +547,50 @@ async function init() {
);
return;
}
// Check if already tracked
const already = state.trackedTokens.find(
(t) => t.address.toLowerCase() === contractAddr.toLowerCase(),
);
if (already) {
showError(
"add-token-error",
already.symbol + " is already being tracked.",
);
return;
}
hideError("add-token-error");
// TODO: look up token name/symbol/decimals from contract via RPC
const addr = currentAddress();
if (addr) {
addr.tokens.push({
contractAddress: contractAddr,
symbol: "TKN",
decimals: 18,
balance: "0",
const infoEl = $("add-token-info");
infoEl.textContent = "Looking up token...";
infoEl.classList.remove("hidden");
try {
const info = await lookupTokenInfo(contractAddr, state.rpcUrl);
state.trackedTokens.push({
address: contractAddr,
symbol: info.symbol,
decimals: info.decimals,
name: info.name,
});
await saveState();
}
invalidateBalanceCache();
await refreshBalances(
state.wallets,
state.trackedTokens,
state.rpcUrl,
);
await saveState();
showAddressDetail();
} catch (e) {
showError(
"add-token-error",
"Could not read token contract. Check the address.",
);
infoEl.classList.add("hidden");
}
});
$("btn-add-token-back").addEventListener("click", () => {
showAddressDetail();
});
$("btn-add-token-back").addEventListener("click", () =>
showAddressDetail(),
);
// -- Settings --
$("btn-save-rpc").addEventListener("click", async () => {
@@ -763,13 +605,11 @@ async function init() {
// -- Approval --
$("btn-approve").addEventListener("click", () => {
// TODO: send approval to background
renderWalletList();
showView("main");
});
$("btn-reject").addEventListener("click", () => {
// TODO: send rejection to background
renderWalletList();
showView("main");
});

130
src/shared/balances.js Normal file
View File

@@ -0,0 +1,130 @@
// Balance fetching: ETH balances, ERC-20 token balances, ENS reverse lookup.
// Cached for 60 seconds.
const {
JsonRpcProvider,
Contract,
formatEther,
formatUnits,
} = require("ethers");
const { ERC20_ABI } = require("./constants");
const BALANCE_CACHE_TTL = 60000; // 60 seconds
let lastFetchedAt = 0;
function getProvider(rpcUrl) {
return new JsonRpcProvider(rpcUrl);
}
function formatBalance(wei) {
const eth = formatEther(wei);
const parts = eth.split(".");
if (parts.length === 1) return eth + ".0";
const dec = parts[1].slice(0, 6).replace(/0+$/, "") || "0";
return parts[0] + "." + dec;
}
function formatTokenBalance(raw, decimals) {
const val = formatUnits(raw, decimals);
const parts = val.split(".");
if (parts.length === 1) return val + ".0";
const dec = parts[1].slice(0, 6).replace(/0+$/, "") || "0";
return parts[0] + "." + dec;
}
// Fetch ETH balances, ENS names, and ERC-20 token balances for all addresses.
// trackedTokens: [{ address, symbol, decimals }]
async function refreshBalances(wallets, trackedTokens, rpcUrl) {
const now = Date.now();
if (now - lastFetchedAt < BALANCE_CACHE_TTL) return;
const provider = getProvider(rpcUrl);
const updates = [];
for (const wallet of wallets) {
for (const addr of wallet.addresses) {
// ETH balance
updates.push(
provider
.getBalance(addr.address)
.then((bal) => {
addr.balance = formatBalance(bal);
})
.catch(() => {}),
);
// ENS reverse lookup
updates.push(
provider
.lookupAddress(addr.address)
.then((name) => {
addr.ensName = name || null;
})
.catch(() => {
addr.ensName = null;
}),
);
// ERC-20 token balances
if (!addr.tokenBalances) addr.tokenBalances = [];
for (const token of trackedTokens) {
updates.push(
(async () => {
try {
const contract = new Contract(
token.address,
ERC20_ABI,
provider,
);
const raw = await contract.balanceOf(addr.address);
const existing = addr.tokenBalances.find(
(t) =>
t.address.toLowerCase() ===
token.address.toLowerCase(),
);
const bal = formatTokenBalance(raw, token.decimals);
if (existing) {
existing.balance = bal;
} else {
addr.tokenBalances.push({
address: token.address,
symbol: token.symbol,
decimals: token.decimals,
balance: bal,
});
}
} catch (e) {
// skip on error
}
})(),
);
}
}
}
await Promise.all(updates);
lastFetchedAt = now;
}
// Look up token metadata from its contract.
async function lookupTokenInfo(contractAddress, rpcUrl) {
const provider = getProvider(rpcUrl);
const contract = new Contract(contractAddress, ERC20_ABI, provider);
const [name, symbol, decimals] = await Promise.all([
contract.name(),
contract.symbol(),
contract.decimals(),
]);
return { name, symbol, decimals: Number(decimals) };
}
// Force-invalidate the balance cache (e.g. after sending a tx).
function invalidateBalanceCache() {
lastFetchedAt = 0;
}
module.exports = {
refreshBalances,
lookupTokenInfo,
invalidateBalanceCache,
getProvider,
};

79
src/shared/prices.js Normal file
View File

@@ -0,0 +1,79 @@
// Price fetching with 5-minute cache, USD formatting, value aggregation.
const { getTopTokenPrices } = require("./tokens");
const PRICE_CACHE_TTL = 300000; // 5 minutes
const prices = {};
let lastFetchedAt = 0;
async function refreshPrices() {
const now = Date.now();
if (now - lastFetchedAt < PRICE_CACHE_TTL) return;
try {
const fetched = await getTopTokenPrices(25);
Object.assign(prices, fetched);
lastFetchedAt = now;
} catch (e) {
// prices stay stale on error
}
}
function getPrice(symbol) {
return prices[symbol] || null;
}
function formatUsd(amount) {
if (amount === null || amount === undefined || isNaN(amount)) return "";
if (amount === 0) return "$0.00";
if (amount < 0.01) return "< $0.01";
return (
"$" +
amount.toLocaleString("en-US", {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})
);
}
function getAddressValueUsd(addr) {
let total = 0;
const ethBal = parseFloat(addr.balance || "0");
const ethPrice = prices.ETH;
if (ethPrice) {
total += ethBal * ethPrice;
}
for (const token of addr.tokenBalances || []) {
const tokenBal = parseFloat(token.balance || "0");
if (tokenBal > 0 && prices[token.symbol]) {
total += tokenBal * prices[token.symbol];
}
}
return total;
}
function getWalletValueUsd(wallet) {
let total = 0;
for (const addr of wallet.addresses) {
total += getAddressValueUsd(addr);
}
return total;
}
function getTotalValueUsd(wallets) {
let total = 0;
for (const wallet of wallets) {
total += getWalletValueUsd(wallet);
}
return total;
}
module.exports = {
prices,
refreshPrices,
getPrice,
formatUsd,
getAddressValueUsd,
getWalletValueUsd,
getTotalValueUsd,
};

49
src/shared/state.js Normal file
View File

@@ -0,0 +1,49 @@
// State management and extension storage persistence.
const storageApi =
typeof browser !== "undefined"
? browser.storage.local
: chrome.storage.local;
const DEFAULT_STATE = {
hasWallet: false,
wallets: [],
trackedTokens: [],
rpcUrl: "https://eth.llamarpc.com",
};
const state = {
...DEFAULT_STATE,
selectedWallet: null,
selectedAddress: null,
};
async function saveState() {
const persisted = {
hasWallet: state.hasWallet,
wallets: state.wallets,
trackedTokens: state.trackedTokens,
rpcUrl: state.rpcUrl,
};
await storageApi.set({ autistmask: persisted });
}
async function loadState() {
const result = await storageApi.get("autistmask");
if (result.autistmask) {
const saved = result.autistmask;
state.hasWallet = saved.hasWallet;
state.wallets = saved.wallets || [];
state.trackedTokens = saved.trackedTokens || [];
state.rpcUrl = saved.rpcUrl || DEFAULT_STATE.rpcUrl;
}
}
function currentAddress() {
if (state.selectedWallet === null || state.selectedAddress === null) {
return null;
}
return state.wallets[state.selectedWallet].addresses[state.selectedAddress];
}
module.exports = { state, saveState, loadState, currentAddress };

63
src/shared/wallet.js Normal file
View File

@@ -0,0 +1,63 @@
// Wallet operations: mnemonic generation, HD derivation, signing.
// All crypto delegated to ethers.js.
const { Mnemonic, HDNodeWallet, Wallet } = require("ethers");
const BIP44_ETH_BASE = "m/44'/60'/0'/0";
const DEBUG = true;
const DEBUG_MNEMONIC =
"cube evolve unfold result inch risk jealous skill hotel bulb night wreck";
function generateMnemonic() {
if (DEBUG) return DEBUG_MNEMONIC;
const m = Mnemonic.fromEntropy(
globalThis.crypto.getRandomValues(new Uint8Array(16)),
);
return m.phrase;
}
function deriveAddressFromXpub(xpub, index) {
const node = HDNodeWallet.fromExtendedKey(xpub);
return node.deriveChild(index).address;
}
function hdWalletFromMnemonic(mnemonic) {
const node = HDNodeWallet.fromPhrase(mnemonic, "", BIP44_ETH_BASE);
const xpub = node.neuter().extendedKey;
const firstAddress = node.deriveChild(0).address;
return { xpub, firstAddress };
}
function addressFromPrivateKey(key) {
const w = new Wallet(key);
return w.address;
}
function getSignerForAddress(walletData, addrIndex, decryptedSecret) {
if (walletData.type === "hd") {
const node = HDNodeWallet.fromPhrase(
decryptedSecret,
"",
BIP44_ETH_BASE,
);
return node.deriveChild(addrIndex);
}
return new Wallet(decryptedSecret);
}
function isValidMnemonic(mnemonic) {
return Mnemonic.isValidMnemonic(mnemonic);
}
module.exports = {
BIP44_ETH_BASE,
DEBUG,
DEBUG_MNEMONIC,
generateMnemonic,
deriveAddressFromXpub,
hdWalletFromMnemonic,
addressFromPrivateKey,
getSignerForAddress,
isValidMnemonic,
};