feat(rwallet): integrate CRDT token flows into wallet visualizations
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
ac9bd302d1
commit
97bf2d7987
|
|
@ -68,6 +68,7 @@ const CHAIN_COLORS: Record<string, string> = {
|
|||
"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<string, string> = {
|
||||
"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,14 +715,14 @@ class FolkWalletViewer extends HTMLElement {
|
|||
const base = this.getApiBase();
|
||||
const chainDataMap = new Map<string, any>();
|
||||
|
||||
// Fan out requests to all detected chains
|
||||
// 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();
|
||||
// Parse results into incoming/outgoing
|
||||
const incoming: any[] = [];
|
||||
const outgoing: any[] = [];
|
||||
const results = data.results || [];
|
||||
|
|
@ -730,7 +730,6 @@ class FolkWalletViewer extends HTMLElement {
|
|||
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()) {
|
||||
|
|
@ -743,6 +742,83 @@ class FolkWalletViewer extends HTMLElement {
|
|||
} 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" });
|
||||
}
|
||||
}
|
||||
} 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 `
|
||||
|
|
|
|||
|
|
@ -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<string, number>();
|
||||
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<string, { from: string; value: number; symbol: string }>();
|
||||
const inflowAgg = new Map<string, { from: string; value: number; symbol: string; label?: string }>();
|
||||
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<string, { to: string; value: number; symbol: string }>();
|
||||
const outflowAgg = new Map<string, { to: string; value: number; symbol: string; label?: string }>();
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -72,6 +72,63 @@ export function getAllBalances(doc: TokenLedgerDoc): Record<string, { did: strin
|
|||
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. */
|
||||
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<TokenLedgerDoc>(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,45 +170,109 @@ export function getTokenDoc(tokenId: string): TokenLedgerDoc | undefined {
|
|||
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() {
|
||||
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<TokenLedgerDoc>(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<TokenLedgerDoc>(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. */
|
||||
export function mintWelcomeBalance(did: string, username: string): boolean {
|
||||
if (!_syncServer) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue