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 <noreply@anthropic.com>
This commit is contained in:
Patrick Mulligan 2026-02-16 16:55:53 -05:00
parent fda5dd6e85
commit fc5a8d795b
2 changed files with 69 additions and 35 deletions

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)