feat(rwallet): HyperSwitch full-loop fiat↔CRDT payment integration

- /api/internal/mint-crdt: on-ramp webhook → cUSDC mint (idempotent)
- /api/internal/escrow-burn: off-ramp escrow with two-step confirm/reverse
- $MYCO bonding curve (server/bonding-curve.ts): quadratic price curve,
  buy/sell/quote/settlement-state endpoints in rwallet
- BFT token renamed to $MYCO (6 decimals) in seed data
- LedgerEntry schema extended with offRampId, status for escrow tracking
- burnTokens, burnTokensEscrow, confirmBurn, reverseBurn in token-service
- Wallet UI: Buy cUSDC, $MYCO Swap, Withdraw sections with live quotes
- scripts/test-full-loop.ts for end-to-end verification

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-23 17:34:48 -07:00
parent b455e639d7
commit 245d2ec3b4
6 changed files with 1142 additions and 26 deletions

View File

@ -1538,6 +1538,172 @@ class FolkWalletViewer extends HTMLElement {
color: #e0e0e0; color: #e0e0e0;
} }
.local-tokens-section table tr:last-child td { border-bottom: none; } .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 { .link-error {
color: var(--rs-error); font-size: 12px; margin-top: 8px; color: var(--rs-error); font-size: 12px; margin-top: 8px;
padding: 6px 10px; background: rgba(239,83,80,0.08); border-radius: 6px; padding: 6px 10px; background: rgba(239,83,80,0.08); border-radius: 6px;
@ -2510,6 +2676,116 @@ class FolkWalletViewer extends HTMLElement {
</table>`; </table>`;
} }
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 `
<div class="payment-actions">
<!-- Buy cUSDC Section -->
<div class="action-card" id="buy-cusdc-card">
<div class="action-header">
<span class="action-icon">💵</span>
<h4>Buy cUSDC</h4>
<span class="action-badge">Fiat CRDT</span>
</div>
<p class="action-desc">Deposit fiat via bank transfer or card. Auto-routed to cheapest provider.</p>
<div class="provider-hints">
<span class="provider-tag eu">EU: Mollie (SEPA/iDEAL)</span>
<span class="provider-tag us">US: Stripe (ACH/Card)</span>
<span class="provider-tag small">< $50: Mt Pelerin (no KYC)</span>
</div>
<div class="action-form" id="buy-cusdc-form" style="display:none">
<div class="form-row">
<label>Amount (USD)</label>
<input type="number" id="buy-amount" min="1" step="0.01" placeholder="100.00" class="action-input">
</div>
<div class="form-row">
<label>Country</label>
<select id="buy-country" class="action-input">
<option value="">Auto-detect</option>
<option value="US">United States</option>
<option value="DE">Germany</option>
<option value="NL">Netherlands</option>
<option value="FR">France</option>
<option value="GB">United Kingdom</option>
<option value="CH">Switzerland</option>
</select>
</div>
<button class="action-btn primary" id="btn-buy-cusdc">Deposit via HyperSwitch</button>
<div id="buy-cusdc-status" class="action-status"></div>
</div>
<button class="action-btn outline" id="btn-toggle-buy">Buy cUSDC</button>
</div>
<!-- $MYCO Swap Section -->
<div class="action-card" id="myco-swap-card">
<div class="action-header">
<span class="action-icon">🌱</span>
<h4>$MYCO Swap</h4>
<span class="action-badge curve">Bonding Curve</span>
</div>
<p class="action-desc">Swap cUSDC $MYCO on the CRDT bonding curve. Price rises with supply.</p>
<div class="swap-balances">
<span>cUSDC: <strong>${cusdcFormatted}</strong></span>
<span>$MYCO: <strong>${mycoFormatted}</strong></span>
</div>
<div class="action-form" id="myco-swap-form" style="display:none">
<div class="swap-tabs">
<button class="swap-tab active" data-swap="buy">Buy $MYCO</button>
<button class="swap-tab" data-swap="sell">Sell $MYCO</button>
</div>
<div class="form-row">
<label id="swap-input-label">cUSDC Amount</label>
<input type="number" id="swap-amount" min="0.01" step="0.01" placeholder="10.00" class="action-input">
</div>
<div id="swap-quote" class="swap-quote"></div>
<button class="action-btn primary" id="btn-swap-execute">Swap</button>
<div id="swap-status" class="action-status"></div>
</div>
<button class="action-btn outline" id="btn-toggle-swap">Swap cUSDC $MYCO</button>
</div>
<!-- Withdraw Section -->
<div class="action-card" id="withdraw-card">
<div class="action-header">
<span class="action-icon">🏦</span>
<h4>Withdraw</h4>
<span class="action-badge">CRDT Fiat</span>
</div>
<p class="action-desc">Convert cUSDC back to fiat. Routed via SEPA (EU) or ACH (US).</p>
<div class="action-form" id="withdraw-form" style="display:none">
<div class="form-row">
<label>cUSDC Amount</label>
<input type="number" id="withdraw-amount" min="10" step="0.01" placeholder="100.00" class="action-input"
max="${cusdcFormatted}">
</div>
<div class="form-row">
<label>Bank Country</label>
<select id="withdraw-country" class="action-input">
<option value="US">United States (ACH ~3 days)</option>
<option value="NL">Netherlands (SEPA ~1 day)</option>
<option value="DE">Germany (SEPA ~1 day)</option>
<option value="FR">France (SEPA ~1 day)</option>
</select>
</div>
<div class="form-row">
<label>IBAN / Account</label>
<input type="text" id="withdraw-iban" placeholder="NL91ABNA0417164300" class="action-input">
</div>
<button class="action-btn primary" id="btn-withdraw-execute">Withdraw to Bank</button>
<div id="withdraw-status" class="action-status"></div>
</div>
<button class="action-btn outline" id="btn-toggle-withdraw">Withdraw to Bank</button>
</div>
</div>`;
}
private renderDashboard(): string { private renderDashboard(): string {
if (!this.hasData()) return ""; if (!this.hasData()) return "";
@ -2569,7 +2845,7 @@ class FolkWalletViewer extends HTMLElement {
${this.renderViewTabs()} ${this.renderViewTabs()}
${this.activeView === "balances" ${this.activeView === "balances"
? this.renderBalanceTable() ? this.renderBalanceTable() + this.renderPaymentActions()
: this.activeView === "yield" : this.activeView === "yield"
? this.renderYieldTab() ? this.renderYieldTab()
: `<div class="viz-container" id="viz-container"> : `<div class="viz-container" id="viz-container">
@ -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<typeof setTimeout> | 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 = `<span style="color:#f87171">${data.error}</span>`;
return;
}
const outToken = this._swapAction === 'buy' ? '$MYCO' : 'cUSDC';
const feeStr = data.fee ? ` | Fee: ${data.fee.amount.toFixed(4)} cUSDC` : '';
quoteEl.innerHTML = `You receive: <strong>${data.output.amount.toFixed(4)} ${outToken}</strong> | Price: ${data.pricePerToken.toFixed(6)} cUSDC/${this._swapAction === 'buy' ? '$MYCO' : 'cUSDC'} | Impact: ${data.priceImpact}%${feeStr}`;
} catch {
quoteEl.innerHTML = '<span style="color:#f87171">Quote unavailable</span>';
}
}
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 = '<span class="error">Enter amount >= $1.00</span>';
return;
}
statusEl.innerHTML = '<span class="loading">Creating payment...</span>';
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 = `<span class="error">${data.error}</span>`;
return;
}
statusEl.innerHTML = `<span class="success">Payment created! Redirecting to checkout...</span>`;
setTimeout(() => {
statusEl.innerHTML = `<span class="success">Payment ID: ${data.paymentId}. Complete payment at pay.rspace.online</span>`;
}, 1500);
} catch {
statusEl.innerHTML = `<span class="error">Failed to create payment</span>`;
}
}
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 = '<span class="error">Enter a valid amount</span>';
return;
}
statusEl.innerHTML = '<span class="loading">Processing swap...</span>';
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 = `<span class="error">${data.error}</span>`;
return;
}
statusEl.innerHTML = `<span class="success">Swapped! Received ${data.received.amount.toFixed(4)} ${data.received.token}</span>`;
this.loadCRDTBalances();
} catch {
statusEl.innerHTML = `<span class="error">Swap failed</span>`;
}
}
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 = '<span class="error">Minimum withdrawal: $10.00</span>';
return;
}
if (!ibanInput?.value) {
statusEl.innerHTML = '<span class="error">Enter bank account details</span>';
return;
}
statusEl.innerHTML = '<span class="loading">Initiating withdrawal...</span>';
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 = `<span class="error">${data.error}</span>`;
return;
}
statusEl.innerHTML = `<span class="success">Withdrawal initiated! Est. arrival: ${new Date(data.estimatedArrival).toLocaleDateString()}</span>`;
this.loadCRDTBalances();
} catch {
statusEl.innerHTML = `<span class="error">Withdrawal failed</span>`;
}
}
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() { private render() {
this.shadow.innerHTML = ` this.shadow.innerHTML = `
${this.renderStyles()} ${this.renderStyles()}
@ -2793,6 +3299,7 @@ class FolkWalletViewer extends HTMLElement {
`; `;
this.attachVisualizerListeners(); this.attachVisualizerListeners();
this.attachPaymentListeners();
this._tour.renderOverlay(); this._tour.renderOverlay();
} }

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, 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 { tokenDocId } from "../../server/token-schemas";
import type { TokenLedgerDoc } 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 }); 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 ── // ── 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";

204
scripts/test-full-loop.ts Normal file
View File

@ -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<StepResult>) {
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);

97
server/bonding-curve.ts Normal file
View File

@ -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,
};
}

View File

@ -21,6 +21,10 @@ export interface LedgerEntry {
issuedBy: string; issuedBy: string;
txHash?: string; txHash?: string;
onChainNetwork?: string; onChainNetwork?: string;
/** Off-ramp ID for escrow burns */
offRampId?: string;
/** Escrow status: 'escrow' → 'confirmed' | 'reversed' */
status?: 'escrow' | 'confirmed' | 'reversed';
} }
export interface TokenDefinition { export interface TokenDefinition {

View File

@ -29,10 +29,12 @@ function ensureTokenDoc(tokenId: string): TokenLedgerDoc {
return doc; 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 { export function getBalance(doc: TokenLedgerDoc, did: string): number {
let balance = 0; let balance = 0;
for (const entry of Object.values(doc.entries)) { 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) { if (entry.type === 'mint' && entry.to === did) {
balance += entry.amount; balance += entry.amount;
} else if (entry.type === 'transfer') { } else if (entry.type === 'transfer') {
@ -206,6 +208,118 @@ export function mintFromOnChain(
return success; 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<TokenLedgerDoc>(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<TokenLedgerDoc>(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<TokenLedgerDoc>(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<TokenLedgerDoc>(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. */ /** List all token doc IDs. */
export function listTokenDocs(): string[] { export function listTokenDocs(): string[] {
return _syncServer!.listDocs().filter((id) => id.startsWith('global:tokens:ledgers:')); 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. */ /** Seed the full DAO token ecosystem — BFT governance, cUSDC stablecoin, multiple treasuries. */
export async function seedCUSDC() { export async function seedCUSDC() {
// Check if already seeded by looking at BFT token (last one created) // Check if already seeded by looking at $MYCO token (last one created)
const bftDoc = getTokenDoc('bft'); const mycoDoc = getTokenDoc('myco');
if (bftDoc && bftDoc.token.name && Object.keys(bftDoc.entries).length > 0) { if (mycoDoc && mycoDoc.token.name && Object.keys(mycoDoc.entries).length > 0) {
console.log('[TokenService] DAO ecosystem already seeded, skipping'); console.log('[TokenService] DAO ecosystem already seeded, skipping');
return; 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', 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)); transferTokens('cusdc', grantsDid, 'Grants Committee', aliceDid, 'Alice', 20_000_000_000, 'Grant: rMaps privacy module', 'system', t(3));
// ── 2. BFT — Governance token ── // ── 2. $MYCO — Governance token (bonding curve) ──
ensureTokenDef('bft', { ensureTokenDef('myco', {
name: 'BioFi Token', symbol: 'BFT', decimals: 18, name: 'MYCO Token', symbol: '$MYCO', decimals: 6,
description: 'Governance token for the rSpace DAO — used for voting, delegation, and reputation', description: 'Governance token for the rSpace DAO — bonding curve with cUSDC reserve, used for voting and delegation',
icon: '🌱', color: '#22c55e', icon: '🌱', color: '#22c55e',
}); });
const e18 = 1_000_000_000_000_000_000; // 10^18 — but we store as number so use smaller scale const myco = (n: number) => n * 1_000_000; // 6 effective decimals
const bft = (n: number) => n * 1_000_000; // Use 6 effective decimals for demo (display divides by 10^18)
// Genesis mint to treasury // 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 // Distribute to sub-treasuries
transferTokens('bft', treasuryDid, 'DAO Treasury', grantsDid, 'Grants Committee', bft(2_000_000), 'Grants committee allocation', 'system', t(56)); transferTokens('myco', treasuryDid, 'DAO Treasury', grantsDid, 'Grants Committee', myco(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', devFundDid, 'Dev Fund', myco(1_500_000), 'Dev fund allocation', 'system', t(56));
// Vest to contributors // Vest to contributors
transferTokens('bft', treasuryDid, 'DAO Treasury', jeffDid, 'jeff', bft(500_000), 'Founder vesting — month 1', 'system', t(50)); transferTokens('myco', treasuryDid, 'DAO Treasury', jeffDid, 'jeff', myco(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('myco', grantsDid, 'Grants Committee', aliceDid, 'Alice', myco(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('myco', grantsDid, 'Grants Committee', bobDid, 'Bob', myco(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('myco', grantsDid, 'Grants Committee', carolDid, 'Carol', myco(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', devFundDid, 'Dev Fund', jeffDid, 'jeff', myco(200_000), 'Dev milestone: rwallet launch', 'system', t(35));
// Second vesting round // Second vesting round
transferTokens('bft', treasuryDid, 'DAO Treasury', jeffDid, 'jeff', bft(500_000), 'Founder vesting — month 2', 'system', t(20)); transferTokens('myco', treasuryDid, 'DAO Treasury', jeffDid, 'jeff', myco(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('myco', grantsDid, 'Grants Committee', aliceDid, 'Alice', myco(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('myco', devFundDid, 'Dev Fund', bobDid, 'Bob', myco(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', devFundDid, 'Dev Fund', carolDid, 'Carol', myco(60_000), 'Design system completion', 'system', t(8));
// Recent // 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. */ /** Helper to define a token if not yet defined. */