|
1 | | -import { NextRequest } from "next/server"; |
2 | | - |
3 | | -// Webhook secret header |
4 | | -const WEBHOOK_SECRET_HEADER = 'x-moneydevkit-webhook-secret'; |
5 | | - |
6 | | -// Lazy load the default handler |
7 | | -let defaultHandlerPromise: Promise<(request: Request) => Promise<Response>> | null = null; |
8 | | -function getDefaultHandler() { |
9 | | - if (!defaultHandlerPromise) { |
10 | | - defaultHandlerPromise = import("@moneydevkit/nextjs/server/route").then(m => m.POST); |
11 | | - } |
12 | | - return defaultHandlerPromise; |
13 | | -} |
14 | | - |
15 | | -// Helper to sleep for a given number of milliseconds |
16 | | -function sleep(ms: number): Promise<void> { |
17 | | - return new Promise(resolve => setTimeout(resolve, ms)); |
18 | | -} |
19 | | - |
20 | | -// Custom webhook handler with proper sync and retry logic |
21 | | -async function handleWebhookWithSync(request: NextRequest): Promise<Response> { |
22 | | - const body = await request.json(); |
23 | | - |
24 | | - // Validate webhook secret |
25 | | - const expectedSecret = process.env.MDK_ACCESS_TOKEN; |
26 | | - if (!expectedSecret) { |
27 | | - console.error('[webhook] MDK_ACCESS_TOKEN not configured'); |
28 | | - return new Response(JSON.stringify({ error: 'Webhook secret not configured' }), { |
29 | | - status: 500, |
30 | | - headers: { 'Content-Type': 'application/json' }, |
31 | | - }); |
32 | | - } |
33 | | - |
34 | | - const providedSecret = request.headers.get(WEBHOOK_SECRET_HEADER); |
35 | | - if (!providedSecret || providedSecret !== expectedSecret) { |
36 | | - console.error('[webhook] Unauthorized webhook request. Expected:', expectedSecret.substring(0, 8) + '..., Got:', providedSecret?.substring(0, 8) + '...'); |
37 | | - return new Response(JSON.stringify({ error: 'Unauthorized' }), { |
38 | | - status: 401, |
39 | | - headers: { 'Content-Type': 'application/json' }, |
40 | | - }); |
41 | | - } |
42 | | - |
43 | | - if (body.event !== 'incoming-payment') { |
44 | | - console.log('[webhook] Unknown event type:', body.event); |
45 | | - return new Response('OK', { status: 200 }); |
46 | | - } |
47 | | - |
48 | | - console.log('[webhook] Processing incoming-payment event with node sync and retry'); |
49 | | - |
50 | | - try { |
51 | | - // Dynamically import to avoid bundling issues |
52 | | - const { createMoneyDevKitNode, createMoneyDevKitClient, markPaymentReceived } = await import("@moneydevkit/core"); |
53 | | - |
54 | | - const client = createMoneyDevKitClient(); |
55 | | - |
56 | | - // Retry logic: try up to 5 times with increasing delays |
57 | | - const maxRetries = 5; |
58 | | - const delays = [1000, 2000, 3000, 5000, 8000]; // Total: up to 19 seconds of waiting |
59 | | - |
60 | | - let payments: Array<{ paymentHash: string; amount: number }> = []; |
61 | | - |
62 | | - for (let attempt = 0; attempt < maxRetries; attempt++) { |
63 | | - // Create a fresh node instance for each attempt |
64 | | - const node = createMoneyDevKitNode(); |
65 | | - |
66 | | - // CRITICAL: Sync wallets BEFORE checking for payments |
67 | | - console.log(`[webhook] Attempt ${attempt + 1}/${maxRetries}: Syncing wallets...`); |
68 | | - node.syncWallets(); |
69 | | - console.log(`[webhook] Attempt ${attempt + 1}/${maxRetries}: Wallet sync complete`); |
70 | | - |
71 | | - // Now receive payments with the synced state |
72 | | - console.log(`[webhook] Attempt ${attempt + 1}/${maxRetries}: Checking for received payments...`); |
73 | | - payments = node.receivePayments(); |
74 | | - console.log(`[webhook] Attempt ${attempt + 1}/${maxRetries}: Found ${payments.length} payment(s)`); |
75 | | - |
76 | | - if (payments.length > 0) { |
77 | | - break; // Found payments, exit retry loop |
78 | | - } |
79 | | - |
80 | | - // If no payments found and we have more retries, wait before trying again |
81 | | - if (attempt < maxRetries - 1) { |
82 | | - const delayMs = delays[attempt]; |
83 | | - console.log(`[webhook] No payments found, waiting ${delayMs}ms before retry...`); |
84 | | - await sleep(delayMs); |
85 | | - } |
86 | | - } |
87 | | - |
88 | | - if (payments.length === 0) { |
89 | | - console.log('[webhook] No payments found after all retries'); |
90 | | - return new Response('OK', { status: 200 }); |
91 | | - } |
92 | | - |
93 | | - // Mark payments as received locally |
94 | | - payments.forEach((payment: { paymentHash: string }) => { |
95 | | - console.log(`[webhook] Marking payment ${payment.paymentHash} as received`); |
96 | | - markPaymentReceived(payment.paymentHash); |
97 | | - }); |
98 | | - |
99 | | - // Notify MDK API about received payments |
100 | | - try { |
101 | | - console.log('[webhook] Notifying MDK API about payments...'); |
102 | | - await client.checkouts.paymentReceived({ |
103 | | - payments: payments.map((payment: { paymentHash: string; amount: number }) => ({ |
104 | | - paymentHash: payment.paymentHash, |
105 | | - amountSats: payment.amount / 1000, |
106 | | - sandbox: false, |
107 | | - })), |
108 | | - }); |
109 | | - console.log('[webhook] MDK API notified successfully'); |
110 | | - } catch (error) { |
111 | | - console.error('[webhook] Failed to notify MDK API:', error); |
112 | | - // Don't throw - local state is already marked |
113 | | - } |
114 | | - |
115 | | - return new Response('OK', { status: 200 }); |
116 | | - } catch (error) { |
117 | | - console.error('[webhook] Error processing webhook:', error); |
118 | | - return new Response(JSON.stringify({ error: 'Internal server error' }), { |
119 | | - status: 500, |
120 | | - headers: { 'Content-Type': 'application/json' }, |
121 | | - }); |
122 | | - } |
123 | | -} |
124 | | - |
125 | | -export async function POST(request: NextRequest): Promise<Response> { |
126 | | - // Clone the request so we can read the body multiple times |
127 | | - const clonedRequest = request.clone(); |
128 | | - |
129 | | - try { |
130 | | - const body = await clonedRequest.json(); |
131 | | - const handler = body?.handler?.toLowerCase?.() ?? body?.route?.toLowerCase?.() ?? body?.target?.toLowerCase?.(); |
132 | | - |
133 | | - // Handle webhook requests with our custom sync logic |
134 | | - if (handler === 'webhooks' || handler === 'webhook') { |
135 | | - // Create a new request with the parsed body since we already consumed it |
136 | | - return handleWebhookWithSync(request); |
137 | | - } |
138 | | - } catch { |
139 | | - // If JSON parsing fails, let the default handler deal with it |
140 | | - } |
141 | | - |
142 | | - // For all other requests, use the default handler |
143 | | - const defaultHandler = await getDefaultHandler(); |
144 | | - return defaultHandler(request); |
145 | | -} |
| 1 | +export { POST } from "@moneydevkit/nextjs/server/route"; |
0 commit comments