commit
ecf805c885
13 changed files with 550 additions and 46 deletions
|
|
@ -18,6 +18,7 @@
|
|||
# LIQUIDITY_PROVIDER_PUB=null
|
||||
# DISABLE_LIQUIDITY_PROVIDER=false
|
||||
# USE_ONLY_LIQUIDITY_PROVIDER=false
|
||||
PROVIDER_RELAY_URL=
|
||||
|
||||
#SWAPS
|
||||
# BOLTZ_HTTP_URL=
|
||||
|
|
|
|||
20
src/index.ts
20
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)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
} */
|
||||
|
|
@ -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
|
||||
|
|
|
|||
300
src/services/nostr/nostrPool.ts
Normal file
300
src/services/nostr/nostrPool.ts
Normal 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]
|
||||
}
|
||||
}
|
||||
152
src/services/nostr/nostrRelayConnection.ts
Normal file
152
src/services/nostr/nostrRelayConnection.ts
Normal 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)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue