From 8fb8bb302f41f24332f9a73edcc9f8435ce1bdf9 Mon Sep 17 00:00:00 2001 From: Patrick Mulligan Date: Mon, 16 Feb 2026 16:56:34 -0500 Subject: [PATCH] feat(extensions): pay from caller's balance via PayAppUserInvoice When userPubkey is provided, resolve the ApplicationUser and call applicationManager.PayAppUserInvoice instead of paymentManager.PayInvoice directly. This ensures notifyAppUserPayment fires, sending LiveUserOperation events via Nostr for real-time balance updates. Co-Authored-By: Claude Opus 4.6 --- src/extensions/context.ts | 19 +++++++++++++-- src/extensions/mainHandlerAdapter.ts | 35 ++++++++++++++++++++++++---- src/extensions/types.ts | 3 ++- 3 files changed, 50 insertions(+), 7 deletions(-) diff --git a/src/extensions/context.ts b/src/extensions/context.ts index b1c6e8d6..f18891f5 100644 --- a/src/extensions/context.ts +++ b/src/extensions/context.ts @@ -20,6 +20,17 @@ export interface MainHandlerInterface { // Application management applicationManager: { getById(id: string): Promise + PayAppUserInvoice(appId: string, req: { + amount: number + invoice: string + user_identifier: string + debit_npub?: string + }): Promise<{ + preimage: string + amount_paid: number + network_fee: number + service_fee: number + }> } // Payment operations @@ -41,6 +52,7 @@ export interface MainHandlerInterface { applicationId: string paymentRequest: string maxFeeSats?: number + userPubkey?: string }): Promise<{ paymentHash: string feeSats: number @@ -156,16 +168,19 @@ export class ExtensionContextImpl implements ExtensionContext { /** * Pay a Lightning invoice + * If userPubkey is provided, pays from that user's balance instead of app.owner */ async payInvoice( applicationId: string, paymentRequest: string, - maxFeeSats?: number + maxFeeSats?: number, + userPubkey?: string ): Promise<{ paymentHash: string; feeSats: number }> { return this.mainHandler.paymentManager.payInvoice({ applicationId, paymentRequest, - maxFeeSats + maxFeeSats, + userPubkey }) } diff --git a/src/extensions/mainHandlerAdapter.ts b/src/extensions/mainHandlerAdapter.ts index eecc96b3..fec73c3f 100644 --- a/src/extensions/mainHandlerAdapter.ts +++ b/src/extensions/mainHandlerAdapter.ts @@ -32,6 +32,10 @@ export function createMainHandlerAdapter(mainHandler: Main): MainHandlerInterfac // GetApplication throws if not found return null } + }, + + async PayAppUserInvoice(appId, req) { + return mainHandler.applicationManager.PayAppUserInvoice(appId, req) } }, @@ -73,6 +77,7 @@ export function createMainHandlerAdapter(mainHandler: Main): MainHandlerInterfac applicationId: string paymentRequest: string maxFeeSats?: number + userPubkey?: string }) { // Get the app to find the user ID and app reference const app = await mainHandler.storage.applicationStorage.GetApplication(params.applicationId) @@ -80,19 +85,41 @@ export function createMainHandlerAdapter(mainHandler: Main): MainHandlerInterfac throw new Error(`Application not found: ${params.applicationId}`) } - // Pay invoice from the app's balance + if (params.userPubkey) { + // Resolve the Nostr user's ApplicationUser to get their identifier + const appUser = await mainHandler.storage.applicationStorage.GetOrCreateNostrAppUser(app, params.userPubkey) + console.log(`[MainHandlerAdapter] Paying via PayAppUserInvoice from Nostr user ${params.userPubkey.slice(0, 8)}... (identifier: ${appUser.identifier})`) + + // Use applicationManager.PayAppUserInvoice so notifyAppUserPayment fires + // This sends LiveUserOperation events via Nostr for real-time balance updates + const result = await mainHandler.applicationManager.PayAppUserInvoice( + params.applicationId, + { + invoice: params.paymentRequest, + amount: 0, // Use invoice amount + user_identifier: appUser.identifier + } + ) + + return { + paymentHash: result.preimage || '', + feeSats: result.network_fee || 0 + } + } + + // Fallback: pay from app owner's balance (no Nostr user context) const result = await mainHandler.paymentManager.PayInvoice( app.owner.user_id, { invoice: params.paymentRequest, - amount: 0 // Use invoice amount + amount: 0 }, - app, // linkedApplication + app, {} ) return { - paymentHash: result.preimage || '', // preimage serves as proof of payment + paymentHash: result.preimage || '', feeSats: result.network_fee || 0 } }, diff --git a/src/extensions/types.ts b/src/extensions/types.ts index 62abf5df..e67c1e4f 100644 --- a/src/extensions/types.ts +++ b/src/extensions/types.ts @@ -140,8 +140,9 @@ export interface ExtensionContext { /** * Pay a Lightning invoice (requires sufficient balance) + * If userPubkey is provided, pays from that user's balance instead of app.owner */ - payInvoice(applicationId: string, paymentRequest: string, maxFeeSats?: number): Promise<{ + payInvoice(applicationId: string, paymentRequest: string, maxFeeSats?: number, userPubkey?: string): Promise<{ paymentHash: string feeSats: number }>