From 97bf2d79871eadb19496015af1ce6f4ca3ce88fb Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Sat, 21 Mar 2026 13:47:14 -0700 Subject: [PATCH] feat(rwallet): integrate CRDT token flows into wallet visualizations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add transferTokens() and getAllTransfers() to token service - Seed DAO ecosystem: cUSDC + BFT tokens, 3 treasuries, 6 participants, ~30 transactions spread over 60 days - Add /api/crdt-tokens/transfers endpoint for ledger history - Wire CRDT transfers into loadTransfers() with DID→pseudo-address mapping - Update data transforms (timeline, sankey, multichain) to support _fromLabel/_toLabel for human-readable CRDT participant names - Show viz tabs (Timeline, Flow Map, Sankey) when CRDT tokens exist - Add "local"/"CRDT" chain to color and name maps Co-Authored-By: Claude Opus 4.6 --- .../rwallet/components/folk-wallet-viewer.ts | 135 ++++++++++--- modules/rwallet/lib/data-transform.ts | 81 +++++--- modules/rwallet/mod.ts | 9 +- server/token-service.ts | 184 +++++++++++++++--- 4 files changed, 325 insertions(+), 84 deletions(-) diff --git a/modules/rwallet/components/folk-wallet-viewer.ts b/modules/rwallet/components/folk-wallet-viewer.ts index f91868d..97e1a96 100644 --- a/modules/rwallet/components/folk-wallet-viewer.ts +++ b/modules/rwallet/components/folk-wallet-viewer.ts @@ -68,6 +68,7 @@ const CHAIN_COLORS: Record = { "80002": "#a855f7", "43113": "#fb923c", "97": "#fbbf24", + "local": "#22c55e", }; const COLORS_TX = { cyan: "#00d4ff", green: "#4ade80", red: "#f87171" }; @@ -75,7 +76,7 @@ const COLORS_TX = { cyan: "#00d4ff", green: "#4ade80", red: "#f87171" }; const CHAIN_NAMES: Record = { "1": "Ethereum", "10": "Optimism", "100": "Gnosis", "137": "Polygon", "8453": "Base", "42161": "Arbitrum", "42220": "Celo", "43114": "Avalanche", - "56": "BSC", "324": "zkSync", + "56": "BSC", "324": "zkSync", "local": "CRDT", }; const EXAMPLE_WALLETS = [ @@ -706,7 +707,6 @@ class FolkWalletViewer extends HTMLElement { private async loadTransfers() { if (this.isDemo || this.transfers || this.transfersLoading) return; - if (this.walletType !== "safe") return; // Transfer API only for Safes this.transfersLoading = true; this.render(); @@ -715,34 +715,110 @@ class FolkWalletViewer extends HTMLElement { const base = this.getApiBase(); const chainDataMap = new Map(); - // Fan out requests to all detected chains - await Promise.allSettled( - this.detectedChains.map(async (ch) => { - try { - const res = await fetch(`${base}/api/safe/${ch.chainId}/${this.address}/transfers?limit=2000`); - if (!res.ok) return; - const data = await res.json(); - // Parse results into incoming/outgoing - const incoming: any[] = []; - const outgoing: any[] = []; - const results = data.results || []; - for (const tx of results) { - if (tx.txType === "MULTISIG_TRANSACTION") { - outgoing.push(tx); - } - // Incoming transfers from transfers array inside txs - if (tx.transfers) { - for (const t of tx.transfers) { - if (t.to?.toLowerCase() === this.address.toLowerCase()) { - incoming.push(t); + // Fan out requests to all detected chains (Safe wallets only) + if (this.walletType === "safe") { + await Promise.allSettled( + this.detectedChains.map(async (ch) => { + try { + const res = await fetch(`${base}/api/safe/${ch.chainId}/${this.address}/transfers?limit=2000`); + if (!res.ok) return; + const data = await res.json(); + const incoming: any[] = []; + const outgoing: any[] = []; + const results = data.results || []; + for (const tx of results) { + if (tx.txType === "MULTISIG_TRANSACTION") { + outgoing.push(tx); + } + if (tx.transfers) { + for (const t of tx.transfers) { + if (t.to?.toLowerCase() === this.address.toLowerCase()) { + incoming.push(t); + } } } } + chainDataMap.set(ch.chainId, { incoming, outgoing, chainId: ch.chainId }); + } catch {} + }), + ); + } + + // Fetch CRDT token transfers (available for all wallet types when authenticated) + if (this.isAuthenticated && this.userDID) { + try { + const res = await fetch(`${base}/api/crdt-tokens/transfers?did=${encodeURIComponent(this.userDID)}`); + if (res.ok) { + const data = await res.json(); + const entries = data.entries || []; + if (entries.length > 0) { + const incoming: any[] = []; + const outgoing: any[] = []; + const walletAddr = this.address.toLowerCase(); + + // Map DIDs to stable pseudo-addresses for the transform layer + // The current user's DID → this.address (so transforms recognize it) + // Other DIDs → deterministic short hex from the DID string + const didToAddr = (did: string): string => { + if (did === this.userDID) return walletAddr; + if (!did || did === 'system') return '0x0000000000000000000000000000000000000000'; + // Deterministic pseudo-address from DID + let hash = 0; + for (let i = 0; i < did.length; i++) hash = ((hash << 5) - hash + did.charCodeAt(i)) | 0; + const hex = Math.abs(hash).toString(16).padStart(8, '0'); + return `0x${hex}${'0'.repeat(32)}`.slice(0, 42); + }; + + // Build a label lookup for DID short names + const didLabels = new Map(); + for (const entry of entries) { + if (entry.toLabel) didLabels.set(entry.to, entry.toLabel); + } + + for (const entry of entries) { + const fromAddr = entry.type === 'mint' ? didToAddr(entry.issuedBy || 'system') : didToAddr(entry.from); + const toAddr = didToAddr(entry.to); + const fromLabel = entry.type === 'mint' ? 'Mint' : (didLabels.get(entry.from) || entry.from?.slice(-12) || 'system'); + const toLabel = entry.toLabel || entry.to?.slice(-12) || ''; + + const shaped = { + type: "ERC20_TRANSFER", + transferType: "ERC20_TRANSFER", + from: fromAddr, + to: toAddr, + value: String(entry.amount), + tokenInfo: { + symbol: entry.tokenSymbol, + name: entry.tokenSymbol, + decimals: entry.tokenDecimals, + }, + executionDate: new Date(entry.timestamp).toISOString(), + blockTimestamp: new Date(entry.timestamp).toISOString(), + _fromLabel: fromLabel, + _toLabel: toLabel, + }; + + if (entry.to === this.userDID) { + incoming.push(shaped); + } + if (entry.from === this.userDID) { + outgoing.push({ + txType: "MULTISIG_TRANSACTION", + isExecuted: true, + executionDate: shaped.executionDate, + value: "0", + to: toAddr, + transfers: [{ ...shaped, from: walletAddr }], + dataDecoded: null, + }); + } + } + + chainDataMap.set("local", { incoming, outgoing, chainId: "local" }); } - chainDataMap.set(ch.chainId, { incoming, outgoing, chainId: ch.chainId }); - } catch {} - }), - ); + } + } catch {} + } this.transfers = chainDataMap; this.computeVizData(); @@ -795,6 +871,7 @@ class FolkWalletViewer extends HTMLElement { for (const ch of this.detectedChains) { chainColorMap[ch.name.toLowerCase()] = ch.color; } + chainColorMap["crdt"] = "#22c55e"; switch (this.activeView) { case "timeline": @@ -952,7 +1029,7 @@ class FolkWalletViewer extends HTMLElement { } private hasData(): boolean { - return this.detectedChains.length > 0; + return this.detectedChains.length > 0 || this.crdtBalances.length > 0; } // ── EIP-6963 Provider Discovery ── @@ -2035,8 +2112,8 @@ class FolkWalletViewer extends HTMLElement { { id: "flow", label: "Flow Map" }, { id: "sankey", label: "Sankey" }, ]; - // Only show viz tabs for Safe wallets (or demo) - const showViz = this.walletType === "safe" || this.isDemo; + // Show viz tabs for Safe wallets, demo, or when CRDT tokens exist + const showViz = this.walletType === "safe" || this.isDemo || (this.isAuthenticated && this.crdtBalances.length > 0); const visibleTabs = showViz ? tabs : [tabs[0], tabs[1]]; return ` diff --git a/modules/rwallet/lib/data-transform.ts b/modules/rwallet/lib/data-transform.ts index 643cc41..76eb9d3 100644 --- a/modules/rwallet/lib/data-transform.ts +++ b/modules/rwallet/lib/data-transform.ts @@ -176,7 +176,7 @@ export function transformToTimelineData( hasUsdEstimate: usd !== null, chain: chainName, chainId, - from: shortenAddress(transfer.from), + from: transfer._fromLabel || shortenAddress(transfer.from), fromFull: transfer.from, }); } @@ -187,7 +187,7 @@ export function transformToTimelineData( for (const tx of data.outgoing) { if (!tx.isExecuted) continue; - const txTransfers: { to: string; value: number; symbol: string; usd: number | null }[] = []; + const txTransfers: { to: string; value: number; symbol: string; usd: number | null; label?: string }[] = []; // Check transfers array if available if (tx.transfers && tx.transfers.length > 0) { @@ -196,7 +196,7 @@ export function transformToTimelineData( const value = getTransferValue(t); const symbol = getTokenSymbol(t); if (value > 0) { - txTransfers.push({ to: t.to, value, symbol, usd: estimateUSD(value, symbol) }); + txTransfers.push({ to: t.to, value, symbol, usd: estimateUSD(value, symbol), label: t._toLabel }); } } } @@ -249,7 +249,7 @@ export function transformToTimelineData( hasUsdEstimate: t.usd !== null, chain: chainName, chainId, - to: shortenAddress(t.to), + to: t.label || shortenAddress(t.to), toFull: t.to, }); } @@ -269,9 +269,9 @@ export function transformToSankeyData(chainData: any, safeAddress: string, chain const nodeMap = new Map(); const nodes: SankeyNode[] = []; const links: SankeyLink[] = []; - const walletLabel = "Safe Wallet"; + const walletLabel = chainId === "local" ? "My Wallet" : "Safe Wallet"; - function getNodeIndex(address: string, type: "wallet" | "source" | "target"): number { + function getNodeIndex(address: string, type: "wallet" | "source" | "target", label?: string): number { const key = address.toLowerCase() === safeAddress.toLowerCase() ? "wallet" : `${type}:${address.toLowerCase()}`; @@ -279,10 +279,10 @@ export function transformToSankeyData(chainData: any, safeAddress: string, chain if (!nodeMap.has(key)) { const idx = nodes.length; nodeMap.set(key, idx); - const label = address.toLowerCase() === safeAddress.toLowerCase() + const name = address.toLowerCase() === safeAddress.toLowerCase() ? walletLabel - : shortenAddress(address); - nodes.push({ name: label, type, address }); + : (label || shortenAddress(address)); + nodes.push({ name, type, address }); } return nodeMap.get(key)!; } @@ -291,7 +291,7 @@ export function transformToSankeyData(chainData: any, safeAddress: string, chain getNodeIndex(safeAddress, "wallet"); // Aggregate inflows by source address + token - const inflowAgg = new Map(); + const inflowAgg = new Map(); if (chainData.incoming) { for (const transfer of chainData.incoming) { const value = getTransferValue(transfer); @@ -299,7 +299,7 @@ export function transformToSankeyData(chainData: any, safeAddress: string, chain if (value <= 0 || !transfer.from) continue; const key = `${transfer.from.toLowerCase()}:${symbol}`; - const existing = inflowAgg.get(key) || { from: transfer.from, value: 0, symbol }; + const existing = inflowAgg.get(key) || { from: transfer.from, value: 0, symbol, label: transfer._fromLabel }; existing.value += value; inflowAgg.set(key, existing); } @@ -307,22 +307,25 @@ export function transformToSankeyData(chainData: any, safeAddress: string, chain // Add inflow links for (const [, agg] of inflowAgg) { - const sourceIdx = getNodeIndex(agg.from, "source"); + const sourceIdx = getNodeIndex(agg.from, "source", agg.label); const walletIdx = nodeMap.get("wallet")!; links.push({ source: sourceIdx, target: walletIdx, value: agg.value, token: agg.symbol }); } // Aggregate outflows by target address + token - const outflowAgg = new Map(); + const outflowAgg = new Map(); if (chainData.outgoing) { for (const tx of chainData.outgoing) { if (!tx.isExecuted) continue; + // Extract label from transfers if available (CRDT entries) + const txLabel = tx.transfers?.[0]?._toLabel; + if (tx.value && tx.value !== "0" && tx.to) { const val = parseFloat(tx.value) / 1e18; const sym = (chainId && CHAIN_NATIVE_SYMBOL[chainId]) || "ETH"; const key = `${tx.to.toLowerCase()}:${sym}`; - const existing = outflowAgg.get(key) || { to: tx.to, value: 0, symbol: sym }; + const existing = outflowAgg.get(key) || { to: tx.to, value: 0, symbol: sym, label: txLabel }; existing.value += val; outflowAgg.set(key, existing); } @@ -366,13 +369,29 @@ export function transformToSankeyData(chainData: any, safeAddress: string, chain } } } + + // Fallback: process embedded transfers (CRDT entries use this path) + if (tx.transfers && tx.transfers.length > 0 && (!tx.value || tx.value === "0") && !tx.dataDecoded) { + for (const t of tx.transfers) { + if (t.from?.toLowerCase() === safeAddress.toLowerCase()) { + const value = getTransferValue(t); + const symbol = getTokenSymbol(t); + if (value > 0 && t.to) { + const key = `${t.to.toLowerCase()}:${symbol}`; + const existing = outflowAgg.get(key) || { to: t.to, value: 0, symbol, label: t._toLabel }; + existing.value += value; + outflowAgg.set(key, existing); + } + } + } + } } } // Add outflow links const walletIdx = nodeMap.get("wallet")!; for (const [, agg] of outflowAgg) { - const targetIdx = getNodeIndex(agg.to, "target"); + const targetIdx = getNodeIndex(agg.to, "target", agg.label); links.push({ source: walletIdx, target: targetIdx, value: agg.value, token: agg.symbol }); } @@ -442,8 +461,9 @@ export function transformToMultichainData( } const from = transfer.from || "Unknown"; - const key = shortenAddress(from); - const existing = inflowAgg.get(key) || { from: shortenAddress(from), value: 0, token: symbol }; + const fromName = transfer._fromLabel || shortenAddress(from); + const key = fromName; + const existing = inflowAgg.get(key) || { from: fromName, value: 0, token: symbol }; existing.value += usdVal; inflowAgg.set(key, existing); @@ -451,7 +471,7 @@ export function transformToMultichainData( chainId, chainName, date: date || "", from: transfer.from, - fromShort: shortenAddress(transfer.from), + fromShort: transfer._fromLabel || shortenAddress(transfer.from), token: symbol, amount: value, usd: usdVal, @@ -477,12 +497,13 @@ export function transformToMultichainData( if (!chainMaxDate || d > chainMaxDate) chainMaxDate = d; } - const outTransfers: { to: string; value: number; symbol: string }[] = []; + const outTransfers: { to: string; value: number; symbol: string; label?: string }[] = []; + const txLabel = tx.transfers?.[0]?._toLabel; if (tx.value && tx.value !== "0" && tx.to) { const val = parseFloat(tx.value) / 1e18; const sym = CHAIN_NATIVE_SYMBOL[chainId] || "ETH"; - outTransfers.push({ to: tx.to, value: val, symbol: sym }); + outTransfers.push({ to: tx.to, value: val, symbol: sym, label: txLabel }); } if (tx.dataDecoded?.method === "transfer") { @@ -510,6 +531,19 @@ export function transformToMultichainData( } } + // Fallback: embedded transfers (CRDT entries) + if (outTransfers.length === 0 && tx.transfers && tx.transfers.length > 0) { + for (const t of tx.transfers) { + if (t.from?.toLowerCase() === safeAddress.toLowerCase()) { + const value = getTransferValue(t); + const symbol = getTokenSymbol(t); + if (value > 0) { + outTransfers.push({ to: t.to, value, symbol, label: t._toLabel }); + } + } + } + } + for (const t of outTransfers) { const usd = estimateUSD(t.value, t.symbol); const usdVal = usd !== null ? usd : t.value; @@ -519,8 +553,9 @@ export function transformToMultichainData( allAddresses.add(t.to.toLowerCase()); } - const key = shortenAddress(t.to); - const existing = outflowAgg.get(key) || { to: shortenAddress(t.to), value: 0, token: t.symbol }; + const toName = t.label || shortenAddress(t.to); + const key = toName; + const existing = outflowAgg.get(key) || { to: toName, value: 0, token: t.symbol }; existing.value += usdVal; outflowAgg.set(key, existing); @@ -528,7 +563,7 @@ export function transformToMultichainData( chainId, chainName, date: date || "", to: t.to, - toShort: shortenAddress(t.to), + toShort: t.label || shortenAddress(t.to), token: t.symbol, amount: t.value, usd: usdVal, diff --git a/modules/rwallet/mod.ts b/modules/rwallet/mod.ts index e9b8ec3..dfe1026 100644 --- a/modules/rwallet/mod.ts +++ b/modules/rwallet/mod.ts @@ -798,7 +798,7 @@ routes.post("/api/safe/:chainId/:address/add-owner-proposal", async (c) => { }); // ── CRDT Token API ── -import { getTokenDoc, listTokenDocs, getAllBalances, getBalance, mintTokens } from "../../server/token-service"; +import { getTokenDoc, listTokenDocs, getAllBalances, getBalance, mintTokens, getAllTransfers } from "../../server/token-service"; import { tokenDocId } from "../../server/token-schemas"; import type { TokenLedgerDoc } from "../../server/token-schemas"; @@ -888,6 +888,13 @@ routes.post("/api/crdt-tokens/:tokenId/mint", async (c) => { return c.json({ ok: true, minted: amount, to: toDid }); }); +// Get all CRDT token transfers (optionally filtered by DID) +routes.get("/api/crdt-tokens/transfers", (c) => { + const did = c.req.query("did"); + const entries = getAllTransfers(did || undefined); + return c.json({ entries }); +}); + // ── Yield API routes ── import { getYieldRates } from "./lib/yield-rates"; import { getYieldPositions } from "./lib/yield-positions"; diff --git a/server/token-service.ts b/server/token-service.ts index e25fd1a..22bfa94 100644 --- a/server/token-service.ts +++ b/server/token-service.ts @@ -72,6 +72,63 @@ export function getAllBalances(doc: TokenLedgerDoc): Record(docId, `transfer ${amount} from ${fromLabel} to ${toLabel}`, (d) => { + d.entries[entryId] = { + id: entryId, + to: toDid, + toLabel, + amount, + memo, + type: 'transfer', + from: fromDid, + timestamp: timestamp || Date.now(), + issuedBy, + }; + }); + return result !== null; +} + +/** Get all ledger entries across all tokens, optionally filtered by DID. */ +export function getAllTransfers(filterDid?: string): Array { + const docIds = listTokenDocs(); + const entries: Array = []; + + for (const docId of docIds) { + const tokenId = docId.replace('global:tokens:ledgers:', ''); + const doc = getTokenDoc(tokenId); + if (!doc || !doc.token.name) continue; + + for (const entry of Object.values(doc.entries)) { + if (filterDid && entry.to !== filterDid && entry.from !== filterDid) continue; + entries.push({ + ...entry, + tokenId: doc.token.id, + tokenSymbol: doc.token.symbol, + tokenDecimals: doc.token.decimals, + tokenIcon: doc.token.icon, + }); + } + } + + return entries.sort((a, b) => a.timestamp - b.timestamp); +} + /** Mint tokens to a DID. */ export function mintTokens( tokenId: string, @@ -80,11 +137,12 @@ export function mintTokens( amount: number, memo: string, issuedBy: string, + timestamp?: number, ): boolean { const docId = tokenDocId(tokenId); ensureTokenDoc(tokenId); - const entryId = `mint-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; + const entryId = `mint-${timestamp || Date.now()}-${Math.random().toString(36).slice(2, 8)}`; const result = _syncServer!.changeDoc(docId, `mint ${amount} to ${toLabel}`, (d) => { d.entries[entryId] = { id: entryId, @@ -94,7 +152,7 @@ export function mintTokens( memo, type: 'mint', from: '', - timestamp: Date.now(), + timestamp: timestamp || Date.now(), issuedBy, }; d.token.totalSupply = (d.token.totalSupply || 0) + amount; @@ -112,43 +170,107 @@ export function getTokenDoc(tokenId: string): TokenLedgerDoc | undefined { return _syncServer!.getDoc(tokenDocId(tokenId)); } -/** Seed the cUSDC token with 5 cUSDC minted to jeff. */ +/** Seed the full DAO token ecosystem — BFT governance, cUSDC stablecoin, multiple treasuries. */ export async function seedCUSDC() { - const tokenId = 'cusdc'; - const doc = ensureTokenDoc(tokenId); - - // Skip if already fully seeded (has entries) - if (doc.token.name && Object.keys(doc.entries).length > 0) { - console.log('[TokenService] cUSDC already seeded, skipping'); + // Check if already seeded by looking at BFT token (last one created) + const bftDoc = getTokenDoc('bft'); + if (bftDoc && bftDoc.token.name && Object.keys(bftDoc.entries).length > 0) { + console.log('[TokenService] DAO ecosystem already seeded, skipping'); return; } - // Set up token definition (idempotent — only writes if not yet defined) + // ── DAO participants ── + const jeffDid = process.env.SEED_JEFF_DID || 'did:key:jAV6y4tg8UbKJEkN0npvX8CTdJkSCGpU'; + const treasuryDid = 'did:key:treasury-main-rspace-dao-2026'; + const grantsDid = 'did:key:treasury-grants-committee-2026'; + const devFundDid = 'did:key:treasury-dev-fund-rspace-2026'; + const aliceDid = 'did:key:alice-contributor-rspace-2026'; + const bobDid = 'did:key:bob-auditor-rspace-2026'; + const carolDid = 'did:key:carol-designer-rspace-2026'; + + // Timestamps spread over the last 60 days + const now = Date.now(); + const day = 86400000; + const t = (daysAgo: number) => now - daysAgo * day; + + // ── 1. cUSDC — CRDT stablecoin ── + ensureTokenDef('cusdc', { + name: 'CRDT USDC', symbol: 'cUSDC', decimals: 6, + description: 'CRDT-native stablecoin pegged to USDC, stored as an Automerge document', + icon: '💵', color: '#2775ca', + }); + + // Mint to main treasury + mintTokens('cusdc', treasuryDid, 'DAO Treasury', 500_000_000_000, 'Genesis allocation — 500K cUSDC', 'system', t(58)); + // Treasury funds grants committee + transferTokens('cusdc', treasuryDid, 'DAO Treasury', grantsDid, 'Grants Committee', 100_000_000_000, 'Q1 2026 grants budget', 'system', t(55)); + // Treasury funds dev fund + transferTokens('cusdc', treasuryDid, 'DAO Treasury', devFundDid, 'Dev Fund', 75_000_000_000, 'Q1 2026 development budget', 'system', t(54)); + // Grants committee pays contributors + transferTokens('cusdc', grantsDid, 'Grants Committee', aliceDid, 'Alice', 15_000_000_000, 'Grant: local-first sync engine', 'system', t(48)); + transferTokens('cusdc', grantsDid, 'Grants Committee', bobDid, 'Bob', 8_000_000_000, 'Grant: security audit Q1', 'system', t(45)); + transferTokens('cusdc', grantsDid, 'Grants Committee', carolDid, 'Carol', 12_000_000_000, 'Grant: UX redesign sprint', 'system', t(42)); + // Dev fund pays jeff + transferTokens('cusdc', devFundDid, 'Dev Fund', jeffDid, 'jeff', 25_000_000_000, 'Core dev compensation — Jan', 'system', t(40)); + transferTokens('cusdc', devFundDid, 'Dev Fund', aliceDid, 'Alice', 10_000_000_000, 'CRDT module development', 'system', t(35)); + // Second round + transferTokens('cusdc', grantsDid, 'Grants Committee', bobDid, 'Bob', 5_000_000_000, 'Follow-up audit: identity module', 'system', t(30)); + transferTokens('cusdc', devFundDid, 'Dev Fund', jeffDid, 'jeff', 25_000_000_000, 'Core dev compensation — Feb', 'system', t(25)); + transferTokens('cusdc', treasuryDid, 'DAO Treasury', grantsDid, 'Grants Committee', 50_000_000_000, 'Grants top-up — mid-quarter', 'system', t(22)); + transferTokens('cusdc', grantsDid, 'Grants Committee', carolDid, 'Carol', 8_000_000_000, 'Design system components', 'system', t(18)); + // Recent activity + transferTokens('cusdc', devFundDid, 'Dev Fund', jeffDid, 'jeff', 25_000_000_000, 'Core dev compensation — Mar', 'system', t(10)); + transferTokens('cusdc', treasuryDid, 'DAO Treasury', devFundDid, 'Dev Fund', 50_000_000_000, 'Q2 2026 dev budget (early)', 'system', t(5)); + transferTokens('cusdc', grantsDid, 'Grants Committee', aliceDid, 'Alice', 20_000_000_000, 'Grant: rMaps privacy module', 'system', t(3)); + + // ── 2. BFT — Governance token ── + ensureTokenDef('bft', { + name: 'BioFi Token', symbol: 'BFT', decimals: 18, + description: 'Governance token for the rSpace DAO — used for voting, delegation, and reputation', + icon: '🌱', color: '#22c55e', + }); + + const e18 = 1_000_000_000_000_000_000; // 10^18 — but we store as number so use smaller scale + const bft = (n: number) => n * 1_000_000; // Use 6 effective decimals for demo (display divides by 10^18) + + // Genesis mint to treasury + mintTokens('bft', treasuryDid, 'DAO Treasury', bft(10_000_000), 'Genesis: 10M BFT', 'system', t(58)); + // Distribute to sub-treasuries + transferTokens('bft', treasuryDid, 'DAO Treasury', grantsDid, 'Grants Committee', bft(2_000_000), 'Grants committee allocation', 'system', t(56)); + transferTokens('bft', treasuryDid, 'DAO Treasury', devFundDid, 'Dev Fund', bft(1_500_000), 'Dev fund allocation', 'system', t(56)); + // Vest to contributors + transferTokens('bft', treasuryDid, 'DAO Treasury', jeffDid, 'jeff', bft(500_000), 'Founder vesting — month 1', 'system', t(50)); + transferTokens('bft', grantsDid, 'Grants Committee', aliceDid, 'Alice', bft(100_000), 'Contributor reward: sync engine', 'system', t(46)); + transferTokens('bft', grantsDid, 'Grants Committee', bobDid, 'Bob', bft(50_000), 'Contributor reward: audit', 'system', t(43)); + transferTokens('bft', grantsDid, 'Grants Committee', carolDid, 'Carol', bft(75_000), 'Contributor reward: UX', 'system', t(40)); + transferTokens('bft', devFundDid, 'Dev Fund', jeffDid, 'jeff', bft(200_000), 'Dev milestone: rwallet launch', 'system', t(35)); + // Second vesting round + transferTokens('bft', treasuryDid, 'DAO Treasury', jeffDid, 'jeff', bft(500_000), 'Founder vesting — month 2', 'system', t(20)); + transferTokens('bft', grantsDid, 'Grants Committee', aliceDid, 'Alice', bft(150_000), 'Grant reward: rMaps', 'system', t(15)); + transferTokens('bft', devFundDid, 'Dev Fund', bobDid, 'Bob', bft(80_000), 'Security bounty payout', 'system', t(12)); + transferTokens('bft', devFundDid, 'Dev Fund', carolDid, 'Carol', bft(60_000), 'Design system completion', 'system', t(8)); + // Recent + transferTokens('bft', treasuryDid, 'DAO Treasury', jeffDid, 'jeff', bft(500_000), 'Founder vesting — month 3', 'system', t(2)); + + console.log('[TokenService] DAO ecosystem seeded: cUSDC + BFT tokens with treasury flows'); +} + +/** Helper to define a token if not yet defined. */ +function ensureTokenDef(tokenId: string, def: { name: string; symbol: string; decimals: number; description: string; icon: string; color: string }) { + const doc = ensureTokenDoc(tokenId); + if (doc.token.name) return; // Already defined const docId = tokenDocId(tokenId); - if (!doc.token.name) { - _syncServer!.changeDoc(docId, 'define cUSDC token', (d) => { - d.token.id = 'cusdc'; - d.token.name = 'CRDT USDC'; - d.token.symbol = 'cUSDC'; - d.token.decimals = 6; - d.token.description = 'CRDT-native stablecoin pegged to USDC, stored as an Automerge document'; - d.token.icon = '💵'; - d.token.color = '#2775ca'; + _syncServer!.changeDoc(docId, `define ${def.symbol} token`, (d) => { + d.token.id = tokenId; + d.token.name = def.name; + d.token.symbol = def.symbol; + d.token.decimals = def.decimals; + d.token.description = def.description; + d.token.icon = def.icon; + d.token.color = def.color; d.token.createdAt = Date.now(); d.token.createdBy = 'system'; }); - } - - // Resolve jeff's DID — use known DID with env override support - const jeffDid = process.env.SEED_JEFF_DID || 'did:key:jAV6y4tg8UbKJEkN0npvX8CTdJkSCGpU'; - - // Mint 5 cUSDC (5 × 10^6 base units) - const success = mintTokens(tokenId, jeffDid, 'jeff', 5_000_000, 'Initial seed mint', 'system'); - if (success) { - console.log(`[TokenService] cUSDC seeded: 5 cUSDC minted to jeff (${jeffDid})`); - } else { - console.error('[TokenService] Failed to mint cUSDC to jeff'); - } } /** Mint $5 fUSDC welcome balance for a new user. */