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 f7c06dec45
commit 8fb8bb302f
3 changed files with 50 additions and 7 deletions

View file

@ -20,6 +20,17 @@ export interface MainHandlerInterface {
// Application management
applicationManager: {
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
@ -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
})
}

View file

@ -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
}
},

View file

@ -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
}>