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:
parent
b455e639d7
commit
245d2ec3b4
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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. */
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue