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 <noreply@anthropic.com>
This commit is contained in:
Patrick Mulligan 2026-02-16 16:56:34 -05:00
parent 121c22c4b8
commit f8d17a91a7
3 changed files with 50 additions and 7 deletions

View file

@ -20,6 +20,17 @@ export interface MainHandlerInterface {
// Application management // Application management
applicationManager: { applicationManager: {
getById(id: string): Promise<any> getById(id: string): Promise<any>
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 // Payment operations
@ -41,6 +52,7 @@ export interface MainHandlerInterface {
applicationId: string applicationId: string
paymentRequest: string paymentRequest: string
maxFeeSats?: number maxFeeSats?: number
userPubkey?: string
}): Promise<{ }): Promise<{
paymentHash: string paymentHash: string
feeSats: number feeSats: number
@ -156,16 +168,19 @@ export class ExtensionContextImpl implements ExtensionContext {
/** /**
* Pay a Lightning invoice * Pay a Lightning invoice
* If userPubkey is provided, pays from that user's balance instead of app.owner
*/ */
async payInvoice( async payInvoice(
applicationId: string, applicationId: string,
paymentRequest: string, paymentRequest: string,
maxFeeSats?: number maxFeeSats?: number,
userPubkey?: string
): Promise<{ paymentHash: string; feeSats: number }> { ): Promise<{ paymentHash: string; feeSats: number }> {
return this.mainHandler.paymentManager.payInvoice({ return this.mainHandler.paymentManager.payInvoice({
applicationId, applicationId,
paymentRequest, paymentRequest,
maxFeeSats maxFeeSats,
userPubkey
}) })
} }

View file

@ -32,6 +32,10 @@ export function createMainHandlerAdapter(mainHandler: Main): MainHandlerInterfac
// GetApplication throws if not found // GetApplication throws if not found
return null return null
} }
},
async PayAppUserInvoice(appId, req) {
return mainHandler.applicationManager.PayAppUserInvoice(appId, req)
} }
}, },
@ -73,6 +77,7 @@ export function createMainHandlerAdapter(mainHandler: Main): MainHandlerInterfac
applicationId: string applicationId: string
paymentRequest: string paymentRequest: string
maxFeeSats?: number maxFeeSats?: number
userPubkey?: string
}) { }) {
// Get the app to find the user ID and app reference // Get the app to find the user ID and app reference
const app = await mainHandler.storage.applicationStorage.GetApplication(params.applicationId) 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}`) 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( const result = await mainHandler.paymentManager.PayInvoice(
app.owner.user_id, app.owner.user_id,
{ {
invoice: params.paymentRequest, invoice: params.paymentRequest,
amount: 0 // Use invoice amount amount: 0
}, },
app, // linkedApplication app,
{} {}
) )
return { return {
paymentHash: result.preimage || '', // preimage serves as proof of payment paymentHash: result.preimage || '',
feeSats: result.network_fee || 0 feeSats: result.network_fee || 0
} }
}, },

View file

@ -140,8 +140,9 @@ export interface ExtensionContext {
/** /**
* Pay a Lightning invoice (requires sufficient balance) * 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 paymentHash: string
feeSats: number feeSats: number
}> }>