chore(aegis): 0.8.8 — coin drilldown, per-address assets, WizardConnect from the page
Wallet strip: - Clicking a coin opens that coin's page (addresses, price, totals, back and close) instead of only flipping the selection and leaving the list sitting there. The page already existed but was reachable only via the small count chip. - The per-coin second action was a gear that selected the wallet and opened the global Settings tab — the same destination for every coin, so it read as a per-coin control that wasn't one. It is now Remove, behind a confirm, with the default/legacy wallet showing a lock instead since it gates legacy funds. - Each address in the drilldown can expand to show what THAT address holds: TRC20/SPL via the adapter's tokens, BCH CashTokens via tokenBalances. walletSummary now carries both per wallet, so the view no longer has to borrow the selected wallet's assets. WizardConnect — the Connect pane was effectively unusable: - The locked-vault branch told the user to unlock and gave them nothing to click. It is reachable without the lock screen ever appearing, because a mounted imported wallet makes overallPhase read "ready". It now carries the same unlock form the lock screen uses. - Imported BCH wallets were never registered with the WC manager — startForWallet ran only in the vault-derived mount branch. They mount as ready, so they appeared in the "Sign with" picker and then failed on pair. They now register from their stored seed. WC derives a child key tree, so single-key (WIF) imports genuinely cannot pair; those are disabled in the picker with the reason, rather than failing on click. - Adds window.wizardconnect so a dapp can hand over the wiz:// URI it already generated instead of making the user copy it between tabs. The protocol is Nostr-relay pairing designed for phone-scans-QR, and the SDK has no in-page discovery at all, so this is our own surface: connect() + isReady(), plus a wizardconnect:announceProvider event shaped like EIP-6963 so several WC wallets can coexist. Pairing always goes through the approval modal; the URI is validated before any UI shows, and the wallet never reads the page to find one.
This commit is contained in:
parent
8785ecc7cf
commit
1b74b10fc5
5 changed files with 363 additions and 34 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"id": "aegis",
|
"id": "aegis",
|
||||||
"name": "Aegis Wallet",
|
"name": "Aegis Wallet",
|
||||||
"version": "0.8.7",
|
"version": "0.8.8",
|
||||||
"category": "plugin",
|
"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.",
|
"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",
|
"author": "Silent Mode",
|
||||||
|
|
|
||||||
|
|
@ -509,6 +509,40 @@ async function mountWallet(entry) {
|
||||||
importId: entry.importId,
|
importId: entry.importId,
|
||||||
});
|
});
|
||||||
adapter.schedulePoll(20_000);
|
adapter.schedulePoll(20_000);
|
||||||
|
// Imported BCH wallets were never registered with WizardConnect —
|
||||||
|
// startForWallet only ran in the vault-derived branch. They showed
|
||||||
|
// up in the "Sign with" picker (they mount as ready) and then failed
|
||||||
|
// on pair with "no manager". Register them here too.
|
||||||
|
//
|
||||||
|
// WC derives a child-key tree, so this needs a seed: mnemonic/seed
|
||||||
|
// imports qualify, WIF single-key imports never can. registerWcEligible
|
||||||
|
// records which is which so the panel can say so up front instead of
|
||||||
|
// offering a pairing that cannot work.
|
||||||
|
if (c.wc) {
|
||||||
|
c.api.vault.imports.signer(entry.importId).then((blob) => {
|
||||||
|
if (ctx !== c) return;
|
||||||
|
if (!blob || blob.kind !== "seed" || !blob.seed) {
|
||||||
|
wcIneligible.set(entry.id, "This wallet was imported from a single private key. WizardConnect needs a seed phrase to derive the per-dapp keys it signs with.");
|
||||||
|
emitStateForWallet(entry.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const seedHex = String(blob.seed).trim();
|
||||||
|
if (!/^[0-9a-f]+$/i.test(seedHex) || seedHex.length < 32) {
|
||||||
|
wcIneligible.set(entry.id, "Imported seed material is not in a form WizardConnect can derive from.");
|
||||||
|
emitStateForWallet(entry.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const root = new Uint8Array(seedHex.match(/../g).map((x) => parseInt(x, 16)));
|
||||||
|
return c.wc.startForWallet({
|
||||||
|
walletId: entry.id, label: entry.label,
|
||||||
|
root32: root, accountPath: entry.accountPath || "m/44'/145'/0'",
|
||||||
|
}).finally(() => { try { root.fill(0); } catch {} });
|
||||||
|
}).catch((e) => {
|
||||||
|
c.api.log(`[${entry.id}] wc start (imported):`, e?.message || e);
|
||||||
|
wcIneligible.set(entry.id, cleanWcErr(e));
|
||||||
|
emitStateForWallet(entry.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
} else if (entry.chain === "btc" || entry.chain === "dgb") {
|
} else if (entry.chain === "btc" || entry.chain === "dgb") {
|
||||||
adapter = new c.d.utxoImportedAdapter.UtxoImportedWallet({
|
adapter = new c.d.utxoImportedAdapter.UtxoImportedWallet({
|
||||||
...commonOpts, chain: entry.chain, address: entry.importedAddress,
|
...commonOpts, chain: entry.chain, address: entry.importedAddress,
|
||||||
|
|
@ -675,6 +709,7 @@ function unmountWallet(walletId) {
|
||||||
const rt = ctx.runtimes.get(walletId);
|
const rt = ctx.runtimes.get(walletId);
|
||||||
if (rt && rt.adapter) { try { rt.adapter.dispose(); } catch {} }
|
if (rt && rt.adapter) { try { rt.adapter.dispose(); } catch {} }
|
||||||
if (ctx.wc) { try { ctx.wc.stopForWallet(walletId); } catch {} }
|
if (ctx.wc) { try { ctx.wc.stopForWallet(walletId); } catch {} }
|
||||||
|
wcIneligible.delete(walletId);
|
||||||
ctx.runtimes.delete(walletId);
|
ctx.runtimes.delete(walletId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -749,6 +784,18 @@ function overallPhase() {
|
||||||
return "ready";
|
return "ready";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Per-wallet reason a BCH wallet can't do WizardConnect even though it's
|
||||||
|
// mounted and ready — today that's single-key (WIF) imports, which have no
|
||||||
|
// seed to derive a per-dapp key tree from. Surfaced in walletSummary so the
|
||||||
|
// picker can grey them out instead of offering a pairing that must fail.
|
||||||
|
const wcIneligible = new Map();
|
||||||
|
function cleanWcErr(e) {
|
||||||
|
const m = e?.message || String(e);
|
||||||
|
return /locked|vault/i.test(m)
|
||||||
|
? "Unlock the password vault to use WizardConnect with this wallet."
|
||||||
|
: m;
|
||||||
|
}
|
||||||
|
|
||||||
function walletSummary(w) {
|
function walletSummary(w) {
|
||||||
const meta = chainMeta(w.chain, w.network);
|
const meta = chainMeta(w.chain, w.network);
|
||||||
const rt = ctx.runtimes.get(w.id);
|
const rt = ctx.runtimes.get(w.id);
|
||||||
|
|
@ -765,8 +812,15 @@ function walletSummary(w) {
|
||||||
// stay null so the picker knows whether to render the mono path line.
|
// stay null so the picker knows whether to render the mono path line.
|
||||||
accountPath: w.accountPath || snap?.accountPath || null,
|
accountPath: w.accountPath || snap?.accountPath || null,
|
||||||
balance: snap?.balance || { confirmed: 0, unconfirmed: 0 },
|
balance: snap?.balance || { confirmed: 0, unconfirmed: 0 },
|
||||||
|
// Per-wallet assets, so the coin drilldown can show what each ADDRESS
|
||||||
|
// holds instead of only the selected wallet's. `tokens` is the account-
|
||||||
|
// model shape (SPL / TRC20); `tokenBalances` is BCH CashTokens, keyed
|
||||||
|
// by category. Both stay null/empty for chains that have neither.
|
||||||
|
tokens: Array.isArray(snap?.tokens) ? snap.tokens : [],
|
||||||
|
tokenBalances: snap?.tokenBalances || null,
|
||||||
phase: rt?.phase || "locked",
|
phase: rt?.phase || "locked",
|
||||||
error: rt?.error || null,
|
error: rt?.error || null,
|
||||||
|
wcBlocked: w.chain === "bch" ? (wcIneligible.get(w.id) || null) : null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1531,6 +1585,66 @@ function registerPanelMessages(api) {
|
||||||
return fullState();
|
return fullState();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---- WizardConnect from the page (0.8.8) --------------------------------
|
||||||
|
//
|
||||||
|
// WC was built for cross-device pairing: the dapp renders a QR, a phone
|
||||||
|
// scans it. Same-device that means copying a wiz:// string out of one
|
||||||
|
// tab and into the wallet by hand. These two handlers back the
|
||||||
|
// window.wizardconnect bridge so a dapp can hand Aegis the URI it has
|
||||||
|
// already generated, and the user just approves.
|
||||||
|
|
||||||
|
// Which BCH wallets can actually pair right now. Used by the page bridge
|
||||||
|
// AND by isReady() so a dapp can decide between "hand it to Aegis" and
|
||||||
|
// "render the QR" before it commits to either.
|
||||||
|
function wcPairableWallets() {
|
||||||
|
if (!ctx.wc) return [];
|
||||||
|
return walletEntries()
|
||||||
|
.filter((w) => w.chain === "bch")
|
||||||
|
.map((w) => ({ entry: w, rt: ctx.runtimes.get(w.id) }))
|
||||||
|
.filter(({ entry, rt }) => rt && rt.phase === "ready" && !wcIneligible.has(entry.id))
|
||||||
|
.map(({ entry }) => entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
api.onMessage("wcPageReady", async (_p, m) => {
|
||||||
|
fromPage(m);
|
||||||
|
// Deliberately coarse: a page learns only whether pairing is possible,
|
||||||
|
// never how many wallets exist or what they are.
|
||||||
|
return { available: !!ctx.wc, pairable: wcPairableWallets().length > 0 };
|
||||||
|
});
|
||||||
|
|
||||||
|
api.onMessage("wcConnectFromPage", async (p, m) => {
|
||||||
|
const origin = fromPage(m);
|
||||||
|
if (!ctx.wc) throw new Error("WizardConnect is still starting up — try again in a moment");
|
||||||
|
const uri = String(p && p.uri || "").trim();
|
||||||
|
// Validate before showing any UI so a malformed or hostile value can't
|
||||||
|
// put a confusing approval in front of the user.
|
||||||
|
if (!/^wiz:\/\//i.test(uri)) throw new Error("not a WizardConnect URI");
|
||||||
|
if (uri.length > 4096) throw new Error("WizardConnect URI is implausibly long");
|
||||||
|
const candidates = wcPairableWallets();
|
||||||
|
if (!candidates.length) {
|
||||||
|
throw new Error("No Bitcoin Cash wallet is ready to pair. Unlock the Aegis vault (or add a BCH wallet) and try again.");
|
||||||
|
}
|
||||||
|
// Prefer the selected wallet when it qualifies, so the approval matches
|
||||||
|
// whatever the user currently sees in the panel.
|
||||||
|
const selId = selectedWalletId();
|
||||||
|
const chosen = candidates.find((w) => w.id === selId) || candidates[0];
|
||||||
|
return withOriginLock(origin, async () => {
|
||||||
|
const pick = await api.approvalModal({
|
||||||
|
title: "Pair this site with your wallet?",
|
||||||
|
origin,
|
||||||
|
body: "The site will be able to ask Aegis to sign Bitcoin Cash transactions over WizardConnect. Every signature still needs your approval — pairing on its own moves no funds.",
|
||||||
|
rows: [
|
||||||
|
{ label: "Wallet", value: `${chosen.label}` },
|
||||||
|
{ label: "Pairing code", value: uri.slice(0, 48) + (uri.length > 48 ? "…" : ""), mono: true },
|
||||||
|
],
|
||||||
|
actions: [{ id: "allow", label: "Pair", primary: true }],
|
||||||
|
});
|
||||||
|
if (!String(pick || "").startsWith("allow")) throw new Error("pairing declined");
|
||||||
|
await ctx.wc.connectUri(chosen.id, uri);
|
||||||
|
return { paired: true, wallet: chosen.label };
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Reorder wallets by an explicit ID list. Silently drops IDs that are
|
// Reorder wallets by an explicit ID list. Silently drops IDs that are
|
||||||
// not in the current wallet set (removed since the panel last read);
|
// 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
|
// appends any wallets missing from `order` to the end of the list so a
|
||||||
|
|
|
||||||
|
|
@ -432,6 +432,21 @@
|
||||||
.wstrip .warow .wact { background: transparent; border: 0; color: var(--dim); cursor: pointer;
|
.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; }
|
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); }
|
.wstrip .warow .wact:hover { color: var(--acid, #d6ff3d); background: rgba(255,255,255,.06); }
|
||||||
|
/* Destructive strip action (remove wallet). Stays quiet until hovered so
|
||||||
|
a row of icons doesn't read as a row of warnings. */
|
||||||
|
.wstrip .wact.wactdel:hover { color: var(--danger, #f6768a); background: rgb(from var(--danger, #f6768a) r g b / .12); }
|
||||||
|
/* Per-address asset list (0.8.8). The count chip expands the row into a
|
||||||
|
nested list of what that ONE address holds beyond the native coin. */
|
||||||
|
.wstrip .waassets { background: rgba(255,255,255,.06); border: 0; color: var(--dim); cursor: pointer;
|
||||||
|
font: inherit; font-size: 10px; padding: 1px 6px; border-radius: 999px; line-height: 1.5; }
|
||||||
|
.wstrip .waassets:hover, .wstrip .waassets.on { color: var(--acid, #d6ff3d); background: rgb(from var(--acid, #d6ff3d) r g b / .14); }
|
||||||
|
.wstrip .waassetlist { padding: 2px 6px 6px 26px; display: flex; flex-direction: column; gap: 2px; }
|
||||||
|
.wstrip .waasset { display: grid; grid-template-columns: minmax(0,1fr) auto auto; gap: 8px; align-items: baseline;
|
||||||
|
font-size: 11px; color: var(--mut); padding: 2px 4px; border-radius: 4px; }
|
||||||
|
.wstrip .waasset:hover { background: rgba(255,255,255,.04); }
|
||||||
|
.wstrip .waasset .waaname { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.wstrip .waasset .waaid { color: var(--dim); font-size: 10px; }
|
||||||
|
.wstrip .waasset .waaamt { font-variant-numeric: tabular-nums; color: var(--ink); }
|
||||||
/* Coin drilldown header: shows the coin's per-unit price + running
|
/* Coin drilldown header: shows the coin's per-unit price + running
|
||||||
total of the addresses below, so the aggregate context isn't lost
|
total of the addresses below, so the aggregate context isn't lost
|
||||||
when the user is deep in the per-address view. */
|
when the user is deep in the per-address view. */
|
||||||
|
|
|
||||||
|
|
@ -893,6 +893,29 @@ function aegisAlert(message, opts) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Shared remove-wallet flow, used by the strip's 🗑 buttons and the coin
|
||||||
|
// drilldown. Confirms first, then unlinks. On-chain funds are untouched —
|
||||||
|
// this only drops Aegis's record of the wallet.
|
||||||
|
async function removeWalletWithConfirm(id) {
|
||||||
|
const w = (state?.wallets || []).find((x) => x.id === id);
|
||||||
|
if (!w) return false;
|
||||||
|
if (w.isDefault || w.isLegacy) {
|
||||||
|
await aegisAlert("This is the default wallet — it holds legacy funds and can't be removed.", { title: "Can't remove", icon: "🔒" });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const ok = await aegisConfirm({
|
||||||
|
title: `Remove "${w.label}"?`,
|
||||||
|
danger: true,
|
||||||
|
confirmLabel: "Remove wallet",
|
||||||
|
body: `On-chain funds stay exactly where they are — this only unlinks the wallet from Aegis.<br><br>You can add it back later on the same coin + network to derive the same addresses ${w.kind === "imported" ? "(or re-import it, since this one was imported)" : "from your vault seed"}.`,
|
||||||
|
});
|
||||||
|
if (!ok) return false;
|
||||||
|
state = await S.invoke("removeWallet", { id });
|
||||||
|
settingsFilled = false;
|
||||||
|
render();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Import modal — M.1 UX. Paste mnemonic + path OR WIF, choose network + label
|
// Import modal — M.1 UX. Paste mnemonic + path OR WIF, choose network + label
|
||||||
// + category. Backend derives cashaddr and stores signer material in
|
// + category. Backend derives cashaddr and stores signer material in
|
||||||
// wallet-imports.enc (design §3.2). Modal is a plain overlay div injected
|
// wallet-imports.enc (design §3.2). Modal is a plain overlay div injected
|
||||||
|
|
@ -1829,12 +1852,31 @@ function paintRcvMode() {
|
||||||
function renderConnectPane(bchWallets) {
|
function renderConnectPane(bchWallets) {
|
||||||
const readyBch = bchWallets.filter((w) => w.phase === "ready");
|
const readyBch = bchWallets.filter((w) => w.phase === "ready");
|
||||||
if (!readyBch.length) {
|
if (!readyBch.length) {
|
||||||
|
// 0.8.8: the locked branch used to be a dead end — it told the user to
|
||||||
|
// unlock the vault but gave them nothing to click, and the panel chrome
|
||||||
|
// stays visible whenever an imported wallet is mounted (those skip the
|
||||||
|
// vault), so this is reachable without the lock screen ever showing.
|
||||||
|
// Inline the same unlock form the lock screen uses.
|
||||||
|
const locked = bchWallets.length > 0;
|
||||||
return `<div class="hint" style="padding:14px">
|
return `<div class="hint" style="padding:14px">
|
||||||
<div style="margin-bottom:6px"><b>WizardConnect</b> pairs Aegis with a BCH dapp (Cauldron, Moria, or any site built on the SDK).</div>
|
<div style="margin-bottom:6px"><b>WizardConnect</b> pairs Aegis with a BCH dapp (Cauldron, Moria, or any site built on the SDK).</div>
|
||||||
<div>${bchWallets.length ? "Unlock your password vault first — WizardConnect uses your BCH keys to sign." : "Add a BCH wallet first via the Add tab, then come back."}</div>
|
${locked ? `
|
||||||
|
<div style="margin-bottom:10px">WizardConnect signs with your BCH keys, so the password vault has to be unlocked first.</div>
|
||||||
|
<div class="field">
|
||||||
|
<input type="password" id="pkConnectUnlockPw" placeholder="Master password" autocomplete="current-password">
|
||||||
|
</div>
|
||||||
|
<div class="actions"><button class="btn primary" id="pkConnectUnlockBtn">Unlock vault</button></div>
|
||||||
|
<div class="msg err" id="pkConnectUnlockMsg" hidden></div>
|
||||||
|
` : `<div>Add a BCH wallet first via the Add tab, then come back.</div>`}
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
const options = readyBch.map((w) => `<option value="${esc(w.id)}" ${w.id === state.selectedWalletId ? "selected" : ""}>${esc(w.label)} · ${esc(w.networkLabel)}</option>`).join("");
|
// Wallets with no derivable seed (WIF single-key imports) can't pair at
|
||||||
|
// all, so they're disabled rather than silently failing on Connect.
|
||||||
|
const pairable = readyBch.filter((w) => !w.wcBlocked);
|
||||||
|
const options = readyBch.map((w) => `<option value="${esc(w.id)}" ${!w.wcBlocked && w.id === state.selectedWalletId ? "selected" : ""} ${w.wcBlocked ? "disabled" : ""}>${esc(w.label)} · ${esc(w.networkLabel)}${w.wcBlocked ? " — can't pair" : ""}</option>`).join("");
|
||||||
|
const blockedNote = (!pairable.length && readyBch.length)
|
||||||
|
? `<div class="msg err" style="margin-bottom:8px">${esc(readyBch[0].wcBlocked)}</div>`
|
||||||
|
: "";
|
||||||
// Flatten all connected dapps (across BCH wallets) into one list — the
|
// Flatten all connected dapps (across BCH wallets) into one list — the
|
||||||
// user thinks "my dapps", not "dapps per wallet".
|
// user thinks "my dapps", not "dapps per wallet".
|
||||||
const rows = [];
|
const rows = [];
|
||||||
|
|
@ -1850,7 +1892,8 @@ function renderConnectPane(bchWallets) {
|
||||||
</div>`).join("")
|
</div>`).join("")
|
||||||
: `<div class="hint" style="padding:6px 8px">No dapps paired yet.</div>`;
|
: `<div class="hint" style="padding:6px 8px">No dapps paired yet.</div>`;
|
||||||
return `<div style="padding:6px">
|
return `<div style="padding:6px">
|
||||||
<div class="hint" style="margin-bottom:8px">Paste a <span class="mono">wiz://</span> URI from a BCH dapp's Connect dialog. Aegis will sign every request after your approval.</div>
|
<div class="hint" style="margin-bottom:8px">Dapps that support Aegis directly can hand the pairing over with one click — no copying. Otherwise paste a <span class="mono">wiz://</span> URI from the dapp's Connect dialog. Aegis signs every request after your approval.</div>
|
||||||
|
${blockedNote}
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<div class="lbl">Sign with</div>
|
<div class="lbl">Sign with</div>
|
||||||
<select id="pkConnectWallet" style="width:100%;padding:7px 9px;border-radius:7px;background:var(--panel);border:1px solid var(--line);color:var(--ink);font-size:13px">${options}</select>
|
<select id="pkConnectWallet" style="width:100%;padding:7px 9px;border-radius:7px;background:var(--panel);border:1px solid var(--line);color:var(--ink);font-size:13px">${options}</select>
|
||||||
|
|
@ -1868,6 +1911,31 @@ function renderConnectPane(bchWallets) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function wireConnectPane() {
|
function wireConnectPane() {
|
||||||
|
// Locked-vault branch: unlock in place, then re-render the pane so the
|
||||||
|
// pairing form replaces the gate without the user reopening the picker.
|
||||||
|
const unlockBtn = document.getElementById("pkConnectUnlockBtn");
|
||||||
|
if (unlockBtn) {
|
||||||
|
const doUnlock = async () => {
|
||||||
|
const pwEl = document.getElementById("pkConnectUnlockPw");
|
||||||
|
const msg = document.getElementById("pkConnectUnlockMsg");
|
||||||
|
msg.hidden = true;
|
||||||
|
const pw = pwEl.value;
|
||||||
|
if (!pw) return;
|
||||||
|
try {
|
||||||
|
state = await S.invoke("vaultUnlock", { masterPassword: pw });
|
||||||
|
pwEl.value = "";
|
||||||
|
render();
|
||||||
|
fillPicker();
|
||||||
|
} catch (e) { msg.textContent = cleanErr(e); msg.hidden = false; }
|
||||||
|
};
|
||||||
|
unlockBtn.addEventListener("click", (e) => { e.stopPropagation(); doUnlock(); });
|
||||||
|
const pwEl = document.getElementById("pkConnectUnlockPw");
|
||||||
|
if (pwEl) {
|
||||||
|
pwEl.addEventListener("keydown", (e) => { e.stopPropagation(); if (e.key === "Enter") doUnlock(); });
|
||||||
|
try { pwEl.focus(); } catch {}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
const btn = document.getElementById("pkConnectBtn"); if (!btn) return;
|
const btn = document.getElementById("pkConnectBtn"); if (!btn) return;
|
||||||
btn.addEventListener("click", async () => {
|
btn.addEventListener("click", async () => {
|
||||||
const walletId = document.getElementById("pkConnectWallet").value;
|
const walletId = document.getElementById("pkConnectWallet").value;
|
||||||
|
|
@ -2116,13 +2184,22 @@ function renderWalletStrip() {
|
||||||
// still get no chip.
|
// still get no chip.
|
||||||
const walletsChip = single ? "" : `<span class="wgcount wgpick" data-openlist="${esc(subKey)}" title="Choose a different ${esc(meta.ticker)} wallet">${gw.length} <span class="wgchev">▾</span></span>`;
|
const walletsChip = single ? "" : `<span class="wgcount wgpick" data-openlist="${esc(subKey)}" title="Choose a different ${esc(meta.ticker)} wallet">${gw.length} <span class="wgchev">▾</span></span>`;
|
||||||
|
|
||||||
// Actions row: on multi-wallet rows the ✎ / ⚙ target the ACTIVE
|
// Actions row: on multi-wallet rows the ✎ / 🗑 target the ACTIVE
|
||||||
// wallet (not "the group") so the buttons still do something specific
|
// wallet (not "the group") so the buttons still do something specific
|
||||||
// without needing a second click.
|
// without needing a second click.
|
||||||
|
//
|
||||||
|
// 0.8.8: the second button used to be ⚙, which just selected the wallet
|
||||||
|
// and opened the global Settings tab — the same destination for every
|
||||||
|
// coin, so it read as a per-coin control that wasn't one. Removing a
|
||||||
|
// wallet is the action people actually wanted there.
|
||||||
const editAttr = `data-wedit="${esc(walletId)}"`;
|
const editAttr = `data-wedit="${esc(walletId)}"`;
|
||||||
const setAttr = `data-wsettings="${esc(walletId)}"`;
|
const activeW = gw.find((w) => w.id === walletId);
|
||||||
|
const canRemove = !(activeW?.isDefault || activeW?.isLegacy);
|
||||||
|
const removeBtn = canRemove
|
||||||
|
? `<button class="wact wactdel" data-wremove="${esc(walletId)}" title="Remove this ${esc(meta.ticker)} wallet from Aegis">🗑</button>`
|
||||||
|
: `<span class="wact" title="Default wallet — cannot be removed" style="opacity:.35;cursor:not-allowed">🔒</span>`;
|
||||||
|
|
||||||
rows.push(`<div class="wrow ${groupHasSel ? "on" : ""}" ${clickAction} title="${esc(meta.coinName)}" draggable="true" data-chain="${esc(chain)}">
|
rows.push(`<div class="wrow ${groupHasSel ? "on" : ""}" ${clickAction} data-groupkey="${esc(subKey)}" title="${esc(meta.coinName)} — open addresses" draggable="true" data-chain="${esc(chain)}">
|
||||||
<span class="wcell wclogo">${logoSvg(meta.logo, 16)}</span>
|
<span class="wcell wclogo">${logoSvg(meta.logo, 16)}</span>
|
||||||
<span class="wcell ${nameCls}" data-netpicker="${esc(chain)}" title="${esc(nameTitle)}">
|
<span class="wcell ${nameCls}" data-netpicker="${esc(chain)}" title="${esc(nameTitle)}">
|
||||||
<span class="wtline">
|
<span class="wtline">
|
||||||
|
|
@ -2140,7 +2217,7 @@ function renderWalletStrip() {
|
||||||
</span>
|
</span>
|
||||||
<span class="wcell wcact">
|
<span class="wcell wcact">
|
||||||
<button class="wact" ${editAttr} title="${single ? "Rename / derivation path / remove" : "Manage wallets"}">✎</button>
|
<button class="wact" ${editAttr} title="${single ? "Rename / derivation path / remove" : "Manage wallets"}">✎</button>
|
||||||
<button class="wact" ${setAttr} title="${esc(meta.coinName)} settings">⚙</button>
|
${removeBtn}
|
||||||
</span>
|
</span>
|
||||||
</div>`);
|
</div>`);
|
||||||
}
|
}
|
||||||
|
|
@ -2167,14 +2244,24 @@ function renderWalletStrip() {
|
||||||
renderWalletStrip();
|
renderWalletStrip();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (row.dataset.wstripid) {
|
// 0.8.8: clicking a coin opens that coin's page (addresses + assets)
|
||||||
const id = row.dataset.wstripid;
|
// instead of only flipping the selection and leaving the list in place.
|
||||||
if (id === selId) return;
|
// Selecting still happens, so Send/Receive/History follow the coin the
|
||||||
try { state = await S.invoke("selectWallet", { id }); settingsFilled = false; render(); }
|
// user just opened — but the strip now navigates, which is what a row
|
||||||
catch (er) { showErr(cleanErr(er)); }
|
// with a balance and a chevron looks like it should do.
|
||||||
} else if (row.dataset.openlist) {
|
if (row.dataset.openlist) {
|
||||||
stripView = { mode: "addresses", groupKey: row.dataset.openlist };
|
stripView = { mode: "addresses", groupKey: row.dataset.openlist };
|
||||||
renderWalletStrip();
|
renderWalletStrip();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (row.dataset.wstripid) {
|
||||||
|
const id = row.dataset.wstripid;
|
||||||
|
const key = row.dataset.groupkey || null;
|
||||||
|
try {
|
||||||
|
if (id !== selId) { state = await S.invoke("selectWallet", { id }); settingsFilled = false; }
|
||||||
|
if (key) stripView = { mode: "addresses", groupKey: key };
|
||||||
|
render();
|
||||||
|
} catch (er) { showErr(cleanErr(er)); }
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
// Ticker click on a multi-network chain → pop the network dropdown.
|
// Ticker click on a multi-network chain → pop the network dropdown.
|
||||||
|
|
@ -2189,17 +2276,11 @@ function renderWalletStrip() {
|
||||||
const w = (state?.wallets || []).find((x) => x.id === b.dataset.wedit);
|
const w = (state?.wallets || []).find((x) => x.id === b.dataset.wedit);
|
||||||
if (w) openWalletManageModal(w);
|
if (w) openWalletManageModal(w);
|
||||||
}));
|
}));
|
||||||
el.querySelectorAll(".wact[data-wsettings]").forEach((b) => b.addEventListener("click", async (e) => {
|
el.querySelectorAll(".wact[data-wremove]").forEach((b) => b.addEventListener("click", async (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const id = b.dataset.wsettings;
|
const id = b.dataset.wremove;
|
||||||
try {
|
try { await removeWalletWithConfirm(id); }
|
||||||
if (id !== state?.selectedWalletId) {
|
catch (er) { showErr(cleanErr(er)); }
|
||||||
state = await S.invoke("selectWallet", { id });
|
|
||||||
settingsFilled = false;
|
|
||||||
}
|
|
||||||
showTab("settings");
|
|
||||||
render();
|
|
||||||
} catch (er) { showErr(cleanErr(er)); }
|
|
||||||
}));
|
}));
|
||||||
el.querySelectorAll(".wact[data-openlist]").forEach((b) => b.addEventListener("click", (e) => {
|
el.querySelectorAll(".wact[data-openlist]").forEach((b) => b.addEventListener("click", (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
@ -2346,7 +2427,46 @@ function wireStripDragDrop(el, chainGroups) {
|
||||||
// Inline replacement for the modal address list. Rendered directly into
|
// Inline replacement for the modal address list. Rendered directly into
|
||||||
// the wallet strip element when stripView.mode === "addresses". Header
|
// the wallet strip element when stripView.mode === "addresses". Header
|
||||||
// row has a back arrow (returns to the coins summary) and the coin's
|
// row has a back arrow (returns to the coins summary) and the coin's
|
||||||
// name/logo; body rows show one wallet each with balance + inline ✎ / ⚙.
|
// Which address rows have their asset list expanded, in the coin drilldown.
|
||||||
|
// Panel-session only — a drilldown is a transient view, so there's nothing
|
||||||
|
// worth persisting across restarts.
|
||||||
|
const expandedAddrAssets = new Set();
|
||||||
|
|
||||||
|
// Normalise a wallet's assets into one row shape the drilldown can render,
|
||||||
|
// regardless of which chain family it came from:
|
||||||
|
// account-model (TRC20 / SPL) → w.tokens: [{mint, symbol, name, decimals, balance}]
|
||||||
|
// BCH CashTokens → w.tokenBalances: { <category>: {fungible, nfts[]} }
|
||||||
|
// Returns [{ id, symbol, name, amount }] with amount already formatted.
|
||||||
|
function assetsForWallet(w) {
|
||||||
|
const out = [];
|
||||||
|
for (const t of (w.tokens || [])) {
|
||||||
|
const dec = Number(t.decimals) || 0;
|
||||||
|
out.push({
|
||||||
|
id: String(t.mint || ""),
|
||||||
|
symbol: t.symbol || "?",
|
||||||
|
name: t.name || "",
|
||||||
|
// Unknown contracts have no decimals — show the raw integer rather
|
||||||
|
// than a number invented from an assumed scale.
|
||||||
|
amount: t.known === false ? `${t.balance} raw` : fmtTokenAmount(t.balance, dec),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const tb = w.tokenBalances || {};
|
||||||
|
for (const cat of Object.keys(tb)) {
|
||||||
|
const b = tb[cat] || {};
|
||||||
|
const fungible = String(b.fungible || "0");
|
||||||
|
const nftCount = Array.isArray(b.nfts) ? b.nfts.length : 0;
|
||||||
|
const parts = [];
|
||||||
|
// Decimals live in BCMR, which the drilldown doesn't fetch — show the
|
||||||
|
// raw fungible amount and let the Tokens card do the named rendering.
|
||||||
|
if (fungible !== "0") parts.push(fungible);
|
||||||
|
if (nftCount) parts.push(`${nftCount} NFT${nftCount === 1 ? "" : "s"}`);
|
||||||
|
if (!parts.length) continue;
|
||||||
|
out.push({ id: cat, symbol: "CashToken", name: "", amount: parts.join(" · ") });
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// name/logo; body rows show one wallet each with balance + inline ✎ / 🗑.
|
||||||
function renderInlineCoinList(el, groupKey, group) {
|
function renderInlineCoinList(el, groupKey, group) {
|
||||||
const { meta, wallets: gw } = group;
|
const { meta, wallets: gw } = group;
|
||||||
const selId = state?.selectedWalletId;
|
const selId = state?.selectedWalletId;
|
||||||
|
|
@ -2379,17 +2499,39 @@ function renderInlineCoinList(el, groupKey, group) {
|
||||||
const shortName = shortLabel(w.label || "", 8);
|
const shortName = shortLabel(w.label || "", 8);
|
||||||
// Fiat is dropped from the row to keep everything on one line; the
|
// Fiat is dropped from the row to keep everything on one line; the
|
||||||
// aggregate coin fiat still shows in the drilldown header above.
|
// aggregate coin fiat still shows in the drilldown header above.
|
||||||
|
// 0.8.8: per-address assets. Each row can expand to show what THIS
|
||||||
|
// address holds beyond the native coin — TRC20/SPL via `tokens`, BCH
|
||||||
|
// CashTokens via `tokenBalances`. Rows with nothing extra get no
|
||||||
|
// chevron so the list stays quiet for plain wallets.
|
||||||
|
const assets = assetsForWallet(w);
|
||||||
|
const expanded = expandedAddrAssets.has(w.id);
|
||||||
|
const assetChip = assets.length
|
||||||
|
? `<button class="waassets ${expanded ? "on" : ""}" data-lpassets="${esc(w.id)}" title="${assets.length} asset${assets.length === 1 ? "" : "s"} on this address">${assets.length} <span class="wgchev">${expanded ? "▾" : "▸"}</span></button>`
|
||||||
|
: "";
|
||||||
|
const canRemoveRow = !(w.isDefault || w.isLegacy);
|
||||||
|
const rowRemove = canRemoveRow
|
||||||
|
? `<button class="wact wactdel" data-lpremove="${esc(w.id)}" title="Remove this wallet from Aegis">🗑</button>`
|
||||||
|
: `<span class="wact" title="Default wallet — cannot be removed" style="opacity:.35;cursor:not-allowed">🔒</span>`;
|
||||||
|
const assetRows = expanded && assets.length
|
||||||
|
? `<div class="waassetlist">${assets.map((a) => `
|
||||||
|
<div class="waasset">
|
||||||
|
<span class="waaname">${esc(a.symbol)}${a.name ? ` <span class="hint">${esc(a.name)}</span>` : ""}</span>
|
||||||
|
<span class="waaid mono" title="${esc(a.id)}">${esc(a.id.slice(0, 8))}…${esc(a.id.slice(-6))}</span>
|
||||||
|
<span class="waaamt">${esc(a.amount)}</span>
|
||||||
|
</div>`).join("")}</div>`
|
||||||
|
: "";
|
||||||
return `<div class="warow ${on}" data-listpick="${esc(w.id)}" title="${esc(w.label || fullAddr)}">
|
return `<div class="warow ${on}" data-listpick="${esc(w.id)}" title="${esc(w.label || fullAddr)}">
|
||||||
<span class="wcell">${logoSvg(meta.logo, 14)}</span>
|
<span class="wcell">${logoSvg(meta.logo, 14)}</span>
|
||||||
<span class="waaddr" title="${esc(fullAddr)}">${esc(displayAddr || "—")}</span>
|
<span class="waaddr" title="${esc(fullAddr)}">${esc(displayAddr || "—")}</span>
|
||||||
${fullAddr ? `<button class="wacopy" data-lpcopy="${esc(fullAddr)}" title="Copy full address (${esc(fullAddr)})">📋</button>` : `<span></span>`}
|
${fullAddr ? `<button class="wacopy" data-lpcopy="${esc(fullAddr)}" title="Copy full address (${esc(fullAddr)})">📋</button>` : `<span></span>`}
|
||||||
<span class="walabel" title="${esc(w.label || "")}">${esc(shortName)}</span>
|
<span class="walabel" title="${esc(w.label || "")}">${esc(shortName)}</span>
|
||||||
<span class="waamt"><span>${esc(bal)}</span><span class="watkr">${esc(meta.ticker)}</span></span>
|
<span class="waamt"><span>${esc(bal)}</span><span class="watkr">${esc(meta.ticker)}</span></span>
|
||||||
<span style="display:inline-flex;gap:2px;justify-self:end">
|
<span style="display:inline-flex;gap:2px;justify-self:end;align-items:center">
|
||||||
|
${assetChip}
|
||||||
<button class="wact" data-lpedit="${esc(w.id)}" title="Rename / derivation path / remove">✎</button>
|
<button class="wact" data-lpedit="${esc(w.id)}" title="Rename / derivation path / remove">✎</button>
|
||||||
<button class="wact" data-lpset="${esc(w.id)}" title="${esc(meta.coinName)} settings">⚙</button>
|
${rowRemove}
|
||||||
</span>
|
</span>
|
||||||
</div>`;
|
</div>${assetRows}`;
|
||||||
}).join("");
|
}).join("");
|
||||||
// Surface any adapter errors from the wallets in this group. If a fetch
|
// Surface any adapter errors from the wallets in this group. If a fetch
|
||||||
// is failing (RPC unreachable, CORS block, rate limit) the display would
|
// is failing (RPC unreachable, CORS block, rate limit) the display would
|
||||||
|
|
@ -2472,16 +2614,27 @@ function renderInlineCoinList(el, groupKey, group) {
|
||||||
const w = (state?.wallets || []).find((x) => x.id === b.dataset.lpedit);
|
const w = (state?.wallets || []).find((x) => x.id === b.dataset.lpedit);
|
||||||
if (w) openWalletManageModal(w);
|
if (w) openWalletManageModal(w);
|
||||||
}));
|
}));
|
||||||
el.querySelectorAll("[data-lpset]").forEach((b) => b.addEventListener("click", async (e) => {
|
el.querySelectorAll("[data-lpremove]").forEach((b) => b.addEventListener("click", async (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const id = b.dataset.lpset;
|
|
||||||
try {
|
try {
|
||||||
if (id !== state?.selectedWalletId) { state = await S.invoke("selectWallet", { id }); settingsFilled = false; }
|
const gone = await removeWalletWithConfirm(b.dataset.lpremove);
|
||||||
stripView = { mode: "coins", groupKey: null };
|
// Removing the last address under this coin leaves the drilldown
|
||||||
showTab("settings");
|
// pointing at an empty group — fall back to the coin list.
|
||||||
render();
|
if (gone) {
|
||||||
|
const left = (state?.wallets || []).filter((w) => w.id !== b.dataset.lpremove && group.wallets.some((g) => g.id === w.id));
|
||||||
|
if (!left.length) stripView = { mode: "coins", groupKey: null };
|
||||||
|
renderWalletStrip();
|
||||||
|
}
|
||||||
} catch (er) { showErr(cleanErr(er)); }
|
} catch (er) { showErr(cleanErr(er)); }
|
||||||
}));
|
}));
|
||||||
|
// Per-address asset list toggle.
|
||||||
|
el.querySelectorAll("[data-lpassets]").forEach((b) => b.addEventListener("click", (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const id = b.dataset.lpassets;
|
||||||
|
if (expandedAddrAssets.has(id)) expandedAddrAssets.delete(id);
|
||||||
|
else expandedAddrAssets.add(id);
|
||||||
|
renderWalletStrip();
|
||||||
|
}));
|
||||||
el.querySelector("#stripAddMore").addEventListener("click", async () => {
|
el.querySelector("#stripAddMore").addEventListener("click", async () => {
|
||||||
// Add another wallet of the same coin+network directly, without
|
// Add another wallet of the same coin+network directly, without
|
||||||
// opening the picker sheet — the user is already inside this coin's
|
// opening the picker sheet — the user is already inside this coin's
|
||||||
|
|
|
||||||
|
|
@ -141,6 +141,36 @@ const tronLink = {
|
||||||
theseus.contextBridge.exposeInMainWorld("tronWeb", tronWeb);
|
theseus.contextBridge.exposeInMainWorld("tronWeb", tronWeb);
|
||||||
theseus.contextBridge.exposeInMainWorld("tronLink", tronLink);
|
theseus.contextBridge.exposeInMainWorld("tronLink", tronLink);
|
||||||
|
|
||||||
|
// -------- WizardConnect bridge (window.wizardconnect), everywhere -----------
|
||||||
|
//
|
||||||
|
// WizardConnect is a Nostr-relay pairing protocol built for CROSS-device use:
|
||||||
|
// the dapp calls initiateDappRelay(), gets a wiz:// uri, and renders it as a
|
||||||
|
// QR for a phone wallet to scan. On the same device that QR round-trip is
|
||||||
|
// pure friction — the dapp and Aegis are in the same browser.
|
||||||
|
//
|
||||||
|
// The SDK has no in-page wallet discovery, so this is Silent Mode's own
|
||||||
|
// surface. A dapp keeps its existing initiateDappRelay() call and simply
|
||||||
|
// hands us the uri it already generated:
|
||||||
|
//
|
||||||
|
// const { uri } = initiateDappRelay(onStatus);
|
||||||
|
// if (window.wizardconnect) await window.wizardconnect.connect(uri);
|
||||||
|
// else renderQr(uri); // unchanged fallback
|
||||||
|
//
|
||||||
|
// connect() resolves once the user approves the pairing in Aegis and the
|
||||||
|
// key exchange completes, and rejects if they decline. Nothing is paired
|
||||||
|
// without an explicit approval, and we never read the page to find a uri —
|
||||||
|
// the dapp hands it to us.
|
||||||
|
const wizardconnect = {
|
||||||
|
isAegis: true,
|
||||||
|
version: "1.0.0",
|
||||||
|
// Present so a dapp can tell "wallet is installed" from "wallet is
|
||||||
|
// installed but has no BCH wallet ready to pair with" before it decides
|
||||||
|
// whether to fall back to a QR.
|
||||||
|
isReady: () => call("wcPageReady"),
|
||||||
|
connect: (uri) => call("wcConnectFromPage", { uri: String(uri ?? "") }),
|
||||||
|
};
|
||||||
|
theseus.contextBridge.exposeInMainWorld("wizardconnect", wizardconnect);
|
||||||
|
|
||||||
// -------- Main-world bridges (window.ethereum, window.solana) ---------------
|
// -------- Main-world bridges (window.ethereum, window.solana) ---------------
|
||||||
//
|
//
|
||||||
// EIP-1193 (Ethereum) and the Solana wallet-adapter both expect the wallet
|
// EIP-1193 (Ethereum) and the Solana wallet-adapter both expect the wallet
|
||||||
|
|
@ -491,6 +521,23 @@ const mainWorldSource = `(function () {
|
||||||
announce();
|
announce();
|
||||||
window.addEventListener("eip6963:requestProvider", announce);
|
window.addEventListener("eip6963:requestProvider", announce);
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
|
// Same announce/request handshake for WizardConnect. The WC SDK defines
|
||||||
|
// no discovery mechanism at all, so we borrow EIP-6963's shape: a dapp
|
||||||
|
// that wants to support several WC wallets dispatches
|
||||||
|
// "wizardconnect:requestProvider" and collects the announcements instead
|
||||||
|
// of reaching for window.wizardconnect and finding whoever won the race.
|
||||||
|
// window.wizardconnect stays for the simple single-wallet case.
|
||||||
|
try {
|
||||||
|
const wcInfo = { uuid: crypto.randomUUID(), name: "Aegis", icon: "data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Cpolygon points='16,2 29,9 29,23 16,30 3,23 3,9' fill='none' stroke='%23d6ff3d' stroke-width='2.5'/%3E%3Ccircle cx='16' cy='16' r='4.3' fill='none' stroke='%23d6ff3d' stroke-width='1.6'/%3E%3Ccircle cx='16' cy='16' r='1.6' fill='%23d6ff3d'/%3E%3C/svg%3E", rdns: "st.silentmode.aegis" };
|
||||||
|
const announceWc = () => {
|
||||||
|
const provider = window.wizardconnect;
|
||||||
|
if (!provider) return;
|
||||||
|
window.dispatchEvent(new CustomEvent("wizardconnect:announceProvider", { detail: Object.freeze({ info: wcInfo, provider }) }));
|
||||||
|
};
|
||||||
|
announceWc();
|
||||||
|
window.addEventListener("wizardconnect:requestProvider", announceWc);
|
||||||
|
} catch {}
|
||||||
})();`;
|
})();`;
|
||||||
|
|
||||||
// Actually push the script into the main world. Doing this at
|
// Actually push the script into the main world. Doing this at
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue