From b8e38aa383dab19eade3b1beaf7518fcce8cfa2d Mon Sep 17 00:00:00 2001 From: Patrick Mulligan Date: Sat, 14 Feb 2026 15:29:04 -0500 Subject: [PATCH] feat(withdraw): add HTTP API for creating withdraw links Add POST /api/v1/withdraw/create endpoint to allow external apps (ATM, web clients) to create LNURL-withdraw links via HTTP instead of RPC. Changes: - Add handleCreateWithdrawLink HTTP handler - Fix route ordering: callback routes before wildcard :unique_hash - Extract app_id from Authorization header (Bearer app_) - Use is_unique=false for simple single-use ATM links Co-Authored-By: Claude Opus 4.5 --- src/extensions/withdraw/index.ts | 91 +++++++++++++++++++++++++++++--- 1 file changed, 85 insertions(+), 6 deletions(-) diff --git a/src/extensions/withdraw/index.ts b/src/extensions/withdraw/index.ts index 0b8259d6..dacf045e 100644 --- a/src/extensions/withdraw/index.ts +++ b/src/extensions/withdraw/index.ts @@ -82,11 +82,17 @@ export default class WithdrawExtension implements Extension { */ getHttpRoutes(): HttpRoute[] { return [ - // Initial LNURL request (simple link) + // Create withdraw link (HTTP API for ATM/external integrations) + { + method: 'POST', + path: '/api/v1/withdraw/create', + handler: this.handleCreateWithdrawLink.bind(this) + }, + // LNURL callback (user submits invoice) - MUST be before :unique_hash routes { method: 'GET', - path: '/api/v1/lnurl/:unique_hash', - handler: this.handleLnurlRequest.bind(this) + path: '/api/v1/lnurl/cb/:unique_hash', + handler: this.handleLnurlCallback.bind(this) }, // Initial LNURL request (unique link with use hash) { @@ -94,11 +100,11 @@ export default class WithdrawExtension implements Extension { path: '/api/v1/lnurl/:unique_hash/:id_unique_hash', handler: this.handleLnurlUniqueRequest.bind(this) }, - // LNURL callback (user submits invoice) + // Initial LNURL request (simple link) - MUST be last (catches all) { method: 'GET', - path: '/api/v1/lnurl/cb/:unique_hash', - handler: this.handleLnurlCallback.bind(this) + path: '/api/v1/lnurl/:unique_hash', + handler: this.handleLnurlRequest.bind(this) } ] } @@ -231,6 +237,79 @@ export default class WithdrawExtension implements Extension { // HTTP Route Handlers // ========================================================================= + /** + * Handle create withdraw link request (HTTP API) + * POST /api/v1/withdraw/create + * + * Body: { + * title: string + * min_withdrawable: number (sats) + * max_withdrawable: number (sats) + * uses?: number (defaults to 1) + * wait_time?: number (seconds between uses, defaults to 0) + * } + * + * Auth: Bearer token in Authorization header (app_) + * + * Returns: { + * link: { lnurl, unique_hash, id, ... } + * } + */ + private async handleCreateWithdrawLink(req: HttpRequest): Promise { + try { + const { title, min_withdrawable, max_withdrawable, uses, wait_time } = req.body + + // Extract app_id from Authorization header (Bearer app_) + const authHeader = req.headers?.authorization || req.headers?.Authorization || '' + let app_id = 'default' + if (authHeader.startsWith('Bearer app_')) { + app_id = authHeader.replace('Bearer app_', '') + } + + if (!title || !min_withdrawable) { + return { + status: 400, + body: { status: 'ERROR', reason: 'Missing required fields: title, min_withdrawable' }, + headers: { 'Content-Type': 'application/json' } + } + } + + const link = await this.manager.create(app_id, { + title, + min_withdrawable, + max_withdrawable: max_withdrawable || min_withdrawable, + uses: uses || 1, + wait_time: wait_time || 0, + is_unique: false // Simple single-use links for ATM + }) + + // Return in format expected by ATM client + return { + status: 200, + body: { + status: 'OK', + link: { + lnurl: link.lnurl, + unique_hash: link.unique_hash, + id: link.id, + title: link.title, + min_withdrawable: link.min_withdrawable, + max_withdrawable: link.max_withdrawable, + uses: link.uses, + used: link.used + } + }, + headers: { 'Content-Type': 'application/json' } + } + } catch (error: any) { + return { + status: 500, + body: { status: 'ERROR', reason: error.message }, + headers: { 'Content-Type': 'application/json' } + } + } + } + /** * Handle initial LNURL request (simple link) * GET /api/v1/lnurl/:unique_hash