/** * 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; } /** Mint tokens to a DID. */ export function mintTokens( tokenId: string, toDid: string, toLabel: string, amount: number, memo: string, issuedBy: string, ): boolean { const docId = tokenDocId(tokenId); ensureTokenDoc(tokenId); const entryId = `mint-${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: 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 cUSDC token with 5 cUSDC minted to jeff. */ 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'); return; } // Set up token definition (idempotent — only writes if not yet 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'; 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'); } }