feat(extensions): pay from caller's balance via PayAppUserInvoice
Some checks are pending
Docker Compose Actions Workflow / test (push) Waiting to run

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 1aad229c19
commit dcb7dca9b5
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
}> }>