Refactor popup into shared modules, wire up real ERC-20 tokens
All checks were successful
check / check (push) Successful in 13s
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:
@@ -1,21 +1,37 @@
|
|||||||
// AutistMask popup UI — view management and event wiring
|
// AutistMask popup UI — view switching and event wiring only.
|
||||||
const {
|
// All business logic lives in src/shared/*.
|
||||||
Mnemonic,
|
|
||||||
HDNodeWallet,
|
const { parseEther } = require("ethers");
|
||||||
Wallet,
|
|
||||||
JsonRpcProvider,
|
|
||||||
formatEther,
|
|
||||||
parseEther,
|
|
||||||
} = require("ethers");
|
|
||||||
const { TOKENS, getTopTokenPrices } = require("../shared/tokens");
|
|
||||||
const { encryptWithPassword, decryptWithPassword } = require("../shared/vault");
|
|
||||||
const QRCode = require("qrcode");
|
const QRCode = require("qrcode");
|
||||||
|
const { TOKENS } = require("../shared/tokens");
|
||||||
const DEBUG = true;
|
const {
|
||||||
const DEBUG_MNEMONIC =
|
state,
|
||||||
"cube evolve unfold result inch risk jealous skill hotel bulb night wreck";
|
saveState,
|
||||||
|
loadState,
|
||||||
const BIP44_ETH_BASE = "m/44'/60'/0'/0";
|
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 = [
|
const VIEWS = [
|
||||||
"welcome",
|
"welcome",
|
||||||
@@ -30,6 +46,21 @@ const VIEWS = [
|
|||||||
"approve",
|
"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) {
|
function showView(name) {
|
||||||
for (const v of VIEWS) {
|
for (const v of VIEWS) {
|
||||||
const el = document.getElementById(`view-${v}`);
|
const el = document.getElementById(`view-${v}`);
|
||||||
@@ -45,228 +76,63 @@ function showView(name) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Browser-agnostic storage API
|
// -- rendering --
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderTotalValue() {
|
function renderTotalValue() {
|
||||||
const el = $("total-value");
|
const el = $("total-value");
|
||||||
if (!el) return;
|
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() {
|
function renderWalletList() {
|
||||||
const container = $("wallet-list");
|
const container = $("wallet-list");
|
||||||
if (state.wallets.length === 0) {
|
if (state.wallets.length === 0) {
|
||||||
@@ -294,8 +160,7 @@ function renderWalletList() {
|
|||||||
html += `<div class="text-xs break-all">${addr.address}</div>`;
|
html += `<div class="text-xs break-all">${addr.address}</div>`;
|
||||||
html += `<div class="flex justify-between items-center">`;
|
html += `<div class="flex justify-between items-center">`;
|
||||||
html += `<span class="text-xs">${addr.balance} ETH</span>`;
|
html += `<span class="text-xs">${addr.balance} ETH</span>`;
|
||||||
const addrUsd = getAddressValueUsd(addr);
|
html += `<span class="text-xs text-muted">${formatUsd(getAddressValueUsd(addr))}</span>`;
|
||||||
html += `<span class="text-xs text-muted">${formatUsd(addrUsd)}</span>`;
|
|
||||||
html += `</div>`;
|
html += `</div>`;
|
||||||
html += `</div>`;
|
html += `</div>`;
|
||||||
});
|
});
|
||||||
@@ -324,7 +189,7 @@ function renderWalletList() {
|
|||||||
wallet.addresses.push({
|
wallet.addresses.push({
|
||||||
address: newAddr,
|
address: newAddr,
|
||||||
balance: "0.0000",
|
balance: "0.0000",
|
||||||
tokens: [],
|
tokenBalances: [],
|
||||||
});
|
});
|
||||||
wallet.nextIndex++;
|
wallet.nextIndex++;
|
||||||
await saveState();
|
await saveState();
|
||||||
@@ -335,62 +200,6 @@ function renderWalletList() {
|
|||||||
renderTotalValue();
|
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) {
|
async function addWalletAndGoToMain(wallet) {
|
||||||
state.wallets.push(wallet);
|
state.wallets.push(wallet);
|
||||||
state.hasWallet = true;
|
state.hasWallet = true;
|
||||||
@@ -401,6 +210,8 @@ async function addWalletAndGoToMain(wallet) {
|
|||||||
|
|
||||||
function showAddWalletView() {
|
function showAddWalletView() {
|
||||||
$("wallet-mnemonic").value = "";
|
$("wallet-mnemonic").value = "";
|
||||||
|
$("add-wallet-password").value = "";
|
||||||
|
$("add-wallet-password-confirm").value = "";
|
||||||
$("add-wallet-phrase-warning").classList.add("hidden");
|
$("add-wallet-phrase-warning").classList.add("hidden");
|
||||||
hideError("add-wallet-error");
|
hideError("add-wallet-error");
|
||||||
showView("add-wallet");
|
showView("add-wallet");
|
||||||
@@ -408,6 +219,8 @@ function showAddWalletView() {
|
|||||||
|
|
||||||
function showImportKeyView() {
|
function showImportKeyView() {
|
||||||
$("import-private-key").value = "";
|
$("import-private-key").value = "";
|
||||||
|
$("import-key-password").value = "";
|
||||||
|
$("import-key-password-confirm").value = "";
|
||||||
hideError("import-key-error");
|
hideError("import-key-error");
|
||||||
showView("import-key");
|
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 --
|
// -- init --
|
||||||
async function init() {
|
async function init() {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
@@ -439,18 +261,15 @@ async function init() {
|
|||||||
} else {
|
} else {
|
||||||
renderWalletList();
|
renderWalletList();
|
||||||
showView("main");
|
showView("main");
|
||||||
// Fetch prices and balances in parallel, re-render as each completes
|
doRefreshAndRender();
|
||||||
refreshPrices().then(() => renderWalletList());
|
|
||||||
refreshBalances().then(() => renderWalletList());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- Welcome --
|
// -- Welcome --
|
||||||
$("btn-welcome-add").addEventListener("click", showAddWalletView);
|
$("btn-welcome-add").addEventListener("click", showAddWalletView);
|
||||||
|
|
||||||
// -- Add wallet (unified create/import) --
|
// -- Add wallet --
|
||||||
$("btn-generate-phrase").addEventListener("click", () => {
|
$("btn-generate-phrase").addEventListener("click", () => {
|
||||||
const phrase = generateMnemonic();
|
$("wallet-mnemonic").value = generateMnemonic();
|
||||||
$("wallet-mnemonic").value = phrase;
|
|
||||||
$("add-wallet-phrase-warning").classList.remove("hidden");
|
$("add-wallet-phrase-warning").classList.remove("hidden");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -473,7 +292,7 @@ async function init() {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!Mnemonic.isValidMnemonic(mnemonic)) {
|
if (!isValidMnemonic(mnemonic)) {
|
||||||
showError(
|
showError(
|
||||||
"add-wallet-error",
|
"add-wallet-error",
|
||||||
"Invalid recovery phrase. Please check for typos.",
|
"Invalid recovery phrase. Please check for typos.",
|
||||||
@@ -498,7 +317,6 @@ async function init() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
hideError("add-wallet-error");
|
hideError("add-wallet-error");
|
||||||
|
|
||||||
const encrypted = await encryptWithPassword(mnemonic, pw);
|
const encrypted = await encryptWithPassword(mnemonic, pw);
|
||||||
const { xpub, firstAddress } = hdWalletFromMnemonic(mnemonic);
|
const { xpub, firstAddress } = hdWalletFromMnemonic(mnemonic);
|
||||||
const walletNum = state.wallets.length + 1;
|
const walletNum = state.wallets.length + 1;
|
||||||
@@ -509,7 +327,7 @@ async function init() {
|
|||||||
encryptedSecret: encrypted,
|
encryptedSecret: encrypted,
|
||||||
nextIndex: 1,
|
nextIndex: 1,
|
||||||
addresses: [
|
addresses: [
|
||||||
{ address: firstAddress, balance: "0.0000", tokens: [] },
|
{ address: firstAddress, balance: "0.0000", tokenBalances: [] },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -555,7 +373,9 @@ async function init() {
|
|||||||
type: "key",
|
type: "key",
|
||||||
name: "Wallet " + walletNum,
|
name: "Wallet " + walletNum,
|
||||||
encryptedSecret: encrypted,
|
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");
|
$("send-status").classList.remove("hidden");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Resolve ENS name if it looks like one
|
|
||||||
let resolvedTo = to;
|
let resolvedTo = to;
|
||||||
if (to.includes(".") && !to.startsWith("0x")) {
|
if (to.includes(".") && !to.startsWith("0x")) {
|
||||||
const statusEl = $("send-status");
|
const statusEl = $("send-status");
|
||||||
statusEl.textContent = "Resolving " + to + "...";
|
statusEl.textContent = "Resolving " + to + "...";
|
||||||
statusEl.classList.remove("hidden");
|
statusEl.classList.remove("hidden");
|
||||||
try {
|
try {
|
||||||
const provider = getProvider();
|
const provider = getProvider(state.rpcUrl);
|
||||||
const resolved = await provider.resolveName(to);
|
const resolved = await provider.resolveName(to);
|
||||||
if (!resolved) {
|
if (!resolved) {
|
||||||
showError("send-status", "Could not resolve " + to);
|
showError("send-status", "Could not resolve " + to);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
resolvedTo = resolved;
|
resolvedTo = resolved;
|
||||||
statusEl.textContent = to + " = " + resolvedTo;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showError("send-status", "Failed to resolve ENS name.");
|
showError("send-status", "Failed to resolve ENS name.");
|
||||||
return;
|
return;
|
||||||
@@ -684,8 +502,12 @@ async function init() {
|
|||||||
}
|
}
|
||||||
statusEl.textContent = "Sending...";
|
statusEl.textContent = "Sending...";
|
||||||
try {
|
try {
|
||||||
const signer = getSignerForCurrentAddress(decryptedSecret);
|
const signer = getSignerForAddress(
|
||||||
const provider = getProvider();
|
wallet,
|
||||||
|
state.selectedAddress,
|
||||||
|
decryptedSecret,
|
||||||
|
);
|
||||||
|
const provider = getProvider(state.rpcUrl);
|
||||||
const connectedSigner = signer.connect(provider);
|
const connectedSigner = signer.connect(provider);
|
||||||
const tx = await connectedSigner.sendTransaction({
|
const tx = await connectedSigner.sendTransaction({
|
||||||
to: resolvedTo,
|
to: resolvedTo,
|
||||||
@@ -698,28 +520,22 @@ async function init() {
|
|||||||
receipt.blockNumber +
|
receipt.blockNumber +
|
||||||
". Tx: " +
|
". Tx: " +
|
||||||
receipt.hash;
|
receipt.hash;
|
||||||
// Refresh balance after send
|
invalidateBalanceCache();
|
||||||
refreshBalances().then(() => renderWalletList());
|
doRefreshAndRender();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
statusEl.textContent = "Failed: " + (e.shortMessage || e.message);
|
statusEl.textContent = "Failed: " + (e.shortMessage || e.message);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$("btn-send-back").addEventListener("click", () => {
|
$("btn-send-back").addEventListener("click", () => showAddressDetail());
|
||||||
showAddressDetail();
|
|
||||||
});
|
|
||||||
|
|
||||||
// -- Receive --
|
// -- Receive --
|
||||||
$("btn-receive-copy").addEventListener("click", () => {
|
$("btn-receive-copy").addEventListener("click", () => {
|
||||||
const addr = $("receive-address").textContent;
|
const addr = $("receive-address").textContent;
|
||||||
if (addr) {
|
if (addr) navigator.clipboard.writeText(addr);
|
||||||
navigator.clipboard.writeText(addr);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$("btn-receive-back").addEventListener("click", () => {
|
$("btn-receive-back").addEventListener("click", () => showAddressDetail());
|
||||||
showAddressDetail();
|
|
||||||
});
|
|
||||||
|
|
||||||
// -- Add Token --
|
// -- Add Token --
|
||||||
$("btn-add-token-confirm").addEventListener("click", async () => {
|
$("btn-add-token-confirm").addEventListener("click", async () => {
|
||||||
@@ -731,24 +547,50 @@ async function init() {
|
|||||||
);
|
);
|
||||||
return;
|
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");
|
hideError("add-token-error");
|
||||||
// TODO: look up token name/symbol/decimals from contract via RPC
|
const infoEl = $("add-token-info");
|
||||||
const addr = currentAddress();
|
infoEl.textContent = "Looking up token...";
|
||||||
if (addr) {
|
infoEl.classList.remove("hidden");
|
||||||
addr.tokens.push({
|
try {
|
||||||
contractAddress: contractAddr,
|
const info = await lookupTokenInfo(contractAddr, state.rpcUrl);
|
||||||
symbol: "TKN",
|
state.trackedTokens.push({
|
||||||
decimals: 18,
|
address: contractAddr,
|
||||||
balance: "0",
|
symbol: info.symbol,
|
||||||
|
decimals: info.decimals,
|
||||||
|
name: info.name,
|
||||||
});
|
});
|
||||||
await saveState();
|
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");
|
||||||
}
|
}
|
||||||
showAddressDetail();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$("btn-add-token-back").addEventListener("click", () => {
|
$("btn-add-token-back").addEventListener("click", () =>
|
||||||
showAddressDetail();
|
showAddressDetail(),
|
||||||
});
|
);
|
||||||
|
|
||||||
// -- Settings --
|
// -- Settings --
|
||||||
$("btn-save-rpc").addEventListener("click", async () => {
|
$("btn-save-rpc").addEventListener("click", async () => {
|
||||||
@@ -763,13 +605,11 @@ async function init() {
|
|||||||
|
|
||||||
// -- Approval --
|
// -- Approval --
|
||||||
$("btn-approve").addEventListener("click", () => {
|
$("btn-approve").addEventListener("click", () => {
|
||||||
// TODO: send approval to background
|
|
||||||
renderWalletList();
|
renderWalletList();
|
||||||
showView("main");
|
showView("main");
|
||||||
});
|
});
|
||||||
|
|
||||||
$("btn-reject").addEventListener("click", () => {
|
$("btn-reject").addEventListener("click", () => {
|
||||||
// TODO: send rejection to background
|
|
||||||
renderWalletList();
|
renderWalletList();
|
||||||
showView("main");
|
showView("main");
|
||||||
});
|
});
|
||||||
|
|||||||
130
src/shared/balances.js
Normal file
130
src/shared/balances.js
Normal 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
79
src/shared/prices.js
Normal 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
49
src/shared/state.js
Normal 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
63
src/shared/wallet.js
Normal 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,
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user