/** * CRDT Token Service — server-side token management via Automerge. * * Manages token ledger documents: minting, balance queries, and seeding. */ import * as Automerge from '@automerge/automerge'; import type { SyncServer } from './local-first/sync-server'; import { tokenLedgerSchema, tokenDocId } from './token-schemas'; import type { TokenLedgerDoc, LedgerEntry } from './token-schemas'; let _syncServer: SyncServer | null = null; export function initTokenService(syncServer: SyncServer) { _syncServer = syncServer; } /** Create a token doc if it doesn't exist yet. */ function ensureTokenDoc(tokenId: string): TokenLedgerDoc { const docId = tokenDocId(tokenId); let doc = _syncServer!.getDoc(docId); if (!doc) { doc = Automerge.change(Automerge.init(), 'init token ledger', (d) => { const init = tokenLedgerSchema.init(); Object.assign(d, init); }); _syncServer!.setDoc(docId, doc); } return doc; } /** Sum all entry amounts for a given DID holder. */ export function getBalance(doc: TokenLedgerDoc, did: string): number { let balance = 0; for (const entry of Object.values(doc.entries)) { if (entry.type === 'mint' && entry.to === did) { balance += entry.amount; } else if (entry.type === 'transfer') { if (entry.to === did) balance += entry.amount; if (entry.from === did) balance -= entry.amount; } else if (entry.type === 'burn' && entry.from === did) { balance -= entry.amount; } } return balance; } /** Aggregate balances for all holders. */ export function getAllBalances(doc: TokenLedgerDoc): Record { const holders: Record = {}; for (const entry of Object.values(doc.entries)) { if (entry.type === 'mint') { if (!holders[entry.to]) holders[entry.to] = { did: entry.to, label: entry.toLabel, balance: 0 }; holders[entry.to].balance += entry.amount; if (entry.toLabel) holders[entry.to].label = entry.toLabel; } else if (entry.type === 'transfer') { if (!holders[entry.to]) holders[entry.to] = { did: entry.to, label: entry.toLabel, balance: 0 }; if (!holders[entry.from]) holders[entry.from] = { did: entry.from, label: '', balance: 0 }; holders[entry.to].balance += entry.amount; holders[entry.from].balance -= entry.amount; } else if (entry.type === 'burn') { if (!holders[entry.from]) holders[entry.from] = { did: entry.from, label: '', balance: 0 }; holders[entry.from].balance -= entry.amount; } } // Remove zero-balance holders for (const [k, v] of Object.entries(holders)) { if (v.balance <= 0) delete holders[k]; } return holders; } /** Transfer tokens between DIDs. */ export function transferTokens( tokenId: string, fromDid: string, fromLabel: string, toDid: string, toLabel: string, amount: number, memo: string, issuedBy: string, timestamp?: number, ): boolean { const docId = tokenDocId(tokenId); ensureTokenDoc(tokenId); const entryId = `xfer-${timestamp || Date.now()}-${Math.random().toString(36).slice(2, 8)}`; const result = _syncServer!.changeDoc(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, toDid: string, toLabel: string, amount: number, memo: string, issuedBy: string, timestamp?: number, ): boolean { const docId = tokenDocId(tokenId); ensureTokenDoc(tokenId); 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, to: toDid, toLabel, amount, memo, type: 'mint', from: '', timestamp: timestamp || Date.now(), issuedBy, }; d.token.totalSupply = (d.token.totalSupply || 0) + amount; }); return result !== null; } /** List all token doc IDs. */ export function listTokenDocs(): string[] { return _syncServer!.listDocs().filter((id) => id.startsWith('global:tokens:ledgers:')); } /** Get a token doc by ID. */ export function getTokenDoc(tokenId: string): TokenLedgerDoc | undefined { return _syncServer!.getDoc(tokenDocId(tokenId)); } /** Seed the full DAO token ecosystem — BFT governance, cUSDC stablecoin, multiple treasuries. */ export async function seedCUSDC() { // 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; } // ── 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); _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'; }); } /** Mint $5 fUSDC welcome balance for a new user. */ export function mintWelcomeBalance(did: string, username: string): boolean { if (!_syncServer) { console.warn('[TokenService] SyncServer not initialized, skipping welcome balance'); return false; } const tokenId = 'fusdc'; const docId = tokenDocId(tokenId); let doc = _syncServer.getDoc(docId); // Create fUSDC token if it doesn't exist if (!doc) { doc = Automerge.change(Automerge.init(), 'init fUSDC ledger', (d) => { const init = tokenLedgerSchema.init(); Object.assign(d, init); }); _syncServer.setDoc(docId, doc); } if (!doc.token.name) { _syncServer.changeDoc(docId, 'define fUSDC token', (d) => { d.token.id = 'fusdc'; d.token.name = 'Fake USDC'; d.token.symbol = 'fUSDC'; d.token.decimals = 6; d.token.description = 'Test stablecoin for new users — $5 welcome balance'; d.token.icon = '💵'; d.token.color = '#2775ca'; d.token.createdAt = Date.now(); d.token.createdBy = 'system'; }); } // Mint 5 fUSDC (5 × 10^6 base units at 6 decimals) const success = mintTokens(tokenId, did, username, 5_000_000, 'Welcome balance', 'system'); if (success) { console.log(`[TokenService] Welcome balance: 5 fUSDC minted to ${username} (${did})`); } return success; }