rspace-online/scripts/test-full-loop.ts

205 lines
6.5 KiB
TypeScript

#!/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 } 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);