N relay support
This commit is contained in:
parent
e2dec7d9b3
commit
61a424447a
5 changed files with 505 additions and 32 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
import Main from "./services/main/index.js"
|
import Main from "./services/main/index.js"
|
||||||
import Nostr from "./services/nostr/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 * as Types from '../proto/autogenerated/ts/types.js'
|
||||||
import NewNostrTransport, { NostrRequest } from '../proto/autogenerated/ts/nostr_transport.js';
|
import NewNostrTransport, { NostrRequest } from '../proto/autogenerated/ts/nostr_transport.js';
|
||||||
import { ERROR, getLogger } from "./services/helpers/logger.js";
|
import { ERROR, getLogger } from "./services/helpers/logger.js";
|
||||||
|
|
@ -50,6 +50,10 @@ export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSett
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (event.kind === 21001) {
|
if (event.kind === 21001) {
|
||||||
|
if (event.relayConstraint === 'provider') {
|
||||||
|
log("got noffer request on provider only relay, ignoring")
|
||||||
|
return
|
||||||
|
}
|
||||||
const offerReq = j as NofferData
|
const offerReq = j as NofferData
|
||||||
log("🎯 [NOSTR EVENT] Received offer request (kind 21001)", {
|
log("🎯 [NOSTR EVENT] Received offer request (kind 21001)", {
|
||||||
fromPub: event.pub,
|
fromPub: event.pub,
|
||||||
|
|
@ -60,18 +64,33 @@ export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSett
|
||||||
mainHandler.offerManager.handleClinkOffer(offerReq, event)
|
mainHandler.offerManager.handleClinkOffer(offerReq, event)
|
||||||
return
|
return
|
||||||
} else if (event.kind === 21002) {
|
} else if (event.kind === 21002) {
|
||||||
|
if (event.relayConstraint === 'provider') {
|
||||||
|
log("got debit request on provider only relay, ignoring")
|
||||||
|
return
|
||||||
|
}
|
||||||
const debitReq = j as NdebitData
|
const debitReq = j as NdebitData
|
||||||
mainHandler.debitManager.handleNip68Debit(debitReq, event)
|
mainHandler.debitManager.handleNip68Debit(debitReq, event)
|
||||||
return
|
return
|
||||||
} else if (event.kind === 21003) {
|
} else if (event.kind === 21003) {
|
||||||
|
if (event.relayConstraint === 'provider') {
|
||||||
|
log("got management request on provider only relay, ignoring")
|
||||||
|
return
|
||||||
|
}
|
||||||
const nmanageReq = j as NmanageRequest
|
const nmanageReq = j as NmanageRequest
|
||||||
mainHandler.managementManager.handleRequest(nmanageReq, event);
|
mainHandler.managementManager.handleRequest(nmanageReq, event);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!j.rpcName) {
|
if (!j.rpcName) {
|
||||||
|
if (event.relayConstraint === 'service') {
|
||||||
|
log("got relay response on service only relay, ignoring")
|
||||||
|
}
|
||||||
onClientEvent(j as { requestId: string }, event.pub)
|
onClientEvent(j as { requestId: string }, event.pub)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (event.relayConstraint === 'provider') {
|
||||||
|
log("got service request on provider only relay, ignoring")
|
||||||
|
return
|
||||||
|
}
|
||||||
if (j.authIdentifier !== event.pub) {
|
if (j.authIdentifier !== event.pub) {
|
||||||
log(ERROR, "authIdentifier does not match", j.authIdentifier || "--", event.pub)
|
log(ERROR, "authIdentifier does not match", j.authIdentifier || "--", event.pub)
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,37 @@
|
||||||
//import { SimplePool, Sub, Event, UnsignedEvent, getEventHash, signEvent } from 'nostr-tools'
|
//import { SimplePool, Sub, Event, UnsignedEvent, getEventHash, signEvent } from 'nostr-tools'
|
||||||
import WebSocket from 'ws'
|
/* import WebSocket from 'ws'
|
||||||
Object.assign(global, { WebSocket: WebSocket });
|
Object.assign(global, { WebSocket: WebSocket }); */
|
||||||
import crypto from 'crypto'
|
/* import crypto from 'crypto'
|
||||||
import { SimplePool, Event, UnsignedEvent, finalizeEvent, Relay, nip44, Filter } from 'nostr-tools'
|
import { SimplePool, Event, UnsignedEvent, finalizeEvent, Relay, nip44, Filter } from 'nostr-tools' */
|
||||||
import { ERROR, getLogger } from '../helpers/logger.js'
|
import { ERROR, getLogger } from '../helpers/logger.js'
|
||||||
import { nip19 } from 'nostr-tools'
|
/* import { nip19 } from 'nostr-tools'
|
||||||
import { encrypt as encryptV1, decrypt as decryptV1, getSharedSecret as getConversationKeyV1 } from './nip44v1.js'
|
import { encrypt as encryptV1, decrypt as decryptV1, getSharedSecret as getConversationKeyV1 } from './nip44v1.js' */
|
||||||
import { ProcessMetrics, ProcessMetricsCollector } from '../storage/tlv/processMetricsCollector.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 { nprofileEncode } = nip19
|
||||||
const { v2 } = nip44
|
const { v2 } = nip44
|
||||||
const { encrypt: encryptV2, decrypt: decryptV2, utils } = v2
|
const { encrypt: encryptV2, decrypt: decryptV2, utils } = v2
|
||||||
const { getConversationKey: getConversationKeyV2 } = utils
|
const { getConversationKey: getConversationKeyV2 } = utils */
|
||||||
const handledEvents: string[] = [] // TODO: - big memory leak here, add TTL
|
/* const handledEvents: string[] = [] // TODO: - big memory leak here, add TTL
|
||||||
type AppInfo = { appId: string, publicKey: string, privateKey: string, name: string }
|
type AppInfo = { appId: string, publicKey: string, privateKey: string, name: string }
|
||||||
type ClientInfo = { clientId: 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 SendDataContent = { type: "content", content: string, pub: string }
|
||||||
type SendDataEvent = { type: "event", event: UnsignedEvent, encrypt?: { toPub: string } }
|
type SendDataEvent = { type: "event", event: UnsignedEvent, encrypt?: { toPub: string } }
|
||||||
export type SendData = SendDataContent | SendDataEvent
|
export type SendData = SendDataContent | SendDataEvent
|
||||||
export type SendInitiator = { type: 'app', appId: string } | { type: 'client', clientId: string }
|
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[]
|
apps: AppInfo[]
|
||||||
relays: string[]
|
relays: string[]
|
||||||
clients: ClientInfo[]
|
clients: ClientInfo[]
|
||||||
maxEventContentLength: number
|
maxEventContentLength: number
|
||||||
providerDestinationPub: string
|
providerDestinationPub: string
|
||||||
}
|
} */
|
||||||
|
|
||||||
export type NostrEvent = {
|
/* export type NostrEvent = {
|
||||||
id: string
|
id: string
|
||||||
pub: string
|
pub: string
|
||||||
content: string
|
content: string
|
||||||
|
|
@ -37,7 +39,8 @@ export type NostrEvent = {
|
||||||
startAtNano: string
|
startAtNano: string
|
||||||
startAtMs: number
|
startAtMs: number
|
||||||
kind: number
|
kind: number
|
||||||
}
|
relayConstraint?: 'service' | 'provider'
|
||||||
|
} */
|
||||||
|
|
||||||
type SettingsRequest = {
|
type SettingsRequest = {
|
||||||
type: 'settings'
|
type: 'settings'
|
||||||
|
|
@ -88,7 +91,7 @@ const send = (message: ChildProcessResponse) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let subProcessHandler: Handler | undefined
|
let subProcessHandler: NostrPool | undefined
|
||||||
process.on("message", (message: ChildProcessRequest) => {
|
process.on("message", (message: ChildProcessRequest) => {
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case 'settings':
|
case 'settings':
|
||||||
|
|
@ -108,11 +111,15 @@ process.on("message", (message: ChildProcessRequest) => {
|
||||||
const handleNostrSettings = (settings: NostrSettings) => {
|
const handleNostrSettings = (settings: NostrSettings) => {
|
||||||
if (subProcessHandler) {
|
if (subProcessHandler) {
|
||||||
getLogger({ component: "nostrMiddleware" })("got new nostr setting, resetting nostr handler")
|
getLogger({ component: "nostrMiddleware" })("got new nostr setting, resetting nostr handler")
|
||||||
subProcessHandler.Stop()
|
subProcessHandler.UpdateSettings(settings)
|
||||||
initNostrHandler(settings)
|
// initNostrHandler(settings)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
initNostrHandler(settings)
|
subProcessHandler = new NostrPool(event => {
|
||||||
|
send(event)
|
||||||
|
})
|
||||||
|
subProcessHandler.UpdateSettings(settings)
|
||||||
|
// initNostrHandler(settings)
|
||||||
new ProcessMetricsCollector((metrics) => {
|
new ProcessMetricsCollector((metrics) => {
|
||||||
send({
|
send({
|
||||||
type: 'processMetrics',
|
type: 'processMetrics',
|
||||||
|
|
@ -120,14 +127,11 @@ const handleNostrSettings = (settings: NostrSettings) => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const initNostrHandler = (settings: NostrSettings) => {
|
/* const initNostrHandler = (settings: NostrSettings) => {
|
||||||
subProcessHandler = new Handler(settings, event => {
|
subProcessHandler = new NostrPool(event => {
|
||||||
send({
|
send(event)
|
||||||
type: 'event',
|
|
||||||
event: event
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
} */
|
||||||
const sendToNostr: NostrSend = (initiator, data, relays) => {
|
const sendToNostr: NostrSend = (initiator, data, relays) => {
|
||||||
if (!subProcessHandler) {
|
if (!subProcessHandler) {
|
||||||
getLogger({ component: "nostrMiddleware" })(ERROR, "nostr was not initialized")
|
getLogger({ component: "nostrMiddleware" })(ERROR, "nostr was not initialized")
|
||||||
|
|
@ -137,7 +141,7 @@ const sendToNostr: NostrSend = (initiator, data, relays) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
send({ type: 'ready' })
|
send({ type: 'ready' })
|
||||||
const supportedKinds = [21000, 21001, 21002, 21003]
|
/* const supportedKinds = [21000, 21001, 21002, 21003]
|
||||||
export default class Handler {
|
export default class Handler {
|
||||||
pool = new SimplePool()
|
pool = new SimplePool()
|
||||||
settings: NostrSettings
|
settings: NostrSettings
|
||||||
|
|
@ -382,4 +386,4 @@ const splitContent = (content: string, maxLength: number) => {
|
||||||
parts.push(content.slice(i, i + maxLength))
|
parts.push(content.slice(i, i + maxLength))
|
||||||
}
|
}
|
||||||
return parts
|
return parts
|
||||||
}
|
} */
|
||||||
|
|
@ -1,13 +1,11 @@
|
||||||
import { ChildProcess, fork } from 'child_process'
|
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 { Utils } from '../helpers/utilsWrapper.js'
|
||||||
import { getLogger, ERROR } from '../helpers/logger.js'
|
import { getLogger, ERROR } from '../helpers/logger.js'
|
||||||
type EventCallback = (event: NostrEvent) => void
|
type EventCallback = (event: NostrEvent) => void
|
||||||
type BeaconCallback = (beacon: { content: string, pub: string }) => void
|
type BeaconCallback = (beacon: { content: string, pub: string }) => void
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default class NostrSubprocess {
|
export default class NostrSubprocess {
|
||||||
childProcess: ChildProcess
|
childProcess: ChildProcess
|
||||||
utils: Utils
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue