Merge branch 'dev'

This commit is contained in:
Jeff Emmett 2026-03-21 13:52:24 -07:00
commit 041f7423de
4 changed files with 325 additions and 84 deletions

View File

@ -68,6 +68,7 @@ const CHAIN_COLORS: Record<string, string> = {
"80002": "#a855f7", "80002": "#a855f7",
"43113": "#fb923c", "43113": "#fb923c",
"97": "#fbbf24", "97": "#fbbf24",
"local": "#22c55e",
}; };
const COLORS_TX = { cyan: "#00d4ff", green: "#4ade80", red: "#f87171" }; 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<string, string> = { const CHAIN_NAMES: Record<string, string> = {
"1": "Ethereum", "10": "Optimism", "100": "Gnosis", "137": "Polygon", "1": "Ethereum", "10": "Optimism", "100": "Gnosis", "137": "Polygon",
"8453": "Base", "42161": "Arbitrum", "42220": "Celo", "43114": "Avalanche", "8453": "Base", "42161": "Arbitrum", "42220": "Celo", "43114": "Avalanche",
"56": "BSC", "324": "zkSync", "56": "BSC", "324": "zkSync", "local": "CRDT",
}; };
const EXAMPLE_WALLETS = [ const EXAMPLE_WALLETS = [
@ -706,7 +707,6 @@ class FolkWalletViewer extends HTMLElement {
private async loadTransfers() { private async loadTransfers() {
if (this.isDemo || this.transfers || this.transfersLoading) return; if (this.isDemo || this.transfers || this.transfersLoading) return;
if (this.walletType !== "safe") return; // Transfer API only for Safes
this.transfersLoading = true; this.transfersLoading = true;
this.render(); this.render();
@ -715,34 +715,110 @@ class FolkWalletViewer extends HTMLElement {
const base = this.getApiBase(); const base = this.getApiBase();
const chainDataMap = new Map<string, any>(); const chainDataMap = new Map<string, any>();
// Fan out requests to all detected chains // Fan out requests to all detected chains (Safe wallets only)
await Promise.allSettled( if (this.walletType === "safe") {
this.detectedChains.map(async (ch) => { await Promise.allSettled(
try { this.detectedChains.map(async (ch) => {
const res = await fetch(`${base}/api/safe/${ch.chainId}/${this.address}/transfers?limit=2000`); try {
if (!res.ok) return; const res = await fetch(`${base}/api/safe/${ch.chainId}/${this.address}/transfers?limit=2000`);
const data = await res.json(); if (!res.ok) return;
// Parse results into incoming/outgoing const data = await res.json();
const incoming: any[] = []; const incoming: any[] = [];
const outgoing: any[] = []; const outgoing: any[] = [];
const results = data.results || []; const results = data.results || [];
for (const tx of results) { for (const tx of results) {
if (tx.txType === "MULTISIG_TRANSACTION") { if (tx.txType === "MULTISIG_TRANSACTION") {
outgoing.push(tx); outgoing.push(tx);
} }
// Incoming transfers from transfers array inside txs if (tx.transfers) {
if (tx.transfers) { for (const t of tx.transfers) {
for (const t of tx.transfers) { if (t.to?.toLowerCase() === this.address.toLowerCase()) {
if (t.to?.toLowerCase() === this.address.toLowerCase()) { incoming.push(t);
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<string, string>();
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.transfers = chainDataMap;
this.computeVizData(); this.computeVizData();
@ -795,6 +871,7 @@ class FolkWalletViewer extends HTMLElement {
for (const ch of this.detectedChains) { for (const ch of this.detectedChains) {
chainColorMap[ch.name.toLowerCase()] = ch.color; chainColorMap[ch.name.toLowerCase()] = ch.color;
} }
chainColorMap["crdt"] = "#22c55e";
switch (this.activeView) { switch (this.activeView) {
case "timeline": case "timeline":
@ -952,7 +1029,7 @@ class FolkWalletViewer extends HTMLElement {
} }
private hasData(): boolean { private hasData(): boolean {
return this.detectedChains.length > 0; return this.detectedChains.length > 0 || this.crdtBalances.length > 0;
} }
// ── EIP-6963 Provider Discovery ── // ── EIP-6963 Provider Discovery ──
@ -2035,8 +2112,8 @@ class FolkWalletViewer extends HTMLElement {
{ id: "flow", label: "Flow Map" }, { id: "flow", label: "Flow Map" },
{ id: "sankey", label: "Sankey" }, { id: "sankey", label: "Sankey" },
]; ];
// Only show viz tabs for Safe wallets (or demo) // Show viz tabs for Safe wallets, demo, or when CRDT tokens exist
const showViz = this.walletType === "safe" || this.isDemo; const showViz = this.walletType === "safe" || this.isDemo || (this.isAuthenticated && this.crdtBalances.length > 0);
const visibleTabs = showViz ? tabs : [tabs[0], tabs[1]]; const visibleTabs = showViz ? tabs : [tabs[0], tabs[1]];
return ` return `

View File

@ -176,7 +176,7 @@ export function transformToTimelineData(
hasUsdEstimate: usd !== null, hasUsdEstimate: usd !== null,
chain: chainName, chain: chainName,
chainId, chainId,
from: shortenAddress(transfer.from), from: transfer._fromLabel || shortenAddress(transfer.from),
fromFull: transfer.from, fromFull: transfer.from,
}); });
} }
@ -187,7 +187,7 @@ export function transformToTimelineData(
for (const tx of data.outgoing) { for (const tx of data.outgoing) {
if (!tx.isExecuted) continue; 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 // Check transfers array if available
if (tx.transfers && tx.transfers.length > 0) { if (tx.transfers && tx.transfers.length > 0) {
@ -196,7 +196,7 @@ export function transformToTimelineData(
const value = getTransferValue(t); const value = getTransferValue(t);
const symbol = getTokenSymbol(t); const symbol = getTokenSymbol(t);
if (value > 0) { 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, hasUsdEstimate: t.usd !== null,
chain: chainName, chain: chainName,
chainId, chainId,
to: shortenAddress(t.to), to: t.label || shortenAddress(t.to),
toFull: t.to, toFull: t.to,
}); });
} }
@ -269,9 +269,9 @@ export function transformToSankeyData(chainData: any, safeAddress: string, chain
const nodeMap = new Map<string, number>(); const nodeMap = new Map<string, number>();
const nodes: SankeyNode[] = []; const nodes: SankeyNode[] = [];
const links: SankeyLink[] = []; 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() const key = address.toLowerCase() === safeAddress.toLowerCase()
? "wallet" ? "wallet"
: `${type}:${address.toLowerCase()}`; : `${type}:${address.toLowerCase()}`;
@ -279,10 +279,10 @@ export function transformToSankeyData(chainData: any, safeAddress: string, chain
if (!nodeMap.has(key)) { if (!nodeMap.has(key)) {
const idx = nodes.length; const idx = nodes.length;
nodeMap.set(key, idx); nodeMap.set(key, idx);
const label = address.toLowerCase() === safeAddress.toLowerCase() const name = address.toLowerCase() === safeAddress.toLowerCase()
? walletLabel ? walletLabel
: shortenAddress(address); : (label || shortenAddress(address));
nodes.push({ name: label, type, address }); nodes.push({ name, type, address });
} }
return nodeMap.get(key)!; return nodeMap.get(key)!;
} }
@ -291,7 +291,7 @@ export function transformToSankeyData(chainData: any, safeAddress: string, chain
getNodeIndex(safeAddress, "wallet"); getNodeIndex(safeAddress, "wallet");
// Aggregate inflows by source address + token // Aggregate inflows by source address + token
const inflowAgg = new Map<string, { from: string; value: number; symbol: string }>(); const inflowAgg = new Map<string, { from: string; value: number; symbol: string; label?: string }>();
if (chainData.incoming) { if (chainData.incoming) {
for (const transfer of chainData.incoming) { for (const transfer of chainData.incoming) {
const value = getTransferValue(transfer); const value = getTransferValue(transfer);
@ -299,7 +299,7 @@ export function transformToSankeyData(chainData: any, safeAddress: string, chain
if (value <= 0 || !transfer.from) continue; if (value <= 0 || !transfer.from) continue;
const key = `${transfer.from.toLowerCase()}:${symbol}`; 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; existing.value += value;
inflowAgg.set(key, existing); inflowAgg.set(key, existing);
} }
@ -307,22 +307,25 @@ export function transformToSankeyData(chainData: any, safeAddress: string, chain
// Add inflow links // Add inflow links
for (const [, agg] of inflowAgg) { for (const [, agg] of inflowAgg) {
const sourceIdx = getNodeIndex(agg.from, "source"); const sourceIdx = getNodeIndex(agg.from, "source", agg.label);
const walletIdx = nodeMap.get("wallet")!; const walletIdx = nodeMap.get("wallet")!;
links.push({ source: sourceIdx, target: walletIdx, value: agg.value, token: agg.symbol }); links.push({ source: sourceIdx, target: walletIdx, value: agg.value, token: agg.symbol });
} }
// Aggregate outflows by target address + token // Aggregate outflows by target address + token
const outflowAgg = new Map<string, { to: string; value: number; symbol: string }>(); const outflowAgg = new Map<string, { to: string; value: number; symbol: string; label?: string }>();
if (chainData.outgoing) { if (chainData.outgoing) {
for (const tx of chainData.outgoing) { for (const tx of chainData.outgoing) {
if (!tx.isExecuted) continue; 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) { if (tx.value && tx.value !== "0" && tx.to) {
const val = parseFloat(tx.value) / 1e18; const val = parseFloat(tx.value) / 1e18;
const sym = (chainId && CHAIN_NATIVE_SYMBOL[chainId]) || "ETH"; const sym = (chainId && CHAIN_NATIVE_SYMBOL[chainId]) || "ETH";
const key = `${tx.to.toLowerCase()}:${sym}`; 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; existing.value += val;
outflowAgg.set(key, existing); 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 // Add outflow links
const walletIdx = nodeMap.get("wallet")!; const walletIdx = nodeMap.get("wallet")!;
for (const [, agg] of outflowAgg) { 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 }); 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 from = transfer.from || "Unknown";
const key = shortenAddress(from); const fromName = transfer._fromLabel || shortenAddress(from);
const existing = inflowAgg.get(key) || { from: shortenAddress(from), value: 0, token: symbol }; const key = fromName;
const existing = inflowAgg.get(key) || { from: fromName, value: 0, token: symbol };
existing.value += usdVal; existing.value += usdVal;
inflowAgg.set(key, existing); inflowAgg.set(key, existing);
@ -451,7 +471,7 @@ export function transformToMultichainData(
chainId, chainName, chainId, chainName,
date: date || "", date: date || "",
from: transfer.from, from: transfer.from,
fromShort: shortenAddress(transfer.from), fromShort: transfer._fromLabel || shortenAddress(transfer.from),
token: symbol, token: symbol,
amount: value, amount: value,
usd: usdVal, usd: usdVal,
@ -477,12 +497,13 @@ export function transformToMultichainData(
if (!chainMaxDate || d > chainMaxDate) chainMaxDate = d; 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) { if (tx.value && tx.value !== "0" && tx.to) {
const val = parseFloat(tx.value) / 1e18; const val = parseFloat(tx.value) / 1e18;
const sym = CHAIN_NATIVE_SYMBOL[chainId] || "ETH"; 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") { 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) { for (const t of outTransfers) {
const usd = estimateUSD(t.value, t.symbol); const usd = estimateUSD(t.value, t.symbol);
const usdVal = usd !== null ? usd : t.value; const usdVal = usd !== null ? usd : t.value;
@ -519,8 +553,9 @@ export function transformToMultichainData(
allAddresses.add(t.to.toLowerCase()); allAddresses.add(t.to.toLowerCase());
} }
const key = shortenAddress(t.to); const toName = t.label || shortenAddress(t.to);
const existing = outflowAgg.get(key) || { to: shortenAddress(t.to), value: 0, token: t.symbol }; const key = toName;
const existing = outflowAgg.get(key) || { to: toName, value: 0, token: t.symbol };
existing.value += usdVal; existing.value += usdVal;
outflowAgg.set(key, existing); outflowAgg.set(key, existing);
@ -528,7 +563,7 @@ export function transformToMultichainData(
chainId, chainName, chainId, chainName,
date: date || "", date: date || "",
to: t.to, to: t.to,
toShort: shortenAddress(t.to), toShort: t.label || shortenAddress(t.to),
token: t.symbol, token: t.symbol,
amount: t.value, amount: t.value,
usd: usdVal, usd: usdVal,

View File

@ -798,7 +798,7 @@ routes.post("/api/safe/:chainId/:address/add-owner-proposal", async (c) => {
}); });
// ── CRDT Token API ── // ── 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 { tokenDocId } from "../../server/token-schemas";
import type { TokenLedgerDoc } 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 }); 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 ── // ── Yield API routes ──
import { getYieldRates } from "./lib/yield-rates"; import { getYieldRates } from "./lib/yield-rates";
import { getYieldPositions } from "./lib/yield-positions"; import { getYieldPositions } from "./lib/yield-positions";

View File

@ -72,6 +72,63 @@ export function getAllBalances(doc: TokenLedgerDoc): Record<string, { did: strin
return holders; 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<TokenLedgerDoc>(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<LedgerEntry & { tokenId: string; tokenSymbol: string; tokenDecimals: number; tokenIcon: string }> {
const docIds = listTokenDocs();
const entries: Array<LedgerEntry & { tokenId: string; tokenSymbol: string; tokenDecimals: number; tokenIcon: string }> = [];
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. */ /** Mint tokens to a DID. */
export function mintTokens( export function mintTokens(
tokenId: string, tokenId: string,
@ -80,11 +137,12 @@ export function mintTokens(
amount: number, amount: number,
memo: string, memo: string,
issuedBy: string, issuedBy: string,
timestamp?: number,
): boolean { ): boolean {
const docId = tokenDocId(tokenId); const docId = tokenDocId(tokenId);
ensureTokenDoc(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<TokenLedgerDoc>(docId, `mint ${amount} to ${toLabel}`, (d) => { const result = _syncServer!.changeDoc<TokenLedgerDoc>(docId, `mint ${amount} to ${toLabel}`, (d) => {
d.entries[entryId] = { d.entries[entryId] = {
id: entryId, id: entryId,
@ -94,7 +152,7 @@ export function mintTokens(
memo, memo,
type: 'mint', type: 'mint',
from: '', from: '',
timestamp: Date.now(), timestamp: timestamp || Date.now(),
issuedBy, issuedBy,
}; };
d.token.totalSupply = (d.token.totalSupply || 0) + amount; d.token.totalSupply = (d.token.totalSupply || 0) + amount;
@ -112,43 +170,107 @@ export function getTokenDoc(tokenId: string): TokenLedgerDoc | undefined {
return _syncServer!.getDoc<TokenLedgerDoc>(tokenDocId(tokenId)); return _syncServer!.getDoc<TokenLedgerDoc>(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() { export async function seedCUSDC() {
const tokenId = 'cusdc'; // Check if already seeded by looking at BFT token (last one created)
const doc = ensureTokenDoc(tokenId); const bftDoc = getTokenDoc('bft');
if (bftDoc && bftDoc.token.name && Object.keys(bftDoc.entries).length > 0) {
// Skip if already fully seeded (has entries) console.log('[TokenService] DAO ecosystem already seeded, skipping');
if (doc.token.name && Object.keys(doc.entries).length > 0) {
console.log('[TokenService] cUSDC already seeded, skipping');
return; 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); const docId = tokenDocId(tokenId);
if (!doc.token.name) { _syncServer!.changeDoc<TokenLedgerDoc>(docId, `define ${def.symbol} token`, (d) => {
_syncServer!.changeDoc<TokenLedgerDoc>(docId, 'define cUSDC token', (d) => { d.token.id = tokenId;
d.token.id = 'cusdc'; d.token.name = def.name;
d.token.name = 'CRDT USDC'; d.token.symbol = def.symbol;
d.token.symbol = 'cUSDC'; d.token.decimals = def.decimals;
d.token.decimals = 6; d.token.description = def.description;
d.token.description = 'CRDT-native stablecoin pegged to USDC, stored as an Automerge document'; d.token.icon = def.icon;
d.token.icon = '💵'; d.token.color = def.color;
d.token.color = '#2775ca';
d.token.createdAt = Date.now(); d.token.createdAt = Date.now();
d.token.createdBy = 'system'; 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. */ /** Mint $5 fUSDC welcome balance for a new user. */