Merge pull request #865 from shocknet/n-relay-support

N relay support
This commit is contained in:
Justin (shocknet) 2025-12-16 14:34:59 -05:00 committed by GitHub
commit ecf805c885
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 550 additions and 46 deletions

View file

@ -18,6 +18,7 @@
# LIQUIDITY_PROVIDER_PUB=null
# DISABLE_LIQUIDITY_PROVIDER=false
# USE_ONLY_LIQUIDITY_PROVIDER=false
PROVIDER_RELAY_URL=
#SWAPS
# BOLTZ_HTTP_URL=

View file

@ -7,6 +7,7 @@ import { getLogger } from './services/helpers/logger.js';
import { initMainHandler, initSettings } from './services/main/init.js';
import { nip19 } from 'nostr-tools'
import { LoadStorageSettingsFromEnv } from './services/storage/index.js';
import { AppInfo } from './services/nostr/nostrPool.js';
//@ts-ignore
const { nprofileEncode } = nip19
@ -22,15 +23,28 @@ const start = async () => {
return
}
const { apps, mainHandler, liquidityProviderInfo, wizard, adminManager } = keepOn
const { mainHandler, liquidityProviderInfo, 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 === liquidityProviderInfo.publicKey ? {
clientId: liquidityProviderInfo.clientId,
pubDestination: settingsManager.getSettings().liquiditySettings.liquidityProviderPub,
relayUrl: settingsManager.getSettings().liquiditySettings.providerRelayUrl || relays[0]
} : undefined
}
})
const { Send, Stop, Ping, Reset } = nostrMiddleware(serverMethods, mainHandler,
{
relays, maxEventContentLength, apps, clients: [liquidityProviderInfo],
providerDestinationPub: settingsManager.getSettings().liquiditySettings.liquidityProviderPub
relays, maxEventContentLength, apps, /* clients: [liquidityProviderInfo], */
/* providerDestinationPub: settingsManager.getSettings().liquiditySettings.liquidityProviderPub */
},
(e, p) => mainHandler.liquidityProvider.onEvent(e, p)
)

View file

@ -1,6 +1,6 @@
import Main from "./services/main/index.js"
import Nostr from "./services/nostr/index.js"
import { NostrEvent, NostrSend, NostrSettings } from "./services/nostr/handler.js"
import { NostrEvent, NostrSend, NostrSettings } from "./services/nostr/nostrPool.js"
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";
@ -50,6 +50,10 @@ export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSett
return
}
if (event.kind === 21001) {
if (event.relayConstraint === 'provider') {
log("got noffer request on provider only relay, ignoring")
return
}
const offerReq = j as NofferData
log("🎯 [NOSTR EVENT] Received offer request (kind 21001)", {
fromPub: event.pub,
@ -60,18 +64,33 @@ export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSett
mainHandler.offerManager.handleClinkOffer(offerReq, event)
return
} else if (event.kind === 21002) {
if (event.relayConstraint === 'provider') {
log("got debit request on provider only relay, ignoring")
return
}
const debitReq = j as NdebitData
mainHandler.debitManager.handleNip68Debit(debitReq, event)
return
} else if (event.kind === 21003) {
if (event.relayConstraint === 'provider') {
log("got management request on provider only relay, ignoring")
return
}
const nmanageReq = j as NmanageRequest
mainHandler.managementManager.handleRequest(nmanageReq, event);
return;
}
if (!j.rpcName) {
if (event.relayConstraint === 'service') {
log("got relay response on service only relay, ignoring")
}
onClientEvent(j as { requestId: string }, event.pub)
return
}
if (event.relayConstraint === 'provider') {
log("got service request on provider only relay, ignoring")
return
}
if (j.authIdentifier !== event.pub) {
log(ERROR, "authIdentifier does not match", j.authIdentifier || "--", event.pub)
return

View file

@ -1,6 +1,6 @@
import { StateBundler } from "../storage/tlv/stateBundler.js";
import { TlvStorageFactory } from "../storage/tlv/tlvFilesStorageFactory.js";
import { NostrSend } from "../nostr/handler.js";
import { NostrSend } from "../nostr/nostrPool.js";
import { ProcessMetricsCollector } from "../storage/tlv/processMetricsCollector.js";
type UtilsSettings = {
noCollector?: boolean

View file

@ -12,7 +12,7 @@ import AppUserManager from "./appUserManager.js"
import { Application } from '../storage/entity/Application.js'
import { UserReceivingInvoice, ZapInfo } from '../storage/entity/UserReceivingInvoice.js'
import { UnsignedEvent } from 'nostr-tools'
import { NostrEvent, NostrSend } from '../nostr/handler.js'
import { NostrSend } from '../nostr/nostrPool.js'
import MetricsManager from '../metrics/index.js'
import { LoggedEvent } from '../storage/eventsLog.js'
import { LiquidityProvider } from "./liquidityProvider.js"
@ -31,7 +31,7 @@ import { Agent } from "https"
import { NotificationsManager } from "./notificationsManager.js"
import { ApplicationUser } from '../storage/entity/ApplicationUser.js'
import SettingsManager from './settingsManager.js'
import { NostrSettings } from '../nostr/handler.js'
import { NostrSettings, AppInfo } from '../nostr/nostrPool.js'
type UserOperationsSub = {
id: string
newIncomingInvoice: (operation: Types.UserOperation) => void
@ -453,12 +453,26 @@ export default class {
publicKey: liquidityProviderApp.nostr_public_key || "",
name: "liquidity_provider", clientId: `client_${liquidityProviderApp.app_id}`
}
const relays = this.settings.getSettings().nostrRelaySettings.relays
const appsInfo: AppInfo[] = apps.map(app => {
return {
appId: app.app_id,
privateKey: app.nostr_private_key || "",
publicKey: app.nostr_public_key || "",
name: app.name,
provider: app.nostr_public_key === liquidityProviderInfo.publicKey ? {
clientId: liquidityProviderInfo.clientId,
pubDestination: this.settings.getSettings().liquiditySettings.liquidityProviderPub,
relayUrl: this.settings.getSettings().liquiditySettings.providerRelayUrl || relays[0]
} : undefined
}
})
const s: NostrSettings = {
apps: apps.map(a => ({ appId: a.app_id, name: a.name, privateKey: a.nostr_private_key || "", publicKey: a.nostr_public_key || "" })),
relays: this.settings.getSettings().nostrRelaySettings.relays,
apps: appsInfo,
relays,
maxEventContentLength: this.settings.getSettings().nostrRelaySettings.maxEventContentLength,
clients: [liquidityProviderInfo],
providerDestinationPub: this.settings.getSettings().liquiditySettings.liquidityProviderPub
/* clients: [liquidityProviderInfo],
providerDestinationPub: this.settings.getSettings().liquiditySettings.liquidityProviderPub */
}
this.nostrReset(s)
}

View file

@ -3,7 +3,7 @@ import { NostrRequest } from '../../../proto/autogenerated/ts/nostr_transport.js
import * as Types from '../../../proto/autogenerated/ts/types.js'
import { ERROR, getLogger } from '../helpers/logger.js'
import { Utils } from '../helpers/utilsWrapper.js'
import { NostrEvent, NostrSend } from '../nostr/handler.js'
import { NostrSend } from '../nostr/nostrPool.js'
import { InvoicePaidCb } from '../lnd/settings.js'
import Storage from '../storage/index.js'
import SettingsManager from './settingsManager.js'

View file

@ -168,13 +168,15 @@ export type LiquiditySettings = {
liquidityProviderPub: string // cold setting
useOnlyLiquidityProvider: boolean // hot setting
disableLiquidityProvider: boolean // hot setting
providerRelayUrl: string
}
export const LoadLiquiditySettingsFromEnv = (dbEnv: Record<string, string | undefined>, addToDb?: EnvCacher): LiquiditySettings => {
//const liquidityProviderPub = process.env.LIQUIDITY_PROVIDER_PUB === "null" ? "" : (process.env.LIQUIDITY_PROVIDER_PUB || "76ed45f00cea7bac59d8d0b7d204848f5319d7b96c140ffb6fcbaaab0a13d44e")
const liquidityProviderPub = chooseEnv("LIQUIDITY_PROVIDER_PUB", dbEnv, "76ed45f00cea7bac59d8d0b7d204848f5319d7b96c140ffb6fcbaaab0a13d44e", addToDb)
const disableLiquidityProvider = chooseEnvBool("DISABLE_LIQUIDITY_PROVIDER", dbEnv, false, addToDb) || liquidityProviderPub === "null"
const useOnlyLiquidityProvider = chooseEnvBool("USE_ONLY_LIQUIDITY_PROVIDER", dbEnv, false, addToDb)
return { liquidityProviderPub, useOnlyLiquidityProvider, disableLiquidityProvider }
const providerRelayUrl = chooseEnv("PROVIDER_RELAY_URL", dbEnv, "", addToDb)
return { liquidityProviderPub, useOnlyLiquidityProvider, disableLiquidityProvider, providerRelayUrl }
}
export type SwapsSettings = {

View file

@ -1,35 +1,37 @@
//import { SimplePool, Sub, Event, UnsignedEvent, getEventHash, signEvent } from 'nostr-tools'
import WebSocket from 'ws'
Object.assign(global, { WebSocket: WebSocket });
import crypto from 'crypto'
import { SimplePool, Event, UnsignedEvent, finalizeEvent, Relay, nip44, Filter } from 'nostr-tools'
/* import WebSocket from 'ws'
Object.assign(global, { WebSocket: WebSocket }); */
/* import crypto from 'crypto'
import { SimplePool, Event, UnsignedEvent, finalizeEvent, Relay, nip44, Filter } from 'nostr-tools' */
import { ERROR, getLogger } from '../helpers/logger.js'
import { nip19 } from 'nostr-tools'
import { encrypt as encryptV1, decrypt as decryptV1, getSharedSecret as getConversationKeyV1 } from './nip44v1.js'
/* import { nip19 } from 'nostr-tools'
import { encrypt as encryptV1, decrypt as decryptV1, getSharedSecret as getConversationKeyV1 } from './nip44v1.js' */
import { ProcessMetrics, ProcessMetricsCollector } from '../storage/tlv/processMetricsCollector.js'
import { Subscription } from 'nostr-tools/lib/types/abstract-relay.js';
/* import { Subscription } from 'nostr-tools/lib/types/abstract-relay.js';
const { nprofileEncode } = nip19
const { v2 } = nip44
const { encrypt: encryptV2, decrypt: decryptV2, utils } = v2
const { getConversationKey: getConversationKeyV2 } = utils
const handledEvents: string[] = [] // TODO: - big memory leak here, add TTL
const { getConversationKey: getConversationKeyV2 } = utils */
/* const handledEvents: string[] = [] // TODO: - big memory leak here, add TTL
type AppInfo = { appId: string, publicKey: string, privateKey: string, name: string }
type ClientInfo = { clientId: string, publicKey: string, privateKey: string, name: string }
type SendDataContent = { type: "content", content: string, pub: string }
type SendDataEvent = { type: "event", event: UnsignedEvent, encrypt?: { toPub: string } }
export type SendData = SendDataContent | SendDataEvent
export type SendInitiator = { type: 'app', appId: string } | { type: 'client', clientId: string }
export type NostrSend = (initiator: SendInitiator, data: SendData, relays?: string[] | undefined) => void
export type NostrSend = (initiator: SendInitiator, data: SendData, relays?: string[] | undefined) => void */
import { NostrPool } from './nostrPool.js'
import { NostrSettings, SendInitiator, SendData, NostrEvent, NostrSend } from './nostrPool.js'
export type NostrSettings = {
/* export type NostrSettings = {
apps: AppInfo[]
relays: string[]
clients: ClientInfo[]
maxEventContentLength: number
providerDestinationPub: string
}
} */
export type NostrEvent = {
/* export type NostrEvent = {
id: string
pub: string
content: string
@ -37,7 +39,8 @@ export type NostrEvent = {
startAtNano: string
startAtMs: number
kind: number
}
relayConstraint?: 'service' | 'provider'
} */
type SettingsRequest = {
type: 'settings'
@ -88,7 +91,7 @@ const send = (message: ChildProcessResponse) => {
})
}
}
let subProcessHandler: Handler | undefined
let subProcessHandler: NostrPool | undefined
process.on("message", (message: ChildProcessRequest) => {
switch (message.type) {
case 'settings':
@ -108,11 +111,15 @@ process.on("message", (message: ChildProcessRequest) => {
const handleNostrSettings = (settings: NostrSettings) => {
if (subProcessHandler) {
getLogger({ component: "nostrMiddleware" })("got new nostr setting, resetting nostr handler")
subProcessHandler.Stop()
initNostrHandler(settings)
subProcessHandler.UpdateSettings(settings)
// initNostrHandler(settings)
return
}
initNostrHandler(settings)
subProcessHandler = new NostrPool(event => {
send(event)
})
subProcessHandler.UpdateSettings(settings)
// initNostrHandler(settings)
new ProcessMetricsCollector((metrics) => {
send({
type: 'processMetrics',
@ -120,14 +127,11 @@ const handleNostrSettings = (settings: NostrSettings) => {
})
})
}
const initNostrHandler = (settings: NostrSettings) => {
subProcessHandler = new Handler(settings, event => {
send({
type: 'event',
event: event
})
/* const initNostrHandler = (settings: NostrSettings) => {
subProcessHandler = new NostrPool(event => {
send(event)
})
}
} */
const sendToNostr: NostrSend = (initiator, data, relays) => {
if (!subProcessHandler) {
getLogger({ component: "nostrMiddleware" })(ERROR, "nostr was not initialized")
@ -137,7 +141,7 @@ const sendToNostr: NostrSend = (initiator, data, relays) => {
}
send({ type: 'ready' })
const supportedKinds = [21000, 21001, 21002, 21003]
/* const supportedKinds = [21000, 21001, 21002, 21003]
export default class Handler {
pool = new SimplePool()
settings: NostrSettings
@ -382,4 +386,4 @@ const splitContent = (content: string, maxLength: number) => {
parts.push(content.slice(i, i + maxLength))
}
return parts
}
} */

View file

@ -1,13 +1,11 @@
import { ChildProcess, fork } from 'child_process'
import { NostrSettings, NostrEvent, ChildProcessRequest, ChildProcessResponse, SendData, SendInitiator } from "./handler.js"
import { NostrSettings, NostrEvent, SendData, SendInitiator } from "./nostrPool.js"
import { ChildProcessRequest, ChildProcessResponse } from "./handler.js"
import { Utils } from '../helpers/utilsWrapper.js'
import { getLogger, ERROR } from '../helpers/logger.js'
type EventCallback = (event: NostrEvent) => void
type BeaconCallback = (beacon: { content: string, pub: string }) => void
export default class NostrSubprocess {
childProcess: ChildProcess
utils: Utils

View file

@ -0,0 +1,300 @@
import WebSocket from 'ws'
Object.assign(global, { WebSocket: WebSocket });
import crypto from 'crypto'
import { SimplePool, Event, UnsignedEvent, finalizeEvent, Relay, nip44, Filter } from 'nostr-tools'
import { ERROR, getLogger, PubLogger } from '../helpers/logger.js'
import { nip19 } from 'nostr-tools'
import { encrypt as encryptV1, decrypt as decryptV1, getSharedSecret as getConversationKeyV1 } from './nip44v1.js'
import { Subscription } from 'nostr-tools/lib/types/abstract-relay.js';
import { RelayConnection, RelaySettings } from './nostrRelayConnection.js'
const { nprofileEncode } = nip19
const { v2 } = nip44
const { encrypt: encryptV2, decrypt: decryptV2, utils } = v2
const { getConversationKey: getConversationKeyV2 } = utils
// const handledEvents: string[] = [] // TODO: - big memory leak here, add TTL
export type SendDataContent = { type: "content", content: string, pub: string }
export type SendDataEvent = { type: "event", event: UnsignedEvent, encrypt?: { toPub: string } }
export type SendData = SendDataContent | SendDataEvent
export type SendInitiator = { type: 'app', appId: string } | { type: 'client', clientId: string }
export type NostrSend = (initiator: SendInitiator, data: SendData, relays?: string[] | undefined) => void
export type LinkedProviderInfo = { pubDestination: string, clientId: string, relayUrl: string }
export type AppInfo = { appId: string, publicKey: string, privateKey: string, name: string, provider?: LinkedProviderInfo }
// export type ClientInfo = { clientId: string, publicKey: string, privateKey: string, name: string }
export type NostrSettings = {
apps: AppInfo[]
relays: string[]
// clients: ClientInfo[]
maxEventContentLength: number
// providerDestinationPub: string
}
export type NostrEvent = {
id: string
pub: string
content: string
appId: string
startAtNano: string
startAtMs: number
kind: number
relayConstraint?: 'service' | 'provider'
}
type RelayEvent = { type: 'event', event: NostrEvent } | { type: 'beacon', content: string, pub: string }
type RelayEventCallback = (event: RelayEvent) => void
const splitContent = (content: string, maxLength: number) => {
const parts = []
for (let i = 0; i < content.length; i += maxLength) {
parts.push(content.slice(i, i + maxLength))
}
return parts
}
const actionKinds = [21000, 21001, 21002, 21003]
const beaconKind = 30078
const appTag = "Lightning.Pub"
export class NostrPool {
relays: Record<string, RelayConnection> = {}
apps: Record<string /* app pubKey */, AppInfo> = {}
maxEventContentLength: number
// providerDestinationPub: string | undefined
eventCallback: RelayEventCallback
log = getLogger({ component: "nostrMiddleware" })
handledEvents: Map<string, { handledAt: number }> = new Map() // add expiration handler
providerInfo: (LinkedProviderInfo & { appPub: string }) | undefined = undefined
cleanupInterval: NodeJS.Timeout | undefined = undefined
constructor(eventCallback: RelayEventCallback) {
this.eventCallback = eventCallback
}
StartCleanupInterval() {
this.cleanupInterval = setInterval(() => {
this.handledEvents.forEach((value, key) => {
if (Date.now() - value.handledAt > 1000 * 60 * 60 * 2) {
this.handledEvents.delete(key)
}
})
}, 1000 * 60 * 60 * 1)
}
UpdateSettings(settings: NostrSettings) {
Object.values(this.relays).forEach(relay => relay.Stop())
settings.apps.forEach(app => {
this.log("appId:", app.appId, "pubkey:", app.publicKey, "nprofile:", nprofileEncode({ pubkey: app.publicKey, relays: settings.relays }))
})
this.maxEventContentLength = settings.maxEventContentLength
const { apps, rSettings, providerInfo } = processApps(settings)
this.providerInfo = providerInfo
this.apps = apps
this.relays = {}
for (const r of rSettings) {
this.relays[r.relayUrl] = new RelayConnection(r, (e, r) => this.onEvent(e, r))
}
}
private onEvent = (e: Event, relay: RelayConnection) => {
const validated = this.validateEvent(e, relay)
if (!validated || this.handledEvents.has(e.id)) {
return
}
this.handledEvents.set(e.id, { handledAt: Date.now() })
if (validated.type === 'beacon') {
this.eventCallback({ type: 'beacon', content: e.content, pub: validated.pub })
return
}
const { app } = validated
const startAtMs = Date.now()
const startAtNano = process.hrtime.bigint().toString()
let content = ""
try {
if (e.kind === 21000) {
content = decryptV1(e.content, getConversationKeyV1(app.privateKey, e.pubkey))
} else {
content = decryptV2(e.content, getConversationKeyV2(Buffer.from(app.privateKey, 'hex'), e.pubkey))
}
} catch (e: any) {
this.log(ERROR, "failed to decrypt event", e.message, e.content)
return
}
const relayConstraint = relay.getConstraint()
const nostrEvent: NostrEvent = { id: e.id, content, pub: e.pubkey, appId: app.appId, startAtNano, startAtMs, kind: e.kind, relayConstraint }
this.eventCallback({ type: 'event', event: nostrEvent })
}
private validateEvent(e: Event, relay: RelayConnection): { type: 'event', pub: string, app: AppInfo } | { type: 'beacon', content: string, pub: string } | null {
if (e.kind === 30078 && this.providerInfo && relay.isProviderRelay() && e.pubkey === this.providerInfo.pubDestination) {
return { type: 'beacon', content: e.content, pub: e.pubkey }
}
if (!actionKinds.includes(e.kind) || !e.pubkey) {
return null
}
const pubTags = e.tags.find(tags => tags && tags.length > 1 && tags[0] === 'p')
if (!pubTags) {
return null
}
const app = this.apps[pubTags[1]]
if (!app) {
return null
}
return { type: 'event', pub: e.pubkey, app }
}
async Send(initiator: SendInitiator, data: SendData, relays?: string[]) {
try {
const keys = this.getSendKeys(initiator)
const privateKey = Buffer.from(keys.privateKey, 'hex')
const toSign = await this.handleSend(data, keys)
await Promise.all(toSign.map(ue => this.sendEvent(ue, keys, relays)))
} catch (e: any) {
this.log(ERROR, "failed to send event", e.message || e)
throw e
}
}
private async handleSend(data: SendData, keys: { name: string, privateKey: string, publicKey: string }): Promise<UnsignedEvent[]> {
if (data.type === 'content') {
const parts = splitContent(data.content, this.maxEventContentLength)
if (parts.length > 1) {
const shardsId = crypto.randomBytes(16).toString('hex')
const totalShards = parts.length
const ues = await Promise.all(parts.map((part, index) => this.handleSendDataContent({ ...data, content: JSON.stringify({ part, index, totalShards, shardsId }) }, keys)))
return ues
}
return [await this.handleSendDataContent(data, keys)]
}
const ue = await this.handleSendDataEvent(data, keys)
return [ue]
}
private async handleSendDataContent(data: SendDataContent, keys: { name: string, privateKey: string, publicKey: string }): Promise<UnsignedEvent> {
let content = encryptV1(data.content, getConversationKeyV1(keys.privateKey, data.pub))
return {
content,
created_at: Math.floor(Date.now() / 1000),
kind: 21000,
pubkey: keys.publicKey,
tags: [['p', data.pub]],
}
}
private async handleSendDataEvent(data: SendDataEvent, keys: { name: string, privateKey: string, publicKey: string }): Promise<UnsignedEvent> {
const toSign = data.event
if (data.encrypt) {
toSign.content = encryptV2(data.event.content, getConversationKeyV2(Buffer.from(keys.privateKey, 'hex'), data.encrypt.toPub))
}
if (!toSign.pubkey) {
toSign.pubkey = keys.publicKey
}
return toSign
}
private getServiceRelays() {
return Object.values(this.relays).filter(r => r.isServiceRelay()).map(r => r.GetUrl())
}
private async sendEvent(event: UnsignedEvent, keys: { name: string, privateKey: string }, relays?: string[]) {
const signed = finalizeEvent(event, Buffer.from(keys.privateKey, 'hex'))
let sent = false
const log = getLogger({ appName: keys.name })
const r = relays ? relays : this.getServiceRelays()
const pool = new SimplePool()
await Promise.all(pool.publish(r, signed).map(async p => {
try {
await p
sent = true
} catch (e: any) {
console.log(e)
log(e)
}
}))
if (!sent) {
log("failed to send event")
} else {
//log("sent event")
}
}
private getSendKeys(initiator: SendInitiator) {
if (initiator.type === 'app') {
const { appId } = initiator
const found = Object.values(this.apps).find((info: AppInfo) => info.appId === appId)
if (!found) {
throw new Error("unkown app")
}
return { name: found.name, publicKey: found.publicKey, privateKey: found.privateKey }
} else if (initiator.type === 'client') {
const { clientId } = initiator
const providerApp = this.apps[this.providerInfo?.appPub || ""]
if (this.providerInfo && this.providerInfo.clientId === clientId && providerApp) {
return { name: providerApp.name, publicKey: providerApp.publicKey, privateKey: providerApp.privateKey }
}
throw new Error("unkown client")
}
throw new Error("unkown initiator type")
}
}
const processApps = (settings: NostrSettings) => {
const apps: Record<string, AppInfo> = {}
let providerInfo: (LinkedProviderInfo & { appPub: string }) | undefined = undefined
for (const app of settings.apps) {
apps[app.publicKey] = app
if (app.provider) {
if (providerInfo) {
throw new Error("found more than one provider")
}
providerInfo = { ...app.provider, appPub: app.publicKey }
}
}
let providerAssigned = false
const rSettings: RelaySettings[] = []
new Set(settings.relays).forEach(r => {
const filters = [getServiceFilter(apps)]
if (providerInfo && providerInfo.relayUrl === r) {
providerAssigned = true
filters.push(getBeaconFilter(providerInfo.pubDestination))
}
rSettings.push({
relayUrl: r,
serviceRelay: true,
providerRelay: r === providerInfo?.relayUrl,
filters: filters,
})
})
if (!providerAssigned && providerInfo) {
rSettings.push({
relayUrl: providerInfo.relayUrl,
providerRelay: true,
serviceRelay: false,
filters: [
getProviderFilter(providerInfo.appPub, providerInfo.pubDestination),
getBeaconFilter(providerInfo.pubDestination),
],
})
}
return { apps, rSettings, providerInfo }
}
const getServiceFilter = (apps: Record<string, AppInfo>): Filter => {
return {
since: Math.ceil(Date.now() / 1000),
kinds: actionKinds,
'#p': Object.keys(apps),
}
}
const getProviderFilter = (appPub: string, providerPub: string): Filter => {
return {
since: Math.ceil(Date.now() / 1000),
kinds: actionKinds,
'#p': [appPub],
authors: [providerPub]
}
}
const getBeaconFilter = (providerPub: string): Filter => {
return {
kinds: [beaconKind], '#d': [appTag],
authors: [providerPub]
}
}

View file

@ -0,0 +1,152 @@
import WebSocket from 'ws'
Object.assign(global, { WebSocket: WebSocket });
import { Event, UnsignedEvent, Relay, Filter } from 'nostr-tools'
import { ERROR, getLogger, PubLogger } from '../helpers/logger.js'
import { Subscription } from 'nostr-tools/lib/types/abstract-relay.js';
// const handledEvents: string[] = [] // TODO: - big memory leak here, add TTL
/* export type SendDataContent = { type: "content", content: string, pub: string }
export type SendDataEvent = { type: "event", event: UnsignedEvent, encrypt?: { toPub: string } }
export type SendData = SendDataContent | SendDataEvent
export type SendInitiator = { type: 'app', appId: string } | { type: 'client', clientId: string }
export type NostrSend = (initiator: SendInitiator, data: SendData, relays?: string[] | undefined) => void */
/* export type LinkedProviderInfo = { pubDestination: string, clientId: string, relayUrl: string }
export type AppInfo = { appId: string, publicKey: string, privateKey: string, name: string, provider?: LinkedProviderInfo } */
// export type ClientInfo = { clientId: string, publicKey: string, privateKey: string, name: string }
/* export type NostrSettings = {
apps: AppInfo[]
relays: string[]
// clients: ClientInfo[]
maxEventContentLength: number
// providerDestinationPub: string
}
export type NostrEvent = {
id: string
pub: string
content: string
appId: string
startAtNano: string
startAtMs: number
kind: number
relayConstraint?: 'service' | 'provider'
} */
type RelayCallback = (event: Event, relay: RelayConnection) => void
export type RelaySettings = { relayUrl: string, filters: Filter[], serviceRelay: boolean, providerRelay: boolean }
export class RelayConnection {
eventCallback: RelayCallback
log: PubLogger
relay: Relay | null = null
sub: Subscription | null = null
// relayUrl: string
stopped = false
// filters: Filter[]
settings: RelaySettings
constructor(settings: RelaySettings, eventCallback: RelayCallback, autoconnect = true) {
this.log = getLogger({ component: "relay:" + settings.relayUrl })
// this.relayUrl = relayUrl
// this.filters = filters
this.settings = settings
this.eventCallback = eventCallback
if (autoconnect) {
this.ConnectLoop()
}
}
GetUrl() {
return this.settings.relayUrl
}
Stop() {
this.stopped = true
this.sub?.close()
this.relay?.close()
this.relay = null
this.sub = null
}
isServiceRelay() {
return this.settings.serviceRelay
}
isProviderRelay() {
return this.settings.providerRelay
}
getConstraint(): 'service' | 'provider' | undefined {
if (this.isProviderRelay() && !this.isServiceRelay()) {
return 'provider'
}
if (this.isServiceRelay() && !this.isProviderRelay()) {
return 'service'
}
return undefined
}
async ConnectLoop() {
let failures = 0
while (!this.stopped) {
await this.ConnectPromise()
const pow = Math.pow(2, failures)
const delay = Math.min(pow, 900)
this.log("connection failed, will try again in", delay, "seconds (failures:", failures, ")")
await new Promise(resolve => setTimeout(resolve, delay * 1000))
failures++
}
this.log("nostr handler stopped")
}
async ConnectPromise() {
return new Promise<void>(async (res) => {
this.relay = await this.GetRelay()
if (!this.relay) {
res()
return
}
this.sub = this.Subscribe(this.relay)
this.relay.onclose = (() => {
this.log("disconnected")
this.sub?.close()
if (this.relay) {
this.relay.onclose = null
this.relay.close()
this.relay = null
}
this.sub = null
res()
})
})
}
async GetRelay(): Promise<Relay | null> {
try {
const relay = await Relay.connect(this.settings.relayUrl)
if (!relay.connected) {
throw new Error("failed to connect to relay")
}
return relay
} catch (err: any) {
this.log("failed to connect to relay", err.message || err)
return null
}
}
Subscribe(relay: Relay) {
this.log("🔍 subscribing...")
return relay.subscribe(this.settings.filters, {
oneose: () => this.log("is ready"),
onevent: (e) => this.eventCallback(e, this)
})
}
Send(e: Event) {
if (!this.relay) {
throw new Error("relay not connected")
}
return this.relay.publish(e)
}
}

View file

@ -1,6 +1,6 @@
import { getLogger } from '../services/helpers/logger.js'
import { initMainHandler, initSettings } from '../services/main/init.js'
import { SendData } from '../services/nostr/handler.js'
import { SendData } from '../services/nostr/nostrPool.js'
import { TestBase, TestUserData } from './testBase.js'
import * as Types from '../../proto/autogenerated/ts/types.js'
import { GetTestStorageSettings, LoadStorageSettingsFromEnv } from '../services/storage/index.js'

View file

@ -83,7 +83,7 @@ export const SetupTest = async (d: Describe, chainTools: ChainTools): Promise<Te
const extermnalUtils = new Utils({ dataDir: storageSettings.dataDir, allowResetMetricsStorages: storageSettings.allowResetMetricsStorages })
/* const externalAccessToMainLnd = new LND(settings.lndSettings, new LiquidityProvider("", extermnalUtils, async () => { }, async () => { }), extermnalUtils, async () => { }, async () => { }, () => { }, () => { })
await externalAccessToMainLnd.Warmup() */
const liquiditySettings: LiquiditySettings = { disableLiquidityProvider: true, liquidityProviderPub: "", useOnlyLiquidityProvider: false }
const liquiditySettings: LiquiditySettings = { disableLiquidityProvider: true, liquidityProviderPub: "", useOnlyLiquidityProvider: false, providerRelayUrl: "" }
const lndSettings = LoadLndSettingsFromEnv({})
const secondLndNodeSettings = LoadSecondLndSettingsFromEnv()
const otherLndSetting = () => ({ lndSettings, lndNodeSettings: secondLndNodeSettings })