feat(extensions): add LNURL-withdraw extension #6

Merged
padreug merged 8 commits from feature/withdraw into dev 2026-04-02 19:05:20 +00:00
2 changed files with 69 additions and 35 deletions
Showing only changes of commit 1273da9020 - Show all commits

View file

@ -35,41 +35,8 @@ const start = async () => {
const { mainHandler, localProviderClient, wizard, adminManager } = keepOn
const serverMethods = GetServerMethods(mainHandler)
log("initializing nostr middleware")
const relays = settingsManager.getSettings().nostrRelaySettings.relays
const maxEventContentLength = settingsManager.getSettings().nostrRelaySettings.maxEventContentLength
const apps: AppInfo[] = keepOn.apps.map(app => {
return {
appId: app.appId,
privateKey: app.privateKey,
publicKey: app.publicKey,
name: app.name,
provider: app.publicKey === localProviderClient.publicKey ? {
clientId: `client_${localProviderClient.appId}`,
pubkey: settingsManager.getSettings().liquiditySettings.liquidityProviderPub,
relayUrl: settingsManager.getSettings().liquiditySettings.providerRelayUrl
} : undefined
}
})
const { Send, Stop, Ping, Reset } = nostrMiddleware(serverMethods, mainHandler,
{
relays, maxEventContentLength, apps
},
(e, p) => mainHandler.liquidityProvider.onEvent(e, p)
)
exitHandler(() => { Stop(); mainHandler.Stop() })
log("starting server")
mainHandler.attachNostrSend(Send)
mainHandler.attachNostrProcessPing(Ping)
mainHandler.attachNostrReset(Reset)
mainHandler.StartBeacons()
const appNprofile = nprofileEncode({ pubkey: localProviderClient.publicKey, relays })
if (wizard) {
wizard.AddConnectInfo(appNprofile, relays)
}
adminManager.setAppNprofile(appNprofile)
// Initialize extension system
// Initialize extension system BEFORE nostrMiddleware so RPC methods are available
let extensionLoader: ExtensionLoader | null = null
const mainPort = settingsManager.getSettings().serviceSettings.servicePort
const extensionPort = mainPort + 1
@ -103,6 +70,42 @@ const start = async () => {
log(`extension system initialization failed: ${e}`)
}
// Initialize nostr middleware with extension loader for RPC routing
log("initializing nostr middleware")
const relays = settingsManager.getSettings().nostrRelaySettings.relays
const maxEventContentLength = settingsManager.getSettings().nostrRelaySettings.maxEventContentLength
const apps: AppInfo[] = keepOn.apps.map(app => {
return {
appId: app.appId,
privateKey: app.privateKey,
publicKey: app.publicKey,
name: app.name,
provider: app.publicKey === localProviderClient.publicKey ? {
clientId: `client_${localProviderClient.appId}`,
pubkey: settingsManager.getSettings().liquiditySettings.liquidityProviderPub,
relayUrl: settingsManager.getSettings().liquiditySettings.providerRelayUrl
} : undefined
}
})
const { Send, Stop, Ping, Reset } = nostrMiddleware(serverMethods, mainHandler,
{
relays, maxEventContentLength, apps
},
(e, p) => mainHandler.liquidityProvider.onEvent(e, p),
{ extensionLoader: extensionLoader || undefined }
)
exitHandler(() => { Stop(); mainHandler.Stop() })
log("starting server")
mainHandler.attachNostrSend(Send)
mainHandler.attachNostrProcessPing(Ping)
mainHandler.attachNostrReset(Reset)
mainHandler.StartBeacons()
const appNprofile = nprofileEncode({ pubkey: localProviderClient.publicKey, relays })
if (wizard) {
wizard.AddConnectInfo(appNprofile, relays)
}
adminManager.setAppNprofile(appNprofile)
// Create Express app for extension HTTP routes
const extensionApp = express()
extensionApp.use(cors()) // Enable CORS for all origins (ATM apps, wallets, etc.)

View file

@ -5,9 +5,15 @@ import * as Types from '../proto/autogenerated/ts/types.js'
import NewNostrTransport, { NostrRequest } from '../proto/autogenerated/ts/nostr_transport.js';
import { ERROR, getLogger } from "./services/helpers/logger.js";
import { NdebitData, NofferData, NmanageRequest } from "@shocknet/clink-sdk";
import type { ExtensionLoader } from "./extensions/loader.js"
type ExportedCalls = { Stop: () => void, Send: NostrSend, Ping: () => Promise<void>, Reset: (settings: NostrSettings) => void }
type ClientEventCallback = (e: { requestId: string }, fromPub: string) => void
export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSettings: NostrSettings, onClientEvent: ClientEventCallback): ExportedCalls => {
export type NostrMiddlewareOptions = {
extensionLoader?: ExtensionLoader
}
export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSettings: NostrSettings, onClientEvent: ClientEventCallback, options?: NostrMiddlewareOptions): ExportedCalls => {
const log = getLogger({})
const nostrTransport = NewNostrTransport(serverMethods, {
NostrUserAuthGuard: async (appId, pub) => {
@ -95,6 +101,31 @@ export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSett
log(ERROR, "authIdentifier does not match", j.authIdentifier || "--", event.pub)
return
}
// Check if this is an extension RPC method
const extensionLoader = options?.extensionLoader
if (extensionLoader && j.rpcName && extensionLoader.hasMethod(j.rpcName)) {
// Route to extension
log(`[Nostr] Routing to extension method: ${j.rpcName}`)
extensionLoader.callMethod(j.rpcName, j.body || {}, event.appId, event.pub)
.then(result => {
const response = { status: 'OK', requestId: j.requestId, ...result }
nostr.Send(
{ type: 'app', appId: event.appId },
{ type: 'content', pub: event.pub, content: JSON.stringify(response) }
)
})
.catch(err => {
log(ERROR, `Extension method ${j.rpcName} failed:`, err.message)
const response = { status: 'ERROR', requestId: j.requestId, reason: err.message }
nostr.Send(
{ type: 'app', appId: event.appId },
{ type: 'content', pub: event.pub, content: JSON.stringify(response) }
)
})
return
}
nostrTransport({ ...j, appId: event.appId }, res => {
nostr.Send({ type: 'app', appId: event.appId }, { type: 'content', pub: event.pub, content: JSON.stringify({ ...res, requestId: j.requestId }) })
}, event.startAtNano, event.startAtMs)