|
1 | | -export { POST } from "@moneydevkit/nextjs/server/route"; |
| 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 | +} |
0 commit comments