#!/usr/bin/env bun /** * Full-loop integration test: fiat in → cUSDC → $MYCO → cUSDC → fiat out. * * Tests all 5 phases of the HyperSwitch payment orchestrator integration. * Run against a local dev instance with: bun scripts/test-full-loop.ts * * Expects: * - rspace-online running at BASE_URL (default: http://localhost:3000) * - payment-infra services running (or mock responses) * - INTERNAL_API_KEY set for internal endpoints */ const BASE_URL = process.env.BASE_URL || "http://localhost:3000"; const INTERNAL_KEY = process.env.INTERNAL_API_KEY || "test-internal-key"; const TEST_DID = process.env.TEST_DID || "did:key:test-full-loop-2026"; const TEST_LABEL = "test-user"; type StepResult = { ok: boolean; data?: any; error?: string }; async function step(name: string, fn: () => Promise) { process.stdout.write(` ${name}... `); try { const result = await fn(); if (result.ok) { console.log("OK", result.data ? JSON.stringify(result.data).slice(0, 120) : ""); } else { console.log("FAIL", result.error || JSON.stringify(result.data)); } return result; } catch (err: any) { console.log("ERROR", err.message); return { ok: false, error: err.message } as StepResult; } } 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);