diff --git a/env.example b/env.example index bc3c040b..982db75e 100644 --- a/env.example +++ b/env.example @@ -18,6 +18,7 @@ # LIQUIDITY_PROVIDER_PUB=null # DISABLE_LIQUIDITY_PROVIDER=false # USE_ONLY_LIQUIDITY_PROVIDER=false +PROVIDER_RELAY_URL= #SWAPS # BOLTZ_HTTP_URL= diff --git a/src/index.ts b/src/index.ts index 0b53b7ce..78b23147 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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) ) diff --git a/src/nostrMiddleware.ts b/src/nostrMiddleware.ts index da1172f3..034dbce8 100644 --- a/src/nostrMiddleware.ts +++ b/src/nostrMiddleware.ts @@ -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 diff --git a/src/services/helpers/utilsWrapper.ts b/src/services/helpers/utilsWrapper.ts index d7d159f3..39a3eebd 100644 --- a/src/services/helpers/utilsWrapper.ts +++ b/src/services/helpers/utilsWrapper.ts @@ -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 diff --git a/src/services/main/index.ts b/src/services/main/index.ts index 4d061721..21b37a25 100644 --- a/src/services/main/index.ts +++ b/src/services/main/index.ts @@ -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) } diff --git a/src/services/main/liquidityProvider.ts b/src/services/main/liquidityProvider.ts index f625558f..e0edd165 100644 --- a/src/services/main/liquidityProvider.ts +++ b/src/services/main/liquidityProvider.ts @@ -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' diff --git a/src/services/main/settings.ts b/src/services/main/settings.ts index c49225a7..0d4ea352 100644 --- a/src/services/main/settings.ts +++ b/src/services/main/settings.ts @@ -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, 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 = { diff --git a/src/services/nostr/handler.ts b/src/services/nostr/handler.ts index 06c83c08..d1fa46ec 100644 --- a/src/services/nostr/handler.ts +++ b/src/services/nostr/handler.ts @@ -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 -} \ No newline at end of file +} */ \ No newline at end of file diff --git a/src/services/nostr/index.ts b/src/services/nostr/index.ts index 68773ee5..3dce7b41 100644 --- a/src/services/nostr/index.ts +++ b/src/services/nostr/index.ts @@ -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 diff --git a/src/services/nostr/nostrPool.ts b/src/services/nostr/nostrPool.ts new file mode 100644 index 00000000..80c1b0c6 --- /dev/null +++ b/src/services/nostr/nostrPool.ts @@ -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 = {} + apps: Record = {} + maxEventContentLength: number + // providerDestinationPub: string | undefined + eventCallback: RelayEventCallback + log = getLogger({ component: "nostrMiddleware" }) + handledEvents: Map = 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 { + 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 { + 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 { + 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 = {} + 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): 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] + } +} diff --git a/src/services/nostr/nostrRelayConnection.ts b/src/services/nostr/nostrRelayConnection.ts new file mode 100644 index 00000000..7d2d3056 --- /dev/null +++ b/src/services/nostr/nostrRelayConnection.ts @@ -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(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 { + 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) + } + + +} \ No newline at end of file diff --git a/src/tests/setupBootstrapped.ts b/src/tests/setupBootstrapped.ts index 32aa3144..8df22268 100644 --- a/src/tests/setupBootstrapped.ts +++ b/src/tests/setupBootstrapped.ts @@ -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' diff --git a/src/tests/testBase.ts b/src/tests/testBase.ts index fb3f0e90..b814e830 100644 --- a/src/tests/testBase.ts +++ b/src/tests/testBase.ts @@ -83,7 +83,7 @@ export const SetupTest = async (d: Describe, chainTools: ChainTools): Promise { }, 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 })