Skip to content
19 changes: 17 additions & 2 deletions src/extensions/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -41,6 +52,7 @@ export interface MainHandlerInterface {
applicationId: string
paymentRequest: string
maxFeeSats?: number
userPubkey?: string
}): Promise<{
paymentHash: string
feeSats: number
Expand Down Expand Up @@ -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
})
}

Expand Down
155 changes: 155 additions & 0 deletions src/extensions/mainHandlerAdapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/**
* MainHandler Adapter for Extension System
*
* Wraps the Lightning.Pub mainHandler to provide the MainHandlerInterface
* required by the extension system.
*/

import { MainHandlerInterface } from './context.js'
import { LnurlPayInfo } from './types.js'
import type Main from '../services/main/index.js'

/**
* Create an adapter that wraps mainHandler for extension use
*/
export function createMainHandlerAdapter(mainHandler: Main): MainHandlerInterface {
return {
applicationManager: {
async getById(id: string) {
// The applicationManager stores apps internally
// We need to access it through the storage layer
try {
const app = await mainHandler.storage.applicationStorage.GetApplication(id)
if (!app) return null

return {
id: app.app_id,
name: app.name,
nostr_public: app.nostr_public_key || '',
balance: app.owner?.balance_sats || 0
}
} catch (e) {
// GetApplication throws if not found
return null
}
},

async PayAppUserInvoice(appId, req) {
return mainHandler.applicationManager.PayAppUserInvoice(appId, req)
}
},

paymentManager: {
async createInvoice(params: {
applicationId: string
amountSats: number
memo?: string
expiry?: number
metadata?: Record<string, any>
}) {
// Get the app to find the user ID
const app = await mainHandler.storage.applicationStorage.GetApplication(params.applicationId)
if (!app) {
throw new Error(`Application not found: ${params.applicationId}`)
}

// Create invoice using the app owner's user ID
const result = await mainHandler.paymentManager.NewInvoice(
app.owner.user_id,
{
amountSats: params.amountSats,
memo: params.memo || ''
},
{
expiry: params.expiry || 3600
}
)

return {
id: result.invoice.split(':')[0] || result.invoice, // Extract ID if present
paymentRequest: result.invoice,
paymentHash: '', // Not directly available from NewInvoice response
expiry: Date.now() + (params.expiry || 3600) * 1000
}
},

async payInvoice(params: {
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)
if (!app) {
throw new Error(`Application not found: ${params.applicationId}`)
}

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
},
app,
{}
)

return {
paymentHash: result.preimage || '',
feeSats: result.network_fee || 0
}
},

async getLnurlPayInfoByPubkey(pubkeyHex: string, options?: {
metadata?: string
description?: string
}): Promise<LnurlPayInfo> {
// This would need implementation based on how Lightning.Pub handles LNURL-pay
// For now, throw not implemented
throw new Error('getLnurlPayInfoByPubkey not yet implemented')
}
},

async sendNostrEvent(event: any): Promise<string | null> {
// The mainHandler doesn't directly expose nostrSend
// This would need to be implemented through the nostrMiddleware
// For now, return null (not implemented)
console.warn('[MainHandlerAdapter] sendNostrEvent not fully implemented')
return null
},

async sendEncryptedDM(
applicationId: string,
recipientPubkey: string,
content: string
): Promise<string> {
// This would need implementation using NIP-44 encryption
// For now, throw not implemented
throw new Error('sendEncryptedDM not yet implemented')
}
}
}
3 changes: 2 additions & 1 deletion src/extensions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}>
Expand Down
Loading