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 df2df9bc26
commit 03f78c0362
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 { mainHandler, localProviderClient, wizard, adminManager } = keepOn
const serverMethods = GetServerMethods(mainHandler) 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 let extensionLoader: ExtensionLoader | null = null
const mainPort = settingsManager.getSettings().serviceSettings.servicePort const mainPort = settingsManager.getSettings().serviceSettings.servicePort
const extensionPort = mainPort + 1 const extensionPort = mainPort + 1
@ -103,6 +70,42 @@ const start = async () => {
log(`extension system initialization failed: ${e}`) 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 // Create Express app for extension HTTP routes
const extensionApp = express() const extensionApp = express()
extensionApp.use(cors()) // Enable CORS for all origins (ATM apps, wallets, etc.) 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 NewNostrTransport, { NostrRequest } from '../proto/autogenerated/ts/nostr_transport.js';
import { ERROR, getLogger } from "./services/helpers/logger.js"; import { ERROR, getLogger } from "./services/helpers/logger.js";
import { NdebitData, NofferData, NmanageRequest } from "@shocknet/clink-sdk"; 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 ExportedCalls = { Stop: () => void, Send: NostrSend, Ping: () => Promise<void>, Reset: (settings: NostrSettings) => void }
type ClientEventCallback = (e: { requestId: string }, fromPub: string) => 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 log = getLogger({})
const nostrTransport = NewNostrTransport(serverMethods, { const nostrTransport = NewNostrTransport(serverMethods, {
NostrUserAuthGuard: async (appId, pub) => { 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) log(ERROR, "authIdentifier does not match", j.authIdentifier || "--", event.pub)
return 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 => { 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 }) }) nostr.Send({ type: 'app', appId: event.appId }, { type: 'content', pub: event.pub, content: JSON.stringify({ ...res, requestId: j.requestId }) })
}, event.startAtNano, event.startAtMs) }, event.startAtNano, event.startAtMs)