tools update
This commit is contained in:
parent
61da2eea77
commit
f702d2be8d
23 changed files with 5901 additions and 7114 deletions
11595
package-lock.json
generated
11595
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -49,7 +49,7 @@
|
|||
"grpc-tools": "^1.12.4",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
"nostr-tools": "^1.9.0",
|
||||
"nostr-tools": "github:shocknet/nostr-tools#b0cc4a0763352c6c0e16a22d4b4bb4e2f9a06ed9",
|
||||
"pg": "^8.4.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rimraf": "^3.0.2",
|
||||
|
|
|
|||
|
|
@ -1,181 +0,0 @@
|
|||
/*
|
||||
This file contains functions that deal with encoding and decoding nprofiles,
|
||||
but with he addition of bridge urls in the nprofile.
|
||||
These functions are basically the same functions from nostr-tools package
|
||||
but with some tweaks to allow for the bridge inclusion.
|
||||
*/
|
||||
import { bytesToHex, concatBytes, hexToBytes } from '@noble/hashes/utils';
|
||||
import { bech32 } from 'bech32';
|
||||
import { LoadNosrtSettingsFromEnv } from './services/nostr/index.js';
|
||||
|
||||
export const utf8Decoder = new TextDecoder('utf-8')
|
||||
export const utf8Encoder = new TextEncoder()
|
||||
|
||||
|
||||
export type CustomProfilePointer = {
|
||||
pubkey: string
|
||||
relays?: string[]
|
||||
bridge?: string[] // one bridge
|
||||
}
|
||||
|
||||
export type OfferPointer = {
|
||||
pubkey: string,
|
||||
relay: string,
|
||||
offer: string
|
||||
priceType: PriceType,
|
||||
price?: number
|
||||
}
|
||||
export enum PriceType {
|
||||
fixed = 0,
|
||||
variable = 1,
|
||||
spontaneous = 2,
|
||||
}
|
||||
export type DebitPointer = {
|
||||
pubkey: string,
|
||||
relay: string,
|
||||
pointerId?: string,
|
||||
}
|
||||
|
||||
type TLV = { [t: number]: Uint8Array[] }
|
||||
|
||||
|
||||
const encodeTLV = (tlv: TLV): Uint8Array => {
|
||||
const entries: Uint8Array[] = []
|
||||
|
||||
Object.entries(tlv)
|
||||
/*
|
||||
the original function does a reverse() here,
|
||||
but here it causes the nprofile string to be different,
|
||||
even though it would still decode to the correct original inputs
|
||||
*/
|
||||
//.reverse()
|
||||
.forEach(([t, vs]) => {
|
||||
vs.forEach(v => {
|
||||
const entry = new Uint8Array(v.length + 2)
|
||||
entry.set([parseInt(t)], 0)
|
||||
entry.set([v.length], 1)
|
||||
entry.set(v, 2)
|
||||
entries.push(entry)
|
||||
})
|
||||
})
|
||||
return concatBytes(...entries);
|
||||
}
|
||||
|
||||
export const encodeNprofile = (profile: CustomProfilePointer): string => {
|
||||
const data = encodeTLV({
|
||||
0: [hexToBytes(profile.pubkey)],
|
||||
1: (profile.relays || []).map(url => utf8Encoder.encode(url)),
|
||||
2: (profile.bridge || []).map(url => utf8Encoder.encode(url))
|
||||
});
|
||||
const words = bech32.toWords(data)
|
||||
return bech32.encode("nprofile", words, 5000);
|
||||
}
|
||||
|
||||
export const encodeNoffer = (offer: OfferPointer): string => {
|
||||
let relay = offer.relay
|
||||
if (!relay) {
|
||||
const settings = LoadNosrtSettingsFromEnv()
|
||||
relay = settings.relays[0]
|
||||
}
|
||||
const o: TLV = {
|
||||
0: [hexToBytes(offer.pubkey)],
|
||||
1: [utf8Encoder.encode(relay)],
|
||||
2: [utf8Encoder.encode(offer.offer)],
|
||||
3: [new Uint8Array([Number(offer.priceType)])],
|
||||
}
|
||||
if (offer.price) {
|
||||
o[4] = [new Uint8Array(new BigUint64Array([BigInt(offer.price)]).buffer)]
|
||||
}
|
||||
const data = encodeTLV(o);
|
||||
const words = bech32.toWords(data)
|
||||
return bech32.encode("noffer", words, 5000);
|
||||
}
|
||||
export const encodeNdebit = (debit: DebitPointer): string => {
|
||||
let relay = debit.relay
|
||||
if (!relay) {
|
||||
const settings = LoadNosrtSettingsFromEnv()
|
||||
relay = settings.relays[0]
|
||||
}
|
||||
const o: TLV = {
|
||||
0: [hexToBytes(debit.pubkey)],
|
||||
1: [utf8Encoder.encode(relay)],
|
||||
}
|
||||
if (debit.pointerId) {
|
||||
o[2] = [utf8Encoder.encode(debit.pointerId)]
|
||||
}
|
||||
const data = encodeTLV(o);
|
||||
const words = bech32.toWords(data)
|
||||
return bech32.encode("ndebit", words, 5000);
|
||||
}
|
||||
|
||||
const parseTLV = (data: Uint8Array): TLV => {
|
||||
const result: TLV = {}
|
||||
let rest = data
|
||||
while (rest.length > 0) {
|
||||
const t = rest[0]
|
||||
const l = rest[1]
|
||||
const v = rest.slice(2, 2 + l)
|
||||
rest = rest.slice(2 + l)
|
||||
if (v.length < l) throw new Error(`not enough data to read on TLV ${t}`)
|
||||
result[t] = result[t] || []
|
||||
result[t].push(v)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export const decodeNoffer = (noffer: string): OfferPointer => {
|
||||
const { prefix, words } = bech32.decode(noffer, 5000)
|
||||
if (prefix !== "noffer") {
|
||||
throw new Error("Expected nprofile prefix");
|
||||
}
|
||||
const data = new Uint8Array(bech32.fromWords(words))
|
||||
|
||||
const tlv = parseTLV(data);
|
||||
if (!tlv[0]?.[0]) throw new Error('missing TLV 0 for noffer')
|
||||
if (tlv[0][0].length !== 32) throw new Error('TLV 0 should be 32 bytes')
|
||||
if (!tlv[1]?.[0]) throw new Error('missing TLV 1 for noffer')
|
||||
if (!tlv[2]?.[0]) throw new Error('missing TLV 2 for noffer')
|
||||
if (!tlv[3]?.[0]) throw new Error('missing TLV 3 for noffer')
|
||||
return {
|
||||
pubkey: bytesToHex(tlv[0][0]),
|
||||
relay: utf8Decoder.decode(tlv[1][0]),
|
||||
offer: utf8Decoder.decode(tlv[2][0]),
|
||||
priceType: tlv[3][0][0],
|
||||
price: tlv[4] ? Number(new BigUint64Array(tlv[4][0])[0]) : undefined
|
||||
}
|
||||
}
|
||||
export const decodeNdebit = (noffer: string): DebitPointer => {
|
||||
const { prefix, words } = bech32.decode(noffer, 5000)
|
||||
if (prefix !== "ndebit") {
|
||||
throw new Error("Expected nprofile prefix");
|
||||
}
|
||||
const data = new Uint8Array(bech32.fromWords(words))
|
||||
|
||||
const tlv = parseTLV(data);
|
||||
if (!tlv[0]?.[0]) throw new Error('missing TLV 0 for noffer')
|
||||
if (tlv[0][0].length !== 32) throw new Error('TLV 0 should be 32 bytes')
|
||||
if (!tlv[1]?.[0]) throw new Error('missing TLV 1 for noffer')
|
||||
return {
|
||||
pubkey: bytesToHex(tlv[0][0]),
|
||||
relay: utf8Decoder.decode(tlv[1][0]),
|
||||
pointerId: tlv[2] ? utf8Decoder.decode(tlv[2][0]) : undefined
|
||||
}
|
||||
}
|
||||
|
||||
export const decodeNprofile = (nprofile: string): CustomProfilePointer => {
|
||||
const { prefix, words } = bech32.decode(nprofile, 5000)
|
||||
if (prefix !== "nprofile") {
|
||||
throw new Error("Expected nprofile prefix");
|
||||
}
|
||||
const data = new Uint8Array(bech32.fromWords(words))
|
||||
|
||||
const tlv = parseTLV(data);
|
||||
if (!tlv[0]?.[0]) throw new Error('missing TLV 0 for nprofile')
|
||||
if (tlv[0][0].length !== 32) throw new Error('TLV 0 should be 32 bytes')
|
||||
|
||||
return {
|
||||
pubkey: bytesToHex(tlv[0][0]),
|
||||
relays: tlv[1] ? tlv[1].map(d => utf8Decoder.decode(d)) : [],
|
||||
bridge: tlv[2] ? tlv[2].map(d => utf8Decoder.decode(d)) : []
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,8 @@ import nostrMiddleware from './nostrMiddleware.js'
|
|||
import { getLogger } from './services/helpers/logger.js';
|
||||
import { initMainHandler } from './services/main/init.js';
|
||||
import { LoadMainSettingsFromEnv } from './services/main/settings.js';
|
||||
import { encodeNprofile } from './custom-nip19.js';
|
||||
import { nip19 } from 'nostr-tools'
|
||||
const { nprofileEncode } = nip19
|
||||
|
||||
const start = async () => {
|
||||
const log = getLogger({})
|
||||
|
|
@ -29,7 +30,7 @@ const start = async () => {
|
|||
log("starting server")
|
||||
mainHandler.attachNostrSend(Send)
|
||||
mainHandler.StartBeacons()
|
||||
const appNprofile = encodeNprofile({ pubkey: liquidityProviderInfo.publicKey, relays: nostrSettings.relays })
|
||||
const appNprofile = nprofileEncode({ pubkey: liquidityProviderInfo.publicKey, relays: nostrSettings.relays })
|
||||
if (wizard) {
|
||||
wizard.AddConnectInfo(appNprofile, nostrSettings.relays)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,6 @@ import { NostrEvent, NostrSend, NostrSettings } from "./services/nostr/handler.j
|
|||
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";
|
||||
import { UnsignedEvent } from "./services/nostr/tools/event.js";
|
||||
import { defaultInvoiceExpiry } from "./services/storage/paymentStorage.js";
|
||||
import { Application } from "./services/storage/entity/Application.js";
|
||||
import { NdebitData } from "./services/main/debitManager.js";
|
||||
|
||||
export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSettings: NostrSettings, onClientEvent: (e: { requestId: string }, fromPub: string) => void): { Stop: () => void, Send: NostrSend } => {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,9 @@ import * as Types from '../../../proto/autogenerated/ts/types.js'
|
|||
|
||||
import { MainSettings } from './settings.js'
|
||||
import ApplicationManager from './applicationManager.js'
|
||||
import { encodeNdebit, encodeNoffer, PriceType } from '../../custom-nip19.js'
|
||||
import { nip19 } from 'nostr-tools'
|
||||
import { LoadNosrtSettingsFromEnv } from '../nostr/index.js'
|
||||
const { ndebitEncode, nofferEncode, OfferPriceType } = nip19
|
||||
export default class {
|
||||
|
||||
storage: Storage
|
||||
|
|
@ -56,6 +58,7 @@ export default class {
|
|||
if (!appUser) {
|
||||
throw new Error(`app user ${ctx.user_id} not found`) // TODO: fix logs doxing
|
||||
}
|
||||
const nostrSettings = LoadNosrtSettingsFromEnv()
|
||||
return {
|
||||
userId: ctx.user_id,
|
||||
balance: user.balance_sats,
|
||||
|
|
@ -64,8 +67,8 @@ export default class {
|
|||
network_max_fee_bps: this.settings.lndSettings.feeRateBps,
|
||||
network_max_fee_fixed: this.settings.lndSettings.feeFixedLimit,
|
||||
service_fee_bps: this.settings.outgoingAppUserInvoiceFeeBps,
|
||||
noffer: encodeNoffer({ pubkey: app.nostr_public_key!, offer: appUser.identifier, priceType: PriceType.spontaneous, relay: "" }),
|
||||
ndebit: encodeNdebit({ pubkey: app.nostr_public_key!, pointerId: appUser.identifier, relay: "" }),
|
||||
noffer: nofferEncode({ pubkey: app.nostr_public_key!, offer: appUser.identifier, priceType: OfferPriceType.Spontaneous, relay: nostrSettings.relays[0] }),
|
||||
ndebit: ndebitEncode({ pubkey: app.nostr_public_key!, pointerId: appUser.identifier, relay: nostrSettings.relays[0] }),
|
||||
callback_url: appUser.callback_url
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,8 +8,10 @@ import { ApplicationUser } from '../storage/entity/ApplicationUser.js'
|
|||
import { PubLogger, getLogger } from '../helpers/logger.js'
|
||||
import crypto from 'crypto'
|
||||
import { Application } from '../storage/entity/Application.js'
|
||||
import { encodeNdebit, encodeNoffer, PriceType } from '../../custom-nip19.js'
|
||||
|
||||
import { nip69, nip19 } from 'nostr-tools'
|
||||
import { LoadNosrtSettingsFromEnv } from '../nostr/index.js'
|
||||
const { SendNofferRequest } = nip69
|
||||
const { nofferEncode, ndebitEncode, OfferPriceType } = nip19
|
||||
const TOKEN_EXPIRY_TIME = 2 * 60 * 1000 // 2 minutes, in milliseconds
|
||||
|
||||
type NsecLinkingData = {
|
||||
|
|
@ -149,6 +151,7 @@ export default class {
|
|||
u = user
|
||||
if (created) log(u.identifier, u.user.user_id, "user created")
|
||||
}
|
||||
const nostrSettings = LoadNosrtSettingsFromEnv()
|
||||
return {
|
||||
identifier: u.identifier,
|
||||
info: {
|
||||
|
|
@ -159,8 +162,8 @@ export default class {
|
|||
network_max_fee_bps: this.settings.lndSettings.feeRateBps,
|
||||
network_max_fee_fixed: this.settings.lndSettings.feeFixedLimit,
|
||||
service_fee_bps: this.settings.outgoingAppUserInvoiceFeeBps,
|
||||
noffer: encodeNoffer({ pubkey: app.nostr_public_key!, offer: u.identifier, priceType: PriceType.spontaneous, relay: "" }),
|
||||
ndebit: encodeNdebit({ pubkey: app.nostr_public_key!, pointerId: u.identifier, relay: "" }),
|
||||
noffer: nofferEncode({ pubkey: app.nostr_public_key!, offer: u.identifier, priceType: OfferPriceType.Spontaneous, relay: nostrSettings.relays[0] }),
|
||||
ndebit: ndebitEncode({ pubkey: app.nostr_public_key!, pointerId: u.identifier, relay: nostrSettings.relays[0] }),
|
||||
callback_url: u.callback_url
|
||||
|
||||
},
|
||||
|
|
@ -194,6 +197,7 @@ export default class {
|
|||
const app = await this.storage.applicationStorage.GetApplication(appId)
|
||||
const user = await this.storage.applicationStorage.GetApplicationUser(app, req.user_identifier)
|
||||
const max = this.paymentManager.GetMaxPayableInvoice(user.user.balance_sats, true)
|
||||
const nostrSettings = LoadNosrtSettingsFromEnv()
|
||||
return {
|
||||
max_withdrawable: max, identifier: req.user_identifier, info: {
|
||||
userId: user.user.user_id, balance: user.user.balance_sats,
|
||||
|
|
@ -202,8 +206,8 @@ export default class {
|
|||
network_max_fee_bps: this.settings.lndSettings.feeRateBps,
|
||||
network_max_fee_fixed: this.settings.lndSettings.feeFixedLimit,
|
||||
service_fee_bps: this.settings.outgoingAppUserInvoiceFeeBps,
|
||||
noffer: encodeNoffer({ pubkey: app.nostr_public_key!, offer: user.identifier, priceType: PriceType.spontaneous, relay: "" }),
|
||||
ndebit: encodeNdebit({ pubkey: app.nostr_public_key!, pointerId: user.identifier, relay: "" }),
|
||||
noffer: nofferEncode({ pubkey: app.nostr_public_key!, offer: user.identifier, priceType: OfferPriceType.Spontaneous, relay: nostrSettings.relays[0] }),
|
||||
ndebit: ndebitEncode({ pubkey: app.nostr_public_key!, pointerId: user.identifier, relay: nostrSettings.relays[0] }),
|
||||
callback_url: user.callback_url
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { ERROR, getLogger, PubLogger } from "../helpers/logger.js"
|
|||
import AppUserManager from "./appUserManager.js"
|
||||
import { Application } from '../storage/entity/Application.js'
|
||||
import { UserReceivingInvoice } from '../storage/entity/UserReceivingInvoice.js'
|
||||
import { UnsignedEvent } from '../nostr/tools/event.js'
|
||||
import { UnsignedEvent } from 'nostr-tools'
|
||||
import { NostrEvent, NostrSend } from '../nostr/handler.js'
|
||||
import MetricsManager from '../metrics/index.js'
|
||||
import { LoggedEvent } from '../storage/eventsLog.js'
|
||||
|
|
@ -22,7 +22,6 @@ import { RugPullTracker } from "./rugPullTracker.js"
|
|||
import { AdminManager } from "./adminManager.js"
|
||||
import { Unlocker } from "./unlocker.js"
|
||||
import { defaultInvoiceExpiry } from "../storage/paymentStorage.js"
|
||||
import { DebitPointer } from "../../custom-nip19.js"
|
||||
import { DebitManager, NdebitData } from "./debitManager.js"
|
||||
|
||||
type UserOperationsSub = {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import { LoadMainSettingsFromEnv, MainSettings } from "./settings.js"
|
|||
import { Utils } from "../helpers/utilsWrapper.js"
|
||||
import { Wizard } from "../wizard/index.js"
|
||||
import { AdminManager } from "./adminManager.js"
|
||||
import { encodeNprofile } from "../../custom-nip19.js"
|
||||
export type AppData = {
|
||||
privateKey: string;
|
||||
publicKey: string;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
import newNostrClient from '../../../proto/autogenerated/ts/nostr_client.js'
|
||||
import { NostrRequest } from '../../../proto/autogenerated/ts/nostr_transport.js'
|
||||
import * as Types from '../../../proto/autogenerated/ts/types.js'
|
||||
import { decodeNprofile } from '../../custom-nip19.js'
|
||||
import { getLogger } from '../helpers/logger.js'
|
||||
import { Utils } from '../helpers/utilsWrapper.js'
|
||||
import { NostrEvent, NostrSend } from '../nostr/handler.js'
|
||||
import { relayInit } from '../nostr/tools/relay.js'
|
||||
import { InvoicePaidCb } from '../lnd/settings.js'
|
||||
import Storage from '../storage/index.js'
|
||||
export type LiquidityRequest = { action: 'spend' | 'receive', amount: number }
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { UserReceivingAddress } from '../storage/entity/UserReceivingAddress.js'
|
|||
import { AddressPaidCb, InvoicePaidCb, PaidInvoice } from '../lnd/settings.js'
|
||||
import { UserReceivingInvoice, ZapInfo } from '../storage/entity/UserReceivingInvoice.js'
|
||||
import { Payment_PaymentStatus, SendCoinsResponse } from '../../../proto/lnd/lightning.js'
|
||||
import { Event, verifiedSymbol, verifySignature } from '../nostr/tools/event.js'
|
||||
import { Event, verifiedSymbol, verifyEvent } from 'nostr-tools'
|
||||
import { AddressReceivingTransaction } from '../storage/entity/AddressReceivingTransaction.js'
|
||||
import { UserTransactionPayment } from '../storage/entity/UserTransactionPayment.js'
|
||||
import { Watchdog } from './watchdog.js'
|
||||
|
|
@ -580,7 +580,7 @@ export default class {
|
|||
validateZapEvent(event: string, amt: number): ZapInfo {
|
||||
const nostrEvent = JSON.parse(event) as Event
|
||||
delete nostrEvent[verifiedSymbol]
|
||||
const verified = verifySignature(nostrEvent)
|
||||
const verified = verifyEvent(nostrEvent)
|
||||
if (!verified) {
|
||||
throw new Error("nostr event not valid")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ import * as Types from '../../../proto/autogenerated/ts/types.js'
|
|||
import { MainSettings } from './settings.js'
|
||||
import PaymentManager from './paymentManager.js'
|
||||
import { defaultInvoiceExpiry } from '../storage/paymentStorage.js'
|
||||
import { encodeNoffer, PriceType } from '../../custom-nip19.js'
|
||||
import { nip19 } from 'nostr-tools'
|
||||
const { nofferEncode, OfferPriceType } = nip19
|
||||
|
||||
export default class {
|
||||
storage: Storage
|
||||
|
|
@ -26,7 +27,7 @@ export default class {
|
|||
id: newProduct.product_id,
|
||||
name: newProduct.name,
|
||||
price_sats: newProduct.price_sats,
|
||||
noffer: encodeNoffer({ pubkey: user.user_id, offer: offer, priceType: PriceType.fixed, price: newProduct.price_sats, relay: "" })
|
||||
noffer: nofferEncode({ pubkey: user.user_id, offer: offer, priceType: OfferPriceType.Fixed, price: newProduct.price_sats, relay: "" })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,13 @@
|
|||
//import { SimplePool, Sub, Event, UnsignedEvent, getEventHash, signEvent } from 'nostr-tools'
|
||||
import { SimplePool, Sub, Event, UnsignedEvent, getEventHash, finishEvent, relayInit } from './tools/index.js'
|
||||
import { encryptData, decryptData, getSharedSecret, decodePayload, encodePayload, EncryptedData } from './nip44.js'
|
||||
import { SimplePool, Event, UnsignedEvent, getEventHash, finalizeEvent, Relay, nip44 } from 'nostr-tools'
|
||||
//import { encryptData, decryptData, getSharedSecret, decodePayload, encodePayload, EncryptedData, nip44 } from 'nostr-tools'
|
||||
import { ERROR, getLogger } from '../helpers/logger.js'
|
||||
import { encodeNprofile } from '../../custom-nip19.js'
|
||||
import { nip19 } from 'nostr-tools'
|
||||
import { encrypt as encryptV1, decrypt as decryptV1, getSharedSecret as getConversationKeyV1 } from './nip44v1.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
|
||||
type AppInfo = { appId: string, publicKey: string, privateKey: string, name: string }
|
||||
type ClientInfo = { clientId: string, publicKey: string, privateKey: string, name: string }
|
||||
|
|
@ -94,7 +99,6 @@ const supportedKinds = [21000, 21001, 21002]
|
|||
export default class Handler {
|
||||
pool = new SimplePool()
|
||||
settings: NostrSettings
|
||||
subs: Sub[] = []
|
||||
apps: Record<string, AppInfo> = {}
|
||||
eventCallback: (event: NostrEvent) => void
|
||||
log = getLogger({ component: "nostrMiddleware" })
|
||||
|
|
@ -102,7 +106,7 @@ export default class Handler {
|
|||
this.settings = settings
|
||||
this.log("connecting to relays:", settings.relays)
|
||||
this.settings.apps.forEach(app => {
|
||||
this.log("appId:", app.appId, "pubkey:", app.publicKey, "nprofile:", encodeNprofile({ pubkey: app.publicKey, relays: settings.relays }))
|
||||
this.log("appId:", app.appId, "pubkey:", app.publicKey, "nprofile:", nprofileEncode({ pubkey: app.publicKey, relays: settings.relays }))
|
||||
})
|
||||
this.eventCallback = eventCallback
|
||||
this.settings.apps.forEach(app => {
|
||||
|
|
@ -114,9 +118,13 @@ export default class Handler {
|
|||
async Connect() {
|
||||
const log = getLogger({})
|
||||
log("conneting to relay...", this.settings.relays[0])
|
||||
const relay = relayInit(this.settings.relays[0]) // TODO: create multiple conns for multiple relays
|
||||
let relay: Relay | null = null
|
||||
//const relay = relayInit(this.settings.relays[0]) // TODO: create multiple conns for multiple relays
|
||||
try {
|
||||
await relay.connect()
|
||||
relay = await Relay.connect(this.settings.relays[0])
|
||||
if (!relay.connected) {
|
||||
throw new Error("failed to connect to relay")
|
||||
}
|
||||
} catch (err) {
|
||||
log("failed to connect to relay, will try again in 2 seconds")
|
||||
setTimeout(() => {
|
||||
|
|
@ -124,34 +132,36 @@ export default class Handler {
|
|||
}, 2000)
|
||||
return
|
||||
}
|
||||
|
||||
log("connected, subbing...")
|
||||
relay.on('disconnect', () => {
|
||||
relay.onclose = (() => {
|
||||
log("relay disconnected, will try to reconnect")
|
||||
relay.close()
|
||||
this.Connect()
|
||||
})
|
||||
const sub = relay.sub([
|
||||
const sub = relay.subscribe([
|
||||
{
|
||||
since: Math.ceil(Date.now() / 1000),
|
||||
kinds: supportedKinds,
|
||||
'#p': Object.keys(this.apps),
|
||||
}
|
||||
])
|
||||
sub.on('eose', () => {
|
||||
log("up to date with nostr events")
|
||||
})
|
||||
sub.on('event', async (e) => {
|
||||
if (!supportedKinds.includes(e.kind) || !e.pubkey) {
|
||||
return
|
||||
}
|
||||
const pubTags = e.tags.find(tags => tags && tags.length > 1 && tags[0] === 'p')
|
||||
if (!pubTags) {
|
||||
return
|
||||
}
|
||||
const app = this.apps[pubTags[1]]
|
||||
if (app) {
|
||||
await this.processEvent(e, app)
|
||||
return
|
||||
], {
|
||||
oneose: () => {
|
||||
log("up to date with nostr events")
|
||||
},
|
||||
onevent: async (e) => {
|
||||
if (!supportedKinds.includes(e.kind) || !e.pubkey) {
|
||||
return
|
||||
}
|
||||
const pubTags = e.tags.find(tags => tags && tags.length > 1 && tags[0] === 'p')
|
||||
if (!pubTags) {
|
||||
return
|
||||
}
|
||||
const app = this.apps[pubTags[1]]
|
||||
if (app) {
|
||||
await this.processEvent(e, app)
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -167,8 +177,11 @@ export default class Handler {
|
|||
const startAtNano = process.hrtime.bigint().toString()
|
||||
let content = ""
|
||||
try {
|
||||
const decoded = decodePayload(e.content)
|
||||
content = await decryptData(decoded, getSharedSecret(app.privateKey, e.pubkey))
|
||||
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
|
||||
|
|
@ -179,12 +192,12 @@ export default class Handler {
|
|||
|
||||
async Send(initiator: SendInitiator, data: SendData, relays?: string[]) {
|
||||
const keys = this.GetSendKeys(initiator)
|
||||
const privateKey = Buffer.from(keys.privateKey, 'hex')
|
||||
let toSign: UnsignedEvent
|
||||
if (data.type === 'content') {
|
||||
let content: string
|
||||
try {
|
||||
const decoded = await encryptData(data.content, getSharedSecret(keys.privateKey, data.pub))
|
||||
content = encodePayload(decoded)
|
||||
content = encryptV1(data.content, getConversationKeyV1(keys.privateKey, data.pub))
|
||||
} catch (e: any) {
|
||||
this.log(ERROR, "failed to encrypt content", e.message, data.content)
|
||||
return
|
||||
|
|
@ -200,8 +213,7 @@ export default class Handler {
|
|||
toSign = data.event
|
||||
if (data.encrypt) {
|
||||
try {
|
||||
const content = await encryptData(data.event.content, getSharedSecret(keys.privateKey, data.encrypt.toPub))
|
||||
toSign.content = encodePayload(content)
|
||||
toSign.content = encryptV2(data.event.content, getConversationKeyV2(Buffer.from(keys.privateKey, 'hex'), data.encrypt.toPub))
|
||||
} catch (e: any) {
|
||||
this.log(ERROR, "failed to encrypt content", e.message)
|
||||
return
|
||||
|
|
@ -212,7 +224,7 @@ export default class Handler {
|
|||
}
|
||||
}
|
||||
|
||||
const signed = finishEvent(toSign, keys.privateKey)
|
||||
const signed = finalizeEvent(toSign, Buffer.from(keys.privateKey, 'hex'))
|
||||
let sent = false
|
||||
const log = getLogger({ appName: keys.name })
|
||||
await Promise.all(this.pool.publish(relays || this.settings.relays, signed).map(async p => {
|
||||
|
|
|
|||
|
|
@ -12,17 +12,15 @@ export const getSharedSecret = (privateKey: string, publicKey: string) => {
|
|||
return sha256(key.slice(1, 33));
|
||||
}
|
||||
|
||||
export const encryptData = (content: string, sharedSecret: Uint8Array) => {
|
||||
export const encrypt = (content: string, sharedSecret: Uint8Array) => {
|
||||
const nonce = randomBytes(24);
|
||||
const plaintext = new TextEncoder().encode(content);
|
||||
const ciphertext = xchacha20(sharedSecret, nonce, plaintext, plaintext);
|
||||
return {
|
||||
ciphertext: Uint8Array.from(ciphertext),
|
||||
nonce: nonce,
|
||||
} as EncryptedData;
|
||||
return encodePayload({ ciphertext, nonce });
|
||||
}
|
||||
|
||||
export const decryptData = (payload: EncryptedData, sharedSecret: Uint8Array) => {
|
||||
export const decrypt = (content: string, sharedSecret: Uint8Array) => {
|
||||
const payload = decodePayload(content);
|
||||
const dst = xchacha20(sharedSecret, payload.nonce, payload.ciphertext, payload.ciphertext);
|
||||
const decoded = new TextDecoder().decode(dst);
|
||||
return decoded;
|
||||
|
|
@ -1,144 +0,0 @@
|
|||
import { schnorr } from '@noble/curves/secp256k1'
|
||||
import { sha256 } from '@noble/hashes/sha256'
|
||||
import { bytesToHex } from '@noble/hashes/utils'
|
||||
|
||||
import { getPublicKey } from './keys.js'
|
||||
import { utf8Encoder } from './utils.js'
|
||||
|
||||
/** Designates a verified event signature. */
|
||||
export const verifiedSymbol = Symbol('verified')
|
||||
|
||||
/** @deprecated Use numbers instead. */
|
||||
/* eslint-disable no-unused-vars */
|
||||
export enum Kind {
|
||||
Metadata = 0,
|
||||
Text = 1,
|
||||
RecommendRelay = 2,
|
||||
Contacts = 3,
|
||||
EncryptedDirectMessage = 4,
|
||||
EventDeletion = 5,
|
||||
Repost = 6,
|
||||
Reaction = 7,
|
||||
BadgeAward = 8,
|
||||
ChannelCreation = 40,
|
||||
ChannelMetadata = 41,
|
||||
ChannelMessage = 42,
|
||||
ChannelHideMessage = 43,
|
||||
ChannelMuteUser = 44,
|
||||
Blank = 255,
|
||||
Report = 1984,
|
||||
ZapRequest = 9734,
|
||||
Zap = 9735,
|
||||
RelayList = 10002,
|
||||
ClientAuth = 22242,
|
||||
HttpAuth = 27235,
|
||||
ProfileBadge = 30008,
|
||||
BadgeDefinition = 30009,
|
||||
Article = 30023,
|
||||
FileMetadata = 1063,
|
||||
}
|
||||
|
||||
export interface Event<K extends number = number> {
|
||||
kind: K
|
||||
tags: string[][]
|
||||
content: string
|
||||
created_at: number
|
||||
pubkey: string
|
||||
id: string
|
||||
sig: string
|
||||
[verifiedSymbol]?: boolean
|
||||
}
|
||||
|
||||
export type EventTemplate<K extends number = number> = Pick<Event<K>, 'kind' | 'tags' | 'content' | 'created_at'>
|
||||
export type UnsignedEvent<K extends number = number> = Pick<
|
||||
Event<K>,
|
||||
'kind' | 'tags' | 'content' | 'created_at' | 'pubkey'
|
||||
>
|
||||
|
||||
/** An event whose signature has been verified. */
|
||||
export interface VerifiedEvent<K extends number = number> extends Event<K> {
|
||||
[verifiedSymbol]: true
|
||||
}
|
||||
|
||||
export function getBlankEvent(): EventTemplate<Kind.Blank>
|
||||
export function getBlankEvent<K extends number>(kind: K): EventTemplate<K>
|
||||
export function getBlankEvent<K>(kind: K | Kind.Blank = Kind.Blank) {
|
||||
return {
|
||||
kind,
|
||||
content: '',
|
||||
tags: [],
|
||||
created_at: 0,
|
||||
}
|
||||
}
|
||||
|
||||
export function finishEvent<K extends number = number>(t: EventTemplate<K>, privateKey: string): VerifiedEvent<K> {
|
||||
const event = t as VerifiedEvent<K>
|
||||
event.pubkey = getPublicKey(privateKey)
|
||||
event.id = getEventHash(event)
|
||||
event.sig = getSignature(event, privateKey)
|
||||
event[verifiedSymbol] = true
|
||||
return event
|
||||
}
|
||||
|
||||
export function serializeEvent(evt: UnsignedEvent<number>): string {
|
||||
if (!validateEvent(evt)) throw new Error("can't serialize event with wrong or missing properties")
|
||||
|
||||
return JSON.stringify([0, evt.pubkey, evt.created_at, evt.kind, evt.tags, evt.content])
|
||||
}
|
||||
|
||||
export function getEventHash(event: UnsignedEvent<number>): string {
|
||||
let eventHash = sha256(utf8Encoder.encode(serializeEvent(event)))
|
||||
return bytesToHex(eventHash)
|
||||
}
|
||||
|
||||
const isRecord = (obj: unknown): obj is Record<string, unknown> => obj instanceof Object
|
||||
|
||||
export function validateEvent<T>(event: T): event is T & UnsignedEvent<number> {
|
||||
if (!isRecord(event)) return false
|
||||
if (typeof event.kind !== 'number') return false
|
||||
if (typeof event.content !== 'string') return false
|
||||
if (typeof event.created_at !== 'number') return false
|
||||
if (typeof event.pubkey !== 'string') return false
|
||||
if (!event.pubkey.match(/^[a-f0-9]{64}$/)) return false
|
||||
|
||||
if (!Array.isArray(event.tags)) return false
|
||||
for (let i = 0; i < event.tags.length; i++) {
|
||||
let tag = event.tags[i]
|
||||
if (!Array.isArray(tag)) return false
|
||||
for (let j = 0; j < tag.length; j++) {
|
||||
if (typeof tag[j] === 'object') return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/** Verify the event's signature. This function mutates the event with a `verified` symbol, making it idempotent. */
|
||||
export function verifySignature<K extends number>(event: Event<K>): event is VerifiedEvent<K> {
|
||||
//@ts-ignore
|
||||
if (typeof event[verifiedSymbol] === 'boolean') return event[verifiedSymbol]
|
||||
|
||||
const hash = getEventHash(event)
|
||||
if (hash !== event.id) {
|
||||
return (event[verifiedSymbol] = false)
|
||||
}
|
||||
|
||||
try {
|
||||
return (event[verifiedSymbol] = schnorr.verify(event.sig, hash, event.pubkey))
|
||||
} catch (err) {
|
||||
return (event[verifiedSymbol] = false)
|
||||
}
|
||||
}
|
||||
|
||||
/** @deprecated Use `getSignature` instead. */
|
||||
export function signEvent(event: UnsignedEvent<number>, key: string): string {
|
||||
console.warn(
|
||||
'nostr-tools: `signEvent` is deprecated and will be removed or changed in the future. Please use `getSignature` instead.',
|
||||
)
|
||||
return getSignature(event, key)
|
||||
}
|
||||
|
||||
/** Calculate the signature for an event. */
|
||||
export function getSignature(event: UnsignedEvent<number>, key: string): string {
|
||||
return bytesToHex(schnorr.sign(getEventHash(event), key))
|
||||
}
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
export function getHex64(json: string, field: string): string {
|
||||
let len = field.length + 3
|
||||
let idx = json.indexOf(`"${field}":`) + len
|
||||
let s = json.slice(idx).indexOf(`"`) + idx + 1
|
||||
return json.slice(s, s + 64)
|
||||
}
|
||||
|
||||
export function getInt(json: string, field: string): number {
|
||||
let len = field.length
|
||||
let idx = json.indexOf(`"${field}":`) + len + 3
|
||||
let sliced = json.slice(idx)
|
||||
let end = Math.min(sliced.indexOf(','), sliced.indexOf('}'))
|
||||
return parseInt(sliced.slice(0, end), 10)
|
||||
}
|
||||
|
||||
export function getSubscriptionId(json: string): string | null {
|
||||
let idx = json.slice(0, 22).indexOf(`"EVENT"`)
|
||||
if (idx === -1) return null
|
||||
|
||||
let pstart = json.slice(idx + 7 + 1).indexOf(`"`)
|
||||
if (pstart === -1) return null
|
||||
let start = idx + 7 + 1 + pstart
|
||||
|
||||
let pend = json.slice(start + 1, 80).indexOf(`"`)
|
||||
if (pend === -1) return null
|
||||
let end = start + 1 + pend
|
||||
|
||||
return json.slice(start + 1, end)
|
||||
}
|
||||
|
||||
export function matchEventId(json: string, id: string): boolean {
|
||||
return id === getHex64(json, 'id')
|
||||
}
|
||||
|
||||
export function matchEventPubkey(json: string, pubkey: string): boolean {
|
||||
return pubkey === getHex64(json, 'pubkey')
|
||||
}
|
||||
|
||||
export function matchEventKind(json: string, kind: number): boolean {
|
||||
return kind === getInt(json, 'kind')
|
||||
}
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
import { Event } from './event.js'
|
||||
|
||||
export type Filter<K extends number = number> = {
|
||||
ids?: string[]
|
||||
kinds?: K[]
|
||||
authors?: string[]
|
||||
since?: number
|
||||
until?: number
|
||||
limit?: number
|
||||
search?: string
|
||||
[key: `#${string}`]: string[] | undefined
|
||||
}
|
||||
|
||||
export function matchFilter(filter: Filter<number>, event: Event<number>): boolean {
|
||||
if (filter.ids && filter.ids.indexOf(event.id) === -1) {
|
||||
if (!filter.ids.some(prefix => event.id.startsWith(prefix))) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if (filter.kinds && filter.kinds.indexOf(event.kind) === -1) return false
|
||||
if (filter.authors && filter.authors.indexOf(event.pubkey) === -1) {
|
||||
if (!filter.authors.some(prefix => event.pubkey.startsWith(prefix))) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
for (let f in filter) {
|
||||
if (f[0] === '#') {
|
||||
let tagName = f.slice(1)
|
||||
let values = filter[`#${tagName}`]
|
||||
if (values && !event.tags.find(([t, v]) => t === f.slice(1) && values!.indexOf(v) !== -1)) return false
|
||||
}
|
||||
}
|
||||
|
||||
if (filter.since && event.created_at < filter.since) return false
|
||||
if (filter.until && event.created_at > filter.until) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export function matchFilters(filters: Filter<number>[], event: Event<number>): boolean {
|
||||
for (let i = 0; i < filters.length; i++) {
|
||||
if (matchFilter(filters[i], event)) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export function mergeFilters(...filters: Filter<number>[]): Filter<number> {
|
||||
let result: Filter<number> = {}
|
||||
for (let i = 0; i < filters.length; i++) {
|
||||
let filter = filters[i]
|
||||
Object.entries(filter).forEach(([property, values]) => {
|
||||
if (property === 'kinds' || property === 'ids' || property === 'authors' || property[0] === '#') {
|
||||
// @ts-ignore
|
||||
result[property] = result[property] || []
|
||||
// @ts-ignore
|
||||
for (let v = 0; v < values.length; v++) {
|
||||
// @ts-ignore
|
||||
let value = values[v]
|
||||
// @ts-ignore
|
||||
if (!result[property].includes(value)) result[property].push(value)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (filter.limit && (!result.limit || filter.limit > result.limit)) result.limit = filter.limit
|
||||
if (filter.until && (!result.until || filter.until > result.until)) result.until = filter.until
|
||||
if (filter.since && (!result.since || filter.since < result.since)) result.since = filter.since
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
export * from './event.js'
|
||||
export * from './fakejson.js'
|
||||
export * from './filter.js'
|
||||
export * from './keys.js'
|
||||
export * from './pool.js'
|
||||
export * from './relay.js'
|
||||
export * from './utils.js'
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
import { schnorr } from '@noble/curves/secp256k1'
|
||||
import { bytesToHex } from '@noble/hashes/utils'
|
||||
|
||||
export function generatePrivateKey(): string {
|
||||
return bytesToHex(schnorr.utils.randomPrivateKey())
|
||||
}
|
||||
|
||||
export function getPublicKey(privateKey: string): string {
|
||||
return bytesToHex(schnorr.getPublicKey(privateKey))
|
||||
}
|
||||
|
|
@ -1,249 +0,0 @@
|
|||
import { relayInit, eventsGenerator, type Relay, type Sub, type SubscriptionOptions } from './relay.js'
|
||||
import { normalizeURL } from './utils.js'
|
||||
|
||||
import type { Event } from './event.js'
|
||||
import { matchFilters, type Filter } from './filter.js'
|
||||
|
||||
type BatchedRequest = {
|
||||
filters: Filter<any>[]
|
||||
relays: string[]
|
||||
resolve: (events: Event<any>[]) => void
|
||||
events: Event<any>[]
|
||||
}
|
||||
|
||||
export class SimplePool {
|
||||
private _conn: { [url: string]: Relay }
|
||||
private _seenOn: { [id: string]: Set<string> } = {} // a map of all events we've seen in each relay
|
||||
private batchedByKey: { [batchKey: string]: BatchedRequest[] } = {}
|
||||
|
||||
private eoseSubTimeout: number
|
||||
private getTimeout: number
|
||||
private seenOnEnabled: boolean = true
|
||||
private batchInterval: number = 100
|
||||
|
||||
constructor(
|
||||
options: {
|
||||
eoseSubTimeout?: number
|
||||
getTimeout?: number
|
||||
seenOnEnabled?: boolean
|
||||
batchInterval?: number
|
||||
} = {},
|
||||
) {
|
||||
this._conn = {}
|
||||
this.eoseSubTimeout = options.eoseSubTimeout || 3400
|
||||
this.getTimeout = options.getTimeout || 3400
|
||||
this.seenOnEnabled = options.seenOnEnabled !== false
|
||||
this.batchInterval = options.batchInterval || 100
|
||||
}
|
||||
|
||||
close(relays: string[]): void {
|
||||
relays.forEach(url => {
|
||||
let relay = this._conn[normalizeURL(url)]
|
||||
if (relay) relay.close()
|
||||
})
|
||||
}
|
||||
|
||||
async ensureRelay(url: string): Promise<Relay> {
|
||||
const nm = normalizeURL(url)
|
||||
|
||||
if (!this._conn[nm]) {
|
||||
this._conn[nm] = relayInit(nm, {
|
||||
getTimeout: this.getTimeout * 0.9,
|
||||
listTimeout: this.getTimeout * 0.9,
|
||||
})
|
||||
}
|
||||
|
||||
const relay = this._conn[nm]
|
||||
await relay.connect()
|
||||
return relay
|
||||
}
|
||||
|
||||
sub<K extends number = number>(relays: string[], filters: Filter<K>[], opts?: SubscriptionOptions): Sub<K> {
|
||||
let _knownIds: Set<string> = new Set()
|
||||
let modifiedOpts = { ...(opts || {}) }
|
||||
modifiedOpts.alreadyHaveEvent = (id, url) => {
|
||||
if (opts?.alreadyHaveEvent?.(id, url)) {
|
||||
return true
|
||||
}
|
||||
if (this.seenOnEnabled) {
|
||||
let set = this._seenOn[id] || new Set()
|
||||
set.add(url)
|
||||
this._seenOn[id] = set
|
||||
}
|
||||
return _knownIds.has(id)
|
||||
}
|
||||
|
||||
let subs: Sub[] = []
|
||||
let eventListeners: Set<any> = new Set()
|
||||
let eoseListeners: Set<() => void> = new Set()
|
||||
let eosesMissing = relays.length
|
||||
|
||||
let eoseSent = false
|
||||
let eoseTimeout = setTimeout(
|
||||
() => {
|
||||
eoseSent = true
|
||||
for (let cb of eoseListeners.values()) cb()
|
||||
},
|
||||
opts?.eoseSubTimeout || this.eoseSubTimeout,
|
||||
)
|
||||
|
||||
relays
|
||||
.filter((r, i, a) => a.indexOf(r) === i)
|
||||
.forEach(async relay => {
|
||||
let r
|
||||
try {
|
||||
r = await this.ensureRelay(relay)
|
||||
} catch (err) {
|
||||
handleEose()
|
||||
return
|
||||
}
|
||||
if (!r) return
|
||||
let s = r.sub(filters, modifiedOpts)
|
||||
s.on('event', event => {
|
||||
_knownIds.add(event.id as string)
|
||||
for (let cb of eventListeners.values()) cb(event)
|
||||
})
|
||||
s.on('eose', () => {
|
||||
if (eoseSent) return
|
||||
handleEose()
|
||||
})
|
||||
subs.push(s)
|
||||
|
||||
function handleEose() {
|
||||
eosesMissing--
|
||||
if (eosesMissing === 0) {
|
||||
clearTimeout(eoseTimeout)
|
||||
for (let cb of eoseListeners.values()) cb()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
let greaterSub: Sub<K> = {
|
||||
sub(filters, opts) {
|
||||
subs.forEach(sub => sub.sub(filters, opts))
|
||||
return greaterSub as any
|
||||
},
|
||||
unsub() {
|
||||
subs.forEach(sub => sub.unsub())
|
||||
},
|
||||
on(type, cb) {
|
||||
if (type === 'event') {
|
||||
eventListeners.add(cb)
|
||||
} else if (type === 'eose') {
|
||||
eoseListeners.add(cb as () => void | Promise<void>)
|
||||
}
|
||||
},
|
||||
off(type, cb) {
|
||||
if (type === 'event') {
|
||||
eventListeners.delete(cb)
|
||||
} else if (type === 'eose') eoseListeners.delete(cb as () => void | Promise<void>)
|
||||
},
|
||||
get events() {
|
||||
return eventsGenerator(greaterSub)
|
||||
},
|
||||
}
|
||||
|
||||
return greaterSub
|
||||
}
|
||||
|
||||
get<K extends number = number>(
|
||||
relays: string[],
|
||||
filter: Filter<K>,
|
||||
opts?: SubscriptionOptions,
|
||||
): Promise<Event<K> | null> {
|
||||
return new Promise(resolve => {
|
||||
let sub = this.sub(relays, [filter], opts)
|
||||
let timeout = setTimeout(() => {
|
||||
sub.unsub()
|
||||
resolve(null)
|
||||
}, this.getTimeout)
|
||||
sub.on('event', event => {
|
||||
resolve(event)
|
||||
clearTimeout(timeout)
|
||||
sub.unsub()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
list<K extends number = number>(
|
||||
relays: string[],
|
||||
filters: Filter<K>[],
|
||||
opts?: SubscriptionOptions,
|
||||
): Promise<Event<K>[]> {
|
||||
return new Promise(resolve => {
|
||||
let events: Event<K>[] = []
|
||||
let sub = this.sub(relays, filters, opts)
|
||||
|
||||
sub.on('event', event => {
|
||||
events.push(event)
|
||||
})
|
||||
|
||||
// we can rely on an eose being emitted here because pool.sub() will fake one
|
||||
sub.on('eose', () => {
|
||||
sub.unsub()
|
||||
resolve(events)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
batchedList<K extends number = number>(
|
||||
batchKey: string,
|
||||
relays: string[],
|
||||
filters: Filter<K>[],
|
||||
): Promise<Event<K>[]> {
|
||||
return new Promise(resolve => {
|
||||
if (!this.batchedByKey[batchKey]) {
|
||||
this.batchedByKey[batchKey] = [
|
||||
{
|
||||
filters,
|
||||
relays,
|
||||
resolve,
|
||||
events: [],
|
||||
},
|
||||
]
|
||||
|
||||
setTimeout(() => {
|
||||
Object.keys(this.batchedByKey).forEach(async batchKey => {
|
||||
const batchedRequests = this.batchedByKey[batchKey]
|
||||
|
||||
const filters = [] as Filter[]
|
||||
const relays = [] as string[]
|
||||
batchedRequests.forEach(br => {
|
||||
filters.push(...br.filters)
|
||||
relays.push(...br.relays)
|
||||
})
|
||||
|
||||
const sub = this.sub(relays, filters)
|
||||
sub.on('event', event => {
|
||||
batchedRequests.forEach(br => matchFilters(br.filters, event) && br.events.push(event))
|
||||
})
|
||||
sub.on('eose', () => {
|
||||
sub.unsub()
|
||||
batchedRequests.forEach(br => br.resolve(br.events))
|
||||
})
|
||||
|
||||
delete this.batchedByKey[batchKey]
|
||||
})
|
||||
}, this.batchInterval)
|
||||
} else {
|
||||
this.batchedByKey[batchKey].push({
|
||||
filters,
|
||||
relays,
|
||||
resolve,
|
||||
events: [],
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
publish(relays: string[], event: Event<number>): Promise<void>[] {
|
||||
return relays.map(async relay => {
|
||||
let r = await this.ensureRelay(relay)
|
||||
return r.publish(event)
|
||||
})
|
||||
}
|
||||
|
||||
seenOn(id: string): string[] {
|
||||
return Array.from(this._seenOn[id]?.values?.() || [])
|
||||
}
|
||||
}
|
||||
|
|
@ -1,402 +0,0 @@
|
|||
/* global WebSocket */
|
||||
import "websocket-polyfill"
|
||||
import { verifySignature, validateEvent, type Event } from './event.js'
|
||||
import { matchFilters, type Filter } from './filter.js'
|
||||
import { getHex64, getSubscriptionId } from './fakejson.js'
|
||||
import { MessageQueue } from './utils.js'
|
||||
|
||||
type RelayEvent = {
|
||||
connect: () => void | Promise<void>
|
||||
disconnect: () => void | Promise<void>
|
||||
error: () => void | Promise<void>
|
||||
notice: (msg: string) => void | Promise<void>
|
||||
auth: (challenge: string) => void | Promise<void>
|
||||
}
|
||||
export type CountPayload = {
|
||||
count: number
|
||||
}
|
||||
export type SubEvent<K extends number> = {
|
||||
event: (event: Event<K>) => void | Promise<void>
|
||||
count: (payload: CountPayload) => void | Promise<void>
|
||||
eose: () => void | Promise<void>
|
||||
}
|
||||
export type Relay = {
|
||||
url: string
|
||||
status: number
|
||||
connect: () => Promise<void>
|
||||
close: () => void
|
||||
sub: <K extends number = number>(filters: Filter<K>[], opts?: SubscriptionOptions) => Sub<K>
|
||||
list: <K extends number = number>(filters: Filter<K>[], opts?: SubscriptionOptions) => Promise<Event<K>[]>
|
||||
get: <K extends number = number>(filter: Filter<K>, opts?: SubscriptionOptions) => Promise<Event<K> | null>
|
||||
count: (filters: Filter[], opts?: SubscriptionOptions) => Promise<CountPayload | null>
|
||||
publish: (event: Event<number>) => Promise<void>
|
||||
auth: (event: Event<number>) => Promise<void>
|
||||
off: <T extends keyof RelayEvent, U extends RelayEvent[T]>(event: T, listener: U) => void
|
||||
on: <T extends keyof RelayEvent, U extends RelayEvent[T]>(event: T, listener: U) => void
|
||||
}
|
||||
export type Sub<K extends number = number> = {
|
||||
sub: <K extends number = number>(filters: Filter<K>[], opts: SubscriptionOptions) => Sub<K>
|
||||
unsub: () => void
|
||||
on: <T extends keyof SubEvent<K>, U extends SubEvent<K>[T]>(event: T, listener: U) => void
|
||||
off: <T extends keyof SubEvent<K>, U extends SubEvent<K>[T]>(event: T, listener: U) => void
|
||||
events: AsyncGenerator<Event<K>, void, unknown>
|
||||
}
|
||||
|
||||
export type SubscriptionOptions = {
|
||||
id?: string
|
||||
verb?: 'REQ' | 'COUNT'
|
||||
skipVerification?: boolean
|
||||
alreadyHaveEvent?: null | ((id: string, relay: string) => boolean)
|
||||
eoseSubTimeout?: number
|
||||
}
|
||||
|
||||
const newListeners = (): { [TK in keyof RelayEvent]: RelayEvent[TK][] } => ({
|
||||
connect: [],
|
||||
disconnect: [],
|
||||
error: [],
|
||||
notice: [],
|
||||
auth: [],
|
||||
})
|
||||
|
||||
export function relayInit(
|
||||
url: string,
|
||||
options: {
|
||||
getTimeout?: number
|
||||
listTimeout?: number
|
||||
countTimeout?: number
|
||||
} = {},
|
||||
): Relay {
|
||||
let { listTimeout = 3000, getTimeout = 3000, countTimeout = 3000 } = options
|
||||
|
||||
var ws: WebSocket
|
||||
var openSubs: { [id: string]: { filters: Filter[] } & SubscriptionOptions } = {}
|
||||
var listeners = newListeners()
|
||||
var subListeners: {
|
||||
[subid: string]: { [TK in keyof SubEvent<any>]: SubEvent<any>[TK][] }
|
||||
} = {}
|
||||
var pubListeners: {
|
||||
[eventid: string]: {
|
||||
resolve: (_: unknown) => void
|
||||
reject: (err: Error) => void
|
||||
}
|
||||
} = {}
|
||||
|
||||
var connectionPromise: Promise<void> | undefined
|
||||
async function connectRelay(): Promise<void> {
|
||||
if (connectionPromise) return connectionPromise
|
||||
connectionPromise = new Promise((resolve, reject) => {
|
||||
try {
|
||||
ws = new WebSocket(url)
|
||||
} catch (err) {
|
||||
reject(err)
|
||||
}
|
||||
|
||||
ws.onopen = () => {
|
||||
listeners.connect.forEach(cb => cb())
|
||||
resolve()
|
||||
}
|
||||
ws.onerror = () => {
|
||||
connectionPromise = undefined
|
||||
listeners.error.forEach(cb => cb())
|
||||
reject()
|
||||
}
|
||||
ws.onclose = async () => {
|
||||
connectionPromise = undefined
|
||||
listeners.disconnect.forEach(cb => cb())
|
||||
}
|
||||
|
||||
let incomingMessageQueue: MessageQueue = new MessageQueue()
|
||||
let handleNextInterval: any
|
||||
|
||||
ws.onmessage = e => {
|
||||
incomingMessageQueue.enqueue(e.data)
|
||||
if (!handleNextInterval) {
|
||||
handleNextInterval = setInterval(handleNext, 0)
|
||||
}
|
||||
}
|
||||
|
||||
function handleNext() {
|
||||
if (incomingMessageQueue.size === 0) {
|
||||
clearInterval(handleNextInterval)
|
||||
handleNextInterval = null
|
||||
return
|
||||
}
|
||||
|
||||
var json = incomingMessageQueue.dequeue()
|
||||
if (!json) return
|
||||
|
||||
let subid = getSubscriptionId(json)
|
||||
if (subid) {
|
||||
let so = openSubs[subid]
|
||||
if (so && so.alreadyHaveEvent && so.alreadyHaveEvent(getHex64(json, 'id'), url)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
let data = JSON.parse(json)
|
||||
|
||||
// we won't do any checks against the data since all failures (i.e. invalid messages from relays)
|
||||
// will naturally be caught by the encompassing try..catch block
|
||||
|
||||
switch (data[0]) {
|
||||
case 'EVENT': {
|
||||
let id = data[1]
|
||||
let event = data[2]
|
||||
if (
|
||||
validateEvent(event) &&
|
||||
openSubs[id] &&
|
||||
(openSubs[id].skipVerification || verifySignature(event)) &&
|
||||
matchFilters(openSubs[id].filters, event)
|
||||
) {
|
||||
openSubs[id]
|
||||
; (subListeners[id]?.event || []).forEach(cb => cb(event))
|
||||
}
|
||||
return
|
||||
}
|
||||
case 'COUNT':
|
||||
let id = data[1]
|
||||
let payload = data[2]
|
||||
if (openSubs[id]) {
|
||||
; (subListeners[id]?.count || []).forEach(cb => cb(payload))
|
||||
}
|
||||
return
|
||||
case 'EOSE': {
|
||||
let id = data[1]
|
||||
if (id in subListeners) {
|
||||
subListeners[id].eose.forEach(cb => cb())
|
||||
subListeners[id].eose = [] // 'eose' only happens once per sub, so stop listeners here
|
||||
}
|
||||
return
|
||||
}
|
||||
case 'OK': {
|
||||
let id: string = data[1]
|
||||
let ok: boolean = data[2]
|
||||
let reason: string = data[3] || ''
|
||||
if (id in pubListeners) {
|
||||
let { resolve, reject } = pubListeners[id]
|
||||
if (ok) resolve(null)
|
||||
else reject(new Error(reason))
|
||||
}
|
||||
return
|
||||
}
|
||||
case 'NOTICE':
|
||||
let notice = data[1]
|
||||
listeners.notice.forEach(cb => cb(notice))
|
||||
return
|
||||
case 'AUTH': {
|
||||
let challenge = data[1]
|
||||
listeners.auth?.forEach(cb => cb(challenge))
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return connectionPromise
|
||||
}
|
||||
|
||||
function connected() {
|
||||
return ws?.readyState === 1
|
||||
}
|
||||
|
||||
async function connect(): Promise<void> {
|
||||
if (connected()) return // ws already open
|
||||
await connectRelay()
|
||||
}
|
||||
|
||||
async function trySend(params: [string, ...any]) {
|
||||
let msg = JSON.stringify(params)
|
||||
if (!connected()) {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
if (!connected()) {
|
||||
return
|
||||
}
|
||||
}
|
||||
try {
|
||||
ws.send(msg)
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
|
||||
const sub = <K extends number = number>(
|
||||
filters: Filter<K>[],
|
||||
{
|
||||
verb = 'REQ',
|
||||
skipVerification = false,
|
||||
alreadyHaveEvent = null,
|
||||
id = Math.random().toString().slice(2),
|
||||
}: SubscriptionOptions = {},
|
||||
): Sub<K> => {
|
||||
let subid = id
|
||||
|
||||
openSubs[subid] = {
|
||||
id: subid,
|
||||
filters,
|
||||
skipVerification,
|
||||
alreadyHaveEvent,
|
||||
}
|
||||
trySend([verb, subid, ...filters])
|
||||
|
||||
let subscription: Sub<K> = {
|
||||
sub: (newFilters, newOpts = {}) =>
|
||||
sub(newFilters || filters, {
|
||||
skipVerification: newOpts.skipVerification || skipVerification,
|
||||
alreadyHaveEvent: newOpts.alreadyHaveEvent || alreadyHaveEvent,
|
||||
id: subid,
|
||||
}),
|
||||
unsub: () => {
|
||||
delete openSubs[subid]
|
||||
delete subListeners[subid]
|
||||
trySend(['CLOSE', subid])
|
||||
},
|
||||
on: (type, cb) => {
|
||||
subListeners[subid] = subListeners[subid] || {
|
||||
event: [],
|
||||
count: [],
|
||||
eose: [],
|
||||
}
|
||||
//@ts-ignore
|
||||
subListeners[subid][type].push(cb)
|
||||
},
|
||||
off: (type, cb): void => {
|
||||
let listeners = subListeners[subid]
|
||||
//@ts-ignore
|
||||
let idx = listeners[type].indexOf(cb)
|
||||
if (idx >= 0) listeners[type].splice(idx, 1)
|
||||
},
|
||||
get events() {
|
||||
return eventsGenerator(subscription)
|
||||
},
|
||||
}
|
||||
|
||||
return subscription
|
||||
}
|
||||
|
||||
function _publishEvent(event: Event<number>, type: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!event.id) {
|
||||
reject(new Error(`event ${event} has no id`))
|
||||
return
|
||||
}
|
||||
|
||||
let id = event.id
|
||||
trySend([type, event])
|
||||
pubListeners[id] = { resolve, reject }
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
url,
|
||||
sub,
|
||||
on: <T extends keyof RelayEvent, U extends RelayEvent[T]>(type: T, cb: U): void => {
|
||||
//@ts-ignore
|
||||
listeners[type].push(cb)
|
||||
if (type === 'connect' && ws?.readyState === 1) {
|
||||
// i would love to know why we need this
|
||||
; (cb as () => void)()
|
||||
}
|
||||
},
|
||||
off: <T extends keyof RelayEvent, U extends RelayEvent[T]>(type: T, cb: U): void => {
|
||||
//@ts-ignore
|
||||
let index = listeners[type].indexOf(cb)
|
||||
if (index !== -1) listeners[type].splice(index, 1)
|
||||
},
|
||||
list: (filters, opts?: SubscriptionOptions) =>
|
||||
new Promise(resolve => {
|
||||
let s = sub(filters, opts)
|
||||
let events: Event<any>[] = []
|
||||
let timeout = setTimeout(() => {
|
||||
s.unsub()
|
||||
resolve(events)
|
||||
}, listTimeout)
|
||||
s.on('eose', () => {
|
||||
s.unsub()
|
||||
clearTimeout(timeout)
|
||||
resolve(events)
|
||||
})
|
||||
s.on('event', event => {
|
||||
events.push(event)
|
||||
})
|
||||
}),
|
||||
get: (filter, opts?: SubscriptionOptions) =>
|
||||
new Promise(resolve => {
|
||||
let s = sub([filter], opts)
|
||||
let timeout = setTimeout(() => {
|
||||
s.unsub()
|
||||
resolve(null)
|
||||
}, getTimeout)
|
||||
s.on('event', event => {
|
||||
s.unsub()
|
||||
clearTimeout(timeout)
|
||||
resolve(event)
|
||||
})
|
||||
}),
|
||||
count: (filters: Filter[]): Promise<CountPayload | null> =>
|
||||
new Promise(resolve => {
|
||||
let s = sub(filters, { ...sub, verb: 'COUNT' })
|
||||
let timeout = setTimeout(() => {
|
||||
s.unsub()
|
||||
resolve(null)
|
||||
}, countTimeout)
|
||||
s.on('count', (event: CountPayload) => {
|
||||
s.unsub()
|
||||
clearTimeout(timeout)
|
||||
resolve(event)
|
||||
})
|
||||
}),
|
||||
async publish(event): Promise<void> {
|
||||
await _publishEvent(event, 'EVENT')
|
||||
},
|
||||
async auth(event): Promise<void> {
|
||||
await _publishEvent(event, 'AUTH')
|
||||
},
|
||||
connect,
|
||||
close(): void {
|
||||
listeners = newListeners()
|
||||
subListeners = {}
|
||||
pubListeners = {}
|
||||
if (ws?.readyState === WebSocket.OPEN) {
|
||||
ws.close()
|
||||
}
|
||||
},
|
||||
get status() {
|
||||
return ws?.readyState ?? 3
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export async function* eventsGenerator<K extends number>(sub: Sub<K>): AsyncGenerator<Event<K>, void, unknown> {
|
||||
let nextResolve: ((event: Event<K>) => void) | undefined
|
||||
const eventQueue: Event<K>[] = []
|
||||
|
||||
const pushToQueue = (event: Event<K>) => {
|
||||
if (nextResolve) {
|
||||
nextResolve(event)
|
||||
nextResolve = undefined
|
||||
} else {
|
||||
eventQueue.push(event)
|
||||
}
|
||||
}
|
||||
|
||||
sub.on('event', pushToQueue)
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
if (eventQueue.length > 0) {
|
||||
yield eventQueue.shift()!
|
||||
} else {
|
||||
const event = await new Promise<Event<K>>(resolve => {
|
||||
nextResolve = resolve
|
||||
})
|
||||
yield event
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
sub.off('event', pushToQueue)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,169 +0,0 @@
|
|||
import type { Event } from './event.js'
|
||||
|
||||
export const utf8Decoder = new TextDecoder('utf-8')
|
||||
export const utf8Encoder = new TextEncoder()
|
||||
|
||||
export function normalizeURL(url: string): string {
|
||||
let p = new URL(url)
|
||||
p.pathname = p.pathname.replace(/\/+/g, '/')
|
||||
if (p.pathname.endsWith('/')) p.pathname = p.pathname.slice(0, -1)
|
||||
if ((p.port === '80' && p.protocol === 'ws:') || (p.port === '443' && p.protocol === 'wss:')) p.port = ''
|
||||
p.searchParams.sort()
|
||||
p.hash = ''
|
||||
return p.toString()
|
||||
}
|
||||
|
||||
//
|
||||
// fast insert-into-sorted-array functions adapted from https://github.com/terrymorse58/fast-sorted-array
|
||||
//
|
||||
export function insertEventIntoDescendingList(sortedArray: Event<number>[], event: Event<number>) {
|
||||
let start = 0
|
||||
let end = sortedArray.length - 1
|
||||
let midPoint
|
||||
let position = start
|
||||
|
||||
if (end < 0) {
|
||||
position = 0
|
||||
} else if (event.created_at < sortedArray[end].created_at) {
|
||||
position = end + 1
|
||||
} else if (event.created_at >= sortedArray[start].created_at) {
|
||||
position = start
|
||||
} else
|
||||
while (true) {
|
||||
if (end <= start + 1) {
|
||||
position = end
|
||||
break
|
||||
}
|
||||
midPoint = Math.floor(start + (end - start) / 2)
|
||||
if (sortedArray[midPoint].created_at > event.created_at) {
|
||||
start = midPoint
|
||||
} else if (sortedArray[midPoint].created_at < event.created_at) {
|
||||
end = midPoint
|
||||
} else {
|
||||
// aMidPoint === num
|
||||
position = midPoint
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// insert when num is NOT already in (no duplicates)
|
||||
if (sortedArray[position]?.id !== event.id) {
|
||||
return [...sortedArray.slice(0, position), event, ...sortedArray.slice(position)]
|
||||
}
|
||||
|
||||
return sortedArray
|
||||
}
|
||||
|
||||
export function insertEventIntoAscendingList(sortedArray: Event<number>[], event: Event<number>) {
|
||||
let start = 0
|
||||
let end = sortedArray.length - 1
|
||||
let midPoint
|
||||
let position = start
|
||||
|
||||
if (end < 0) {
|
||||
position = 0
|
||||
} else if (event.created_at > sortedArray[end].created_at) {
|
||||
position = end + 1
|
||||
} else if (event.created_at <= sortedArray[start].created_at) {
|
||||
position = start
|
||||
} else
|
||||
while (true) {
|
||||
if (end <= start + 1) {
|
||||
position = end
|
||||
break
|
||||
}
|
||||
midPoint = Math.floor(start + (end - start) / 2)
|
||||
if (sortedArray[midPoint].created_at < event.created_at) {
|
||||
start = midPoint
|
||||
} else if (sortedArray[midPoint].created_at > event.created_at) {
|
||||
end = midPoint
|
||||
} else {
|
||||
// aMidPoint === num
|
||||
position = midPoint
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// insert when num is NOT already in (no duplicates)
|
||||
if (sortedArray[position]?.id !== event.id) {
|
||||
return [...sortedArray.slice(0, position), event, ...sortedArray.slice(position)]
|
||||
}
|
||||
|
||||
return sortedArray
|
||||
}
|
||||
|
||||
export class MessageNode {
|
||||
private _value: string
|
||||
private _next: MessageNode | null
|
||||
|
||||
public get value(): string {
|
||||
return this._value
|
||||
}
|
||||
public set value(message: string) {
|
||||
this._value = message
|
||||
}
|
||||
public get next(): MessageNode | null {
|
||||
return this._next
|
||||
}
|
||||
public set next(node: MessageNode | null) {
|
||||
this._next = node
|
||||
}
|
||||
|
||||
constructor(message: string) {
|
||||
this._value = message
|
||||
this._next = null
|
||||
}
|
||||
}
|
||||
|
||||
export class MessageQueue {
|
||||
private _first: MessageNode | null
|
||||
private _last: MessageNode | null
|
||||
|
||||
public get first(): MessageNode | null {
|
||||
return this._first
|
||||
}
|
||||
public set first(messageNode: MessageNode | null) {
|
||||
this._first = messageNode
|
||||
}
|
||||
public get last(): MessageNode | null {
|
||||
return this._last
|
||||
}
|
||||
public set last(messageNode: MessageNode | null) {
|
||||
this._last = messageNode
|
||||
}
|
||||
private _size: number
|
||||
public get size(): number {
|
||||
return this._size
|
||||
}
|
||||
public set size(v: number) {
|
||||
this._size = v
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this._first = null
|
||||
this._last = null
|
||||
this._size = 0
|
||||
}
|
||||
enqueue(message: string): boolean {
|
||||
const newNode = new MessageNode(message)
|
||||
if (this._size === 0 || !this._last) {
|
||||
this._first = newNode
|
||||
this._last = newNode
|
||||
} else {
|
||||
this._last.next = newNode
|
||||
this._last = newNode
|
||||
}
|
||||
this._size++
|
||||
return true
|
||||
}
|
||||
dequeue(): string | null {
|
||||
if (this._size === 0 || !this._first) return null
|
||||
|
||||
let prev = this._first
|
||||
this._first = prev.next
|
||||
prev.next = null
|
||||
|
||||
this._size--
|
||||
return prev.value
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import crypto from 'crypto';
|
||||
import { Between, DataSource, EntityManager, FindOperator, IsNull, LessThanOrEqual, MoreThanOrEqual } from "typeorm"
|
||||
import { generatePrivateKey, getPublicKey } from 'nostr-tools';
|
||||
import { generateSecretKey, getPublicKey } from 'nostr-tools';
|
||||
import { Application } from "./entity/Application.js"
|
||||
import UserStorage from './userStorage.js';
|
||||
import { ApplicationUser } from './entity/ApplicationUser.js';
|
||||
|
|
@ -67,10 +67,11 @@ export default class {
|
|||
}
|
||||
|
||||
async GenerateApplicationKeys(app: Application) {
|
||||
const priv = generatePrivateKey()
|
||||
const priv = generateSecretKey()
|
||||
const pub = getPublicKey(priv)
|
||||
await this.UpdateApplication(app, { nostr_private_key: priv, nostr_public_key: pub })
|
||||
return { privateKey: priv, publicKey: pub, appId: app.app_id, name: app.name }
|
||||
const privString = Buffer.from(priv).toString('hex')
|
||||
await this.UpdateApplication(app, { nostr_private_key: privString, nostr_public_key: pub })
|
||||
return { privateKey: privString, publicKey: pub, appId: app.app_id, name: app.name }
|
||||
}
|
||||
|
||||
async AddApplicationUser(application: Application, userIdentifier: string, balance: number, nostrPub?: string) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue