feat: Transak credit card → USDC fiat on-ramp for rFunds TBFF
Add Transak widget integration so users can fund flows with a credit card. Server receives webhook on order completion and deposits USDC into the flow via the existing Flow Service API. Includes HMAC signature verification when TRANSAK_WEBHOOK_SECRET is configured. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
46c2a0b035
commit
34c96b5a45
|
|
@ -1405,6 +1405,14 @@ class FolkFundsApp extends HTMLElement {
|
|||
<select class="editor-select" data-field="sourceType">
|
||||
${["card", "safe_wallet", "ridentity", "unconfigured"].map((t) => `<option value="${t}" ${d.sourceType === t ? "selected" : ""}>${t}</option>`).join("")}
|
||||
</select></div>`;
|
||||
if (d.sourceType === "card") {
|
||||
html += `<div class="editor-field" style="margin-top:12px">
|
||||
<button class="editor-btn fund-card-btn" data-action="fund-with-card"
|
||||
style="width:100%;padding:10px;background:#6366f1;color:white;border:none;border-radius:8px;cursor:pointer;font-weight:600">
|
||||
Fund with Card
|
||||
</button>
|
||||
</div>`;
|
||||
}
|
||||
html += this.renderAllocEditor("Target Allocations", d.targetAllocations);
|
||||
return html;
|
||||
}
|
||||
|
|
@ -1476,6 +1484,62 @@ class FolkFundsApp extends HTMLElement {
|
|||
return html;
|
||||
}
|
||||
|
||||
private async openTransakWidget(flowId: string, walletAddress: string) {
|
||||
// Fetch Transak config from server
|
||||
let apiKey = "STAGING_KEY";
|
||||
let env = "STAGING";
|
||||
try {
|
||||
const base = this.space ? `/s/${this.space}/rfunds` : "/rfunds";
|
||||
const res = await fetch(`${base}/api/transak/config`);
|
||||
if (res.ok) {
|
||||
const cfg = await res.json();
|
||||
apiKey = cfg.apiKey || apiKey;
|
||||
env = cfg.environment || env;
|
||||
}
|
||||
} catch { /* use defaults */ }
|
||||
|
||||
const baseUrl = env === "PRODUCTION"
|
||||
? "https://global.transak.com"
|
||||
: "https://global-stg.transak.com";
|
||||
|
||||
const params = new URLSearchParams({
|
||||
apiKey,
|
||||
environment: env,
|
||||
cryptoCurrencyCode: "USDC",
|
||||
network: "base",
|
||||
defaultCryptoCurrency: "USDC",
|
||||
walletAddress,
|
||||
partnerOrderId: flowId,
|
||||
themeColor: "6366f1",
|
||||
hideMenu: "true",
|
||||
});
|
||||
|
||||
const modal = document.createElement("div");
|
||||
modal.id = "transak-modal";
|
||||
modal.style.cssText = `position:fixed;inset:0;z-index:99999;background:rgba(0,0,0,0.7);display:flex;align-items:center;justify-content:center;`;
|
||||
modal.innerHTML = `
|
||||
<div style="position:relative;width:450px;height:680px;border-radius:16px;overflow:hidden;background:#1e1e2e">
|
||||
<button id="transak-close" style="position:absolute;top:8px;right:12px;z-index:10;
|
||||
background:none;border:none;color:white;font-size:24px;cursor:pointer">×</button>
|
||||
<iframe src="${baseUrl}?${params}" style="width:100%;height:100%;border:none"
|
||||
allow="camera;microphone;payment" sandbox="allow-scripts allow-same-origin allow-popups allow-forms"></iframe>
|
||||
</div>`;
|
||||
|
||||
document.body.appendChild(modal);
|
||||
|
||||
modal.querySelector("#transak-close")!.addEventListener("click", () => modal.remove());
|
||||
modal.addEventListener("click", (e) => { if (e.target === modal) modal.remove(); });
|
||||
|
||||
const handler = (e: MessageEvent) => {
|
||||
if (e.data?.event_id === "TRANSAK_ORDER_SUCCESSFUL") {
|
||||
console.log("[Transak] Order successful:", e.data.data);
|
||||
modal.remove();
|
||||
window.removeEventListener("message", handler);
|
||||
}
|
||||
};
|
||||
window.addEventListener("message", handler);
|
||||
}
|
||||
|
||||
private attachEditorListeners(panel: HTMLElement, node: FlowNode) {
|
||||
// Close button
|
||||
panel.querySelector('[data-editor-action="close"]')?.addEventListener("click", () => this.closeEditor());
|
||||
|
|
@ -1499,6 +1563,18 @@ class FolkFundsApp extends HTMLElement {
|
|||
this.updateSufficiencyBadge();
|
||||
});
|
||||
});
|
||||
|
||||
// Fund with Card button (source nodes with sourceType "card")
|
||||
panel.querySelector('[data-action="fund-with-card"]')?.addEventListener("click", () => {
|
||||
const flowId = this.flowId || this.getAttribute("flow-id") || "";
|
||||
const sourceData = node.data as SourceNodeData;
|
||||
const walletAddress = sourceData.walletAddress || "";
|
||||
if (!walletAddress) {
|
||||
alert("Configure a wallet address first (use rIdentity passkey or enter manually)");
|
||||
return;
|
||||
}
|
||||
this.openTransakWidget(flowId, walletAddress);
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Node CRUD ────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ export interface SourceNodeData {
|
|||
walletAddress?: string;
|
||||
chainId?: number;
|
||||
safeAddress?: string;
|
||||
transakOrderId?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -145,6 +145,70 @@ routes.get("/api/flows/:flowId/transactions", async (c) => {
|
|||
return c.json(await res.json(), res.status as any);
|
||||
});
|
||||
|
||||
// ─── Transak fiat on-ramp ────────────────────────────────
|
||||
|
||||
routes.get("/api/transak/config", (c) => {
|
||||
return c.json({
|
||||
apiKey: process.env.TRANSAK_API_KEY || "STAGING_KEY",
|
||||
environment: process.env.TRANSAK_ENV || "STAGING",
|
||||
});
|
||||
});
|
||||
|
||||
routes.post("/api/transak/webhook", async (c) => {
|
||||
const rawBody = await c.req.text();
|
||||
|
||||
// HMAC verification — if TRANSAK_WEBHOOK_SECRET is set, validate signature
|
||||
const webhookSecret = process.env.TRANSAK_WEBHOOK_SECRET;
|
||||
if (webhookSecret) {
|
||||
const signature = c.req.header("x-transak-signature") || "";
|
||||
const { createHmac } = await import("crypto");
|
||||
const expected = createHmac("sha256", webhookSecret).update(rawBody).digest("hex");
|
||||
if (signature !== expected) {
|
||||
console.error("[Transak] Invalid webhook signature");
|
||||
return c.json({ error: "Invalid signature" }, 401);
|
||||
}
|
||||
}
|
||||
|
||||
const body = JSON.parse(rawBody);
|
||||
const { webhookData } = body;
|
||||
|
||||
// Ack non-completion events (Transak sends multiple status updates)
|
||||
if (!webhookData || webhookData.status !== "COMPLETED") {
|
||||
return c.json({ ok: true });
|
||||
}
|
||||
|
||||
const { partnerOrderId, cryptoAmount, cryptocurrency, network } = webhookData;
|
||||
if (!partnerOrderId || cryptocurrency !== "USDC" || !network?.toLowerCase().includes("base")) {
|
||||
return c.json({ error: "Invalid webhook data" }, 400);
|
||||
}
|
||||
|
||||
// partnerOrderId format: "flowId" or "flowId:funnelId"
|
||||
const [flowId, funnelId] = partnerOrderId.split(":");
|
||||
if (!flowId) return c.json({ error: "Missing flowId in partnerOrderId" }, 400);
|
||||
|
||||
// Convert crypto amount to USDC units (6 decimals)
|
||||
const amountUnits = Math.round(parseFloat(cryptoAmount) * 1e6).toString();
|
||||
|
||||
const depositUrl = `${FLOW_SERVICE_URL}/api/flows/${flowId}/deposit`;
|
||||
const res = await fetch(depositUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
amount: amountUnits,
|
||||
source: "transak",
|
||||
...(funnelId ? { funnelId } : {}),
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
console.error(`[Transak] Deposit failed: ${await res.text()}`);
|
||||
return c.json({ error: "Deposit failed" }, 500);
|
||||
}
|
||||
|
||||
console.log(`[Transak] Deposit OK: flow=${flowId} amount=${amountUnits} USDC`);
|
||||
return c.json({ ok: true });
|
||||
});
|
||||
|
||||
// ─── Space-flow association endpoints ────────────────────
|
||||
|
||||
routes.post("/api/space-flows", async (c) => {
|
||||
|
|
|
|||
|
|
@ -69,6 +69,9 @@ RUNPOD_API_KEY|AI/MI|RunPod API key
|
|||
X402_PAY_TO|payments|Payment recipient address
|
||||
MAILCOW_API_KEY|EncryptID|Mailcow admin API key
|
||||
ENCRYPTID_DEMO_SPACES|EncryptID|Comma-separated demo space slugs
|
||||
TRANSAK_API_KEY|rFunds|Transak widget API key (public, scoped to app)
|
||||
TRANSAK_WEBHOOK_SECRET|rFunds|Transak webhook HMAC secret for signature verification
|
||||
TRANSAK_ENV|rFunds|Transak environment: STAGING or PRODUCTION (default: STAGING)
|
||||
"
|
||||
|
||||
ADDED=0
|
||||
|
|
|
|||
Loading…
Reference in New Issue