From fc5a8d795b1672fbd6a90b8610d326510a3f6347 Mon Sep 17 00:00:00 2001 From: Patrick Mulligan Date: Mon, 16 Feb 2026 16:55:53 -0500 Subject: [PATCH] feat: route Nostr RPC to extension methods Initialize extension system before nostrMiddleware so registered RPC methods are available. Extension methods (e.g. withdraw.createLink) are intercepted and routed to the extension loader before falling through to the standard nostrTransport. Co-Authored-By: Claude Opus 4.6 --- src/index.ts | 71 ++++++++++++++++++++++-------------------- src/nostrMiddleware.ts | 33 +++++++++++++++++++- 2 files changed, 69 insertions(+), 35 deletions(-) diff --git a/src/index.ts b/src/index.ts index 2788c5d0..5fbb21dc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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.) diff --git a/src/nostrMiddleware.ts b/src/nostrMiddleware.ts index 4dd3b281..a7131375 100644 --- a/src/nostrMiddleware.ts +++ b/src/nostrMiddleware.ts @@ -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, 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)