feat(rwallet): show EncryptID identity card with both CRDT and EVM wallets

Every EncryptID identity has two wallets: a CRDT wallet (DID-based for
local tokens) and an EVM wallet (passkey-derived for on-chain tokens).
The My Wallets tab now always shows both in a dedicated EncryptID card
with distinct sections, rather than burying CRDT tokens as a subsection
that only appears when balances are non-empty.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-12 11:41:42 -07:00
parent a4a4175e9f
commit 8af4309b41
2 changed files with 123 additions and 30 deletions

View File

@ -101,6 +101,7 @@ class FolkWalletViewer extends HTMLElement {
// Linked wallets state
private isAuthenticated = false;
private passKeyEOA = "";
private userDID = "";
private linkedWallets: LinkedWallet[] = [];
private showProviderPicker = false;
private discoveredProviders: DiscoveredProvider[] = [];
@ -193,6 +194,7 @@ class FolkWalletViewer extends HTMLElement {
this.isAuthenticated = true;
this.topTab = "my-wallets";
this.passKeyEOA = parsed.claims?.eid?.walletAddress || "";
this.userDID = parsed.claims?.did || "";
this.loadLinkedWallets().then(() => this.loadMyWalletBalances());
this.loadCRDTBalances();
}
@ -1235,6 +1237,24 @@ class FolkWalletViewer extends HTMLElement {
font-size: 13px; color: var(--rs-text-primary);
}
/* ── EncryptID identity card ── */
.encryptid-card { border-color: rgba(168,85,247,0.25); }
.encryptid-wallets { display: flex; flex-direction: column; gap: 12px; }
.encryptid-wallet-section {
padding: 10px 12px; border-radius: 8px;
background: var(--rs-bg-hover); border: 1px solid var(--rs-border-subtle);
}
.encryptid-wallet-label {
display: flex; align-items: center; gap: 6px;
font-size: 12px; font-weight: 600; text-transform: uppercase;
letter-spacing: 0.5px; color: var(--rs-text-secondary); margin-bottom: 8px;
}
.wallet-type-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
.wallet-type-addr {
font-family: monospace; font-weight: 400; font-size: 11px;
color: var(--rs-text-muted); margin-left: auto; text-transform: none; letter-spacing: 0;
}
/* ── Aggregate stats ── */
.aggregate-stats {
display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
@ -1421,20 +1441,15 @@ class FolkWalletViewer extends HTMLElement {
let html = '<div class="my-wallets-grid">';
// EncryptID wallet card
if (this.passKeyEOA) {
html += this.renderWalletCard(this.passKeyEOA, "EncryptID", "encryptid", true);
}
// EncryptID identity card — always shown for authenticated users
// Every EncryptID identity has both a CRDT wallet (DID) and an EVM wallet (passkey-derived)
html += this.renderEncryptIdCard();
// Linked wallet cards
// Linked wallet cards (external wallets)
for (const w of this.linkedWallets) {
html += this.renderWalletCard(w.address, w.providerName || w.type.toUpperCase(), w.type, false, w.id);
}
if (!this.passKeyEOA && this.linkedWallets.length === 0) {
html += '<div style="text-align:center;color:var(--rs-text-muted);padding:24px;font-size:13px">No wallets linked yet. Link a browser wallet to get started.</div>';
}
html += '</div>';
// Aggregate total
@ -1450,7 +1465,104 @@ class FolkWalletViewer extends HTMLElement {
return html;
}
private renderWalletCard(address: string, label: string, badgeClass: string, isEncryptId: boolean, walletId?: string): string {
private renderEncryptIdCard(): string {
// ── CRDT Wallet (DID) ──
const didShort = this.userDID
? `${this.userDID.slice(0, 16)}...${this.userDID.slice(-6)}`
: "loading...";
let crdtRows = "";
if (this.crdtLoading) {
crdtRows = `<div style="padding:8px;color:var(--rs-text-muted);font-size:12px"><span class="spinner" style="width:12px;height:12px;border-width:2px;display:inline-block;vertical-align:middle;margin-right:6px"></span> Loading...</div>`;
} else if (this.crdtBalances.length > 0) {
crdtRows = this.crdtBalances.map(t => {
const formatted = (t.balance / Math.pow(10, t.decimals)).toFixed(2);
return `<div class="crdt-row">
<span style="font-size:1.1em">${t.icon || '\u{1FA99}'}</span>
<strong>${this.esc(t.symbol)}</strong>
<span class="token-name" style="margin-left:4px">${this.esc(t.name)}</span>
<span style="margin-left:auto;font-family:monospace;font-weight:600">${formatted}</span>
</div>`;
}).join("");
} else {
crdtRows = `<div style="padding:8px;color:var(--rs-text-muted);font-size:12px">No CRDT token balances</div>`;
}
// ── EVM Wallet (passkey-derived) ──
const chainBalances = this.passKeyEOA
? (this.myWalletBalances.get(this.passKeyEOA.toLowerCase()) || [])
: [];
const allBals: Array<BalanceItem & { chainId: string; chainName: string }> = [];
for (const ch of chainBalances) {
for (const b of ch.balances) {
allBals.push({ ...b, chainId: ch.chainId, chainName: ch.chainName });
}
}
const sorted = allBals
.filter(b => parseFloat(b.fiatBalance || "0") > 0.01 || BigInt(b.balance || "0") > 0n)
.sort((a, b) => parseFloat(b.fiatBalance || "0") - parseFloat(a.fiatBalance || "0"));
const totalUSD = sorted.reduce((sum, b) => sum + parseFloat(b.fiatBalance || "0"), 0);
let evmBalanceRows = "";
if (sorted.length > 0) {
evmBalanceRows = sorted.slice(0, 10).map(b => {
const color = CHAIN_COLORS[b.chainId] || "#888";
return `<tr>
<td><div class="chain-cell"><span class="chain-dot-sm" style="background:${color}"></span>${this.esc(b.chainName)}</div></td>
<td><span class="token-symbol">${this.esc(b.token?.symbol || "ETH")}</span></td>
<td class="amount-cell">${this.formatBalance(b.balance, b.token?.decimals || 18)}</td>
<td class="amount-cell fiat">${this.formatUSD(b.fiatBalance)}</td>
</tr>`;
}).join("");
if (sorted.length > 10) {
evmBalanceRows += `<tr><td colspan="4" style="text-align:center;color:var(--rs-text-muted);font-size:11px;padding:8px">+ ${sorted.length - 10} more tokens</td></tr>`;
}
}
return `
<div class="wallet-card encryptid-card">
<div class="wallet-card-header">
<div>
<span class="wallet-badge encryptid">EncryptID</span>
<span class="wallet-card-total" style="margin-left:12px">${this.formatUSD(String(totalUSD))}</span>
</div>
${this.passKeyEOA ? `<button class="view-flows-btn" data-view-in-viz="${this.esc(this.passKeyEOA)}">View Flows &rarr;</button>` : ""}
</div>
<div class="encryptid-wallets">
<!-- CRDT Wallet -->
<div class="encryptid-wallet-section">
<div class="encryptid-wallet-label">
<span class="wallet-type-dot" style="background:#2775ca"></span>
CRDT Wallet
<span class="wallet-type-addr">${this.esc(didShort)}</span>
</div>
${crdtRows}
</div>
<!-- EVM Wallet -->
<div class="encryptid-wallet-section">
<div class="encryptid-wallet-label">
<span class="wallet-type-dot" style="background:#627eea"></span>
EVM Wallet
<span class="wallet-type-addr">${this.passKeyEOA ? this.shortenAddress(this.passKeyEOA) : "not derived"}</span>
</div>
${evmBalanceRows ? `
<table class="balance-table compact">
<thead>
<tr><th>Chain</th><th>Token</th><th class="amount-cell">Balance</th><th class="amount-cell">USD</th></tr>
</thead>
<tbody>${evmBalanceRows}</tbody>
</table>` : `<div style="padding:8px;color:var(--rs-text-muted);font-size:12px">No on-chain balances found</div>`}
</div>
</div>
</div>`;
}
private renderWalletCard(address: string, label: string, badgeClass: string, _isEncryptId: boolean, walletId?: string): string {
const chainBalances = this.myWalletBalances.get(address.toLowerCase()) || [];
// Flatten all balances for this wallet
@ -1483,24 +1595,6 @@ class FolkWalletViewer extends HTMLElement {
}
}
// CRDT tokens for EncryptID wallet
let crdtSection = "";
if (isEncryptId && this.crdtBalances.length > 0) {
const crdtRows = this.crdtBalances.map(t => {
const formatted = (t.balance / Math.pow(10, t.decimals)).toFixed(2);
return `<div class="crdt-row">
<span style="font-size:1.1em">${t.icon || '\u{1FA99}'}</span>
<strong>${this.esc(t.symbol)}</strong>
<span style="margin-left:auto;font-family:monospace;font-weight:600">${formatted}</span>
</div>`;
}).join("");
crdtSection = `
<div class="crdt-section">
<div class="crdt-label">Local Tokens (CRDT)</div>
${crdtRows}
</div>`;
}
return `
<div class="wallet-card">
<div class="wallet-card-header">
@ -1521,7 +1615,6 @@ class FolkWalletViewer extends HTMLElement {
</thead>
<tbody>${balanceRows}</tbody>
</table>` : `<div style="padding:12px;text-align:center;color:var(--rs-text-muted);font-size:12px">No on-chain balances found</div>`}
${crdtSection}
</div>`;
}

View File

@ -840,7 +840,7 @@ function renderWallet(spaceSlug: string, initialView?: string) {
modules: getModuleInfoList(),
theme: "dark",
body: `<folk-wallet-viewer${viewAttr}></folk-wallet-viewer>`,
scripts: `<script type="module" src="/modules/rwallet/folk-wallet-viewer.js?v=7"></script>`,
scripts: `<script type="module" src="/modules/rwallet/folk-wallet-viewer.js?v=8"></script>`,
styles: `<link rel="stylesheet" href="/modules/rwallet/wallet.css">`,
});
}