diff --git a/modules/rwallet/components/folk-wallet-viewer.ts b/modules/rwallet/components/folk-wallet-viewer.ts
index b81e691..cd3e37b 100644
--- a/modules/rwallet/components/folk-wallet-viewer.ts
+++ b/modules/rwallet/components/folk-wallet-viewer.ts
@@ -1538,6 +1538,172 @@ class FolkWalletViewer extends HTMLElement {
color: #e0e0e0;
}
.local-tokens-section table tr:last-child td { border-bottom: none; }
+
+ /* ── Payment Actions (Buy/Swap/Withdraw) ── */
+ .payment-actions {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
+ gap: 16px;
+ margin-top: 20px;
+ }
+ .action-card {
+ background: var(--rs-surface, #1a1a2e);
+ border: 1px solid rgba(255,255,255,0.08);
+ border-radius: 12px;
+ padding: 20px;
+ transition: border-color 0.2s;
+ }
+ .action-card:hover { border-color: rgba(255,255,255,0.15); }
+ .action-header {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-bottom: 8px;
+ }
+ .action-header h4 {
+ margin: 0;
+ font-size: 1rem;
+ color: #e0e0e0;
+ flex: 1;
+ }
+ .action-icon { font-size: 1.3em; }
+ .action-badge {
+ font-size: 0.7rem;
+ padding: 2px 8px;
+ border-radius: 20px;
+ background: rgba(39,117,202,0.15);
+ color: #6bb5ff;
+ border: 1px solid rgba(39,117,202,0.3);
+ }
+ .action-badge.curve {
+ background: rgba(34,197,94,0.12);
+ color: #4ade80;
+ border-color: rgba(34,197,94,0.3);
+ }
+ .action-desc {
+ font-size: 0.82rem;
+ color: #999;
+ margin: 0 0 12px;
+ line-height: 1.4;
+ }
+ .provider-hints {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+ margin-bottom: 12px;
+ }
+ .provider-tag {
+ font-size: 0.7rem;
+ padding: 2px 8px;
+ border-radius: 12px;
+ background: rgba(255,255,255,0.04);
+ color: #aaa;
+ border: 1px solid rgba(255,255,255,0.08);
+ }
+ .provider-tag.eu { color: #6bb5ff; border-color: rgba(39,117,202,0.25); }
+ .provider-tag.us { color: #a78bfa; border-color: rgba(167,139,250,0.25); }
+ .provider-tag.small { color: #fbbf24; border-color: rgba(251,191,36,0.25); }
+ .swap-balances {
+ display: flex;
+ gap: 16px;
+ font-size: 0.82rem;
+ color: #bbb;
+ margin-bottom: 12px;
+ }
+ .swap-balances strong { color: #e0e0e0; }
+ .swap-tabs {
+ display: flex;
+ gap: 4px;
+ margin-bottom: 12px;
+ background: rgba(255,255,255,0.04);
+ border-radius: 8px;
+ padding: 3px;
+ }
+ .swap-tab {
+ flex: 1;
+ padding: 6px;
+ font-size: 0.82rem;
+ border: none;
+ border-radius: 6px;
+ background: transparent;
+ color: #999;
+ cursor: pointer;
+ transition: all 0.2s;
+ }
+ .swap-tab.active {
+ background: rgba(34,197,94,0.15);
+ color: #4ade80;
+ }
+ .swap-quote {
+ font-size: 0.82rem;
+ color: #bbb;
+ padding: 8px;
+ margin-bottom: 8px;
+ background: rgba(255,255,255,0.02);
+ border-radius: 6px;
+ min-height: 20px;
+ }
+ .action-form {
+ margin-top: 12px;
+ }
+ .form-row {
+ margin-bottom: 10px;
+ }
+ .form-row label {
+ display: block;
+ font-size: 0.75rem;
+ color: #999;
+ margin-bottom: 4px;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ }
+ .action-input {
+ width: 100%;
+ padding: 8px 10px;
+ font-size: 0.88rem;
+ border: 1px solid rgba(255,255,255,0.1);
+ border-radius: 6px;
+ background: rgba(0,0,0,0.2);
+ color: #e0e0e0;
+ box-sizing: border-box;
+ }
+ .action-input:focus {
+ outline: none;
+ border-color: var(--rs-accent, #14b8a6);
+ }
+ .action-btn {
+ width: 100%;
+ padding: 10px;
+ font-size: 0.88rem;
+ font-weight: 600;
+ border: none;
+ border-radius: 8px;
+ cursor: pointer;
+ transition: all 0.2s;
+ }
+ .action-btn.primary {
+ background: var(--rs-accent, #14b8a6);
+ color: #000;
+ }
+ .action-btn.primary:hover { opacity: 0.9; }
+ .action-btn.outline {
+ background: transparent;
+ border: 1px solid rgba(255,255,255,0.15);
+ color: #ccc;
+ }
+ .action-btn.outline:hover {
+ background: rgba(255,255,255,0.05);
+ border-color: rgba(255,255,255,0.25);
+ }
+ .action-status {
+ font-size: 0.82rem;
+ margin-top: 8px;
+ min-height: 20px;
+ }
+ .action-status.success { color: #4ade80; }
+ .action-status.error { color: #f87171; }
+ .action-status.loading { color: #fbbf24; }
+
.link-error {
color: var(--rs-error); font-size: 12px; margin-top: 8px;
padding: 6px 10px; background: rgba(239,83,80,0.08); border-radius: 6px;
@@ -2510,6 +2676,116 @@ class FolkWalletViewer extends HTMLElement {
`;
}
+ private renderPaymentActions(): string {
+ if (!this.isAuthenticated) return "";
+
+ // Get cUSDC and $MYCO balances from CRDT
+ const cusdcBal = this.crdtBalances.find(b => b.symbol === 'cUSDC');
+ const mycoBal = this.crdtBalances.find(b => b.symbol === '$MYCO');
+ const cusdcFormatted = cusdcBal ? (cusdcBal.balance / Math.pow(10, cusdcBal.decimals)).toFixed(2) : '0.00';
+ const mycoFormatted = mycoBal ? (mycoBal.balance / Math.pow(10, mycoBal.decimals)).toFixed(2) : '0.00';
+
+ return `
+
+
+
+
+
Deposit fiat via bank transfer or card. Auto-routed to cheapest provider.
+
+ EU: Mollie (SEPA/iDEAL)
+ US: Stripe (ACH/Card)
+ < $50: Mt Pelerin (no KYC)
+
+
+
+
+
+
+
+
+
Swap cUSDC ↔ $MYCO on the CRDT bonding curve. Price rises with supply.
+
+ cUSDC: ${cusdcFormatted}
+ $MYCO: ${mycoFormatted}
+
+
+
+
+
+
+
+
+
Convert cUSDC back to fiat. Routed via SEPA (EU) or ACH (US).
+
+
+
+
`;
+ }
+
private renderDashboard(): string {
if (!this.hasData()) return "";
@@ -2569,7 +2845,7 @@ class FolkWalletViewer extends HTMLElement {
${this.renderViewTabs()}
${this.activeView === "balances"
- ? this.renderBalanceTable()
+ ? this.renderBalanceTable() + this.renderPaymentActions()
: this.activeView === "yield"
? this.renderYieldTab()
: `
@@ -2786,6 +3062,236 @@ class FolkWalletViewer extends HTMLElement {
});
}
+ private _swapAction: 'buy' | 'sell' = 'buy';
+
+ private attachPaymentListeners() {
+ // Toggle form visibility
+ const toggleBtn = (toggleId: string, formId: string) => {
+ const btn = this.shadow.querySelector(`#${toggleId}`);
+ const form = this.shadow.querySelector(`#${formId}`) as HTMLElement;
+ if (btn && form) {
+ btn.addEventListener('click', () => {
+ const visible = form.style.display !== 'none';
+ form.style.display = visible ? 'none' : 'block';
+ (btn as HTMLElement).style.display = visible ? 'block' : 'none';
+ });
+ }
+ };
+
+ toggleBtn('btn-toggle-buy', 'buy-cusdc-form');
+ toggleBtn('btn-toggle-swap', 'myco-swap-form');
+ toggleBtn('btn-toggle-withdraw', 'withdraw-form');
+
+ // Buy cUSDC
+ this.shadow.querySelector('#btn-buy-cusdc')?.addEventListener('click', () => this.handleBuyCUSDC());
+
+ // $MYCO Swap tabs
+ this.shadow.querySelectorAll('.swap-tab').forEach(tab => {
+ tab.addEventListener('click', () => {
+ this._swapAction = (tab as HTMLElement).dataset.swap as 'buy' | 'sell';
+ this.shadow.querySelectorAll('.swap-tab').forEach(t => t.classList.remove('active'));
+ tab.classList.add('active');
+ const label = this.shadow.querySelector('#swap-input-label');
+ if (label) label.textContent = this._swapAction === 'buy' ? 'cUSDC Amount' : '$MYCO Amount';
+ this.updateSwapQuote();
+ });
+ });
+
+ // Live quote on input
+ this.shadow.querySelector('#swap-amount')?.addEventListener('input', () => this.updateSwapQuote());
+
+ // Execute swap
+ this.shadow.querySelector('#btn-swap-execute')?.addEventListener('click', () => this.handleSwapExecute());
+
+ // Withdraw
+ this.shadow.querySelector('#btn-withdraw-execute')?.addEventListener('click', () => this.handleWithdraw());
+ }
+
+ private _quoteTimer: ReturnType | null = null;
+
+ private async updateSwapQuote() {
+ const input = this.shadow.querySelector('#swap-amount') as HTMLInputElement;
+ const quoteEl = this.shadow.querySelector('#swap-quote');
+ if (!input || !quoteEl) return;
+
+ const amount = parseFloat(input.value);
+ if (!amount || amount <= 0) {
+ quoteEl.textContent = '';
+ return;
+ }
+
+ // Debounce quote requests
+ if (this._quoteTimer) clearTimeout(this._quoteTimer);
+ this._quoteTimer = setTimeout(async () => {
+ await this._fetchQuote(amount, quoteEl);
+ }, 300);
+ }
+
+ private async _fetchQuote(amount: number, quoteEl: Element) {
+ try {
+ const base = this.getApiBase();
+ const resp = await fetch(`${base}/api/crdt-tokens/myco/quote?action=${this._swapAction}&amount=${amount}`);
+ const data = await resp.json();
+ if (data.error) {
+ quoteEl.innerHTML = `${data.error}`;
+ return;
+ }
+ const outToken = this._swapAction === 'buy' ? '$MYCO' : 'cUSDC';
+ const feeStr = data.fee ? ` | Fee: ${data.fee.amount.toFixed(4)} cUSDC` : '';
+ quoteEl.innerHTML = `You receive: ${data.output.amount.toFixed(4)} ${outToken} | Price: ${data.pricePerToken.toFixed(6)} cUSDC/${this._swapAction === 'buy' ? '$MYCO' : 'cUSDC'} | Impact: ${data.priceImpact}%${feeStr}`;
+ } catch {
+ quoteEl.innerHTML = 'Quote unavailable';
+ }
+ }
+
+ private async handleBuyCUSDC() {
+ const amountInput = this.shadow.querySelector('#buy-amount') as HTMLInputElement;
+ const countrySelect = this.shadow.querySelector('#buy-country') as HTMLSelectElement;
+ const statusEl = this.shadow.querySelector('#buy-cusdc-status')!;
+ const amount = parseFloat(amountInput?.value || '0');
+
+ if (!amount || amount < 1) {
+ statusEl.innerHTML = 'Enter amount >= $1.00';
+ return;
+ }
+
+ statusEl.innerHTML = 'Creating payment...';
+
+ try {
+ const resp = await fetch(`/api/onramp/hs/create-intent`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ amount: Math.round(amount * 100), // cents
+ currency: 'usd',
+ did: this.getDid(),
+ label: this.getLabel(),
+ billingCountry: countrySelect?.value || undefined,
+ }),
+ });
+ const data = await resp.json();
+
+ if (data.error) {
+ statusEl.innerHTML = `${data.error}`;
+ return;
+ }
+
+ statusEl.innerHTML = `Payment created! Redirecting to checkout...`;
+ setTimeout(() => {
+ statusEl.innerHTML = `Payment ID: ${data.paymentId}. Complete payment at pay.rspace.online`;
+ }, 1500);
+ } catch {
+ statusEl.innerHTML = `Failed to create payment`;
+ }
+ }
+
+ private async handleSwapExecute() {
+ const input = this.shadow.querySelector('#swap-amount') as HTMLInputElement;
+ const statusEl = this.shadow.querySelector('#swap-status')!;
+ const amount = parseFloat(input?.value || '0');
+
+ if (!amount || amount <= 0) {
+ statusEl.innerHTML = 'Enter a valid amount';
+ return;
+ }
+
+ statusEl.innerHTML = 'Processing swap...';
+
+ try {
+ const base = this.getApiBase();
+ const endpoint = this._swapAction === 'buy'
+ ? `${base}/api/crdt-tokens/myco/buy`
+ : `${base}/api/crdt-tokens/myco/sell`;
+ const body = this._swapAction === 'buy'
+ ? { cUSDCAmount: amount }
+ : { mycoAmount: amount };
+
+ const token = this.getAuthToken();
+ const resp = await fetch(endpoint, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(token ? { 'Authorization': `Bearer ${token}` } : {}),
+ },
+ body: JSON.stringify(body),
+ });
+ const data = await resp.json();
+
+ if (data.error) {
+ statusEl.innerHTML = `${data.error}`;
+ return;
+ }
+
+ statusEl.innerHTML = `Swapped! Received ${data.received.amount.toFixed(4)} ${data.received.token}`;
+ this.loadCRDTBalances();
+ } catch {
+ statusEl.innerHTML = `Swap failed`;
+ }
+ }
+
+ private async handleWithdraw() {
+ const amountInput = this.shadow.querySelector('#withdraw-amount') as HTMLInputElement;
+ const countrySelect = this.shadow.querySelector('#withdraw-country') as HTMLSelectElement;
+ const ibanInput = this.shadow.querySelector('#withdraw-iban') as HTMLInputElement;
+ const statusEl = this.shadow.querySelector('#withdraw-status')!;
+ const amount = parseFloat(amountInput?.value || '0');
+
+ if (!amount || amount < 10) {
+ statusEl.innerHTML = 'Minimum withdrawal: $10.00';
+ return;
+ }
+ if (!ibanInput?.value) {
+ statusEl.innerHTML = 'Enter bank account details';
+ return;
+ }
+
+ statusEl.innerHTML = 'Initiating withdrawal...';
+
+ try {
+ const resp = await fetch(`/api/offramp/hs/initiate`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ did: this.getDid(),
+ label: this.getLabel(),
+ amount,
+ currency: 'usd',
+ bankCountry: countrySelect?.value || 'US',
+ bankDetails: { iban: ibanInput.value },
+ }),
+ });
+ const data = await resp.json();
+
+ if (data.error) {
+ statusEl.innerHTML = `${data.error}`;
+ return;
+ }
+
+ statusEl.innerHTML = `Withdrawal initiated! Est. arrival: ${new Date(data.estimatedArrival).toLocaleDateString()}`;
+ this.loadCRDTBalances();
+ } catch {
+ statusEl.innerHTML = `Withdrawal failed`;
+ }
+ }
+
+ private getDid(): string {
+ try {
+ const token = this.getAuthToken();
+ if (!token) return '';
+ const payload = JSON.parse(atob(token.split('.')[1]));
+ return payload.did || payload.sub || '';
+ } catch { return ''; }
+ }
+
+ private getLabel(): string {
+ try {
+ const token = this.getAuthToken();
+ if (!token) return '';
+ const payload = JSON.parse(atob(token.split('.')[1]));
+ return payload.username || payload.name || 'Unknown';
+ } catch { return 'Unknown'; }
+ }
+
private render() {
this.shadow.innerHTML = `
${this.renderStyles()}
@@ -2793,6 +3299,7 @@ class FolkWalletViewer extends HTMLElement {
`;
this.attachVisualizerListeners();
+ this.attachPaymentListeners();
this._tour.renderOverlay();
}
diff --git a/modules/rwallet/mod.ts b/modules/rwallet/mod.ts
index 353efcb..bbb42e9 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, getAllTransfers } from "../../server/token-service";
+import { getTokenDoc, listTokenDocs, getAllBalances, getBalance, mintTokens, burnTokens, burnTokensEscrow, getAllTransfers } from "../../server/token-service";
import { tokenDocId } from "../../server/token-schemas";
import type { TokenLedgerDoc } from "../../server/token-schemas";
@@ -895,6 +895,197 @@ routes.get("/api/crdt-tokens/transfers", (c) => {
return c.json({ entries });
});
+// ── $MYCO Bonding Curve (cUSDC ↔ $MYCO swaps) ──
+import { calculatePrice, calculateBuyReturn, calculateSellReturn, getCurveConfig } from "../../server/bonding-curve";
+
+/** Get current $MYCO supply from the CRDT ledger. */
+function getMycoSupply(): number {
+ const doc = getTokenDoc("myco");
+ if (!doc || !doc.token.name) return 0;
+ return doc.token.totalSupply || 0;
+}
+
+// Quote endpoint — no auth required
+routes.get("/api/crdt-tokens/myco/quote", (c) => {
+ const action = c.req.query("action");
+ const amountStr = c.req.query("amount");
+ if (!action || !amountStr) {
+ return c.json({ error: "action (buy|sell) and amount are required" }, 400);
+ }
+ const amount = parseFloat(amountStr);
+ if (isNaN(amount) || amount <= 0) {
+ return c.json({ error: "amount must be a positive number" }, 400);
+ }
+
+ const supply = getMycoSupply();
+ const amountBaseUnits = Math.round(amount * 1_000_000);
+
+ if (action === "buy") {
+ const result = calculateBuyReturn(amountBaseUnits, supply);
+ return c.json({
+ action: "buy",
+ input: { amount, token: "cUSDC" },
+ output: { amount: result.tokensOut / 1_000_000, token: "$MYCO", baseUnits: result.tokensOut },
+ pricePerToken: result.averagePrice / 1_000_000,
+ priceImpact: result.priceImpact,
+ currentSupply: supply / 1_000_000,
+ config: getCurveConfig(),
+ });
+ } else if (action === "sell") {
+ if (amountBaseUnits > supply) {
+ return c.json({ error: "Cannot sell more than current supply" }, 400);
+ }
+ const result = calculateSellReturn(amountBaseUnits, supply);
+ return c.json({
+ action: "sell",
+ input: { amount, token: "$MYCO" },
+ output: { amount: result.cUSDCOut / 1_000_000, token: "cUSDC", baseUnits: result.cUSDCOut },
+ fee: { amount: result.fee / 1_000_000, bps: 50 },
+ pricePerToken: result.averagePrice / 1_000_000,
+ priceImpact: result.priceImpact,
+ currentSupply: supply / 1_000_000,
+ config: getCurveConfig(),
+ });
+ }
+ return c.json({ error: 'action must be "buy" or "sell"' }, 400);
+});
+
+// Buy $MYCO with cUSDC (authenticated)
+routes.post("/api/crdt-tokens/myco/buy", async (c) => {
+ const claims = await verifyWalletAuth(c);
+ if (!claims) return c.json({ error: "Authentication required" }, 401);
+
+ const body = await c.req.json<{ cUSDCAmount?: number }>();
+ const { cUSDCAmount } = body;
+ if (!cUSDCAmount || cUSDCAmount <= 0) {
+ return c.json({ error: "cUSDCAmount (positive number in display units) required" }, 400);
+ }
+
+ const did = (claims as any).did || claims.sub;
+ const amountBaseUnits = Math.round(cUSDCAmount * 1_000_000);
+ const supply = getMycoSupply();
+
+ // Check user has enough cUSDC
+ const cusdcDoc = getTokenDoc("cusdc");
+ if (!cusdcDoc) return c.json({ error: "cUSDC token not found" }, 500);
+ const cusdcBalance = getBalance(cusdcDoc, did);
+ if (cusdcBalance < amountBaseUnits) {
+ return c.json({ error: `Insufficient cUSDC: have ${cusdcBalance / 1_000_000}, need ${cUSDCAmount}` }, 400);
+ }
+
+ // Calculate swap
+ const result = calculateBuyReturn(amountBaseUnits, supply);
+ if (result.tokensOut <= 0) {
+ return c.json({ error: "Amount too small to produce tokens" }, 400);
+ }
+
+ // Atomic-ish: burn cUSDC, mint $MYCO with shared swap ID
+ const swapId = `swap-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
+ const burnOk = burnTokens("cusdc", did, "", amountBaseUnits, `Bonding curve buy: ${swapId}`, "bonding-curve");
+ if (!burnOk) return c.json({ error: "Failed to burn cUSDC" }, 500);
+
+ const mintOk = mintTokens("myco", did, "", result.tokensOut, `Bonding curve buy: ${swapId}`, "bonding-curve");
+ if (!mintOk) {
+ // Compensate: re-mint the burned cUSDC
+ mintTokens("cusdc", did, "", amountBaseUnits, `Refund: failed swap ${swapId}`, "bonding-curve");
+ return c.json({ error: "Failed to mint $MYCO, cUSDC refunded" }, 500);
+ }
+
+ return c.json({
+ ok: true,
+ swapId,
+ burned: { token: "cUSDC", amount: amountBaseUnits / 1_000_000 },
+ received: { token: "$MYCO", amount: result.tokensOut / 1_000_000 },
+ pricePerToken: result.averagePrice / 1_000_000,
+ priceImpact: result.priceImpact,
+ });
+});
+
+// Sell $MYCO for cUSDC (authenticated)
+routes.post("/api/crdt-tokens/myco/sell", async (c) => {
+ const claims = await verifyWalletAuth(c);
+ if (!claims) return c.json({ error: "Authentication required" }, 401);
+
+ const body = await c.req.json<{ mycoAmount?: number }>();
+ const { mycoAmount } = body;
+ if (!mycoAmount || mycoAmount <= 0) {
+ return c.json({ error: "mycoAmount (positive number in display units) required" }, 400);
+ }
+
+ const did = (claims as any).did || claims.sub;
+ const amountBaseUnits = Math.round(mycoAmount * 1_000_000);
+ const supply = getMycoSupply();
+
+ // Check user has enough $MYCO
+ const mycoDoc = getTokenDoc("myco");
+ if (!mycoDoc) return c.json({ error: "$MYCO token not found" }, 500);
+ const mycoBalance = getBalance(mycoDoc, did);
+ if (mycoBalance < amountBaseUnits) {
+ return c.json({ error: `Insufficient $MYCO: have ${mycoBalance / 1_000_000}, need ${mycoAmount}` }, 400);
+ }
+
+ // Calculate swap
+ const result = calculateSellReturn(amountBaseUnits, supply);
+ if (result.cUSDCOut <= 0) {
+ return c.json({ error: "Amount too small to produce cUSDC" }, 400);
+ }
+
+ // Atomic-ish: burn $MYCO, mint cUSDC with shared swap ID
+ const swapId = `swap-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
+ const burnOk = burnTokens("myco", did, "", amountBaseUnits, `Bonding curve sell: ${swapId}`, "bonding-curve");
+ if (!burnOk) return c.json({ error: "Failed to burn $MYCO" }, 500);
+
+ const mintOk = mintTokens("cusdc", did, "", result.cUSDCOut, `Bonding curve sell: ${swapId}`, "bonding-curve");
+ if (!mintOk) {
+ // Compensate: re-mint the burned $MYCO
+ mintTokens("myco", did, "", amountBaseUnits, `Refund: failed swap ${swapId}`, "bonding-curve");
+ return c.json({ error: "Failed to mint cUSDC, $MYCO refunded" }, 500);
+ }
+
+ return c.json({
+ ok: true,
+ swapId,
+ burned: { token: "$MYCO", amount: amountBaseUnits / 1_000_000 },
+ received: { token: "cUSDC", amount: result.cUSDCOut / 1_000_000 },
+ fee: { token: "cUSDC", amount: result.fee / 1_000_000, bps: 50 },
+ pricePerToken: result.averagePrice / 1_000_000,
+ priceImpact: result.priceImpact,
+ });
+});
+
+// Settlement state — for bonding-curve-service cron to read CRDT state for on-chain sync
+routes.get("/api/crdt-tokens/myco/settlement-state", (c) => {
+ const mycoDoc = getTokenDoc("myco");
+ const cusdcDoc = getTokenDoc("cusdc");
+
+ const mycoSupply = mycoDoc?.token.totalSupply || 0;
+ const currentPrice = calculatePrice(mycoSupply);
+
+ // Calculate reserve: sum of all cUSDC burned via bonding curve buys
+ let reserveBalance = 0;
+ if (cusdcDoc) {
+ for (const entry of Object.values(cusdcDoc.entries)) {
+ if (entry.type === 'burn' && entry.memo.startsWith('Bonding curve buy:')) {
+ reserveBalance += entry.amount;
+ } else if (entry.type === 'mint' && entry.memo.startsWith('Bonding curve sell:')) {
+ reserveBalance -= entry.amount;
+ }
+ }
+ }
+
+ return c.json({
+ mycoSupply: mycoSupply / 1_000_000,
+ mycoSupplyBaseUnits: mycoSupply,
+ reserveBalance: reserveBalance / 1_000_000,
+ reserveBalanceBaseUnits: reserveBalance,
+ currentPrice: currentPrice / 1_000_000,
+ currentPriceBaseUnits: currentPrice,
+ marketCap: (mycoSupply * currentPrice) / (1_000_000 * 1_000_000),
+ config: getCurveConfig(),
+ timestamp: Date.now(),
+ });
+});
+
// ── Yield API routes ──
import { getYieldRates } from "./lib/yield-rates";
import { getYieldPositions } from "./lib/yield-positions";
diff --git a/scripts/test-full-loop.ts b/scripts/test-full-loop.ts
new file mode 100644
index 0000000..46e9cc3
--- /dev/null
+++ b/scripts/test-full-loop.ts
@@ -0,0 +1,204 @@
+#!/usr/bin/env bun
+/**
+ * Full-loop integration test: fiat in → cUSDC → $MYCO → cUSDC → fiat out.
+ *
+ * Tests all 5 phases of the HyperSwitch payment orchestrator integration.
+ * Run against a local dev instance with: bun scripts/test-full-loop.ts
+ *
+ * Expects:
+ * - rspace-online running at BASE_URL (default: http://localhost:3000)
+ * - payment-infra services running (or mock responses)
+ * - INTERNAL_API_KEY set for internal endpoints
+ */
+
+const BASE_URL = process.env.BASE_URL || "http://localhost:3000";
+const INTERNAL_KEY = process.env.INTERNAL_API_KEY || "test-internal-key";
+const TEST_DID = process.env.TEST_DID || "did:key:test-full-loop-2026";
+const TEST_LABEL = "test-user";
+
+type StepResult = { ok: boolean; data?: any; error?: string };
+
+async function step(name: string, fn: () => Promise) {
+ process.stdout.write(` ${name}... `);
+ try {
+ const result = await fn();
+ if (result.ok) {
+ console.log("OK", result.data ? JSON.stringify(result.data).slice(0, 120) : "");
+ } else {
+ console.log("FAIL", result.error || JSON.stringify(result.data));
+ }
+ return result;
+ } catch (err: any) {
+ console.log("ERROR", err.message);
+ return { ok: false, error: err.message };
+ }
+}
+
+async function main() {
+ console.log("\n=== HyperSwitch Full Loop Test ===\n");
+ console.log(`Base URL: ${BASE_URL}`);
+ console.log(`Test DID: ${TEST_DID}\n`);
+
+ // ── Phase 1: Health check ──
+ console.log("Phase 1: Infrastructure");
+ await step("rspace-online health", async () => {
+ const resp = await fetch(`${BASE_URL}/api/communities`);
+ return { ok: resp.ok, data: { status: resp.status } };
+ });
+
+ // ── Phase 2: Fiat on-ramp (simulate mint-crdt call) ──
+ console.log("\nPhase 2: Fiat On-Ramp (cUSDC mint)");
+
+ const mintResult = await step("POST /api/internal/mint-crdt", async () => {
+ const resp = await fetch(`${BASE_URL}/api/internal/mint-crdt`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "X-Internal-Key": INTERNAL_KEY,
+ },
+ body: JSON.stringify({
+ did: TEST_DID,
+ label: TEST_LABEL,
+ amountDecimal: "100.000000",
+ txHash: `hs-test-${Date.now()}`,
+ network: "hyperswitch:fiat:usd",
+ }),
+ });
+ const data = await resp.json();
+ return { ok: resp.ok && data.ok, data };
+ });
+
+ await step("Idempotency check (same txHash)", async () => {
+ const resp = await fetch(`${BASE_URL}/api/internal/mint-crdt`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "X-Internal-Key": INTERNAL_KEY,
+ },
+ body: JSON.stringify({
+ did: TEST_DID,
+ label: TEST_LABEL,
+ amountDecimal: "100.000000",
+ txHash: mintResult.data?.txHash || `hs-test-idempotent`,
+ network: "hyperswitch:fiat:usd",
+ }),
+ });
+ const data = await resp.json();
+ // Should return ok:false because already minted
+ return { ok: resp.ok && !data.ok, data };
+ });
+
+ // ── Phase 3: $MYCO bonding curve ──
+ console.log("\nPhase 3: $MYCO Bonding Curve");
+
+ await step("GET /api/crdt-tokens/myco/quote?action=buy&amount=10", async () => {
+ const resp = await fetch(`${BASE_URL}/demo/rwallet/api/crdt-tokens/myco/quote?action=buy&amount=10`);
+ const data = await resp.json();
+ return {
+ ok: resp.ok && data.output?.amount > 0,
+ data: { output: data.output?.amount, price: data.pricePerToken, impact: data.priceImpact },
+ };
+ });
+
+ await step("GET /api/crdt-tokens/myco/quote?action=sell&amount=100", async () => {
+ const resp = await fetch(`${BASE_URL}/demo/rwallet/api/crdt-tokens/myco/quote?action=sell&amount=100`);
+ const data = await resp.json();
+ return {
+ ok: resp.ok && data.output?.amount > 0,
+ data: { output: data.output?.amount, fee: data.fee?.amount, impact: data.priceImpact },
+ };
+ });
+
+ await step("GET /api/crdt-tokens/myco/settlement-state", async () => {
+ const resp = await fetch(`${BASE_URL}/demo/rwallet/api/crdt-tokens/myco/settlement-state`);
+ const data = await resp.json();
+ return {
+ ok: resp.ok && data.mycoSupply >= 0,
+ data: { supply: data.mycoSupply, price: data.currentPrice, reserve: data.reserveBalance },
+ };
+ });
+
+ // ── Phase 4: Off-ramp (simulate escrow + confirm) ──
+ console.log("\nPhase 4: Fiat Off-Ramp (escrow burn)");
+
+ const offRampId = `offramp-test-${Date.now()}`;
+ await step("POST /api/internal/escrow-burn", async () => {
+ const resp = await fetch(`${BASE_URL}/api/internal/escrow-burn`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "X-Internal-Key": INTERNAL_KEY,
+ },
+ body: JSON.stringify({
+ did: TEST_DID,
+ label: TEST_LABEL,
+ amount: 10_000_000, // 10 cUSDC
+ offRampId,
+ }),
+ });
+ const data = await resp.json();
+ return { ok: resp.ok && data.ok, data };
+ });
+
+ await step("POST /api/internal/confirm-offramp (confirmed)", async () => {
+ const resp = await fetch(`${BASE_URL}/api/internal/confirm-offramp`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "X-Internal-Key": INTERNAL_KEY,
+ },
+ body: JSON.stringify({ offRampId, status: "confirmed" }),
+ });
+ const data = await resp.json();
+ return { ok: resp.ok && data.ok, data };
+ });
+
+ // Test reversal flow
+ const offRampId2 = `offramp-test-reverse-${Date.now()}`;
+ await step("Escrow + reverse flow", async () => {
+ // Escrow
+ const escrowResp = await fetch(`${BASE_URL}/api/internal/escrow-burn`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "X-Internal-Key": INTERNAL_KEY,
+ },
+ body: JSON.stringify({
+ did: TEST_DID,
+ label: TEST_LABEL,
+ amount: 5_000_000, // 5 cUSDC
+ offRampId: offRampId2,
+ }),
+ });
+ if (!escrowResp.ok) return { ok: false, error: "escrow failed" };
+
+ // Reverse
+ const reverseResp = await fetch(`${BASE_URL}/api/internal/confirm-offramp`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "X-Internal-Key": INTERNAL_KEY,
+ },
+ body: JSON.stringify({ offRampId: offRampId2, status: "reversed" }),
+ });
+ const data = await reverseResp.json();
+ return { ok: reverseResp.ok && data.ok, data: { action: "reversed", refunded: "5 cUSDC" } };
+ });
+
+ // ── Phase 5: CRDT token list ──
+ console.log("\nPhase 5: Token Verification");
+
+ await step("GET /api/crdt-tokens (list all tokens)", async () => {
+ const resp = await fetch(`${BASE_URL}/demo/rwallet/api/crdt-tokens`);
+ const data = await resp.json();
+ const symbols = (data.tokens || []).map((t: any) => t.symbol);
+ return {
+ ok: resp.ok && symbols.includes("cUSDC") && symbols.includes("$MYCO"),
+ data: { tokens: symbols },
+ };
+ });
+
+ console.log("\n=== Full Loop Test Complete ===\n");
+}
+
+main().catch(console.error);
diff --git a/server/bonding-curve.ts b/server/bonding-curve.ts
new file mode 100644
index 0000000..a62ac46
--- /dev/null
+++ b/server/bonding-curve.ts
@@ -0,0 +1,97 @@
+/**
+ * CRDT-native bonding curve — pure math functions for $MYCO ↔ cUSDC swaps.
+ *
+ * Polynomial curve: price = basePrice + coefficient × supply²
+ * Ported from payment-infra/services/bonding-curve-service for CRDT ledger integration.
+ *
+ * All amounts in base units:
+ * - cUSDC: 6 decimals (1_000_000 = $1.00)
+ * - $MYCO: 6 decimals (using 6 effective decimals same as cUSDC for simplicity)
+ */
+
+// ── Curve parameters ──
+
+/** Base price in cUSDC units (6 decimals) — $0.01 */
+const BASE_PRICE = 10_000;
+
+/** Coefficient — controls curve steepness */
+const COEFFICIENT = 0.000001;
+
+/** Exponent — quadratic curve */
+const EXPONENT = 2;
+
+/** Sell fee in basis points (0.5% = 50 bps) */
+const SELL_FEE_BPS = 50;
+
+// ── Price functions ──
+
+/** Calculate token price at a given supply (both in base units). Returns cUSDC base units per MYCO. */
+export function calculatePrice(supplyBaseUnits: number): number {
+ const supplyTokens = supplyBaseUnits / 1_000_000;
+ return Math.round(BASE_PRICE + COEFFICIENT * Math.pow(supplyTokens, EXPONENT) * 1_000_000);
+}
+
+/** Calculate tokens received for cUSDC input (buy). */
+export function calculateBuyReturn(cUSDCAmount: number, currentSupply: number): {
+ tokensOut: number;
+ averagePrice: number;
+ priceImpact: number;
+ endPrice: number;
+} {
+ const startPrice = calculatePrice(currentSupply);
+
+ // First estimate with start price
+ const estimatedTokens = Math.floor((cUSDCAmount * 1_000_000) / startPrice);
+ const endPrice = calculatePrice(currentSupply + estimatedTokens);
+ const averagePrice = Math.round((startPrice + endPrice) / 2);
+
+ // Recalculate with average price
+ const tokensOut = Math.floor((cUSDCAmount * 1_000_000) / averagePrice);
+
+ // Price impact as percentage (0-100)
+ const priceImpact = startPrice > 0
+ ? Number(((endPrice - startPrice) / startPrice * 100).toFixed(2))
+ : 0;
+
+ return { tokensOut, averagePrice, priceImpact, endPrice };
+}
+
+/** Calculate cUSDC received for $MYCO input (sell), after fee. */
+export function calculateSellReturn(mycoAmount: number, currentSupply: number): {
+ cUSDCOut: number;
+ grossAmount: number;
+ fee: number;
+ averagePrice: number;
+ priceImpact: number;
+ endPrice: number;
+} {
+ if (mycoAmount > currentSupply) {
+ throw new Error('Cannot sell more than current supply');
+ }
+
+ const startPrice = calculatePrice(currentSupply);
+ const endPrice = calculatePrice(currentSupply - mycoAmount);
+ const averagePrice = Math.round((startPrice + endPrice) / 2);
+
+ const grossAmount = Math.floor((mycoAmount * averagePrice) / 1_000_000);
+ const fee = Math.floor((grossAmount * SELL_FEE_BPS) / 10_000);
+ const cUSDCOut = grossAmount - fee;
+
+ const priceImpact = startPrice > 0
+ ? Number(((startPrice - endPrice) / startPrice * 100).toFixed(2))
+ : 0;
+
+ return { cUSDCOut, grossAmount, fee, averagePrice, priceImpact, endPrice };
+}
+
+/** Get current curve configuration (for UI display). */
+export function getCurveConfig() {
+ return {
+ basePrice: BASE_PRICE,
+ coefficient: COEFFICIENT,
+ exponent: EXPONENT,
+ sellFeeBps: SELL_FEE_BPS,
+ priceDecimals: 6,
+ tokenDecimals: 6,
+ };
+}
diff --git a/server/token-schemas.ts b/server/token-schemas.ts
index 07a27ad..33905e6 100644
--- a/server/token-schemas.ts
+++ b/server/token-schemas.ts
@@ -21,6 +21,10 @@ export interface LedgerEntry {
issuedBy: string;
txHash?: string;
onChainNetwork?: string;
+ /** Off-ramp ID for escrow burns */
+ offRampId?: string;
+ /** Escrow status: 'escrow' → 'confirmed' | 'reversed' */
+ status?: 'escrow' | 'confirmed' | 'reversed';
}
export interface TokenDefinition {
diff --git a/server/token-service.ts b/server/token-service.ts
index e9472ec..cc6193e 100644
--- a/server/token-service.ts
+++ b/server/token-service.ts
@@ -29,10 +29,12 @@ function ensureTokenDoc(tokenId: string): TokenLedgerDoc {
return doc;
}
-/** Sum all entry amounts for a given DID holder. */
+/** Sum all entry amounts for a given DID holder. Excludes reversed burns. */
export function getBalance(doc: TokenLedgerDoc, did: string): number {
let balance = 0;
for (const entry of Object.values(doc.entries)) {
+ // Skip reversed burns (the compensating mint handles the refund)
+ if (entry.type === 'burn' && (entry as any).status === 'reversed') continue;
if (entry.type === 'mint' && entry.to === did) {
balance += entry.amount;
} else if (entry.type === 'transfer') {
@@ -206,6 +208,118 @@ export function mintFromOnChain(
return success;
}
+/** Burn tokens from a DID. */
+export function burnTokens(
+ tokenId: string,
+ fromDid: string,
+ fromLabel: string,
+ amount: number,
+ memo: string,
+ issuedBy: string,
+ timestamp?: number,
+): boolean {
+ const docId = tokenDocId(tokenId);
+ ensureTokenDoc(tokenId);
+
+ const entryId = `burn-${timestamp || Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
+ const result = _syncServer!.changeDoc(docId, `burn ${amount} from ${fromLabel}`, (d) => {
+ d.entries[entryId] = {
+ id: entryId,
+ to: '',
+ toLabel: '',
+ amount,
+ memo,
+ type: 'burn',
+ from: fromDid,
+ timestamp: timestamp || Date.now(),
+ issuedBy,
+ };
+ d.token.totalSupply = Math.max(0, (d.token.totalSupply || 0) - amount);
+ });
+ return result !== null;
+}
+
+/**
+ * Burn tokens into escrow (Phase 4 off-ramp).
+ * Creates a burn entry with offRampId + status:'escrow'. Balance reduced immediately.
+ */
+export function burnTokensEscrow(
+ tokenId: string,
+ fromDid: string,
+ fromLabel: string,
+ amount: number,
+ offRampId: string,
+ memo: string,
+): boolean {
+ const docId = tokenDocId(tokenId);
+ ensureTokenDoc(tokenId);
+
+ const entryId = `escrow-${offRampId}`;
+ const result = _syncServer!.changeDoc(docId, `escrow burn ${amount} from ${fromLabel}`, (d) => {
+ d.entries[entryId] = {
+ id: entryId,
+ to: '',
+ toLabel: '',
+ amount,
+ memo,
+ type: 'burn',
+ from: fromDid,
+ timestamp: Date.now(),
+ issuedBy: 'offramp-service',
+ offRampId,
+ status: 'escrow',
+ } as any; // extended fields
+ d.token.totalSupply = Math.max(0, (d.token.totalSupply || 0) - amount);
+ });
+ return result !== null;
+}
+
+/** Confirm an escrow burn (payout succeeded). */
+export function confirmBurn(tokenId: string, offRampId: string): boolean {
+ const docId = tokenDocId(tokenId);
+ const doc = ensureTokenDoc(tokenId);
+ const entryId = `escrow-${offRampId}`;
+ if (!doc.entries[entryId]) return false;
+
+ const result = _syncServer!.changeDoc(docId, `confirm escrow ${offRampId}`, (d) => {
+ if (d.entries[entryId]) {
+ (d.entries[entryId] as any).status = 'confirmed';
+ }
+ });
+ return result !== null;
+}
+
+/** Reverse an escrow burn (payout failed) — mints tokens back to the original holder. */
+export function reverseBurn(tokenId: string, offRampId: string): boolean {
+ const docId = tokenDocId(tokenId);
+ const doc = ensureTokenDoc(tokenId);
+ const entryId = `escrow-${offRampId}`;
+ const entry = doc.entries[entryId];
+ if (!entry) return false;
+
+ const result = _syncServer!.changeDoc(docId, `reverse escrow ${offRampId}`, (d) => {
+ // Mark original burn as reversed
+ if (d.entries[entryId]) {
+ (d.entries[entryId] as any).status = 'reversed';
+ }
+ // Compensating mint to refund the holder
+ const refundId = `refund-${offRampId}`;
+ d.entries[refundId] = {
+ id: refundId,
+ to: entry.from,
+ toLabel: '',
+ amount: entry.amount,
+ memo: `Refund: off-ramp ${offRampId} reversed`,
+ type: 'mint',
+ from: '',
+ timestamp: Date.now(),
+ issuedBy: 'offramp-service',
+ };
+ d.token.totalSupply = (d.token.totalSupply || 0) + entry.amount;
+ });
+ return result !== null;
+}
+
/** List all token doc IDs. */
export function listTokenDocs(): string[] {
return _syncServer!.listDocs().filter((id) => id.startsWith('global:tokens:ledgers:'));
@@ -218,9 +332,9 @@ export function getTokenDoc(tokenId: string): TokenLedgerDoc | undefined {
/** 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) {
+ // Check if already seeded by looking at $MYCO token (last one created)
+ const mycoDoc = getTokenDoc('myco');
+ if (mycoDoc && mycoDoc.token.name && Object.keys(mycoDoc.entries).length > 0) {
console.log('[TokenService] DAO ecosystem already seeded, skipping');
return;
}
@@ -269,36 +383,35 @@ export async function seedCUSDC() {
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',
+ // ── 2. $MYCO — Governance token (bonding curve) ──
+ ensureTokenDef('myco', {
+ name: 'MYCO Token', symbol: '$MYCO', decimals: 6,
+ description: 'Governance token for the rSpace DAO — bonding curve with cUSDC reserve, used for voting and delegation',
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)
+ const myco = (n: number) => n * 1_000_000; // 6 effective decimals
// Genesis mint to treasury
- mintTokens('bft', treasuryDid, 'DAO Treasury', bft(10_000_000), 'Genesis: 10M BFT', 'system', t(58));
+ mintTokens('myco', treasuryDid, 'DAO Treasury', myco(10_000_000), 'Genesis: 10M $MYCO', '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));
+ transferTokens('myco', treasuryDid, 'DAO Treasury', grantsDid, 'Grants Committee', myco(2_000_000), 'Grants committee allocation', 'system', t(56));
+ transferTokens('myco', treasuryDid, 'DAO Treasury', devFundDid, 'Dev Fund', myco(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));
+ transferTokens('myco', treasuryDid, 'DAO Treasury', jeffDid, 'jeff', myco(500_000), 'Founder vesting — month 1', 'system', t(50));
+ transferTokens('myco', grantsDid, 'Grants Committee', aliceDid, 'Alice', myco(100_000), 'Contributor reward: sync engine', 'system', t(46));
+ transferTokens('myco', grantsDid, 'Grants Committee', bobDid, 'Bob', myco(50_000), 'Contributor reward: audit', 'system', t(43));
+ transferTokens('myco', grantsDid, 'Grants Committee', carolDid, 'Carol', myco(75_000), 'Contributor reward: UX', 'system', t(40));
+ transferTokens('myco', devFundDid, 'Dev Fund', jeffDid, 'jeff', myco(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));
+ transferTokens('myco', treasuryDid, 'DAO Treasury', jeffDid, 'jeff', myco(500_000), 'Founder vesting — month 2', 'system', t(20));
+ transferTokens('myco', grantsDid, 'Grants Committee', aliceDid, 'Alice', myco(150_000), 'Grant reward: rMaps', 'system', t(15));
+ transferTokens('myco', devFundDid, 'Dev Fund', bobDid, 'Bob', myco(80_000), 'Security bounty payout', 'system', t(12));
+ transferTokens('myco', devFundDid, 'Dev Fund', carolDid, 'Carol', myco(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));
+ transferTokens('myco', treasuryDid, 'DAO Treasury', jeffDid, 'jeff', myco(500_000), 'Founder vesting — month 3', 'system', t(2));
- console.log('[TokenService] DAO ecosystem seeded: cUSDC + BFT tokens with treasury flows');
+ console.log('[TokenService] DAO ecosystem seeded: cUSDC + $MYCO tokens with treasury flows');
}
/** Helper to define a token if not yet defined. */