205 lines
6.5 KiB
TypeScript
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);
|