merge: 0.3.47 plug-in category + panel-driven add-on self-update, aegis 0.6.31 into master line
This commit is contained in:
commit
f9a7063635
13 changed files with 3667 additions and 251 deletions
|
|
@ -148,13 +148,19 @@ function validateManifest(raw, folderName) {
|
|||
}
|
||||
if (a === id) throw new Error(`addon "${id}": absorbs cannot list its own id`);
|
||||
}
|
||||
return { id, name, version, description, author, icon, main, capabilities, pageInject, toolbarMenu, absorbs };
|
||||
// Category: "plugin" for first-class Silent Mode components (Aegis and
|
||||
// future Ariadne-as-addon) that are surfaced in Settings › Plug-ins with
|
||||
// their own copy instead of the raw Extensions list. Anything else falls
|
||||
// back to plain-extension rendering.
|
||||
const category = m.category && ["plugin"].includes(String(m.category))
|
||||
? String(m.category) : null;
|
||||
return { id, name, version, description, author, icon, main, capabilities, pageInject, toolbarMenu, absorbs, category };
|
||||
}
|
||||
|
||||
// Loader singleton. `discoverAndActivate(opts)` returns a snapshot the rest
|
||||
// of the app queries via `getActive()` / `getInstalled()`.
|
||||
class AddonHost {
|
||||
constructor({ addonsDir, dataDir, isDisabled, logger, setSessionProxy, vaultDerive, vaultImports, approvalModal, emitToPanel, hostRequire, hostImport, openTab, openAddonTab, openSettings, captureTab, saveCapture }) {
|
||||
constructor({ addonsDir, dataDir, isDisabled, logger, setSessionProxy, vaultDerive, vaultImports, approvalModal, emitToPanel, hostRequire, hostImport, openTab, openAddonTab, openSettings, captureTab, saveCapture, checkAndStageUpdates, restartApp }) {
|
||||
this.addonsDir = addonsDir;
|
||||
this.dataDir = dataDir;
|
||||
this.isDisabled = isDisabled || (() => false);
|
||||
|
|
@ -190,6 +196,13 @@ class AddonHost {
|
|||
// named section (e.g. "passwords"). Uses the same IPC route the picker
|
||||
// uses for "Search settings…". Signature: (section?: string) => void.
|
||||
this._openSettings = typeof openSettings === "function" ? openSettings : null;
|
||||
// Panel-driven self-update: an add-on may ask the host to run the
|
||||
// OTA check + verify + stage flow for itself and, if a newer signed
|
||||
// build lands, restart Theseus so promoteStagedUpdates picks it up.
|
||||
// Owns the entire trust chain (sig, hash, manifest match) so no
|
||||
// add-on ever gets to hand-write into its own installed folder.
|
||||
this._checkAndStageUpdates = typeof checkAndStageUpdates === "function" ? checkAndStageUpdates : null;
|
||||
this._restartApp = typeof restartApp === "function" ? restartApp : null;
|
||||
// Session-proxy hook — injected by main so add-ons can swap the default
|
||||
// session's proxy rules (e.g. a "route everything through my VPS" add-on).
|
||||
// Signature: (rules: string | { proxyRules, proxyBypassRules }) => Promise<void>
|
||||
|
|
@ -445,6 +458,33 @@ class AddonHost {
|
|||
if (!this._openSettings) throw new Error("openSettings unavailable (host not wired)");
|
||||
this._openSettings(typeof section === "string" ? section : "");
|
||||
},
|
||||
// Check the OTA channel for a newer signed build of THIS add-on and
|
||||
// stage it if one is found. Returns { status, staged, current, next }
|
||||
// — status matches the shared addon-updater report vocabulary
|
||||
// ("up-to-date" | "staged" | "already-staged" | "fetch-failed" | …).
|
||||
// The staged copy activates on the next Theseus launch, so pair with
|
||||
// restartApp() when the caller wants an immediate apply. Scoped to
|
||||
// the calling add-on so a plug-in can't stage updates for its
|
||||
// neighbours.
|
||||
checkAndStageSelfUpdate: async () => {
|
||||
if (!this._checkAndStageUpdates) throw new Error("checkAndStageSelfUpdate unavailable (host not wired)");
|
||||
const full = await this._checkAndStageUpdates();
|
||||
const own = (full?.report || []).find((r) => r.id === manifest.id) || { status: "no-update-url" };
|
||||
return {
|
||||
status: own.status || "unknown",
|
||||
detail: own.detail || null,
|
||||
current: own.currentVer || manifest.version,
|
||||
next: own.newVer || null,
|
||||
staged: (full?.staged || []).find((s) => s.id === manifest.id) || null,
|
||||
};
|
||||
},
|
||||
// Cleanly relaunch Theseus. Used by the plug-in card's "apply
|
||||
// update" chip to activate a staged build without asking the user
|
||||
// to hunt for the app menu.
|
||||
restartApp: () => {
|
||||
if (!this._restartApp) throw new Error("restartApp unavailable (host not wired)");
|
||||
this._restartApp();
|
||||
},
|
||||
// Resolves once the browser chrome has painted (immediately if it
|
||||
// already has). Put expensive dependency loading behind this so it
|
||||
// never competes with the first frame at launch.
|
||||
|
|
@ -576,6 +616,10 @@ class AddonHost {
|
|||
author: manifest?.author ?? "",
|
||||
icon: manifest?.icon ?? "🧩",
|
||||
capabilities: manifest?.capabilities ?? [],
|
||||
// "plugin" — first-class Silent Mode component (Aegis, future
|
||||
// Ariadne-as-addon) surfaced in Settings › Plug-ins instead of
|
||||
// the raw Extensions list. Absent → plain extension.
|
||||
category: manifest?.category || null,
|
||||
folder,
|
||||
enabled: manifest?.id ? this._active.has(manifest.id) : false,
|
||||
error: error || null,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
{
|
||||
"id": "aegis",
|
||||
"name": "Aegis Wallet",
|
||||
"version": "0.6.2",
|
||||
"version": "0.6.31",
|
||||
"category": "plugin",
|
||||
"description": "Multi-chain wallet (BCH, BTC, TRX, ETH, SOL, SC, DGB) derived from your Theseus vault. Dapps get window.bitcoincash on .x sites; window.tronWeb / window.tronLink / window.ethereum / window.solana on any https page.",
|
||||
"author": "Silent Mode",
|
||||
"icon": "data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32' fill='none'%3E%3Cpolygon points='16,2 28,9 28,23 16,30 4,23 4,9' fill='%230a0a0d' stroke='%23D6FF3D' stroke-width='1.6' stroke-linejoin='round'/%3E%3Ccircle cx='16' cy='16' r='4.5' fill='none' stroke='%23D6FF3D' stroke-width='1.4'/%3E%3Ccircle cx='16' cy='16' r='1.6' fill='%23D6FF3D'/%3E%3C/svg%3E",
|
||||
|
|
|
|||
|
|
@ -92,6 +92,21 @@ async function loadDeps(api) {
|
|||
const importedBchAdapter = require("./lib/chain-bch-imported.js")({
|
||||
sha256, ripemd160, cashaddr, electrum, WebSocket, tx,
|
||||
});
|
||||
// Multi-chain imported adapters. UTXO chains (BTC, DGB) share an electrum-
|
||||
// based reader; account-model chains (ETH, TRX, SOL) share a JSON-RPC
|
||||
// reader. Every runtime is read-only in M.1b, matching chain-bch-imported.
|
||||
const utxoImportedAdapter = require("./lib/chain-utxo-imported.js")({
|
||||
sha256, bitcoinjs, dgbCore, electrum, WebSocket,
|
||||
});
|
||||
const genericImportedAdapter = require("./lib/chain-generic-imported.js")();
|
||||
// Per-chain address derivation from raw material (mnemonic + path or
|
||||
// chain-native private key). Used by the importWallet handler to compute
|
||||
// the address client-side before wallet-imports.enc stores the material.
|
||||
const derive = require("./lib/import-derive.js")({
|
||||
HDKey, secp256k1, ed25519, sha256, ripemd160, keccak_256,
|
||||
cashaddr, base58check, bitcoinjs, bip32Factory: BIP32Factory,
|
||||
ecpairFactory: ECPairFactory, ecc, bip39, dgbCore,
|
||||
});
|
||||
// WizardConnect: LGPL-3.0-or-later. Dynamic-linked via api.import so the
|
||||
// §4d combined-work requirement (dynamic linkage + license notice + source
|
||||
// availability) is met — package sources ship with npm.
|
||||
|
|
@ -101,7 +116,8 @@ async function loadDeps(api) {
|
|||
return { HDKey, secp256k1, ed25519, sha256, hkdf, ripemd160, keccak_256, blake2b,
|
||||
cashaddr, keysLib, tx, electrum, base58check,
|
||||
bchAdapter, tronAdapter, siaAdapter, dgbAdapter, ethAdapter, solAdapter, btcAdapter,
|
||||
importedBchAdapter, bip39,
|
||||
importedBchAdapter, utxoImportedAdapter, genericImportedAdapter,
|
||||
derive, bip39,
|
||||
dgbCore, dgbPsbt, bitcoinjs, ecc, eip712,
|
||||
wcCore, wcWallet, libauth };
|
||||
}
|
||||
|
|
@ -477,14 +493,34 @@ async function mountWallet(entry) {
|
|||
// it doesn't need the material until spend support ships (M.1b).
|
||||
if (entry.kind === "imported") {
|
||||
try {
|
||||
const adapter = new c.d.importedBchAdapter.ImportedBchWallet({
|
||||
let adapter;
|
||||
const commonOpts = {
|
||||
walletId: entry.id, storage: c.api.storage,
|
||||
log: (...a) => c.api.log(`[${entry.id}]`, ...a),
|
||||
onChange: () => emitStateForWallet(entry.id),
|
||||
network: entry.network,
|
||||
cashaddr: entry.importedCashaddr,
|
||||
servers: entry.network === "mainnet" ? bchServerList(c.api) : undefined,
|
||||
});
|
||||
};
|
||||
if (entry.chain === "bch") {
|
||||
adapter = new c.d.importedBchAdapter.ImportedBchWallet({
|
||||
...commonOpts,
|
||||
cashaddr: entry.importedCashaddr || entry.importedAddress,
|
||||
servers: entry.network === "mainnet" ? bchServerList(c.api) : undefined,
|
||||
});
|
||||
adapter.schedulePoll(20_000);
|
||||
} else if (entry.chain === "btc" || entry.chain === "dgb") {
|
||||
adapter = new c.d.utxoImportedAdapter.UtxoImportedWallet({
|
||||
...commonOpts, chain: entry.chain, address: entry.importedAddress,
|
||||
});
|
||||
adapter.schedulePoll(20_000);
|
||||
} else if (entry.chain === "eth" || entry.chain === "trx" || entry.chain === "sol") {
|
||||
adapter = new c.d.genericImportedAdapter.GenericImportedWallet({
|
||||
...commonOpts, chain: entry.chain, address: entry.importedAddress,
|
||||
rpcUrl: String(c.api.storage.get(`wallets/${entry.id}/rpcUrl`, "") || undefined),
|
||||
});
|
||||
adapter.schedulePoll(20_000);
|
||||
} else {
|
||||
throw new Error(`no imported adapter for chain "${entry.chain}"`);
|
||||
}
|
||||
rt.adapter = adapter; rt.phase = "ready";
|
||||
adapter.refresh(true).catch((e) => c.api.log(`[${entry.id}] initial refresh:`, e?.message || e));
|
||||
emitStateForWallet(entry.id);
|
||||
|
|
@ -631,7 +667,15 @@ function deriveCashaddrFromSeed(seedHex, path, prefix) {
|
|||
function deriveCashaddrFromWif(wif, prefix) {
|
||||
const d = ctx.d;
|
||||
// WIF layout: base58check(networkByte || privkey32 || [compressionByte 0x01])
|
||||
const raw = d.base58check.decode(wif);
|
||||
// Wrap decodeCheck so a malformed WIF (bad chars, bad checksum, or an
|
||||
// internal library shape change) surfaces as a user-facing "invalid WIF"
|
||||
// instead of leaking "TypeError: base58check.decode is not a function".
|
||||
let raw;
|
||||
try {
|
||||
raw = d.base58check.decodeCheck(wif);
|
||||
} catch (e) {
|
||||
throw new Error("invalid WIF format (base58check decode failed)");
|
||||
}
|
||||
if (raw.length !== 33 && raw.length !== 34) throw new Error(`bad WIF length ${raw.length}`);
|
||||
// First byte is version (network); we allow any — BCH mainnet uses 0x80,
|
||||
// testnet 0xEF. Both round-trip through the same address derivation below.
|
||||
|
|
@ -683,10 +727,15 @@ function walletSummary(w) {
|
|||
const snap = rt && rt.adapter ? rt.adapter.snapshot() : null;
|
||||
return {
|
||||
id: w.id, label: w.label, chain: w.chain, network: w.network, isDefault: !!w.isDefault, isLegacy: !!w.isLegacy,
|
||||
kind: w.kind || null,
|
||||
logo: meta?.logo || null, color: meta?.color || "#888",
|
||||
coinLabel: meta?.coinLabel || w.chain, networkLabel: meta?.networkLabel || w.network, testnet: !!meta?.testnet,
|
||||
ticker: meta?.ticker || "?", short: meta?.short || w.chain, decimals: meta?.decimals || 8,
|
||||
address: snap?.address || null,
|
||||
// Prefer the wallet-registry's stored accountPath; fall back to whatever
|
||||
// the runtime derived (default when the user hasn't overridden). Nulls
|
||||
// stay null so the picker knows whether to render the mono path line.
|
||||
accountPath: w.accountPath || snap?.accountPath || null,
|
||||
balance: snap?.balance || { confirmed: 0, unconfirmed: 0 },
|
||||
phase: rt?.phase || "locked",
|
||||
error: rt?.error || null,
|
||||
|
|
@ -705,6 +754,7 @@ function snapshotForSelected() {
|
|||
chain: entry?.chain,
|
||||
network: entry?.network,
|
||||
isLegacy: !!entry?.isLegacy,
|
||||
kind: entry?.kind || null,
|
||||
meta: meta ? {
|
||||
logo: meta.logo, color: meta.color, short: meta.short, ticker: meta.ticker, decimals: meta.decimals,
|
||||
coinLabel: meta.coinLabel, networkLabel: meta.networkLabel, testnet: meta.testnet,
|
||||
|
|
@ -851,57 +901,143 @@ function registerPanelMessages(api) {
|
|||
api.onMessage("importWallet", async (p, m) => {
|
||||
fromPanel(m);
|
||||
const chain = String(p && p.chain || "bch");
|
||||
if (chain !== "bch") throw new Error("only BCH imports are supported in this build");
|
||||
const network = String(p && p.network || "chipnet");
|
||||
if (network !== "mainnet" && network !== "chipnet") throw new Error(`unsupported network ${network}`);
|
||||
const network = String(p && p.network || "").trim();
|
||||
const label = String(p && p.label || "").trim();
|
||||
if (!label) throw new Error("label required");
|
||||
const category = String(p && p.category || "operational").trim();
|
||||
const prefix = network === "mainnet" ? "bitcoincash" : "bchtest";
|
||||
const source = String(p && p.source || "manual-paste");
|
||||
|
||||
let kind, seedHex, path, wif, cashaddrStr;
|
||||
if (p && p.wif) {
|
||||
kind = "wif";
|
||||
wif = String(p.wif).trim();
|
||||
cashaddrStr = deriveCashaddrFromWif(wif, prefix);
|
||||
} else if (p && p.mnemonic) {
|
||||
kind = "seed";
|
||||
const words = String(p.mnemonic).trim().split(/\s+/).length;
|
||||
if (words !== 12 && words !== 15 && words !== 18 && words !== 21 && words !== 24) {
|
||||
throw new Error(`mnemonic must be 12/15/18/21/24 words (got ${words})`);
|
||||
}
|
||||
if (!ctx.d.bip39.validateMnemonic(String(p.mnemonic).trim())) {
|
||||
throw new Error("invalid BIP39 mnemonic (unknown word or bad checksum)");
|
||||
}
|
||||
const seed = ctx.d.bip39.mnemonicToSeedSync(String(p.mnemonic).trim());
|
||||
seedHex = Buffer.from(seed).toString("hex");
|
||||
path = String(p.path || (network === "mainnet" ? "m/44'/145'/0'/0/0" : "m/44'/1'/0'/0/0"));
|
||||
cashaddrStr = deriveCashaddrFromSeed(seedHex, path, prefix);
|
||||
} else if (p && p.seedHex) {
|
||||
kind = "seed";
|
||||
seedHex = String(p.seedHex).trim().toLowerCase().replace(/^0x/, "");
|
||||
if (!/^[0-9a-f]{64,128}$/.test(seedHex)) throw new Error("seedHex must be 32-64 bytes of hex");
|
||||
path = String(p.path || (network === "mainnet" ? "m/44'/145'/0'/0/0" : "m/44'/1'/0'/0/0"));
|
||||
cashaddrStr = deriveCashaddrFromSeed(seedHex, path, prefix);
|
||||
// Chain-specific address derivation. Every branch has to produce an
|
||||
// `address` string + fill spec.{seed,path} or spec.wif/privkey. The
|
||||
// spec is what lands in wallet-imports.enc; the address gets stored on
|
||||
// the Aegis wallet entry so the picker/strip can show it without
|
||||
// touching the imports file.
|
||||
const spec = { kind: null, label, category, source };
|
||||
let address = null;
|
||||
const der = ctx.d.derive;
|
||||
|
||||
if (chain === "bch") {
|
||||
const net = network || "chipnet";
|
||||
if (net !== "mainnet" && net !== "chipnet") throw new Error(`BCH network must be mainnet or chipnet (got ${net})`);
|
||||
const prefix = net === "mainnet" ? "bitcoincash" : "bchtest";
|
||||
if (p && p.wif) {
|
||||
spec.kind = "wif"; spec.wif = String(p.wif).trim();
|
||||
address = deriveCashaddrFromWif(spec.wif, prefix);
|
||||
} else if (p && p.mnemonic) {
|
||||
spec.kind = "seed"; spec.seed = der.mnemonicToSeedHex(String(p.mnemonic).trim());
|
||||
spec.path = String(p.path || (net === "mainnet" ? "m/44'/145'/0'/0/0" : "m/44'/1'/0'/0/0"));
|
||||
address = deriveCashaddrFromSeed(spec.seed, spec.path, prefix);
|
||||
} else if (p && p.seedHex) {
|
||||
spec.kind = "seed"; spec.seed = String(p.seedHex).trim().toLowerCase().replace(/^0x/, "");
|
||||
if (!/^[0-9a-f]{64,128}$/.test(spec.seed)) throw new Error("seedHex must be 32-64 bytes of hex");
|
||||
spec.path = String(p.path || (net === "mainnet" ? "m/44'/145'/0'/0/0" : "m/44'/1'/0'/0/0"));
|
||||
address = deriveCashaddrFromSeed(spec.seed, spec.path, prefix);
|
||||
} else { throw new Error("supply mnemonic, seedHex, or wif"); }
|
||||
spec.cashaddr = address;
|
||||
} else if (chain === "btc" || chain === "dgb") {
|
||||
const defaults = { btc: { network: "mainnet", path: "m/84'/0'/0'/0/0" }, dgb: { network: "mainnet", path: "m/84'/20'/0'/0/0" } };
|
||||
const net = network || defaults[chain].network;
|
||||
const purposeHint = Number(p && p.purpose || 84);
|
||||
if (p && p.wif) {
|
||||
spec.kind = "wif"; spec.wif = String(p.wif).trim();
|
||||
address = chain === "btc" ? der.btc.fromWif(spec.wif, net, purposeHint) : der.dgb.fromWif(spec.wif, purposeHint);
|
||||
} else if (p && p.mnemonic) {
|
||||
spec.kind = "seed"; spec.seed = der.mnemonicToSeedHex(String(p.mnemonic).trim());
|
||||
spec.path = String(p.path || defaults[chain].path);
|
||||
address = chain === "btc" ? der.btc.fromSeed(spec.seed, spec.path, net) : der.dgb.fromSeed(spec.seed, spec.path);
|
||||
} else { throw new Error("supply mnemonic or wif"); }
|
||||
// Theseus's api.vault.imports.add validates a `cashaddr` field (from
|
||||
// when only BCH imports existed). Reuse the same field name for
|
||||
// every chain — Aegis reads it back by importId and knows the shape
|
||||
// via entry.chain. Doesn't have to be a real cashaddr.
|
||||
spec.cashaddr = address;
|
||||
} else if (chain === "eth" || chain === "trx" || chain === "sol") {
|
||||
const defaults = {
|
||||
eth: { network: "mainnet", path: "m/44'/60'/0'/0/0" },
|
||||
trx: { network: "mainnet", path: "m/44'/195'/0'/0/0" },
|
||||
sol: { network: "mainnet", path: "m/44'/501'/0'/0'" },
|
||||
};
|
||||
const net = network || defaults[chain].network;
|
||||
if (p && p.mnemonic) {
|
||||
spec.kind = "seed"; spec.seed = der.mnemonicToSeedHex(String(p.mnemonic).trim());
|
||||
spec.path = String(p.path || defaults[chain].path);
|
||||
if (chain === "eth") address = der.eth.fromSeed(spec.seed, spec.path);
|
||||
else if (chain === "trx") address = der.trx.fromSeed(spec.seed, spec.path);
|
||||
else address = der.sol.fromSeed(spec.seed, spec.path);
|
||||
} else if (p && p.privHex) {
|
||||
// Theseus's vault.imports.add only recognises kind "seed" (BIP39
|
||||
// + path) and "wif" (a base58check Bitcoin key). Raw hex keys
|
||||
// for ETH/TRX/SOL don't fit either shape, so we pack them into
|
||||
// the wif slot with a scheme prefix (`aegis-privhex:<hex>`) —
|
||||
// the vault doesn't inspect the value, just stores it. Aegis
|
||||
// reads its own prefix back when spending ships. Panel state
|
||||
// + address are computed here, so read-only balance / receive
|
||||
// work today without touching the vault field.
|
||||
spec.kind = "wif";
|
||||
// Normalise raw hex the user pasted. Tolerate every common mangle
|
||||
// path so the panel error surface is a clear "expected 32-byte
|
||||
// hex" instead of the raw noble/hashes error string:
|
||||
// - leading / trailing whitespace, mixed case
|
||||
// - "0x" or "0X" prefix
|
||||
// - internal whitespace, tabs, newlines, commas, colons, dashes
|
||||
// - accidental quotes wrapping the paste
|
||||
// - a preamble like "private key: <hex>" (e.g. from an AI-agent
|
||||
// transcript) — pick the longest hex-shaped substring.
|
||||
let raw = String(p.privHex).trim();
|
||||
raw = raw.replace(/^['"`]+|['"`]+$/g, "");
|
||||
// If the user pasted a multi-line block, extract the longest
|
||||
// run of hex characters and treat that as the key.
|
||||
const hexRuns = raw.match(/[0-9a-fA-F]{16,}/g);
|
||||
if (hexRuns && hexRuns.length) {
|
||||
hexRuns.sort((a, b) => b.length - a.length);
|
||||
raw = hexRuns[0];
|
||||
}
|
||||
raw = raw.toLowerCase().replace(/^0x/, "").replace(/[\s,:_\-]/g, "");
|
||||
if (!/^[0-9a-f]+$/.test(raw)) {
|
||||
// Give the user something concrete to act on. Tron-specific
|
||||
// hints: base58-shaped strings that start with T (34 chars) are
|
||||
// addresses, not private keys; whitespace-separated words look
|
||||
// like a mnemonic.
|
||||
const original = String(p.privHex).trim();
|
||||
if (/^T[1-9A-HJ-NP-Za-km-z]{33}$/.test(original)) {
|
||||
throw new Error("That looks like a Tron address (T…), not a private key. Paste the 64-hex-character private key instead.");
|
||||
}
|
||||
if (/^([a-z]+\s+){11,}[a-z]+$/i.test(original)) {
|
||||
throw new Error("That looks like a BIP39 mnemonic. Switch the import format to 'Mnemonic + path'.");
|
||||
}
|
||||
throw new Error("Private key must be hex (with or without 0x). Whitespace, dashes and colons are ignored, but non-hex characters aren't accepted.");
|
||||
}
|
||||
if (raw.length !== 64) {
|
||||
throw new Error(`Private key must be 32 bytes (64 hex characters). Got ${raw.length} hex character${raw.length === 1 ? "" : "s"} after normalising the paste.`);
|
||||
}
|
||||
// Derive first so any bad key surfaces before we write to disk.
|
||||
if (chain === "eth") address = der.eth.fromPrivHex(raw);
|
||||
else if (chain === "trx") address = der.trx.fromPrivHex(raw);
|
||||
else address = der.sol.fromPrivHex(raw);
|
||||
spec.wif = `aegis-privhex:${raw}`;
|
||||
} else if (p && p.privB58 && chain === "sol") {
|
||||
// Same repacking trick as privhex above — Solana's Phantom-style
|
||||
// base58 key gets packed into wif with an `aegis-privb58:` tag.
|
||||
const raw = String(p.privB58).trim();
|
||||
address = der.sol.fromBase58(raw);
|
||||
spec.kind = "wif";
|
||||
spec.wif = `aegis-privb58:${raw}`;
|
||||
} else { throw new Error("supply mnemonic, privHex" + (chain === "sol" ? ", or privB58" : "")); }
|
||||
spec.cashaddr = address; // storage-key reuse — see BTC/DGB comment above
|
||||
} else {
|
||||
throw new Error("supply mnemonic, seedHex, or wif");
|
||||
throw new Error(`import not supported for chain "${chain}"`);
|
||||
}
|
||||
|
||||
const spec = { kind, cashaddr: cashaddrStr, label, category, source };
|
||||
if (kind === "seed") { spec.seed = seedHex; spec.path = path; }
|
||||
else { spec.wif = wif; }
|
||||
const { id: importId } = await api.vault.imports.add(spec);
|
||||
|
||||
// Persist as an Aegis wallet entry with kind=imported. Uses a distinct
|
||||
// id prefix so it's obvious in storage that this row references the
|
||||
// imports file rather than a vault-derive purpose.
|
||||
const netForId = network || "mainnet";
|
||||
const list = walletEntries().slice();
|
||||
const walletId = `bch-imported-${importId}`;
|
||||
const walletId = `${chain}-imported-${importId}`;
|
||||
if (list.some((w) => w.id === walletId)) throw new Error("duplicate import id");
|
||||
const entry = {
|
||||
id: walletId, label, chain: "bch", network,
|
||||
kind: "imported", importId, importedCashaddr: cashaddrStr, importedCategory: category,
|
||||
id: walletId, label, chain, network: netForId,
|
||||
kind: "imported", importId,
|
||||
importedAddress: address, importedCategory: category,
|
||||
accountPath: spec.path || null,
|
||||
createdAt: Date.now(),
|
||||
};
|
||||
list.push(entry);
|
||||
|
|
@ -954,9 +1090,42 @@ function registerPanelMessages(api) {
|
|||
return snapshotForSelected();
|
||||
});
|
||||
api.onMessage("openUrl", (p, m) => { fromPanel(m); api.openTab(String(p && p.url || "")); return true; });
|
||||
api.onMessage("aegisVersion", (_p, m) => {
|
||||
fromPanel(m);
|
||||
try { return require("./addon.json").version; } catch { return ""; }
|
||||
});
|
||||
// Panel gate uses this to jump to Settings › Passwords when the vault is
|
||||
// locked / not yet created — one-click bridge to Theseus's built-in UI.
|
||||
api.onMessage("openSettings", (p, m) => { fromPanel(m); api.openSettings(String(p && p.section || "")); return true; });
|
||||
// Panel-initiated update flow. Preferred path: Theseus exposes
|
||||
// checkAndStageSelfUpdate + restartApp (added 0.3.48). The panel calls
|
||||
// "requestUpdate" for the two-step chip flow:
|
||||
// step "stage" — verify + stage the newest signed build; the reply
|
||||
// carries { status, current, next } so the panel can
|
||||
// show "Update to vX.Y.Z ready — restart to apply".
|
||||
// step "apply" — cleanly relaunches Theseus, which runs
|
||||
// promoteStagedUpdates() before activating add-ons.
|
||||
// Falls back to opening Settings › Extensions when running under an
|
||||
// older Theseus that lacks either hook.
|
||||
api.onMessage("requestUpdate", async (p, m) => {
|
||||
fromPanel(m);
|
||||
const step = String(p?.step || "stage");
|
||||
if (step === "apply") {
|
||||
if (typeof api.restartApp !== "function") return { restarted: false, fallback: "settings" };
|
||||
try { api.restartApp(); return { restarted: true }; }
|
||||
catch (e) { return { restarted: false, err: e?.message || String(e) }; }
|
||||
}
|
||||
if (typeof api.checkAndStageSelfUpdate !== "function") return { staged: false, fallback: "settings" };
|
||||
try {
|
||||
const r = await api.checkAndStageSelfUpdate();
|
||||
// "staged" and "already-staged" both mean a newer signed build is
|
||||
// waiting for the next launch — surface it to the panel identically.
|
||||
const ok = r?.status === "staged" || r?.status === "already-staged";
|
||||
return { staged: ok, status: r?.status || "unknown", detail: r?.detail || null, current: r?.current || null, next: r?.next || null };
|
||||
} catch (e) {
|
||||
return { staged: false, err: e?.message || String(e) };
|
||||
}
|
||||
});
|
||||
|
||||
api.onMessage("setBchServers", (p, m) => {
|
||||
fromPanel(m);
|
||||
|
|
@ -1139,6 +1308,14 @@ function registerPanelMessages(api) {
|
|||
if (ctx.priceFeed) await ctx.priceFeed.refresh();
|
||||
return fullState();
|
||||
});
|
||||
api.onMessage("setPricesSource", async (p, m) => {
|
||||
fromPanel(m);
|
||||
const id = String(p && p.source || "").trim();
|
||||
if (!id) throw new Error("source required");
|
||||
api.storage.set("pricesSource", id);
|
||||
if (ctx.priceFeed) await ctx.priceFeed.setSource(id);
|
||||
return fullState();
|
||||
});
|
||||
|
||||
// WizardConnect: pair a wiz:// URI with a specific BCH wallet.
|
||||
api.onMessage("wcConnect", async (p, m) => {
|
||||
|
|
@ -1156,6 +1333,26 @@ function registerPanelMessages(api) {
|
|||
return fullState();
|
||||
});
|
||||
|
||||
// Reorder wallets by an explicit ID list. Silently drops IDs that are
|
||||
// not in the current wallet set (removed since the panel last read);
|
||||
// appends any wallets missing from `order` to the end of the list so a
|
||||
// stale panel reorder cannot make a wallet vanish from the strip.
|
||||
api.onMessage("reorderWallets", (p, m) => {
|
||||
fromPanel(m);
|
||||
const order = Array.isArray(p && p.order) ? p.order.map(String) : [];
|
||||
const current = walletEntries();
|
||||
const byId = new Map(current.map((w) => [w.id, w]));
|
||||
const next = [];
|
||||
const seen = new Set();
|
||||
for (const id of order) {
|
||||
if (byId.has(id) && !seen.has(id)) { next.push(byId.get(id)); seen.add(id); }
|
||||
}
|
||||
for (const w of current) if (!seen.has(w.id)) next.push(w);
|
||||
writeWallets(api, next);
|
||||
emitState();
|
||||
return fullState();
|
||||
});
|
||||
|
||||
api.onMessage("permissions", (_p, m) => { fromPanel(m); return permissions(api); });
|
||||
api.onMessage("revoke", (p, m) => {
|
||||
fromPanel(m);
|
||||
|
|
@ -1164,6 +1361,195 @@ function registerPanelMessages(api) {
|
|||
api.storage.set("permissions", perms);
|
||||
return perms;
|
||||
});
|
||||
|
||||
// ---- security: quick-access PIN + policy flags ------------------------
|
||||
// The PIN blob is a WebCrypto AES-GCM ciphertext of the master password,
|
||||
// derived from PBKDF2(pin, salt). Panel handles the actual encryption /
|
||||
// decryption inside its iframe — the master password never crosses the
|
||||
// process boundary except via vaultUnlock. These handlers only shuttle
|
||||
// the opaque blob + a small policy object in and out of api.storage.
|
||||
api.onMessage("pinBlobGet", (_p, m) => {
|
||||
fromPanel(m);
|
||||
const b = api.storage.get("aegis/pin/v1", null);
|
||||
return (b && typeof b === "object") ? b : null;
|
||||
});
|
||||
api.onMessage("pinBlobSet", (p, m) => {
|
||||
fromPanel(m);
|
||||
const blob = p && p.blob;
|
||||
if (!blob || typeof blob !== "object") throw new Error("blob required");
|
||||
if (typeof blob.salt !== "string" || typeof blob.iv !== "string" || typeof blob.ct !== "string" || typeof blob.iters !== "number") {
|
||||
throw new Error("blob shape invalid");
|
||||
}
|
||||
api.storage.set("aegis/pin/v1", { salt: blob.salt, iv: blob.iv, ct: blob.ct, iters: blob.iters });
|
||||
return true;
|
||||
});
|
||||
api.onMessage("pinBlobClear", (_p, m) => {
|
||||
fromPanel(m);
|
||||
api.storage.set("aegis/pin/v1", null);
|
||||
api.storage.set("aegis/pin/failCount", 0);
|
||||
return true;
|
||||
});
|
||||
// Track failed PIN attempts in the addon so a panel reload cannot bypass
|
||||
// rate-limiting by dropping panel-side counters.
|
||||
api.onMessage("pinFailInc", (_p, m) => {
|
||||
fromPanel(m);
|
||||
const cur = Number(api.storage.get("aegis/pin/failCount", 0)) || 0;
|
||||
const next = cur + 1;
|
||||
api.storage.set("aegis/pin/failCount", next);
|
||||
api.storage.set("aegis/pin/failLast", Date.now());
|
||||
return { count: next, at: Date.now() };
|
||||
});
|
||||
api.onMessage("pinFailReset", (_p, m) => {
|
||||
fromPanel(m);
|
||||
api.storage.set("aegis/pin/failCount", 0);
|
||||
api.storage.set("aegis/pin/failLast", 0);
|
||||
return true;
|
||||
});
|
||||
api.onMessage("pinFailStatus", (_p, m) => {
|
||||
fromPanel(m);
|
||||
return {
|
||||
count: Number(api.storage.get("aegis/pin/failCount", 0)) || 0,
|
||||
last: Number(api.storage.get("aegis/pin/failLast", 0)) || 0,
|
||||
};
|
||||
});
|
||||
|
||||
api.onMessage("securityGet", (_p, m) => {
|
||||
fromPanel(m);
|
||||
const cfg = api.storage.get("aegis/security/v1", {}) || {};
|
||||
return {
|
||||
hasPin: !!api.storage.get("aegis/pin/v1", null),
|
||||
requirePinForSending: !!cfg.requirePinForSending,
|
||||
};
|
||||
});
|
||||
api.onMessage("securitySet", (p, m) => {
|
||||
fromPanel(m);
|
||||
const cur = api.storage.get("aegis/security/v1", {}) || {};
|
||||
const next = { ...cur };
|
||||
if (p && typeof p.requirePinForSending === "boolean") next.requirePinForSending = p.requirePinForSending;
|
||||
api.storage.set("aegis/security/v1", next);
|
||||
return {
|
||||
hasPin: !!api.storage.get("aegis/pin/v1", null),
|
||||
requirePinForSending: !!next.requirePinForSending,
|
||||
};
|
||||
});
|
||||
|
||||
// ---- session: stay-signed-in + idle-lock + manual sign out ------------
|
||||
// "Stay signed in" persists the master password across Theseus restarts
|
||||
// using electron.safeStorage — an OS-level protected keystore (Windows
|
||||
// DPAPI, macOS Keychain, libsecret on Linux). The encrypted blob only
|
||||
// decrypts under the same OS user account, so filesystem-only access
|
||||
// (SSH from another user, a lost backup) cannot use it.
|
||||
//
|
||||
// Storage:
|
||||
// aegis/session/enc — { encPwB64, savedAt } — safeStorage blob
|
||||
// aegis/session/cfg — { lockOnClose: bool, idleMinutes: number }
|
||||
//
|
||||
// Defaults: lockOnClose=true, idleMinutes=15. The user opts in to
|
||||
// remember-me by turning "Lock on Navigator close" off in Settings.
|
||||
api.onMessage("sessionStatus", (_p, m) => {
|
||||
fromPanel(m);
|
||||
return sessionStatusFor(api);
|
||||
});
|
||||
api.onMessage("sessionConfigSet", (p, m) => {
|
||||
fromPanel(m);
|
||||
const cur = api.storage.get("aegis/session/cfg", null) || { lockOnClose: true, idleMinutes: 15 };
|
||||
const next = { ...cur };
|
||||
if (p && typeof p.lockOnClose === "boolean") next.lockOnClose = p.lockOnClose;
|
||||
if (p && typeof p.idleMinutes === "number") {
|
||||
const im = Math.max(0, Math.min(180, Math.floor(p.idleMinutes)));
|
||||
next.idleMinutes = im;
|
||||
}
|
||||
api.storage.set("aegis/session/cfg", next);
|
||||
// Turning "Lock on close" on invalidates any stored remember-me blob.
|
||||
if (next.lockOnClose) api.storage.set("aegis/session/enc", null);
|
||||
return sessionStatusFor(api);
|
||||
});
|
||||
api.onMessage("sessionEnable", (p, m) => {
|
||||
fromPanel(m);
|
||||
const pw = String(p && p.masterPassword || "");
|
||||
if (!pw) throw new Error("master password required");
|
||||
const ss = safeStorageOr(api);
|
||||
if (!ss || !ss.isEncryptionAvailable()) throw new Error("OS keystore unavailable — remember-me needs Windows DPAPI / macOS Keychain / libsecret");
|
||||
const enc = ss.encryptString(pw).toString("base64");
|
||||
api.storage.set("aegis/session/enc", { encPwB64: enc, savedAt: Date.now() });
|
||||
// Force lockOnClose = false alongside — semantically they're the same
|
||||
// switch as far as the user's UI expects.
|
||||
const cur = api.storage.get("aegis/session/cfg", {}) || {};
|
||||
api.storage.set("aegis/session/cfg", { ...cur, lockOnClose: false });
|
||||
return sessionStatusFor(api);
|
||||
});
|
||||
api.onMessage("sessionDisable", (_p, m) => {
|
||||
fromPanel(m);
|
||||
api.storage.set("aegis/session/enc", null);
|
||||
const cur = api.storage.get("aegis/session/cfg", {}) || {};
|
||||
api.storage.set("aegis/session/cfg", { ...cur, lockOnClose: true });
|
||||
return sessionStatusFor(api);
|
||||
});
|
||||
|
||||
// Manual sign-out: locks the vault (main-process re-locks it in memory)
|
||||
// and drops the runtime cache. Also wipes any remember-me blob so the
|
||||
// NEXT Theseus launch will require the master password again — the user
|
||||
// just said "sign me out", not "sign me out just for this restart".
|
||||
api.onMessage("vaultLock", async (_p, m) => {
|
||||
fromPanel(m);
|
||||
api.storage.set("aegis/session/enc", null);
|
||||
for (const walletId of Array.from(ctx.runtimes.keys())) unmountWallet(walletId);
|
||||
try { await api.vault.lifecycle.lock(); } catch (e) { api.log("vault lock:", e?.message || e); }
|
||||
emitState();
|
||||
return fullState();
|
||||
});
|
||||
}
|
||||
|
||||
// Read session status. Kept as a plain helper so both the message handler
|
||||
// and the startup auto-unlock path can call it without duplicating shape.
|
||||
function sessionStatusFor(api) {
|
||||
const cfg = api.storage.get("aegis/session/cfg", null) || { lockOnClose: true, idleMinutes: 15 };
|
||||
const blob = api.storage.get("aegis/session/enc", null);
|
||||
const ss = safeStorageOr(api);
|
||||
return {
|
||||
lockOnClose: !!cfg.lockOnClose,
|
||||
idleMinutes: Number(cfg.idleMinutes) || 0,
|
||||
hasSession: !!(blob && blob.encPwB64),
|
||||
safeStorageAvailable: !!(ss && ss.isEncryptionAvailable && ss.isEncryptionAvailable()),
|
||||
};
|
||||
}
|
||||
|
||||
// Best-effort access to electron.safeStorage from inside the addon. The
|
||||
// addon runs in the main process, so require("electron") gives us the
|
||||
// full main-process API; on hosts that shadow this (tests, older builds)
|
||||
// we degrade to "unavailable" instead of throwing.
|
||||
function safeStorageOr(api) {
|
||||
try {
|
||||
const e = api.require ? api.require("electron") : require("electron");
|
||||
return e && e.safeStorage ? e.safeStorage : null;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
// Called from activate() after deps + WC init, BEFORE mountAllWallets.
|
||||
// If the user opted into stay-signed-in AND we have a stored blob AND
|
||||
// safeStorage can decrypt it under this OS user → auto-unlock the vault.
|
||||
// Any failure is silent (log-only) — mountAllWallets will fall back to
|
||||
// the panel's lock screen exactly as before.
|
||||
async function tryAutoUnlock(api) {
|
||||
try {
|
||||
const status = await api.vault.lifecycle.status();
|
||||
if (status && status.unlocked) return;
|
||||
} catch {}
|
||||
const cfg = api.storage.get("aegis/session/cfg", null) || { lockOnClose: true, idleMinutes: 15 };
|
||||
if (cfg.lockOnClose) return;
|
||||
const blob = api.storage.get("aegis/session/enc", null);
|
||||
if (!blob || !blob.encPwB64) return;
|
||||
const ss = safeStorageOr(api);
|
||||
if (!ss || !ss.isEncryptionAvailable()) return;
|
||||
try {
|
||||
const pw = ss.decryptString(Buffer.from(blob.encPwB64, "base64"));
|
||||
await api.vault.lifecycle.unlock(pw);
|
||||
api.log("auto-unlocked via safeStorage session");
|
||||
} catch (e) {
|
||||
api.log("auto-unlock failed:", e?.message || e);
|
||||
// Drop the stale blob so we don't retry every launch.
|
||||
api.storage.set("aegis/session/enc", null);
|
||||
}
|
||||
}
|
||||
|
||||
// One "describePlan" is enough for both chains because plan() returns a
|
||||
|
|
@ -1871,7 +2257,10 @@ module.exports = {
|
|||
registerPanelMessages(api);
|
||||
registerPageMessages(api);
|
||||
// Restore the user's opt-in choice from storage. Off by default so a
|
||||
// fresh install never hits CoinGecko without asking.
|
||||
// fresh install never hits any oracle without asking. Source can also
|
||||
// be pre-restored so a user who picked Kraken stays on Kraken.
|
||||
const savedSource = String(api.storage.get("pricesSource", "") || "").trim();
|
||||
if (savedSource) c.priceFeed.setSource(savedSource).catch(() => {});
|
||||
if (api.storage.get("pricesEnabled", false)) c.priceFeed.setEnabled(true).catch(() => {});
|
||||
// The deps are heavy to evaluate (noble curve precompute, bitcoinjs,
|
||||
// libauth, WizardConnect) and that all happens on the main thread. Wait
|
||||
|
|
@ -1903,7 +2292,7 @@ module.exports = {
|
|||
},
|
||||
});
|
||||
c.wc.onStateChange(() => emitState());
|
||||
return mountAllWallets();
|
||||
return tryAutoUnlock(api).then(() => mountAllWallets());
|
||||
}).catch((e) => {
|
||||
if (ctx !== c) return;
|
||||
api.log("startup failed:", e?.message);
|
||||
|
|
|
|||
|
|
@ -83,6 +83,10 @@ module.exports = function makeImportedBchAdapter({ sha256, ripemd160, cashaddr,
|
|||
this._servers = Array.isArray(list) && list.length ? list : this._net.defaultServers.slice();
|
||||
this._client.setServers(this._servers);
|
||||
}
|
||||
schedulePoll(ms) {
|
||||
clearTimeout(this._pollTimer);
|
||||
this._pollTimer = setTimeout(() => { this.refresh(false).catch(() => {}); this.schedulePoll(ms); }, ms);
|
||||
}
|
||||
|
||||
_emit() { try { this.onChange(); } catch {} }
|
||||
|
||||
|
|
@ -139,7 +143,7 @@ module.exports = function makeImportedBchAdapter({ sha256, ripemd160, cashaddr,
|
|||
|
||||
recovery() { return { accountPath: null, xpub: null, xprv: null, note: "Recovery lives in the source of the import (Deviant keystore or wherever you got the seed/WIF from)." }; }
|
||||
|
||||
dispose() { try { this._client.disconnect(); } catch {} }
|
||||
dispose() { clearTimeout(this._pollTimer); try { this._client.disconnect(); } catch {} }
|
||||
}
|
||||
|
||||
return { ImportedBchWallet, IMPORTED_BCH_NETWORKS };
|
||||
|
|
|
|||
137
bundled-addons/aegis/lib/chain-generic-imported.js
Normal file
137
bundled-addons/aegis/lib/chain-generic-imported.js
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
// Generic single-address read-only imported adapter for account-model
|
||||
// chains. One config-driven runtime handles ETH-family, Tron, and Solana
|
||||
// balance polling — every chain differs only in the RPC verb and the
|
||||
// JSON path to the balance number.
|
||||
//
|
||||
// The adapter mirrors the public shape every Aegis chain runtime exposes
|
||||
// (snapshot, refresh, plan, signAndBroadcast, dispose) so mountWallet
|
||||
// stays chain-agnostic. planSend/send throw a "read-only" error until
|
||||
// M.1b delivers the sign path per chain.
|
||||
|
||||
module.exports = function makeGenericImportedAdapter() {
|
||||
|
||||
const CHAIN_CFGS = {
|
||||
eth: {
|
||||
ticker: "ETH", decimals: 18,
|
||||
networks: {
|
||||
mainnet: { id: "mainnet", label: "Mainnet", rpc: "https://eth.llamarpc.com", explorerAddr: "https://etherscan.io/address/", explorerTx: "https://etherscan.io/tx/" },
|
||||
sepolia: { id: "sepolia", label: "Sepolia", rpc: "https://ethereum-sepolia-rpc.publicnode.com", explorerAddr: "https://sepolia.etherscan.io/address/", explorerTx: "https://sepolia.etherscan.io/tx/", testnet: true, faucet: "https://sepoliafaucet.com/" },
|
||||
},
|
||||
// JSON-RPC eth_getBalance → hex-string wei.
|
||||
async fetchBalance({ rpc, address }) {
|
||||
const r = await fetch(rpc, { method: "POST", headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "eth_getBalance", params: [address, "latest"] }) });
|
||||
const j = await r.json();
|
||||
const hex = String(j?.result || "0x0").replace(/^0x/, "");
|
||||
return BigInt("0x" + hex).toString();
|
||||
},
|
||||
},
|
||||
trx: {
|
||||
ticker: "TRX", decimals: 6,
|
||||
networks: {
|
||||
mainnet: { id: "mainnet", label: "Mainnet", rpc: "https://api.trongrid.io", explorerAddr: "https://tronscan.org/#/address/", explorerTx: "https://tronscan.org/#/transaction/" },
|
||||
nile: { id: "nile", label: "Nile testnet", rpc: "https://api.nileex.io", explorerAddr: "https://nile.tronscan.org/#/address/", explorerTx: "https://nile.tronscan.org/#/transaction/", testnet: true, faucet: "https://nileex.io/join/getJoinPage" },
|
||||
},
|
||||
// Tron HTTP API returns account.balance in SUN (10^-6 TRX).
|
||||
async fetchBalance({ rpc, address }) {
|
||||
const r = await fetch(rpc.replace(/\/+$/, "") + "/wallet/getaccount", { method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ address, visible: true }) });
|
||||
const j = await r.json();
|
||||
return String(j?.balance || 0);
|
||||
},
|
||||
},
|
||||
sol: {
|
||||
ticker: "SOL", decimals: 9,
|
||||
networks: {
|
||||
mainnet: { id: "mainnet", label: "Mainnet-beta", rpc: "https://api.mainnet-beta.solana.com", explorerAddr: "https://explorer.solana.com/address/", explorerTx: "https://explorer.solana.com/tx/" },
|
||||
devnet: { id: "devnet", label: "Devnet", rpc: "https://api.devnet.solana.com", explorerAddr: "https://explorer.solana.com/address/", explorerTx: "https://explorer.solana.com/tx/", explorerSuffix: "?cluster=devnet", testnet: true, faucet: "https://faucet.solana.com/" },
|
||||
},
|
||||
// Solana JSON-RPC getBalance returns lamports as a number.
|
||||
async fetchBalance({ rpc, address }) {
|
||||
const r = await fetch(rpc, { method: "POST", headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "getBalance", params: [address] }) });
|
||||
const j = await r.json();
|
||||
return String(j?.result?.value || 0);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
class GenericImportedWallet {
|
||||
constructor({ chain, network, address, log = () => {}, onChange = () => {}, rpcUrl } = {}) {
|
||||
const cfg = CHAIN_CFGS[chain]; if (!cfg) throw new Error(`chain-generic-imported: unknown chain ${chain}`);
|
||||
const net = cfg.networks[network]; if (!net) throw new Error(`chain-generic-imported: ${chain} has no network ${network}`);
|
||||
if (!address) throw new Error("address required");
|
||||
this.chain = chain;
|
||||
this.network = network;
|
||||
this._cfg = cfg;
|
||||
this._net = { ...net, rpc: rpcUrl || net.rpc };
|
||||
this.log = log;
|
||||
this.onChange = onChange;
|
||||
this._address = address;
|
||||
this._state = {
|
||||
balance: { confirmed: "0", unconfirmed: "0" },
|
||||
history: [],
|
||||
scanning: false,
|
||||
error: null,
|
||||
};
|
||||
this._pollTimer = null;
|
||||
}
|
||||
|
||||
setServers() { /* no-op: this adapter uses HTTP RPC, not electrum */ }
|
||||
schedulePoll(ms) {
|
||||
clearTimeout(this._pollTimer);
|
||||
this._pollTimer = setTimeout(() => { this.refresh(false).catch(() => {}); this.schedulePoll(ms); }, ms);
|
||||
}
|
||||
|
||||
_emit() { try { this.onChange(); } catch {} }
|
||||
|
||||
snapshot() {
|
||||
return {
|
||||
chain: this.chain, network: this.network,
|
||||
ticker: this._cfg.ticker, decimals: this._cfg.decimals,
|
||||
address: this._address,
|
||||
addressIndex: 0,
|
||||
addressPath: null,
|
||||
balance: this._state.balance,
|
||||
history: this._state.history,
|
||||
scanning: this._state.scanning,
|
||||
error: this._state.error,
|
||||
server: this._net.rpc,
|
||||
rpcUrl: this._net.rpc,
|
||||
imported: true,
|
||||
explorerAddr: this._net.explorerAddr,
|
||||
explorerTx: this._net.explorerTx,
|
||||
explorerSuffix: this._net.explorerSuffix || "",
|
||||
faucet: this._net.faucet || null,
|
||||
};
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
this._state.scanning = true; this._emit();
|
||||
try {
|
||||
const confirmed = await this._cfg.fetchBalance({ rpc: this._net.rpc, address: this._address });
|
||||
this._state.balance = { confirmed: String(confirmed || 0), unconfirmed: "0" };
|
||||
this._state.error = null;
|
||||
} catch (e) {
|
||||
this._state.error = e?.message || String(e);
|
||||
} finally {
|
||||
this._state.scanning = false;
|
||||
this._emit();
|
||||
}
|
||||
}
|
||||
|
||||
nextAddress() { return { address: this._address, index: 0 }; }
|
||||
current() { return { address: this._address, index: 0, branch: 0, path: null }; }
|
||||
|
||||
plan() { throw new Error(`Imported ${this.chain.toUpperCase()} wallets are read-only in this build. Spending support ships in the next Aegis update.`); }
|
||||
signAndBroadcast() { throw new Error("read-only"); }
|
||||
signMessage() { throw new Error("read-only"); }
|
||||
|
||||
recovery() { return { accountPath: null, xpub: null, xprv: null, note: "Recovery lives in the source of the import." }; }
|
||||
|
||||
dispose() { clearTimeout(this._pollTimer); }
|
||||
}
|
||||
|
||||
return { GenericImportedWallet, CHAIN_CFGS };
|
||||
};
|
||||
140
bundled-addons/aegis/lib/chain-utxo-imported.js
Normal file
140
bundled-addons/aegis/lib/chain-utxo-imported.js
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
// Generic single-address read-only imported adapter for UTXO chains
|
||||
// (BTC + DGB). Uses electrum for balance + history, bitcoinjs to convert
|
||||
// the address back into a locking script for the scripthash.
|
||||
//
|
||||
// M.1b will add spending; for now these wallets show as read-only,
|
||||
// matching chain-bch-imported.js's stance.
|
||||
|
||||
module.exports = function makeUtxoImportedAdapter({ sha256, bitcoinjs, dgbCore, electrum, WebSocket }) {
|
||||
|
||||
const toHex = (b) => Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
|
||||
const scripthashOf = (script) => toHex(sha256(script).slice().reverse());
|
||||
|
||||
const NETWORKS = {
|
||||
btc: {
|
||||
ticker: "BTC", decimals: 8,
|
||||
networks: {
|
||||
mainnet: {
|
||||
id: "mainnet", label: "Mainnet",
|
||||
bitcoinjsNet: bitcoinjs.networks.bitcoin,
|
||||
servers: ["wss://electrum.blockstream.info:50004", "wss://fulcrum.sethforprivacy.com:50002"],
|
||||
explorerAddr: "https://mempool.space/address/", explorerTx: "https://mempool.space/tx/",
|
||||
},
|
||||
testnet3: {
|
||||
id: "testnet3", label: "Testnet3",
|
||||
bitcoinjsNet: bitcoinjs.networks.testnet,
|
||||
servers: ["wss://electrumx.tomasi.name:50004"],
|
||||
explorerAddr: "https://mempool.space/testnet/address/", explorerTx: "https://mempool.space/testnet/tx/",
|
||||
testnet: true, faucet: "https://coinfaucet.eu/en/btc-testnet/",
|
||||
},
|
||||
signet: {
|
||||
id: "signet", label: "Signet",
|
||||
bitcoinjsNet: bitcoinjs.networks.testnet,
|
||||
servers: ["wss://signet-electrumx.wakiyamap.dev:50004"],
|
||||
explorerAddr: "https://mempool.space/signet/address/", explorerTx: "https://mempool.space/signet/tx/",
|
||||
testnet: true, faucet: "https://signet.bc-2.jp/",
|
||||
},
|
||||
},
|
||||
},
|
||||
dgb: {
|
||||
ticker: "DGB", decimals: 8,
|
||||
networks: {
|
||||
mainnet: {
|
||||
id: "mainnet", label: "Mainnet",
|
||||
bitcoinjsNet: dgbCore ? dgbCore.digibyte : null,
|
||||
servers: ["wss://electrum1.cipig.net:20063", "wss://electrum2.cipig.net:20063"],
|
||||
explorerAddr: "https://chainz.cryptoid.info/dgb/address.dws?", explorerTx: "https://chainz.cryptoid.info/dgb/tx.dws?",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
class UtxoImportedWallet {
|
||||
constructor({ chain, network, address, log = () => {}, onChange = () => {} } = {}) {
|
||||
const cfg = NETWORKS[chain]; if (!cfg) throw new Error(`chain-utxo-imported: unknown chain ${chain}`);
|
||||
const net = cfg.networks[network]; if (!net) throw new Error(`chain-utxo-imported: ${chain} has no network ${network}`);
|
||||
if (!address) throw new Error("address required");
|
||||
if (!net.bitcoinjsNet) throw new Error(`chain-utxo-imported: ${chain}/${network} missing bitcoinjs network params`);
|
||||
this.chain = chain;
|
||||
this.network = network;
|
||||
this._cfg = cfg;
|
||||
this._net = net;
|
||||
this.log = log;
|
||||
this.onChange = onChange;
|
||||
this._address = address;
|
||||
try {
|
||||
this._script = bitcoinjs.address.toOutputScript(address, net.bitcoinjsNet);
|
||||
} catch (e) {
|
||||
throw new Error(`invalid ${chain} address for ${network}: ${e?.message || e}`);
|
||||
}
|
||||
this._scripthash = scripthashOf(this._script);
|
||||
this._client = new electrum.Client(net.servers.slice());
|
||||
this._client.onServer = () => this._emit();
|
||||
this._state = {
|
||||
balance: { confirmed: 0, unconfirmed: 0 },
|
||||
history: [],
|
||||
height: 0,
|
||||
scanning: false,
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
setServers(list) { this._client.setServers(list && list.length ? list : this._net.servers.slice()); }
|
||||
schedulePoll(ms) {
|
||||
clearTimeout(this._pollTimer);
|
||||
this._pollTimer = setTimeout(() => { this.refresh(false).catch(() => {}); this.schedulePoll(ms); }, ms);
|
||||
}
|
||||
_emit() { try { this.onChange(); } catch {} }
|
||||
|
||||
snapshot() {
|
||||
return {
|
||||
chain: this.chain, network: this.network,
|
||||
ticker: this._cfg.ticker, decimals: this._cfg.decimals,
|
||||
address: this._address,
|
||||
addressIndex: 0,
|
||||
addressPath: null,
|
||||
balance: this._state.balance,
|
||||
history: this._state.history,
|
||||
height: this._state.height,
|
||||
scanning: this._state.scanning,
|
||||
error: this._state.error,
|
||||
server: this._client.url || null,
|
||||
imported: true,
|
||||
explorerAddr: this._net.explorerAddr,
|
||||
explorerTx: this._net.explorerTx,
|
||||
faucet: this._net.faucet || null,
|
||||
};
|
||||
}
|
||||
|
||||
async refresh(full) {
|
||||
this._state.scanning = true; this._emit();
|
||||
try {
|
||||
const bal = await this._client.request("blockchain.scripthash.get_balance", [this._scripthash]);
|
||||
this._state.balance = { confirmed: Number(bal?.confirmed || 0), unconfirmed: Number(bal?.unconfirmed || 0) };
|
||||
if (full) {
|
||||
const hist = await this._client.request("blockchain.scripthash.get_history", [this._scripthash]);
|
||||
this._state.history = (hist || []).slice(-50).map((h) => ({
|
||||
txid: h.tx_hash, time: 0, delta: 0, confirmations: h.height > 0 ? 1 : 0,
|
||||
}));
|
||||
}
|
||||
this._state.error = null;
|
||||
} catch (e) {
|
||||
this._state.error = e?.message || String(e);
|
||||
} finally {
|
||||
this._state.scanning = false;
|
||||
this._emit();
|
||||
}
|
||||
}
|
||||
|
||||
nextAddress() { return { address: this._address, index: 0 }; }
|
||||
current() { return { address: this._address, index: 0, branch: 0, path: null }; }
|
||||
|
||||
plan() { throw new Error(`Imported ${this.chain.toUpperCase()} wallets are read-only in this build.`); }
|
||||
signAndBroadcast() { throw new Error("read-only"); }
|
||||
signMessage() { throw new Error("read-only"); }
|
||||
recovery() { return { accountPath: null, xpub: null, xprv: null, note: "Recovery lives in the source of the import." }; }
|
||||
dispose() { clearTimeout(this._pollTimer); try { this._client.disconnect(); } catch {} }
|
||||
}
|
||||
|
||||
return { UtxoImportedWallet, NETWORKS };
|
||||
};
|
||||
218
bundled-addons/aegis/lib/import-derive.js
Normal file
218
bundled-addons/aegis/lib/import-derive.js
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
// Per-chain address derivation for imported wallets. Every helper turns
|
||||
// either a BIP39 mnemonic (+ path) OR a raw private key (chain-native
|
||||
// format — WIF for UTXO chains, hex for account chains, base58 for Solana)
|
||||
// into the canonical address that chain uses.
|
||||
//
|
||||
// Deps arrive from index.js loadDeps() so nothing here has to know about
|
||||
// npm packages — same "hand it in" pattern the other adapters use.
|
||||
|
||||
module.exports = function makeImportDerive({
|
||||
HDKey, secp256k1, ed25519, sha256, ripemd160, keccak_256,
|
||||
cashaddr, base58check, bitcoinjs, bip32Factory, ecpairFactory, ecc, bip39,
|
||||
dgbCore,
|
||||
}) {
|
||||
const toHex = (b) => Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
|
||||
const fromHex = (h) => {
|
||||
const s = String(h || "").replace(/^0x/i, "");
|
||||
const out = new Uint8Array(s.length / 2);
|
||||
for (let i = 0; i < out.length; i++) out[i] = parseInt(s.substr(i * 2, 2), 16);
|
||||
return out;
|
||||
};
|
||||
const hash160 = (b) => ripemd160(sha256(b));
|
||||
|
||||
// BIP39 mnemonic → 64-byte seed hex. Same wire format the vault-derive
|
||||
// path stores, so keystore-mirrored seeds land in wallet-imports.enc
|
||||
// identically whether they came from a mnemonic or hex directly.
|
||||
function mnemonicToSeedHex(m) {
|
||||
if (!bip39.validateMnemonic(m)) throw new Error("invalid BIP39 mnemonic");
|
||||
return toHex(bip39.mnemonicToSeedSync(m));
|
||||
}
|
||||
|
||||
// ---- BTC ------------------------------------------------------------------
|
||||
const BTC_NET = {
|
||||
mainnet: bitcoinjs.networks.bitcoin,
|
||||
testnet3: bitcoinjs.networks.testnet,
|
||||
signet: bitcoinjs.networks.testnet, // signet uses testnet params here
|
||||
};
|
||||
function btcAddressFromNode(node, path, network) {
|
||||
const net = BTC_NET[network];
|
||||
if (!net) throw new Error(`unknown BTC network ${network}`);
|
||||
// Purpose byte in the path decides the address type. m/84' -> bech32,
|
||||
// m/49' -> P2SH-P2WPKH, m/86' -> P2TR, m/44' -> P2PKH.
|
||||
const m = /^m\/(\d+)'/.exec(String(path || ""));
|
||||
const purpose = m ? Number(m[1]) : 84;
|
||||
const pk = Buffer.from(node.publicKey);
|
||||
if (purpose === 86) {
|
||||
// Taproot — bitcoinjs.p2tr wants the 32-byte x-only pubkey.
|
||||
const xonly = pk.slice(1, 33);
|
||||
return bitcoinjs.payments.p2tr({ internalPubkey: xonly, network: net }).address;
|
||||
}
|
||||
if (purpose === 84) return bitcoinjs.payments.p2wpkh({ pubkey: pk, network: net }).address;
|
||||
if (purpose === 49) return bitcoinjs.payments.p2sh({ redeem: bitcoinjs.payments.p2wpkh({ pubkey: pk, network: net }) }).address;
|
||||
return bitcoinjs.payments.p2pkh({ pubkey: pk, network: net }).address;
|
||||
}
|
||||
function deriveBtcFromSeed(seedHex, path, network) {
|
||||
const bip32 = bip32Factory(ecc);
|
||||
const node = bip32.fromSeed(Buffer.from(fromHex(seedHex)), BTC_NET[network]).derivePath(path);
|
||||
return btcAddressFromNode(node, path, network);
|
||||
}
|
||||
function deriveBtcFromWif(wif, network, hint) {
|
||||
const ECPair = ecpairFactory(ecc);
|
||||
const kp = ECPair.fromWIF(wif, BTC_NET[network]);
|
||||
// WIF alone doesn't tell us the address family; caller passes hint = 44/49/84/86.
|
||||
const purpose = hint || 84;
|
||||
const pk = kp.publicKey;
|
||||
const net = BTC_NET[network];
|
||||
if (purpose === 86) {
|
||||
const xonly = pk.slice(1, 33);
|
||||
return bitcoinjs.payments.p2tr({ internalPubkey: xonly, network: net }).address;
|
||||
}
|
||||
if (purpose === 84) return bitcoinjs.payments.p2wpkh({ pubkey: pk, network: net }).address;
|
||||
if (purpose === 49) return bitcoinjs.payments.p2sh({ redeem: bitcoinjs.payments.p2wpkh({ pubkey: pk, network: net }) }).address;
|
||||
return bitcoinjs.payments.p2pkh({ pubkey: pk, network: net }).address;
|
||||
}
|
||||
|
||||
// ---- DGB (mirrors BTC pattern with digibyte params) -----------------------
|
||||
function digibyteNetwork() {
|
||||
if (!dgbCore) throw new Error("DGB adapter not available");
|
||||
return dgbCore.digibyte;
|
||||
}
|
||||
function deriveDgbFromSeed(seedHex, path) {
|
||||
const bip32 = bip32Factory(ecc);
|
||||
const net = digibyteNetwork();
|
||||
const node = bip32.fromSeed(Buffer.from(fromHex(seedHex)), net).derivePath(path);
|
||||
const pk = Buffer.from(node.publicKey);
|
||||
const m = /^m\/(\d+)'/.exec(String(path || ""));
|
||||
const purpose = m ? Number(m[1]) : 84;
|
||||
if (purpose === 86) {
|
||||
const xonly = pk.slice(1, 33);
|
||||
return bitcoinjs.payments.p2tr({ internalPubkey: xonly, network: net }).address;
|
||||
}
|
||||
if (purpose === 84) return bitcoinjs.payments.p2wpkh({ pubkey: pk, network: net }).address;
|
||||
if (purpose === 49) return bitcoinjs.payments.p2sh({ redeem: bitcoinjs.payments.p2wpkh({ pubkey: pk, network: net }) }).address;
|
||||
return bitcoinjs.payments.p2pkh({ pubkey: pk, network: net }).address;
|
||||
}
|
||||
function deriveDgbFromWif(wif, hint) {
|
||||
const ECPair = ecpairFactory(ecc);
|
||||
const net = digibyteNetwork();
|
||||
const kp = ECPair.fromWIF(wif, net);
|
||||
const purpose = hint || 84;
|
||||
const pk = kp.publicKey;
|
||||
if (purpose === 86) return bitcoinjs.payments.p2tr({ internalPubkey: pk.slice(1, 33), network: net }).address;
|
||||
if (purpose === 84) return bitcoinjs.payments.p2wpkh({ pubkey: pk, network: net }).address;
|
||||
if (purpose === 49) return bitcoinjs.payments.p2sh({ redeem: bitcoinjs.payments.p2wpkh({ pubkey: pk, network: net }) }).address;
|
||||
return bitcoinjs.payments.p2pkh({ pubkey: pk, network: net }).address;
|
||||
}
|
||||
|
||||
// ---- ETH (EIP-55 checksummed 0x address) ----------------------------------
|
||||
function ethAddressFromPubkey(pubUncompressed64) {
|
||||
// Strip the 0x04 prefix if present so we hash just the 64 raw bytes.
|
||||
const raw = pubUncompressed64.length === 65 ? pubUncompressed64.slice(1) : pubUncompressed64;
|
||||
const h = keccak_256(raw);
|
||||
const addr20 = h.slice(-20);
|
||||
const hex = toHex(addr20);
|
||||
// EIP-55 checksum
|
||||
const hashOfLower = toHex(keccak_256(new TextEncoder().encode(hex)));
|
||||
let out = "0x";
|
||||
for (let i = 0; i < hex.length; i++) {
|
||||
out += parseInt(hashOfLower[i], 16) >= 8 ? hex[i].toUpperCase() : hex[i];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
function deriveEthFromSeed(seedHex, path) {
|
||||
const node = HDKey.fromMasterSeed(fromHex(seedHex)).derive(path);
|
||||
// secp256k1.getPublicKey with compressed=false gives 65 bytes (04||X||Y).
|
||||
const pub = secp256k1.getPublicKey(node.privateKey, false);
|
||||
return ethAddressFromPubkey(pub);
|
||||
}
|
||||
function deriveEthFromPrivHex(hex) {
|
||||
const priv = fromHex(hex);
|
||||
if (priv.length !== 32) throw new Error("ETH private key must be 32 bytes hex");
|
||||
const pub = secp256k1.getPublicKey(priv, false);
|
||||
return ethAddressFromPubkey(pub);
|
||||
}
|
||||
|
||||
// ---- TRX (T... base58check, network 0x41) --------------------------------
|
||||
function tronAddressFromPubkey(pubUncompressed65) {
|
||||
const raw = pubUncompressed65.length === 65 ? pubUncompressed65.slice(1) : pubUncompressed65;
|
||||
const h = keccak_256(raw);
|
||||
const last20 = h.slice(-20);
|
||||
const versioned = new Uint8Array(21);
|
||||
versioned[0] = 0x41; // Tron mainnet address prefix — same for Nile testnet
|
||||
versioned.set(last20, 1);
|
||||
return base58check.encodeCheck(versioned);
|
||||
}
|
||||
function deriveTrxFromSeed(seedHex, path) {
|
||||
const node = HDKey.fromMasterSeed(fromHex(seedHex)).derive(path);
|
||||
const pub = secp256k1.getPublicKey(node.privateKey, false);
|
||||
return tronAddressFromPubkey(pub);
|
||||
}
|
||||
function deriveTrxFromPrivHex(hex) {
|
||||
const priv = fromHex(hex);
|
||||
if (priv.length !== 32) throw new Error("TRX private key must be 32 bytes hex");
|
||||
const pub = secp256k1.getPublicKey(priv, false);
|
||||
return tronAddressFromPubkey(pub);
|
||||
}
|
||||
|
||||
// ---- SOL (base58 pubkey, ed25519) ----------------------------------------
|
||||
// SLIP-0010 ed25519 hardened derivation. Slightly different HD scheme
|
||||
// from BIP32 secp256k1 — every step is hardened, index >= 0x80000000.
|
||||
function slip0010DeriveEd25519(seed, path) {
|
||||
const HMAC_KEY = new TextEncoder().encode("ed25519 seed");
|
||||
const parts = String(path).split("/").slice(1);
|
||||
// Compute master
|
||||
const enc = new (require("crypto")).createHmac ? require("crypto") : null;
|
||||
// Not using node crypto — the deps hand in @noble/hashes hmac via sha512.
|
||||
// We rely on secp256k1's helpers? No — use ed25519 utils.
|
||||
// Simplified: compute HMAC-SHA512(HMAC_KEY, seed) → I=I_L||I_R, sk=I_L, cc=I_R.
|
||||
// Then each step: HMAC-SHA512(cc, 0x00 || sk || idx).
|
||||
// Implementation via @noble/hashes/hmac imported as `hmacSha512`. We
|
||||
// require it lazily so unavailable deps error out here rather than at
|
||||
// load time.
|
||||
const { hmac } = require("@noble/hashes/hmac");
|
||||
const { sha512 } = require("@noble/hashes/sha2");
|
||||
let I = hmac(sha512, HMAC_KEY, seed);
|
||||
let sk = I.slice(0, 32); let cc = I.slice(32);
|
||||
for (const seg of parts) {
|
||||
const m = /^(\d+)'?$/.exec(seg);
|
||||
if (!m) throw new Error(`bad path segment: ${seg}`);
|
||||
const idx = (Number(m[1]) | 0x80000000) >>> 0;
|
||||
const data = new Uint8Array(1 + 32 + 4);
|
||||
data[0] = 0;
|
||||
data.set(sk, 1);
|
||||
data[33] = (idx >>> 24) & 0xff; data[34] = (idx >>> 16) & 0xff;
|
||||
data[35] = (idx >>> 8) & 0xff; data[36] = idx & 0xff;
|
||||
I = hmac(sha512, cc, data);
|
||||
sk = I.slice(0, 32); cc = I.slice(32);
|
||||
}
|
||||
return sk;
|
||||
}
|
||||
function deriveSolFromSeed(seedHex, path) {
|
||||
const sk = slip0010DeriveEd25519(fromHex(seedHex), path);
|
||||
const pub = ed25519.getPublicKey(sk);
|
||||
return base58check.encodeBase58(pub);
|
||||
}
|
||||
function deriveSolFromPrivHex(hex) {
|
||||
const priv = fromHex(hex);
|
||||
if (priv.length !== 32 && priv.length !== 64) throw new Error("SOL private key must be 32 or 64 bytes hex");
|
||||
const seed = priv.length === 64 ? priv.slice(0, 32) : priv;
|
||||
const pub = ed25519.getPublicKey(seed);
|
||||
return base58check.encodeBase58(pub);
|
||||
}
|
||||
function deriveSolFromBase58(b58) {
|
||||
const bytes = base58check.decodeBase58(b58);
|
||||
if (bytes.length !== 32 && bytes.length !== 64) throw new Error("SOL private key base58 must decode to 32 or 64 bytes");
|
||||
const seed = bytes.length === 64 ? bytes.slice(0, 32) : bytes;
|
||||
const pub = ed25519.getPublicKey(seed);
|
||||
return base58check.encodeBase58(pub);
|
||||
}
|
||||
|
||||
return {
|
||||
mnemonicToSeedHex,
|
||||
btc: { fromSeed: deriveBtcFromSeed, fromWif: deriveBtcFromWif },
|
||||
dgb: { fromSeed: deriveDgbFromSeed, fromWif: deriveDgbFromWif },
|
||||
eth: { fromSeed: deriveEthFromSeed, fromPrivHex: deriveEthFromPrivHex },
|
||||
trx: { fromSeed: deriveTrxFromSeed, fromPrivHex: deriveTrxFromPrivHex },
|
||||
sol: { fromSeed: deriveSolFromSeed, fromPrivHex: deriveSolFromPrivHex, fromBase58: deriveSolFromBase58 },
|
||||
};
|
||||
};
|
||||
|
|
@ -1,31 +1,108 @@
|
|||
// Fiat prices for every Aegis-supported coin — CoinGecko's free /simple/price
|
||||
// endpoint, one request covers the lot. Opt-in via Settings so a
|
||||
// privacy-conscious user isn't quietly telling CoinGecko when Aegis is open.
|
||||
// Fiat prices for every Aegis-supported coin. Opt-in via Settings so a
|
||||
// privacy-conscious user isn't quietly telling ANY oracle when Aegis is
|
||||
// open. Source is user-selectable — different oracles trade off privacy,
|
||||
// coverage, and freshness:
|
||||
//
|
||||
// Cache is in-memory (returned by fullState() → panel). The addon polls
|
||||
// every 5 min while enabled; each fetch is cheap (~200 B response) and
|
||||
// the free tier tolerates one call/5 min per client easily.
|
||||
// - coingecko : one HTTP request covers all 7 coins, best coverage,
|
||||
// default. Sees the browser IP + User-Agent every poll.
|
||||
// - kraken : per-pair spot from Kraken's public /Ticker; fewer
|
||||
// pairs (BCH/BTC/ETH/SOL/TRX; no SC/DGB). Sees IP but
|
||||
// no user id.
|
||||
// - coinbase : Coinbase's public spot endpoint; similar coverage to
|
||||
// Kraken, similar IP-only exposure.
|
||||
//
|
||||
// Trade-off named in the settings copy: CoinGecko sees the browser's IP
|
||||
// + a User-Agent every poll. Not seed-linked, not address-linked, but a
|
||||
// data point. Off by default.
|
||||
// New sources plug in by adding an entry to SOURCES. Each provider takes a
|
||||
// list of chain keys and returns { <chain>: usd } for the ones it knows
|
||||
// about; unknown chains just stay absent from the snapshot. The poller is
|
||||
// generic.
|
||||
//
|
||||
// Cache is in-memory (returned by fullState() → panel). Poll interval is
|
||||
// per-source since some rate-limit tighter than others. Off by default.
|
||||
|
||||
const COIN_GECKO_IDS = {
|
||||
bch: "bitcoin-cash",
|
||||
btc: "bitcoin",
|
||||
trx: "tron",
|
||||
eth: "ethereum",
|
||||
sol: "solana",
|
||||
sc: "siacoin",
|
||||
dgb: "digibyte",
|
||||
const CHAINS = ["bch", "btc", "trx", "eth", "sol", "sc", "dgb"];
|
||||
|
||||
const SOURCES = {
|
||||
coingecko: {
|
||||
id: "coingecko",
|
||||
label: "CoinGecko",
|
||||
origin: "api.coingecko.com",
|
||||
pollMs: 5 * 60 * 1000,
|
||||
coversAll: true,
|
||||
fetch: async () => {
|
||||
const ids = {
|
||||
bch: "bitcoin-cash", btc: "bitcoin", trx: "tron",
|
||||
eth: "ethereum", sol: "solana", sc: "siacoin", dgb: "digibyte",
|
||||
};
|
||||
const url = `https://api.coingecko.com/api/v3/simple/price?ids=${encodeURIComponent(Object.values(ids).join(","))}&vs_currencies=usd`;
|
||||
const r = await fetch(url);
|
||||
if (!r.ok) throw new Error(`CoinGecko HTTP ${r.status}`);
|
||||
const body = await r.json();
|
||||
const out = {};
|
||||
for (const [chain, cgId] of Object.entries(ids)) {
|
||||
const usd = body?.[cgId]?.usd;
|
||||
if (typeof usd === "number") out[chain] = usd;
|
||||
}
|
||||
return out;
|
||||
},
|
||||
},
|
||||
kraken: {
|
||||
id: "kraken",
|
||||
label: "Kraken",
|
||||
origin: "api.kraken.com",
|
||||
pollMs: 60 * 1000,
|
||||
coversAll: false,
|
||||
fetch: async () => {
|
||||
// Kraken uses non-standard pair names (XBT, ZUSD…). Only cover the
|
||||
// coins Kraken lists with USD spot. SC + DGB are not on Kraken.
|
||||
const pairs = { bch: "BCHUSD", btc: "XBTUSD", eth: "ETHUSD", sol: "SOLUSD", trx: "TRXUSD" };
|
||||
const url = `https://api.kraken.com/0/public/Ticker?pair=${Object.values(pairs).join(",")}`;
|
||||
const r = await fetch(url);
|
||||
if (!r.ok) throw new Error(`Kraken HTTP ${r.status}`);
|
||||
const body = await r.json();
|
||||
if (body?.error?.length) throw new Error("Kraken: " + body.error.join(";"));
|
||||
// Kraken returns keys like "XBCHZUSD" — match by suffix.
|
||||
const out = {};
|
||||
const result = body?.result || {};
|
||||
const entries = Object.entries(result);
|
||||
for (const [chain, pair] of Object.entries(pairs)) {
|
||||
const hit = entries.find(([k]) => k === pair || k.endsWith(pair) || k.endsWith(pair.replace("XBT", "BT")));
|
||||
const last = hit && parseFloat(hit[1]?.c?.[0]);
|
||||
if (Number.isFinite(last)) out[chain] = last;
|
||||
}
|
||||
return out;
|
||||
},
|
||||
},
|
||||
coinbase: {
|
||||
id: "coinbase",
|
||||
label: "Coinbase",
|
||||
origin: "api.coinbase.com",
|
||||
pollMs: 60 * 1000,
|
||||
coversAll: false,
|
||||
fetch: async () => {
|
||||
// Coinbase publishes one spot per pair via /v2/prices/<pair>/spot.
|
||||
// Runs the requests in parallel — 5 calls, each ~150 B response.
|
||||
const map = { bch: "BCH-USD", btc: "BTC-USD", eth: "ETH-USD", sol: "SOL-USD" };
|
||||
const out = {};
|
||||
await Promise.all(Object.entries(map).map(async ([chain, pair]) => {
|
||||
try {
|
||||
const r = await fetch(`https://api.coinbase.com/v2/prices/${pair}/spot`);
|
||||
if (!r.ok) return;
|
||||
const body = await r.json();
|
||||
const usd = parseFloat(body?.data?.amount);
|
||||
if (Number.isFinite(usd)) out[chain] = usd;
|
||||
} catch { /* one pair failing shouldn't kill the others */ }
|
||||
}));
|
||||
return out;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const ENDPOINT = "https://api.coingecko.com/api/v3/simple/price";
|
||||
const POLL_MS = 5 * 60 * 1000;
|
||||
const DEFAULT_SOURCE = "coingecko";
|
||||
|
||||
module.exports = function makePriceFeed({ log = () => {}, onChange = () => {} } = {}) {
|
||||
const state = {
|
||||
enabled: false,
|
||||
source: DEFAULT_SOURCE,
|
||||
prices: {}, // { <chain>: usd (number) }
|
||||
fetchedAt: null,
|
||||
error: null,
|
||||
|
|
@ -33,26 +110,20 @@ module.exports = function makePriceFeed({ log = () => {}, onChange = () => {} }
|
|||
};
|
||||
let timer = null;
|
||||
|
||||
function currentProvider() { return SOURCES[state.source] || SOURCES[DEFAULT_SOURCE]; }
|
||||
|
||||
async function fetchOnce() {
|
||||
if (!state.enabled) return;
|
||||
state.loading = true; state.error = null; onChange();
|
||||
try {
|
||||
const ids = Object.values(COIN_GECKO_IDS).join(",");
|
||||
const url = `${ENDPOINT}?ids=${encodeURIComponent(ids)}&vs_currencies=usd`;
|
||||
const r = await fetch(url);
|
||||
if (!r.ok) throw new Error(`CoinGecko HTTP ${r.status}`);
|
||||
const body = await r.json();
|
||||
const next = {};
|
||||
for (const [chain, cgId] of Object.entries(COIN_GECKO_IDS)) {
|
||||
const usd = body?.[cgId]?.usd;
|
||||
if (typeof usd === "number") next[chain] = usd;
|
||||
}
|
||||
state.prices = next;
|
||||
const src = currentProvider();
|
||||
const next = await src.fetch();
|
||||
state.prices = next || {};
|
||||
state.fetchedAt = Date.now();
|
||||
state.error = null;
|
||||
} catch (e) {
|
||||
state.error = e?.message || String(e);
|
||||
log("price fetch failed:", state.error);
|
||||
log(`price fetch (${state.source}) failed:`, state.error);
|
||||
} finally {
|
||||
state.loading = false;
|
||||
onChange();
|
||||
|
|
@ -62,22 +133,25 @@ module.exports = function makePriceFeed({ log = () => {}, onChange = () => {} }
|
|||
function schedule() {
|
||||
clearTimeout(timer);
|
||||
if (!state.enabled) return;
|
||||
timer = setTimeout(async () => { await fetchOnce(); schedule(); }, POLL_MS);
|
||||
timer = setTimeout(async () => { await fetchOnce(); schedule(); }, currentProvider().pollMs);
|
||||
}
|
||||
|
||||
return {
|
||||
// Snapshot for the panel: only what the UI needs.
|
||||
snapshot() {
|
||||
return {
|
||||
enabled: state.enabled,
|
||||
source: state.source,
|
||||
prices: state.prices,
|
||||
fetchedAt: state.fetchedAt,
|
||||
error: state.error,
|
||||
loading: state.loading,
|
||||
sources: Object.values(SOURCES).map((s) => ({
|
||||
id: s.id, label: s.label, origin: s.origin, coversAll: s.coversAll,
|
||||
})),
|
||||
};
|
||||
},
|
||||
// Turn the feed on/off. Enabling triggers an immediate fetch so the
|
||||
// panel doesn't wait 5 minutes for the first price.
|
||||
// panel doesn't wait a full poll interval for the first price.
|
||||
async setEnabled(on) {
|
||||
const changed = !!on !== state.enabled;
|
||||
state.enabled = !!on;
|
||||
|
|
@ -91,7 +165,15 @@ module.exports = function makePriceFeed({ log = () => {}, onChange = () => {} }
|
|||
await fetchOnce();
|
||||
schedule();
|
||||
},
|
||||
// Force-refresh — bound to a manual "refresh" button in the panel.
|
||||
// Switch source. Clears the current cache, kicks a fresh fetch if the
|
||||
// feed is enabled. No-op when the source is already current.
|
||||
async setSource(id) {
|
||||
if (!SOURCES[id] || id === state.source) return;
|
||||
state.source = id;
|
||||
state.prices = {}; state.fetchedAt = null;
|
||||
onChange();
|
||||
if (state.enabled) { await fetchOnce(); schedule(); }
|
||||
},
|
||||
refresh() { return fetchOnce(); },
|
||||
dispose() { clearTimeout(timer); state.enabled = false; },
|
||||
};
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
html, body { margin: 0; height: 100%; }
|
||||
body { background: var(--bg); color: var(--ink);
|
||||
font: 13.5px/1.5 system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
|
||||
display: flex; flex-direction: column; }
|
||||
display: flex; flex-direction: column; position: relative; }
|
||||
header { padding: 10px 14px 10px; border-bottom: 1px solid var(--line); background: var(--panel); position: relative; }
|
||||
.picker { display: flex; align-items: center; justify-content: space-between; gap: 8px; cursor: pointer; user-select: none; }
|
||||
.picker .t { display: flex; align-items: center; gap: 7px; font-weight: 600; min-width: 0; }
|
||||
|
|
@ -37,10 +37,21 @@
|
|||
.bal .big small { font-size: 13px; color: var(--mut); font-weight: 500; margin-left: 4px; }
|
||||
.bal .sub { color: var(--dim); font-size: 11.5px; display: flex; justify-content: space-between; gap: 8px; }
|
||||
.bal .sub .netlbl { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
#drop { position: absolute; left: 10px; right: 10px; top: 100%; background: var(--panel); border: 1px solid var(--line);
|
||||
border-radius: 10px; box-shadow: 0 8px 26px rgba(0,0,0,.3); z-index: 20; padding: 4px; margin-top: 4px; max-height: 60vh; overflow-y: auto; }
|
||||
/* Full-panel sheet — fills the sidebar so long wallet lists and the
|
||||
import form aren't squeezed into a small popover. */
|
||||
#drop { position: fixed; left: 0; right: 0; top: 60px; bottom: 0;
|
||||
background: var(--panel); border-top: 1px solid var(--line);
|
||||
box-shadow: 0 -6px 26px rgba(0,0,0,.35); z-index: 20;
|
||||
display: flex; flex-direction: column; }
|
||||
#drop[hidden] { display: none; }
|
||||
#drop .row, #drop .coinrow { display: flex; align-items: center; gap: 9px; padding: 7px 8px; border-radius: 7px; cursor: pointer; }
|
||||
#drop .droptabs { display: flex; border-bottom: 1px solid var(--line); flex: none; }
|
||||
#drop .droptabs button { flex: 1; background: transparent; border: 0; color: var(--dim);
|
||||
padding: 10px 8px; font: inherit; font-size: 13px; cursor: pointer; border-bottom: 2px solid transparent; }
|
||||
#drop .droptabs button.on { color: var(--ink); border-bottom-color: var(--acid, #d6ff3d); font-weight: 600; }
|
||||
#drop .droptabs .closex { flex: none; width: 40px; font-size: 16px; color: var(--dim); border-left: 1px solid var(--line); }
|
||||
#drop .droppane { flex: 1; overflow-y: auto; padding: 6px; }
|
||||
#drop .droppane[hidden] { display: none; }
|
||||
#drop .row, #drop .coinrow { display: flex; align-items: center; gap: 9px; padding: 8px 10px; border-radius: 7px; cursor: pointer; }
|
||||
#drop .row:hover, #drop .coinrow:hover { background: rgba(255,255,255,.05); }
|
||||
#drop .row .m, #drop .coinrow .m { flex: 1; min-width: 0; }
|
||||
#drop .row .m .l, #drop .coinrow .m .l { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
|
|
@ -73,6 +84,45 @@
|
|||
nav button.on { color: var(--acid); border-bottom-color: var(--acid); }
|
||||
nav button:hover { color: var(--ink); }
|
||||
main { flex: 1; overflow: auto; padding: 14px; }
|
||||
/* Persistent aegis.x brand strip. Sticks to the panel's bottom edge so the
|
||||
wallet always advertises its own front-door site — helps users bookmark
|
||||
it, and doubles as a version marker for support triage. */
|
||||
.brandfoot { flex: none; display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 6px 12px; border-top: 1px solid var(--line); background: var(--panel);
|
||||
font-size: 11px; color: var(--dim); }
|
||||
.brandlink { display: inline-flex; align-items: center; gap: 5px; color: var(--dim); text-decoration: none;
|
||||
padding: 2px 4px; border-radius: 4px; }
|
||||
.brandlink:hover { color: var(--acid, #d6ff3d); }
|
||||
.brandlink svg { color: currentColor; }
|
||||
.brandver { font-variant-numeric: tabular-nums; letter-spacing: .3px; }
|
||||
.brandfoot-right { display: inline-flex; align-items: center; gap: 8px; }
|
||||
.brandcheck { background: transparent; border: 0; color: var(--dim); cursor: pointer; padding: 0 2px;
|
||||
font: inherit; font-size: 12px; line-height: 1; opacity: .7; transition: opacity .12s, color .12s; }
|
||||
.brandcheck:hover { color: var(--acid, #d6ff3d); opacity: 1; }
|
||||
.brandcheck.spin { animation: brandspin 1s linear infinite; }
|
||||
@keyframes brandspin { to { transform: rotate(360deg); } }
|
||||
.brandupd { display: inline-flex; align-items: center; gap: 5px; font-size: 11px; padding: 2px 8px;
|
||||
border-radius: 999px; background: rgb(from var(--acid, #d6ff3d) r g b / .14); color: var(--acid, #d6ff3d);
|
||||
cursor: pointer; text-decoration: none; font-weight: 600;
|
||||
transition: opacity .2s; }
|
||||
.brandupd[hidden] { display: none; }
|
||||
.brandupd:hover { background: rgb(from var(--acid, #d6ff3d) r g b / .22); }
|
||||
/* Transient states painted by paintFooterUpdate: "✓ Up to date" after a
|
||||
manual check when no newer version is available, and "⚠ Check failed"
|
||||
when the OTA fetch errors. Both auto-hide after ~2s; the styling reads
|
||||
as ephemeral confirmation rather than a persistent chip. */
|
||||
.brandupd.brandok { background: rgba(255,255,255,.06); color: var(--dim); font-weight: 500;
|
||||
cursor: default; }
|
||||
.brandupd.brandok:hover { background: rgba(255,255,255,.06); }
|
||||
.brandupd.branderr { background: rgba(224,90,90,.18); color: #e05a5a; font-weight: 500;
|
||||
cursor: default; }
|
||||
.brandupd.branderr:hover { background: rgba(224,90,90,.18); }
|
||||
/* In-progress state: painted while the panel is fetching / staging /
|
||||
restarting. Not clickable, dimmer than the acid CTA chip so users
|
||||
don't try to smash it. */
|
||||
.brandupd.brandwait { background: rgba(255,255,255,.06); color: var(--mut);
|
||||
font-weight: 500; cursor: default; }
|
||||
.brandupd.brandwait:hover { background: rgba(255,255,255,.06); }
|
||||
section[hidden] { display: none; }
|
||||
.card { background: var(--card); border: 1px solid var(--line); border-radius: 10px; padding: 12px; }
|
||||
.mono { font: 12.5px/1.45 ui-monospace, "Cascadia Code", Consolas, monospace; word-break: break-all; }
|
||||
|
|
@ -126,6 +176,218 @@
|
|||
.kv { margin-top: 10px; }
|
||||
.kv .lbl { margin-top: 8px; }
|
||||
a.link { color: var(--acid); text-decoration: none; cursor: pointer; }
|
||||
/* Preset-server checkbox list — one row per known Electrum endpoint, plus
|
||||
any URL the user added via the "Add a custom server" reveal. Rows show
|
||||
the host + a small hover-to-remove for user-added entries. */
|
||||
.serverlist { display: flex; flex-direction: column; gap: 3px; margin-top: 4px; }
|
||||
.serverlist label { display: flex; align-items: center; gap: 8px; padding: 5px 6px;
|
||||
border-radius: 6px; cursor: pointer; font-size: 12.5px; color: var(--mut); }
|
||||
.serverlist label:hover { background: rgba(255,255,255,.04); color: var(--ink); }
|
||||
.serverlist input[type=checkbox] { accent-color: var(--acid, #d6ff3d); width: 14px; height: 14px; margin: 0; }
|
||||
.serverlist .surl { flex: 1; font: 11.5px/1.4 ui-monospace, Consolas, monospace; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.serverlist .sremove { flex: none; background: transparent; border: 0; color: var(--dim); cursor: pointer; padding: 0 4px; opacity: 0; }
|
||||
.serverlist label:hover .sremove { opacity: 1; }
|
||||
.serverlist .sremove:hover { color: var(--danger, #f6768a); }
|
||||
/* Always-visible wallet list — vertical column of rows so users can scan
|
||||
names + balances without dragging chips sideways. Capped at ~40vh so a
|
||||
giant list still leaves the main content readable; scrolls inside. */
|
||||
.wstrip { flex: none; display: flex; flex-direction: column; gap: 1px; padding: 4px 6px 5px 6px;
|
||||
max-height: 44vh; overflow-y: auto; scrollbar-width: thin;
|
||||
background: var(--panel); border-bottom: 1px solid var(--line); }
|
||||
.wstrip::-webkit-scrollbar { width: 4px; }
|
||||
.wstrip::-webkit-scrollbar-thumb { background: var(--line); border-radius: 2px; }
|
||||
.wstrip .wchip { display: flex; align-items: center; gap: 4px; padding: 4px 6px 4px 4px;
|
||||
background: transparent; border: 1px solid transparent; border-radius: 7px;
|
||||
font: inherit; font-size: 12.5px; color: var(--mut); min-width: 0; }
|
||||
.wstrip .wchip:hover { background: rgba(255,255,255,.04); color: var(--ink); }
|
||||
.wstrip .wchip.on { background: rgb(from var(--acid, #d6ff3d) r g b / .10);
|
||||
border-color: rgb(from var(--acid, #d6ff3d) r g b / .35);
|
||||
color: var(--ink); }
|
||||
.wstrip .wchip .wclick { display: flex; align-items: center; gap: 8px; flex: 1; min-width: 0;
|
||||
cursor: pointer; padding: 2px 4px; border-radius: 5px; }
|
||||
.wstrip .wchip .wtext { flex: 1; min-width: 0; overflow: hidden; display: inline-flex;
|
||||
align-items: center; gap: 6px; flex-wrap: nowrap; }
|
||||
.wstrip .wchip .wname { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.wstrip .wchip .wsub { color: var(--dim); font-size: 10.5px; font-weight: 400; margin-left: 6px;
|
||||
font-variant-numeric: tabular-nums; white-space: nowrap; }
|
||||
/* Price chip sitting right next to the wallet label. Acid tint with a
|
||||
subtle glow lifts it off the grey row so users spot movement without
|
||||
hunting; the small background gives it structural presence too. */
|
||||
.wstrip .wchip .wprice { color: var(--acid, #d6ff3d); font-size: 11px; font-weight: 600;
|
||||
font-variant-numeric: tabular-nums; white-space: nowrap;
|
||||
padding: 1px 6px; border-radius: 999px;
|
||||
background: rgb(from var(--acid, #d6ff3d) r g b / .10);
|
||||
text-shadow: 0 0 6px rgb(from var(--acid, #d6ff3d) r g b / .35); }
|
||||
.wstrip .wchip .wact { flex: none; background: transparent; border: 0; color: var(--dim);
|
||||
cursor: pointer; padding: 3px 6px; border-radius: 5px; font-size: 12px; line-height: 1; }
|
||||
.wstrip .wchip .wact:hover { color: var(--acid, #d6ff3d); background: rgba(255,255,255,.06); }
|
||||
/* Four-column layout: [logo][ticker+price stack][amount+fiat stack][actions].
|
||||
The old separate Price and Chain columns collapsed into the ticker cell —
|
||||
per-unit price sits directly under the ticker so the eye can read
|
||||
"BCH · $430" as one unit before jumping to the balance on the right.
|
||||
A non-mainnet row also carries a small network pill inline with the
|
||||
ticker (Chipnet / Sepolia / …); mainnet rows omit it entirely. */
|
||||
.wstrip { padding: 4px 6px 6px 6px; }
|
||||
.wstrip .wrow { display: grid; grid-template-columns: 18px minmax(72px,auto) minmax(0,1fr) auto auto;
|
||||
gap: 8px; align-items: center; padding: 5px 4px;
|
||||
border-radius: 6px; border: 1px solid transparent; cursor: pointer;
|
||||
min-height: 30px; }
|
||||
.wstrip .wrow:hover { background: rgba(255,255,255,.04); }
|
||||
.wstrip .wrow.on { background: rgb(from var(--acid, #d6ff3d) r g b / .10);
|
||||
border-color: rgb(from var(--acid, #d6ff3d) r g b / .35); }
|
||||
.wstrip .wrow.dragging { opacity: .45; cursor: grabbing; }
|
||||
.wstrip .wrow.drop-before { box-shadow: 0 -2px 0 0 var(--acid, #d6ff3d); }
|
||||
.wstrip .wrow.drop-after { box-shadow: 0 2px 0 0 var(--acid, #d6ff3d); }
|
||||
.wstrip .wgrip { display: inline-block; color: var(--dim); font-size: 10px; padding: 0 2px;
|
||||
cursor: grab; user-select: none; opacity: 0; transition: opacity .12s; }
|
||||
.wstrip .wrow:hover .wgrip { opacity: 1; }
|
||||
.wstrip .wrow[draggable="true"] { cursor: grab; }
|
||||
.wstrip .wcell { min-width: 0; display: inline-flex; align-items: center; gap: 3px; }
|
||||
.wstrip .wclogo { justify-self: start; align-self: start; padding-top: 2px; }
|
||||
/* Ticker cell: two-line stack. Line 1 has the ticker, an optional ▾
|
||||
chevron (only when the chain has more than one network to switch
|
||||
between), an optional Chipnet/Testnet pill, and the wallet count.
|
||||
Line 2 is the per-unit price in acid-green. The cell as a whole is
|
||||
the click target for the network dropdown. */
|
||||
.wstrip .wcname { flex-direction: column; align-items: flex-start; line-height: 1.15; gap: 1px; }
|
||||
.wstrip .wcname .wtline { display: inline-flex; align-items: center; gap: 5px; }
|
||||
.wstrip .wcname .wtck { font-weight: 700; font-size: 13px; color: var(--ink); }
|
||||
.wstrip .wcname .wchev { color: var(--dim); font-size: 9px; line-height: 1; padding: 0 1px;
|
||||
transition: color .12s, transform .12s; }
|
||||
.wstrip .wcname.wswitchable { cursor: pointer; }
|
||||
.wstrip .wcname.wswitchable:hover .wchev { color: var(--acid, #d6ff3d); }
|
||||
.wstrip .wcname .wnetpill { font-size: 10px; padding: 1px 6px; border-radius: 999px;
|
||||
background: rgba(255,255,255,.06); color: var(--mut);
|
||||
white-space: nowrap; line-height: 1.35; }
|
||||
.wstrip .wcname .wnetpill.wchipnet { background: rgb(from var(--acid, #d6ff3d) r g b / .18);
|
||||
color: var(--acid, #d6ff3d); font-weight: 600; }
|
||||
.wstrip .wcname .wnetpill.wtestnet { background: rgba(224,179,65,.18); color: #e0b341; font-weight: 600; }
|
||||
.wstrip .wcname .wgcount { color: var(--dim); font-size: 10.5px; padding: 0 6px; border-radius: 999px;
|
||||
background: rgba(255,255,255,.06); font-variant-numeric: tabular-nums; line-height: 1.4; }
|
||||
.wstrip .wcname .wcprice { color: var(--acid, #d6ff3d); font-size: 11px; font-weight: 600;
|
||||
font-variant-numeric: tabular-nums; white-space: nowrap;
|
||||
text-shadow: 0 0 5px rgb(from var(--acid, #d6ff3d) r g b / .30); }
|
||||
.wstrip .wcamt { flex-direction: column; align-items: flex-end; text-align: right;
|
||||
font-variant-numeric: tabular-nums; line-height: 1.2; }
|
||||
.wstrip .wcamt .wnative { font-size: 12.5px; color: var(--ink); font-weight: 500; }
|
||||
.wstrip .wcamt .wfiat { font-size: 11px; color: var(--dim); }
|
||||
.wstrip .wcact { display: inline-flex; gap: 2px; }
|
||||
.wstrip .wcact .wact { background: transparent; border: 0; color: var(--dim); cursor: pointer;
|
||||
padding: 2px 5px; border-radius: 4px; font-size: 12.5px; line-height: 1; }
|
||||
.wstrip .wcact .wact:hover { color: var(--acid, #d6ff3d); background: rgba(255,255,255,.06); }
|
||||
/* Network dropdown menu — anchored below the ticker cell on click. Shows
|
||||
every network under the chain (mainnet + testnets) with its own totals,
|
||||
so switching networks feels like flipping a segment on the same coin
|
||||
row rather than jumping to a separate wallet. */
|
||||
.netmenu { position: absolute; z-index: 60; min-width: 180px; background: var(--panel, #1a1a1a);
|
||||
border: 1px solid var(--line); border-radius: 8px; padding: 4px;
|
||||
box-shadow: 0 6px 20px rgba(0,0,0,.35); }
|
||||
.netmenu .nmitem { display: flex; align-items: center; justify-content: space-between;
|
||||
gap: 10px; padding: 6px 8px; border-radius: 5px; cursor: pointer;
|
||||
color: var(--ink); font-size: 12.5px; }
|
||||
.netmenu .nmitem:hover { background: rgba(255,255,255,.06); }
|
||||
.netmenu .nmitem.on { background: rgb(from var(--acid, #d6ff3d) r g b / .12);
|
||||
color: var(--acid, #d6ff3d); }
|
||||
.netmenu .nmitem .nmname { display: inline-flex; align-items: center; gap: 6px; }
|
||||
.netmenu .nmitem .nmnet { font-weight: 600; }
|
||||
.netmenu .nmitem .nmcount { color: var(--dim); font-size: 10.5px; padding: 0 5px; border-radius: 999px;
|
||||
background: rgba(255,255,255,.06); }
|
||||
.netmenu .nmitem .nmamt { font-variant-numeric: tabular-nums; font-size: 11.5px; color: var(--dim); }
|
||||
.netmenu .nmitem.on .nmamt { color: var(--acid, #d6ff3d); }
|
||||
/* Acid-green testnet tag for the Chipnet group. Overrides the warm-amber
|
||||
default so chipnet reads as "the friendly BCH testnet" instead of
|
||||
borrowing the caution palette. */
|
||||
.ttag.acid { background: rgb(from var(--acid, #d6ff3d) r g b / .18); color: var(--acid, #d6ff3d); }
|
||||
.wstrip .waddwrap { display: flex; gap: 5px; padding: 0 0 4px 0; margin-bottom: 4px; border-bottom: 1px solid var(--line); }
|
||||
.wstrip .waddwrap button { flex: 1; background: transparent; border: 1px dashed var(--line); border-radius: 6px;
|
||||
padding: 5px 8px; color: var(--dim); cursor: pointer; font: inherit; font-size: 12px; line-height: 1; }
|
||||
.wstrip .waddwrap button:hover { color: var(--acid, #d6ff3d); border-color: var(--acid, #d6ff3d); }
|
||||
/* Inline coin-address view — takes the place of the six-column grid when
|
||||
a user opens a multi-wallet group. Back-arrow row + one row per wallet
|
||||
under the coin. Same container so the visual context stays put. */
|
||||
.wstrip .wcoinhead { display: flex; align-items: center; gap: 6px; padding: 3px 4px 4px 4px;
|
||||
border-bottom: 1px solid var(--line); }
|
||||
.wstrip .wcoinhead .wback { background: transparent; border: 0; color: var(--dim); cursor: pointer;
|
||||
padding: 2px 4px; border-radius: 4px; font: inherit; font-size: 12px; line-height: 1; }
|
||||
.wstrip .wcoinhead .wback:hover { color: var(--acid, #d6ff3d); background: rgba(255,255,255,.06); }
|
||||
.wstrip .wcoinhead .wctitle { flex: 1; min-width: 0; display: inline-flex; align-items: center; gap: 6px;
|
||||
font-size: 13px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.wstrip .wcoinhead .wcount { color: var(--dim); font-size: 11.5px; font-weight: 500; }
|
||||
.wstrip .warow { display: grid; grid-template-columns: 16px minmax(60px,1fr) auto auto auto;
|
||||
gap: 6px; align-items: center; padding: 5px 4px;
|
||||
border-radius: 6px; border: 1px solid transparent; cursor: pointer; min-height: 30px; }
|
||||
.wstrip .warow:hover { background: rgba(255,255,255,.04); }
|
||||
.wstrip .warow.on { background: rgb(from var(--acid, #d6ff3d) r g b / .10);
|
||||
border-color: rgb(from var(--acid, #d6ff3d) r g b / .35); }
|
||||
.wstrip .warow .waname { font-size: 13px; color: var(--ink); overflow: hidden; text-overflow: ellipsis;
|
||||
white-space: nowrap; font-weight: 500; }
|
||||
.wstrip .warow .waaddr { font: 11px/1.15 ui-monospace, Consolas, monospace; color: var(--dim); margin-top: 2px;
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.wstrip .warow .waamt { text-align: right; font-variant-numeric: tabular-nums; font-size: 12.5px; color: var(--ink); }
|
||||
.wstrip .warow .wafiat { font-size: 11px; color: var(--dim); }
|
||||
.wstrip .warow .wact { background: transparent; border: 0; color: var(--dim); cursor: pointer;
|
||||
padding: 2px 5px; border-radius: 4px; font-size: 12.5px; line-height: 1; }
|
||||
.wstrip .warow .wact:hover { color: var(--acid, #d6ff3d); background: rgba(255,255,255,.06); }
|
||||
|
||||
/* Full-panel lock screen — takes over the entire panel below the aegis
|
||||
footer when the vault is locked or awaiting first-time setup. Rest of
|
||||
the app (header picker, tabs, wallet strip) is hidden until unlocked
|
||||
so nothing sensitive shows and there is no accidental interaction
|
||||
surface. */
|
||||
#lockScreen[hidden] { display: none; }
|
||||
#lockScreen { position: absolute; inset: 0 0 30px 0; z-index: 50; background: var(--bg);
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
padding: 24px 18px; color: var(--ink); text-align: center; }
|
||||
#lockScreen .aegisMark { width: 64px; height: 64px; margin-bottom: 14px; }
|
||||
#lockScreen .aegisMark svg { width: 100%; height: 100%; color: var(--acid, #d6ff3d);
|
||||
filter: drop-shadow(0 0 12px rgb(from var(--acid, #d6ff3d) r g b / .35)); }
|
||||
#lockScreen h1 { font: 600 16px/1.3 inherit; margin: 0 0 4px 0; letter-spacing: .2px; }
|
||||
#lockScreen .subhint { color: var(--mut); font-size: 12px; max-width: 320px; margin: 0 0 20px 0; }
|
||||
#lockScreen .lockform { width: min(320px, 100%); display: flex; flex-direction: column; gap: 10px; text-align: left; }
|
||||
#lockScreen .lockform input[type=password],
|
||||
#lockScreen .lockform input[type=text],
|
||||
#lockScreen .lockform textarea { text-align: center; }
|
||||
#lockScreen .altline { color: var(--dim); font-size: 11.5px; margin-top: 12px; text-align: center; }
|
||||
#lockScreen .altline a { color: var(--acid, #d6ff3d); cursor: pointer; text-decoration: none; }
|
||||
#lockScreen .altline a:hover { text-decoration: underline; }
|
||||
/* PIN pad — six dots + a 3x4 keypad. Used both in lock screen and in the
|
||||
PIN approval modal. */
|
||||
.pinpad { display: flex; flex-direction: column; align-items: center; gap: 14px; }
|
||||
.pinpad .pindots { display: flex; gap: 12px; }
|
||||
.pinpad .pindot { width: 12px; height: 12px; border-radius: 50%; border: 1.5px solid var(--dim);
|
||||
background: transparent; transition: background .12s, border-color .12s; }
|
||||
.pinpad .pindot.on { background: var(--acid, #d6ff3d); border-color: var(--acid, #d6ff3d);
|
||||
box-shadow: 0 0 6px rgb(from var(--acid, #d6ff3d) r g b / .5); }
|
||||
.pinpad .pinkeys { display: grid; grid-template-columns: repeat(3, 62px); gap: 8px; }
|
||||
.pinpad .pinkeys button { height: 46px; border-radius: 10px; border: 1px solid var(--line);
|
||||
background: var(--card); color: var(--ink); font: 500 18px inherit;
|
||||
cursor: pointer; }
|
||||
.pinpad .pinkeys button:hover { border-color: var(--acid, #d6ff3d); color: var(--acid, #d6ff3d); }
|
||||
.pinpad .pinkeys button.util { background: transparent; font-size: 13px; color: var(--dim); }
|
||||
.pinpad .pinerr { color: var(--danger); font-size: 12px; text-align: center; min-height: 16px; }
|
||||
|
||||
/* PIN modal overlay — used for set-PIN / change-PIN / verify-PIN flows. */
|
||||
.pinmodal { position: fixed; inset: 0; background: rgba(0,0,0,.55); display: flex;
|
||||
align-items: center; justify-content: center; z-index: 9999; padding: 20px; }
|
||||
.pinmodal .pincard { width: min(94vw, 340px); background: var(--panel); border: 1px solid var(--line);
|
||||
border-radius: 12px; padding: 18px 16px 14px; box-shadow: 0 10px 40px rgba(0,0,0,.4); }
|
||||
.pinmodal h2 { margin: 0 0 6px 0; font: 600 15px inherit; }
|
||||
.pinmodal .pinsub { color: var(--mut); font-size: 12px; margin-bottom: 12px; text-align: center; }
|
||||
.pinmodal .pinactions { display: flex; gap: 6px; justify-content: center; margin-top: 12px; }
|
||||
|
||||
/* General settings cards — pinned at top of Settings tab so cross-cutting
|
||||
security/policy controls stay in one place, ahead of the per-wallet
|
||||
stuff below. */
|
||||
.gsec { margin-bottom: 14px; }
|
||||
.gsec .gtitle { font: 600 13px inherit; color: var(--ink); margin-bottom: 6px; }
|
||||
.gsec .gline { display: flex; align-items: center; justify-content: space-between; gap: 10px;
|
||||
padding: 6px 0; border-top: 1px solid var(--line); }
|
||||
.gsec .gline:first-of-type { border-top: 0; }
|
||||
.gsec .gline .glabel { min-width: 0; font-size: 12.5px; color: var(--ink); }
|
||||
.gsec .gline .ghint { display: block; color: var(--dim); font-size: 11px; margin-top: 2px; }
|
||||
.gsec .gline .gactions { flex: none; display: inline-flex; gap: 6px; }
|
||||
.gsec .gsoon { font-size: 10px; padding: 1px 6px; border-radius: 999px; letter-spacing: .04em;
|
||||
background: rgba(224,179,65,.14); color: #e0b341; text-transform: uppercase; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
|
@ -137,8 +399,9 @@
|
|||
<span id="hNet"></span>
|
||||
</div>
|
||||
<div class="picker-actions">
|
||||
<button type="button" id="hAddWallet" class="chip" title="Add a new wallet">+</button>
|
||||
<span class="caret">▾</span>
|
||||
<button type="button" id="hAdd" class="chip" title="Add a new wallet">+</button>
|
||||
<button type="button" id="hMore" class="chip" title="Import / Connect / About">⋯</button>
|
||||
<button type="button" id="hManage" class="chip" title="Edit this wallet — rename, derivation path, remove">✎</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="drop" hidden></div>
|
||||
|
|
@ -160,6 +423,11 @@
|
|||
<button data-tab="history">History</button>
|
||||
<button data-tab="settings">Settings</button>
|
||||
</nav>
|
||||
<!-- Always-visible wallet list. Sits under the Receive/Send/History/Settings
|
||||
tab bar so the top of the panel stays about the SELECTED wallet, and
|
||||
the list of siblings/switch-targets reads as a secondary layer below.
|
||||
[+ Add] opens the Add-only picker; [⋯ More] opens Import/Connect/Manage. -->
|
||||
<div id="walletStrip" class="wstrip" aria-label="Your wallets"></div>
|
||||
<main>
|
||||
<div id="gate" class="gate" hidden></div>
|
||||
<div id="tabs">
|
||||
|
|
@ -193,7 +461,7 @@
|
|||
<div class="hint" id="sendToHint"></div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="lbl">Amount</div>
|
||||
<div class="lbl">Amount <span id="sendAmtFiat" class="fiat" style="margin-left:8px" hidden></span></div>
|
||||
<div class="amt">
|
||||
<input type="text" id="sendAmt" inputmode="decimal" autocomplete="off" placeholder="0.00">
|
||||
<div class="unit" id="unitPicker"></div>
|
||||
|
|
@ -218,6 +486,104 @@
|
|||
<div id="txlist"></div>
|
||||
</section>
|
||||
<section id="tab-settings" hidden>
|
||||
<!-- Cross-cutting security / policy controls, ahead of anything
|
||||
per-wallet or chain-specific. Present even when the vault is
|
||||
still locked so users can set up a PIN or read the multi-sig
|
||||
roadmap without unlocking first (they only do the ACTUAL PIN
|
||||
enrollment after a fresh unlock, though). -->
|
||||
<div class="card gsec" id="genSecurity" style="margin-bottom:14px">
|
||||
<div class="gtitle">Security</div>
|
||||
<div class="gline">
|
||||
<div class="glabel">
|
||||
Quick-access PIN
|
||||
<span class="ghint" id="pinStatusHint">Off — Aegis asks for the master password every time.</span>
|
||||
</div>
|
||||
<div class="gactions">
|
||||
<button class="btn sm" id="gsPinSet" hidden>Set PIN…</button>
|
||||
<button class="btn sm" id="gsPinChange" hidden>Change…</button>
|
||||
<button class="btn sm danger" id="gsPinRemove" hidden>Remove</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gline" id="gsRequirePinLine" hidden>
|
||||
<div class="glabel">
|
||||
Require PIN for sending
|
||||
<span class="ghint">Prompts for your PIN on every panel-initiated Send. Dapp-driven approvals still use the standard approval overlay.</span>
|
||||
</div>
|
||||
<div class="gactions">
|
||||
<label class="switch"><input type="checkbox" id="gsRequirePin"><span></span></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Session: stay-signed-in + idle auto-lock + manual sign-out.
|
||||
Uses electron.safeStorage under the hood so the master password
|
||||
is only decryptable under this OS user account. When "Lock on
|
||||
Navigator close" is on (the default), Aegis stores nothing on
|
||||
disk and forces a fresh unlock on every launch. -->
|
||||
<div class="card gsec" id="genSession" style="margin-bottom:14px">
|
||||
<div class="gtitle">Session</div>
|
||||
<div class="gline">
|
||||
<div class="glabel">
|
||||
Lock on Navigator close
|
||||
<span class="ghint" id="gsSessionHint">On — Aegis asks for the master password (or PIN) every time Theseus starts. Turn off to stay signed in across restarts via the OS keystore.</span>
|
||||
</div>
|
||||
<div class="gactions">
|
||||
<label class="switch"><input type="checkbox" id="gsLockOnClose" checked><span></span></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gline">
|
||||
<div class="glabel">
|
||||
Auto-lock when idle
|
||||
<span class="ghint">Locks the vault after this long with no panel activity. Applies inside the wallet panel; a locked panel elsewhere in the browser doesn't count as idle.</span>
|
||||
</div>
|
||||
<div class="gactions">
|
||||
<select id="gsIdleMinutes" style="padding:5px 8px;border-radius:6px;background:var(--panel);border:1px solid var(--line);color:var(--ink);font-size:12px">
|
||||
<option value="0">Never</option>
|
||||
<option value="5">5 min</option>
|
||||
<option value="15" selected>15 min</option>
|
||||
<option value="30">30 min</option>
|
||||
<option value="60">1 hour</option>
|
||||
<option value="180">3 hours</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gline">
|
||||
<div class="glabel">
|
||||
Sign out now
|
||||
<span class="ghint">Locks the vault immediately. Any dapp connections stay paired, but Aegis will refuse to sign until you unlock again.</span>
|
||||
</div>
|
||||
<div class="gactions">
|
||||
<button class="btn sm danger" id="gsSignOut">Sign out</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card gsec" id="genMasterPw" style="margin-bottom:14px">
|
||||
<div class="gtitle">Master password</div>
|
||||
<div class="gline">
|
||||
<div class="glabel">
|
||||
Change master password
|
||||
<span class="ghint">Opens Theseus <span class="mono">Settings › Passwords</span>. The vault re-encrypts under the new password; every derived wallet keeps working.</span>
|
||||
</div>
|
||||
<div class="gactions">
|
||||
<button class="btn sm" id="gsOpenPasswords">Open Passwords…</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card gsec" id="genMultiSig" style="margin-bottom:14px">
|
||||
<div class="gtitle">Second-device approval <span class="gsoon">soon</span></div>
|
||||
<div class="gline">
|
||||
<div class="glabel">
|
||||
Co-sign sends from a second device
|
||||
<span class="ghint">Aegis will pair with Ariadne on your phone so large transfers need a nod from both places before they broadcast. Not shipped yet.</span>
|
||||
</div>
|
||||
<div class="gactions">
|
||||
<button class="btn sm" disabled title="Not shipped yet">Set up…</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" id="walletManage" style="margin-bottom:14px">
|
||||
<div class="lbl">This wallet</div>
|
||||
<div class="field" style="margin-top:6px;margin-bottom:8px">
|
||||
|
|
@ -237,8 +603,15 @@
|
|||
<div class="hint">Changing this switches to a different set of addresses under the same wallet seed.</div>
|
||||
</div>
|
||||
<div class="field" id="bchServersRow">
|
||||
<div class="lbl">Electrum servers (shared across BCH mainnet wallets, one per line)</div>
|
||||
<textarea id="setServers" spellcheck="false"></textarea>
|
||||
<div class="lbl">Electrum servers <span class="hint" style="font-weight:normal">(BCH mainnet — shared across BCH wallets)</span></div>
|
||||
<div id="setServersList" class="serverlist"></div>
|
||||
<details style="margin-top:6px">
|
||||
<summary style="cursor:pointer;color:var(--dim);font-size:11.5px">Add a custom server…</summary>
|
||||
<div style="display:flex;gap:6px;margin-top:6px">
|
||||
<input type="text" id="setServersCustom" spellcheck="false" placeholder="wss://host:port" style="flex:1">
|
||||
<button class="btn sm" id="addCustomServer" type="button">Add</button>
|
||||
</div>
|
||||
</details>
|
||||
<div class="hint" id="serverHint"></div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
|
|
@ -255,18 +628,29 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-top:16px">
|
||||
<div class="card" style="margin-top:16px" id="wcCard">
|
||||
<div class="lbl">WizardConnect</div>
|
||||
<div class="hint" style="margin-bottom:8px">
|
||||
Pair this BCH wallet with a dapp that speaks the WizardConnect protocol
|
||||
(Cauldron, Moria, or any site built on the SDK). Paste the <span class="mono">wiz://</span>
|
||||
URI the dapp shows in its Connect dialog. Aegis signs every transaction only after you approve it.
|
||||
</div>
|
||||
<div class="field" style="margin-top:8px">
|
||||
<input type="text" id="wcUri" spellcheck="false" placeholder="wiz://?p=…&s=…">
|
||||
<!-- Warning that appears when the selected wallet is imported —
|
||||
WC signing needs the derive-based signer path, which imported
|
||||
wallets don't have yet. Shown in place of the input so users
|
||||
don't try to paste a URI and hit "wallet not ready". -->
|
||||
<div class="msg err" id="wcImportedNotice" hidden style="margin-bottom:8px">
|
||||
WizardConnect isn't available for imported wallets yet — pairing
|
||||
uses Aegis' vault-derived signer path, and imported wallets are
|
||||
read-only for now. Coming in a later release.
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn primary" id="wcConnectBtn">Connect</button>
|
||||
<div id="wcInputs">
|
||||
<div class="field" style="margin-top:8px">
|
||||
<input type="text" id="wcUri" spellcheck="false" placeholder="wiz://?p=…&s=…">
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn primary" id="wcConnectBtn">Connect</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="msg" id="wcMsg" hidden></div>
|
||||
<div class="kv" id="wcSites" style="margin-top:8px"></div>
|
||||
|
|
@ -390,10 +774,15 @@
|
|||
<div class="card" style="margin-top:16px">
|
||||
<div class="lbl">Fiat prices</div>
|
||||
<div class="hint" style="margin-bottom:10px">
|
||||
Off by default. When enabled, Aegis fetches USD prices for the seven supported coins from
|
||||
<span class="mono">api.coingecko.com</span> every 5 minutes while the panel is open.
|
||||
One HTTP request per interval, no keys and no address data — but CoinGecko can see your IP,
|
||||
which is a signal that a wallet is open on this machine.
|
||||
Off by default. When enabled, Aegis polls the chosen oracle for USD prices
|
||||
on each supported coin. No keys and no address data are ever sent — but the
|
||||
oracle sees your IP + a User-Agent every poll, which is a signal that a
|
||||
wallet is open on this machine.
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="lbl">Oracle</div>
|
||||
<select id="pricesSource" style="width:100%;padding:7px 9px;border-radius:7px;background:var(--panel);border:1px solid var(--line);color:var(--ink);font-size:13px"></select>
|
||||
<div class="hint" id="pricesSourceHint"></div>
|
||||
</div>
|
||||
<div class="actions" style="align-items:center;gap:12px">
|
||||
<label class="switch">
|
||||
|
|
@ -414,6 +803,40 @@
|
|||
</section>
|
||||
</div>
|
||||
</main>
|
||||
<!-- Full-panel lock screen. Absolutely positioned so it takes over the
|
||||
entire content area (everything above the aegis footer) whenever the
|
||||
vault is locked or awaiting first-time setup — the header picker, nav
|
||||
tabs, and wallet strip stay in the DOM but sit visually under it. The
|
||||
Settings tab still hides itself; nothing here interacts with it. -->
|
||||
<div id="lockScreen" hidden>
|
||||
<div class="aegisMark" aria-hidden="true">
|
||||
<svg viewBox="0 0 32 32" fill="none">
|
||||
<polygon points="16,2 28,9 28,23 16,30 4,23 4,9" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/>
|
||||
<circle cx="16" cy="16" r="4.5" fill="none" stroke="currentColor" stroke-width="1.4"/>
|
||||
<circle cx="16" cy="16" r="1.6" fill="currentColor"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h1 id="lockTitle">Unlock Aegis</h1>
|
||||
<div class="subhint" id="lockSub">Aegis derives its keys from your Theseus vault. There's nothing separate to unlock — the vault is your wallet.</div>
|
||||
<div id="lockBody"></div>
|
||||
</div>
|
||||
<footer class="brandfoot" id="brandFoot">
|
||||
<a href="#" id="brandLink" class="brandlink" title="Open aegis.x">
|
||||
<svg viewBox="0 0 32 32" width="14" height="14" aria-hidden="true"><polygon points="16,2 28,9 28,23 16,30 4,23 4,9" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/><circle cx="16" cy="16" r="4.3" fill="none" stroke="currentColor" stroke-width="1.4"/><circle cx="16" cy="16" r="1.3" fill="currentColor"/></svg>
|
||||
<span>aegis.x</span>
|
||||
</a>
|
||||
<!-- Update controls: a subtle "check" link that polls the OTA manifest,
|
||||
then swaps to an "Update to vX.Y.Z" chip when a newer version is
|
||||
ready. Panel-only view since the addon can't promote itself; the
|
||||
chip links to Settings > Extensions > Aegis where the Apply flow
|
||||
lives. Version marker on the far right doubles as the up-to-date
|
||||
affordance so a happy panel says nothing extra. -->
|
||||
<span id="brandUpdate" class="brandupd" hidden></span>
|
||||
<span class="brandfoot-right">
|
||||
<button id="brandCheck" class="brandcheck" type="button" title="Check for updates">↻</button>
|
||||
<span class="brandver" id="brandVer"></span>
|
||||
</span>
|
||||
</footer>
|
||||
<script src="qr.js"></script>
|
||||
<script src="panel.js"></script>
|
||||
</body>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
24
main.js
24
main.js
|
|
@ -1833,6 +1833,30 @@ function initAddons() {
|
|||
}
|
||||
createTab(null, { settings: true, settingsSection: slug });
|
||||
},
|
||||
// Panel-driven update flow. Runs the same signed-payload verify + stage
|
||||
// path used by Settings › Extensions › Check-for-updates and the boot
|
||||
// timer, but on demand from an add-on's own UI so a plug-in card can
|
||||
// offer "Update now" in one click. checkAndStageUpdates itself iterates
|
||||
// every installed add-on; the API wrapper filters the report down to
|
||||
// the caller. restartApp mirrors the "app-restart" IPC so the plug-in
|
||||
// can apply a freshly-staged build without asking the user to hunt
|
||||
// for the OS menu.
|
||||
checkAndStageUpdates: async () => {
|
||||
const stagedDir = addonsStagedDir();
|
||||
try {
|
||||
const result = await addonUpdater.checkAndStageUpdates({
|
||||
addonsDir: addonsUserDir(),
|
||||
stagedDir,
|
||||
pubkeysHex: ADDON_UPDATE_PUBKEYS,
|
||||
logger: (...a) => console.log("[addons]", ...a),
|
||||
});
|
||||
return { report: result?.report || [], skipped: result?.skipped || null, staged: listStagedAddons(stagedDir) };
|
||||
} catch (e) {
|
||||
console.warn("[addons] panel-driven check-updates failed:", e?.message || e);
|
||||
return { report: [], skipped: "unexpected-error", staged: [] };
|
||||
}
|
||||
},
|
||||
restartApp: () => { try { app.relaunch(); } catch {} app.quit(); },
|
||||
// open-tab (addon-file variant): open one of the add-on's OWN files in a
|
||||
// full tab. The path is joined against the resolved add-on folder and
|
||||
// rejected if the result escapes it — belt-and-braces with the sanity
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "theseus-navigator",
|
||||
"version": "0.3.46",
|
||||
"version": "0.3.47",
|
||||
"description": "Theseus Navigator — a browser that follows the thread. By Silent Mode, a Deviant project.",
|
||||
"author": "Silent Mode",
|
||||
"main": "main.js",
|
||||
|
|
|
|||
|
|
@ -1272,7 +1272,13 @@
|
|||
return '<div class="d" style="' + cls + ';margin-top:4px">' + msg + '</div>';
|
||||
}
|
||||
function renderAddons(snap) {
|
||||
const items = (snap && snap.installed) || [];
|
||||
// Plug-in category add-ons (Aegis and future first-class Silent Mode
|
||||
// components) live in Settings › Plug-ins with their own product-facing
|
||||
// copy — showing them again here as raw extension rows was confusing
|
||||
// ("why can I toggle my wallet off from two places?"). Filter them
|
||||
// out of the Extensions listing entirely; the Plug-ins tab is the
|
||||
// single source of truth for those.
|
||||
const items = ((snap && snap.installed) || []).filter((a) => a.category !== "plugin");
|
||||
if (!items.length) {
|
||||
addonsList.innerHTML = '<div class="d" style="color:var(--dim)">No extensions installed. Drop a folder into the extensions directory to install one.</div>';
|
||||
return;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue