tools update
This commit is contained in:
parent
61da2eea77
commit
f702d2be8d
23 changed files with 5901 additions and 7114 deletions
137
package-lock.json
generated
137
package-lock.json
generated
|
|
@ -32,7 +32,7 @@
|
||||||
"grpc-tools": "^1.12.4",
|
"grpc-tools": "^1.12.4",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"nostr-tools": "^1.9.0",
|
"nostr-tools": "github:shocknet/nostr-tools#b0cc4a0763352c6c0e16a22d4b4bb4e2f9a06ed9",
|
||||||
"pg": "^8.4.0",
|
"pg": "^8.4.0",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
|
|
@ -191,30 +191,50 @@
|
||||||
"node-pre-gyp": "bin/node-pre-gyp"
|
"node-pre-gyp": "bin/node-pre-gyp"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@noble/curves": {
|
"node_modules/@noble/ciphers": {
|
||||||
"version": "1.0.0",
|
"version": "0.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.5.3.tgz",
|
||||||
"integrity": "sha512-2upgEu0iLiDVDZkNLeFV2+ht0BAVgQnEmCk6JsOch9Rp8xfkMCbvbAZlA2pBHQc73dbl+vFOXfqkf4uemdn0bw==",
|
"integrity": "sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w==",
|
||||||
"funding": [
|
"license": "MIT",
|
||||||
{
|
"funding": {
|
||||||
"type": "individual",
|
|
||||||
"url": "https://paulmillr.com/funding/"
|
"url": "https://paulmillr.com/funding/"
|
||||||
}
|
}
|
||||||
],
|
},
|
||||||
|
"node_modules/@noble/curves": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@noble/hashes": "1.3.0"
|
"@noble/hashes": "1.3.2"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@noble/curves/node_modules/@noble/hashes": {
|
||||||
|
"version": "1.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz",
|
||||||
|
"integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@noble/hashes": {
|
"node_modules/@noble/hashes": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz",
|
||||||
"integrity": "sha512-ilHEACi9DwqJB0pw7kv+Apvh50jiiSyR/cQ3y4W7lOR5mhvn/50FLUfsnfJz0BDZtl/RR16kXvptiv6q1msYZg==",
|
"integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==",
|
||||||
"funding": [
|
"license": "MIT",
|
||||||
{
|
"engines": {
|
||||||
"type": "individual",
|
"node": ">= 16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
"url": "https://paulmillr.com/funding/"
|
"url": "https://paulmillr.com/funding/"
|
||||||
}
|
}
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"node_modules/@nodelib/fs.scandir": {
|
"node_modules/@nodelib/fs.scandir": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
|
|
@ -417,37 +437,46 @@
|
||||||
"type": "individual",
|
"type": "individual",
|
||||||
"url": "https://paulmillr.com/funding/"
|
"url": "https://paulmillr.com/funding/"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@scure/bip32": {
|
"node_modules/@scure/bip32": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.1.tgz",
|
||||||
"integrity": "sha512-bcKpo1oj54hGholplGLpqPHRbIsnbixFtc06nwuNM5/dwSXOq/AAYoIBRsBmnZJSdfeNW5rnff7NTAz3ZCqR9Q==",
|
"integrity": "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==",
|
||||||
"funding": [
|
"license": "MIT",
|
||||||
{
|
"dependencies": {
|
||||||
"type": "individual",
|
"@noble/curves": "~1.1.0",
|
||||||
|
"@noble/hashes": "~1.3.1",
|
||||||
|
"@scure/base": "~1.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
"url": "https://paulmillr.com/funding/"
|
"url": "https://paulmillr.com/funding/"
|
||||||
}
|
}
|
||||||
],
|
},
|
||||||
|
"node_modules/@scure/bip32/node_modules/@noble/curves": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@noble/curves": "~1.0.0",
|
"@noble/hashes": "1.3.1"
|
||||||
"@noble/hashes": "~1.3.0",
|
},
|
||||||
"@scure/base": "~1.1.0"
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@scure/bip39": {
|
"node_modules/@scure/bip39": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.1.tgz",
|
||||||
"integrity": "sha512-SX/uKq52cuxm4YFXWFaVByaSHJh2w3BnokVSeUJVCv6K7WulT9u2BuNRBhuFl8vAuYnzx9bEu9WgpcNYTrYieg==",
|
"integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==",
|
||||||
"funding": [
|
"license": "MIT",
|
||||||
{
|
|
||||||
"type": "individual",
|
|
||||||
"url": "https://paulmillr.com/funding/"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@noble/hashes": "~1.3.0",
|
"@noble/hashes": "~1.3.0",
|
||||||
"@scure/base": "~1.1.0"
|
"@scure/base": "~1.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sqltools/formatter": {
|
"node_modules/@sqltools/formatter": {
|
||||||
|
|
@ -3760,16 +3789,36 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/nostr-tools": {
|
"node_modules/nostr-tools": {
|
||||||
"version": "1.9.0",
|
"version": "2.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-1.9.0.tgz",
|
"resolved": "git+ssh://git@github.com/shocknet/nostr-tools.git#b0cc4a0763352c6c0e16a22d4b4bb4e2f9a06ed9",
|
||||||
"integrity": "sha512-ZvFf1uiBqWLWhLBHD2nY0KsdSdNWKb3PrQUmYMWxSzfT4k48cDrDJu2qgULkOhQbFX7oty8IpaKnLvixhqefqA==",
|
"integrity": "sha512-PvgkpUeMffZRGqdzAXU+uOuK9UNWBQ6Dosr8Ie53P0DsVduZGvtChMcaidpczu0gjST33II8anpvbbdluh0+gg==",
|
||||||
|
"license": "Unlicense",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@noble/curves": "1.0.0",
|
"@noble/ciphers": "^0.5.1",
|
||||||
"@noble/hashes": "1.3.0",
|
"@noble/curves": "1.2.0",
|
||||||
|
"@noble/hashes": "1.3.1",
|
||||||
"@scure/base": "1.1.1",
|
"@scure/base": "1.1.1",
|
||||||
"@scure/bip32": "1.3.0",
|
"@scure/bip32": "1.3.1",
|
||||||
"@scure/bip39": "1.2.0"
|
"@scure/bip39": "1.2.1"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"nostr-wasm": "v0.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": ">=5.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"typescript": {
|
||||||
|
"optional": true
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/nostr-wasm": {
|
||||||
|
"version": "0.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/nostr-wasm/-/nostr-wasm-0.1.0.tgz",
|
||||||
|
"integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"node_modules/npmlog": {
|
"node_modules/npmlog": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@
|
||||||
"grpc-tools": "^1.12.4",
|
"grpc-tools": "^1.12.4",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"nostr-tools": "^1.9.0",
|
"nostr-tools": "github:shocknet/nostr-tools#b0cc4a0763352c6c0e16a22d4b4bb4e2f9a06ed9",
|
||||||
"pg": "^8.4.0",
|
"pg": "^8.4.0",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rimraf": "^3.0.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 { getLogger } from './services/helpers/logger.js';
|
||||||
import { initMainHandler } from './services/main/init.js';
|
import { initMainHandler } from './services/main/init.js';
|
||||||
import { LoadMainSettingsFromEnv } from './services/main/settings.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 start = async () => {
|
||||||
const log = getLogger({})
|
const log = getLogger({})
|
||||||
|
|
@ -29,7 +30,7 @@ const start = async () => {
|
||||||
log("starting server")
|
log("starting server")
|
||||||
mainHandler.attachNostrSend(Send)
|
mainHandler.attachNostrSend(Send)
|
||||||
mainHandler.StartBeacons()
|
mainHandler.StartBeacons()
|
||||||
const appNprofile = encodeNprofile({ pubkey: liquidityProviderInfo.publicKey, relays: nostrSettings.relays })
|
const appNprofile = nprofileEncode({ pubkey: liquidityProviderInfo.publicKey, relays: nostrSettings.relays })
|
||||||
if (wizard) {
|
if (wizard) {
|
||||||
wizard.AddConnectInfo(appNprofile, nostrSettings.relays)
|
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 * 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";
|
||||||
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";
|
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 } => {
|
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 { MainSettings } from './settings.js'
|
||||||
import ApplicationManager from './applicationManager.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 {
|
export default class {
|
||||||
|
|
||||||
storage: Storage
|
storage: Storage
|
||||||
|
|
@ -56,6 +58,7 @@ export default class {
|
||||||
if (!appUser) {
|
if (!appUser) {
|
||||||
throw new Error(`app user ${ctx.user_id} not found`) // TODO: fix logs doxing
|
throw new Error(`app user ${ctx.user_id} not found`) // TODO: fix logs doxing
|
||||||
}
|
}
|
||||||
|
const nostrSettings = LoadNosrtSettingsFromEnv()
|
||||||
return {
|
return {
|
||||||
userId: ctx.user_id,
|
userId: ctx.user_id,
|
||||||
balance: user.balance_sats,
|
balance: user.balance_sats,
|
||||||
|
|
@ -64,8 +67,8 @@ export default class {
|
||||||
network_max_fee_bps: this.settings.lndSettings.feeRateBps,
|
network_max_fee_bps: this.settings.lndSettings.feeRateBps,
|
||||||
network_max_fee_fixed: this.settings.lndSettings.feeFixedLimit,
|
network_max_fee_fixed: this.settings.lndSettings.feeFixedLimit,
|
||||||
service_fee_bps: this.settings.outgoingAppUserInvoiceFeeBps,
|
service_fee_bps: this.settings.outgoingAppUserInvoiceFeeBps,
|
||||||
noffer: encodeNoffer({ pubkey: app.nostr_public_key!, offer: appUser.identifier, priceType: PriceType.spontaneous, relay: "" }),
|
noffer: nofferEncode({ pubkey: app.nostr_public_key!, offer: appUser.identifier, priceType: OfferPriceType.Spontaneous, relay: nostrSettings.relays[0] }),
|
||||||
ndebit: encodeNdebit({ pubkey: app.nostr_public_key!, pointerId: appUser.identifier, relay: "" }),
|
ndebit: ndebitEncode({ pubkey: app.nostr_public_key!, pointerId: appUser.identifier, relay: nostrSettings.relays[0] }),
|
||||||
callback_url: appUser.callback_url
|
callback_url: appUser.callback_url
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,10 @@ import { ApplicationUser } from '../storage/entity/ApplicationUser.js'
|
||||||
import { PubLogger, getLogger } from '../helpers/logger.js'
|
import { PubLogger, getLogger } from '../helpers/logger.js'
|
||||||
import crypto from 'crypto'
|
import crypto from 'crypto'
|
||||||
import { Application } from '../storage/entity/Application.js'
|
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
|
const TOKEN_EXPIRY_TIME = 2 * 60 * 1000 // 2 minutes, in milliseconds
|
||||||
|
|
||||||
type NsecLinkingData = {
|
type NsecLinkingData = {
|
||||||
|
|
@ -149,6 +151,7 @@ export default class {
|
||||||
u = user
|
u = user
|
||||||
if (created) log(u.identifier, u.user.user_id, "user created")
|
if (created) log(u.identifier, u.user.user_id, "user created")
|
||||||
}
|
}
|
||||||
|
const nostrSettings = LoadNosrtSettingsFromEnv()
|
||||||
return {
|
return {
|
||||||
identifier: u.identifier,
|
identifier: u.identifier,
|
||||||
info: {
|
info: {
|
||||||
|
|
@ -159,8 +162,8 @@ export default class {
|
||||||
network_max_fee_bps: this.settings.lndSettings.feeRateBps,
|
network_max_fee_bps: this.settings.lndSettings.feeRateBps,
|
||||||
network_max_fee_fixed: this.settings.lndSettings.feeFixedLimit,
|
network_max_fee_fixed: this.settings.lndSettings.feeFixedLimit,
|
||||||
service_fee_bps: this.settings.outgoingAppUserInvoiceFeeBps,
|
service_fee_bps: this.settings.outgoingAppUserInvoiceFeeBps,
|
||||||
noffer: encodeNoffer({ pubkey: app.nostr_public_key!, offer: u.identifier, priceType: PriceType.spontaneous, relay: "" }),
|
noffer: nofferEncode({ pubkey: app.nostr_public_key!, offer: u.identifier, priceType: OfferPriceType.Spontaneous, relay: nostrSettings.relays[0] }),
|
||||||
ndebit: encodeNdebit({ pubkey: app.nostr_public_key!, pointerId: u.identifier, relay: "" }),
|
ndebit: ndebitEncode({ pubkey: app.nostr_public_key!, pointerId: u.identifier, relay: nostrSettings.relays[0] }),
|
||||||
callback_url: u.callback_url
|
callback_url: u.callback_url
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
@ -194,6 +197,7 @@ export default class {
|
||||||
const app = await this.storage.applicationStorage.GetApplication(appId)
|
const app = await this.storage.applicationStorage.GetApplication(appId)
|
||||||
const user = await this.storage.applicationStorage.GetApplicationUser(app, req.user_identifier)
|
const user = await this.storage.applicationStorage.GetApplicationUser(app, req.user_identifier)
|
||||||
const max = this.paymentManager.GetMaxPayableInvoice(user.user.balance_sats, true)
|
const max = this.paymentManager.GetMaxPayableInvoice(user.user.balance_sats, true)
|
||||||
|
const nostrSettings = LoadNosrtSettingsFromEnv()
|
||||||
return {
|
return {
|
||||||
max_withdrawable: max, identifier: req.user_identifier, info: {
|
max_withdrawable: max, identifier: req.user_identifier, info: {
|
||||||
userId: user.user.user_id, balance: user.user.balance_sats,
|
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_bps: this.settings.lndSettings.feeRateBps,
|
||||||
network_max_fee_fixed: this.settings.lndSettings.feeFixedLimit,
|
network_max_fee_fixed: this.settings.lndSettings.feeFixedLimit,
|
||||||
service_fee_bps: this.settings.outgoingAppUserInvoiceFeeBps,
|
service_fee_bps: this.settings.outgoingAppUserInvoiceFeeBps,
|
||||||
noffer: encodeNoffer({ pubkey: app.nostr_public_key!, offer: user.identifier, priceType: PriceType.spontaneous, relay: "" }),
|
noffer: nofferEncode({ pubkey: app.nostr_public_key!, offer: user.identifier, priceType: OfferPriceType.Spontaneous, relay: nostrSettings.relays[0] }),
|
||||||
ndebit: encodeNdebit({ pubkey: app.nostr_public_key!, pointerId: user.identifier, relay: "" }),
|
ndebit: ndebitEncode({ pubkey: app.nostr_public_key!, pointerId: user.identifier, relay: nostrSettings.relays[0] }),
|
||||||
callback_url: user.callback_url
|
callback_url: user.callback_url
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import { ERROR, getLogger, PubLogger } from "../helpers/logger.js"
|
||||||
import AppUserManager from "./appUserManager.js"
|
import AppUserManager from "./appUserManager.js"
|
||||||
import { Application } from '../storage/entity/Application.js'
|
import { Application } from '../storage/entity/Application.js'
|
||||||
import { UserReceivingInvoice } from '../storage/entity/UserReceivingInvoice.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 { NostrEvent, NostrSend } from '../nostr/handler.js'
|
||||||
import MetricsManager from '../metrics/index.js'
|
import MetricsManager from '../metrics/index.js'
|
||||||
import { LoggedEvent } from '../storage/eventsLog.js'
|
import { LoggedEvent } from '../storage/eventsLog.js'
|
||||||
|
|
@ -22,7 +22,6 @@ import { RugPullTracker } from "./rugPullTracker.js"
|
||||||
import { AdminManager } from "./adminManager.js"
|
import { AdminManager } from "./adminManager.js"
|
||||||
import { Unlocker } from "./unlocker.js"
|
import { Unlocker } from "./unlocker.js"
|
||||||
import { defaultInvoiceExpiry } from "../storage/paymentStorage.js"
|
import { defaultInvoiceExpiry } from "../storage/paymentStorage.js"
|
||||||
import { DebitPointer } from "../../custom-nip19.js"
|
|
||||||
import { DebitManager, NdebitData } from "./debitManager.js"
|
import { DebitManager, NdebitData } from "./debitManager.js"
|
||||||
|
|
||||||
type UserOperationsSub = {
|
type UserOperationsSub = {
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import { LoadMainSettingsFromEnv, MainSettings } from "./settings.js"
|
||||||
import { Utils } from "../helpers/utilsWrapper.js"
|
import { Utils } from "../helpers/utilsWrapper.js"
|
||||||
import { Wizard } from "../wizard/index.js"
|
import { Wizard } from "../wizard/index.js"
|
||||||
import { AdminManager } from "./adminManager.js"
|
import { AdminManager } from "./adminManager.js"
|
||||||
import { encodeNprofile } from "../../custom-nip19.js"
|
|
||||||
export type AppData = {
|
export type AppData = {
|
||||||
privateKey: string;
|
privateKey: string;
|
||||||
publicKey: string;
|
publicKey: string;
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,9 @@
|
||||||
import newNostrClient from '../../../proto/autogenerated/ts/nostr_client.js'
|
import newNostrClient from '../../../proto/autogenerated/ts/nostr_client.js'
|
||||||
import { NostrRequest } from '../../../proto/autogenerated/ts/nostr_transport.js'
|
import { NostrRequest } from '../../../proto/autogenerated/ts/nostr_transport.js'
|
||||||
import * as Types from '../../../proto/autogenerated/ts/types.js'
|
import * as Types from '../../../proto/autogenerated/ts/types.js'
|
||||||
import { decodeNprofile } from '../../custom-nip19.js'
|
|
||||||
import { getLogger } from '../helpers/logger.js'
|
import { getLogger } from '../helpers/logger.js'
|
||||||
import { Utils } from '../helpers/utilsWrapper.js'
|
import { Utils } from '../helpers/utilsWrapper.js'
|
||||||
import { NostrEvent, NostrSend } from '../nostr/handler.js'
|
import { NostrEvent, NostrSend } from '../nostr/handler.js'
|
||||||
import { relayInit } from '../nostr/tools/relay.js'
|
|
||||||
import { InvoicePaidCb } from '../lnd/settings.js'
|
import { InvoicePaidCb } from '../lnd/settings.js'
|
||||||
import Storage from '../storage/index.js'
|
import Storage from '../storage/index.js'
|
||||||
export type LiquidityRequest = { action: 'spend' | 'receive', amount: number }
|
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 { AddressPaidCb, InvoicePaidCb, PaidInvoice } from '../lnd/settings.js'
|
||||||
import { UserReceivingInvoice, ZapInfo } from '../storage/entity/UserReceivingInvoice.js'
|
import { UserReceivingInvoice, ZapInfo } from '../storage/entity/UserReceivingInvoice.js'
|
||||||
import { Payment_PaymentStatus, SendCoinsResponse } from '../../../proto/lnd/lightning.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 { AddressReceivingTransaction } from '../storage/entity/AddressReceivingTransaction.js'
|
||||||
import { UserTransactionPayment } from '../storage/entity/UserTransactionPayment.js'
|
import { UserTransactionPayment } from '../storage/entity/UserTransactionPayment.js'
|
||||||
import { Watchdog } from './watchdog.js'
|
import { Watchdog } from './watchdog.js'
|
||||||
|
|
@ -580,7 +580,7 @@ export default class {
|
||||||
validateZapEvent(event: string, amt: number): ZapInfo {
|
validateZapEvent(event: string, amt: number): ZapInfo {
|
||||||
const nostrEvent = JSON.parse(event) as Event
|
const nostrEvent = JSON.parse(event) as Event
|
||||||
delete nostrEvent[verifiedSymbol]
|
delete nostrEvent[verifiedSymbol]
|
||||||
const verified = verifySignature(nostrEvent)
|
const verified = verifyEvent(nostrEvent)
|
||||||
if (!verified) {
|
if (!verified) {
|
||||||
throw new Error("nostr event not valid")
|
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 { MainSettings } from './settings.js'
|
||||||
import PaymentManager from './paymentManager.js'
|
import PaymentManager from './paymentManager.js'
|
||||||
import { defaultInvoiceExpiry } from '../storage/paymentStorage.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 {
|
export default class {
|
||||||
storage: Storage
|
storage: Storage
|
||||||
|
|
@ -26,7 +27,7 @@ export default class {
|
||||||
id: newProduct.product_id,
|
id: newProduct.product_id,
|
||||||
name: newProduct.name,
|
name: newProduct.name,
|
||||||
price_sats: newProduct.price_sats,
|
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, signEvent } from 'nostr-tools'
|
||||||
import { SimplePool, Sub, Event, UnsignedEvent, getEventHash, finishEvent, relayInit } from './tools/index.js'
|
import { SimplePool, Event, UnsignedEvent, getEventHash, finalizeEvent, Relay, nip44 } from 'nostr-tools'
|
||||||
import { encryptData, decryptData, getSharedSecret, decodePayload, encodePayload, EncryptedData } from './nip44.js'
|
//import { encryptData, decryptData, getSharedSecret, decodePayload, encodePayload, EncryptedData, nip44 } from 'nostr-tools'
|
||||||
import { ERROR, getLogger } from '../helpers/logger.js'
|
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
|
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 }
|
||||||
|
|
@ -94,7 +99,6 @@ const supportedKinds = [21000, 21001, 21002]
|
||||||
export default class Handler {
|
export default class Handler {
|
||||||
pool = new SimplePool()
|
pool = new SimplePool()
|
||||||
settings: NostrSettings
|
settings: NostrSettings
|
||||||
subs: Sub[] = []
|
|
||||||
apps: Record<string, AppInfo> = {}
|
apps: Record<string, AppInfo> = {}
|
||||||
eventCallback: (event: NostrEvent) => void
|
eventCallback: (event: NostrEvent) => void
|
||||||
log = getLogger({ component: "nostrMiddleware" })
|
log = getLogger({ component: "nostrMiddleware" })
|
||||||
|
|
@ -102,7 +106,7 @@ export default class Handler {
|
||||||
this.settings = settings
|
this.settings = settings
|
||||||
this.log("connecting to relays:", settings.relays)
|
this.log("connecting to relays:", settings.relays)
|
||||||
this.settings.apps.forEach(app => {
|
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.eventCallback = eventCallback
|
||||||
this.settings.apps.forEach(app => {
|
this.settings.apps.forEach(app => {
|
||||||
|
|
@ -114,9 +118,13 @@ export default class Handler {
|
||||||
async Connect() {
|
async Connect() {
|
||||||
const log = getLogger({})
|
const log = getLogger({})
|
||||||
log("conneting to relay...", this.settings.relays[0])
|
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 {
|
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) {
|
} catch (err) {
|
||||||
log("failed to connect to relay, will try again in 2 seconds")
|
log("failed to connect to relay, will try again in 2 seconds")
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|
@ -124,23 +132,24 @@ export default class Handler {
|
||||||
}, 2000)
|
}, 2000)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log("connected, subbing...")
|
log("connected, subbing...")
|
||||||
relay.on('disconnect', () => {
|
relay.onclose = (() => {
|
||||||
log("relay disconnected, will try to reconnect")
|
log("relay disconnected, will try to reconnect")
|
||||||
relay.close()
|
relay.close()
|
||||||
this.Connect()
|
this.Connect()
|
||||||
})
|
})
|
||||||
const sub = relay.sub([
|
const sub = relay.subscribe([
|
||||||
{
|
{
|
||||||
since: Math.ceil(Date.now() / 1000),
|
since: Math.ceil(Date.now() / 1000),
|
||||||
kinds: supportedKinds,
|
kinds: supportedKinds,
|
||||||
'#p': Object.keys(this.apps),
|
'#p': Object.keys(this.apps),
|
||||||
}
|
}
|
||||||
])
|
], {
|
||||||
sub.on('eose', () => {
|
oneose: () => {
|
||||||
log("up to date with nostr events")
|
log("up to date with nostr events")
|
||||||
})
|
},
|
||||||
sub.on('event', async (e) => {
|
onevent: async (e) => {
|
||||||
if (!supportedKinds.includes(e.kind) || !e.pubkey) {
|
if (!supportedKinds.includes(e.kind) || !e.pubkey) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -153,6 +162,7 @@ export default class Handler {
|
||||||
await this.processEvent(e, app)
|
await this.processEvent(e, app)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -167,8 +177,11 @@ export default class Handler {
|
||||||
const startAtNano = process.hrtime.bigint().toString()
|
const startAtNano = process.hrtime.bigint().toString()
|
||||||
let content = ""
|
let content = ""
|
||||||
try {
|
try {
|
||||||
const decoded = decodePayload(e.content)
|
if (e.kind === 21000) {
|
||||||
content = await decryptData(decoded, getSharedSecret(app.privateKey, e.pubkey))
|
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) {
|
} catch (e: any) {
|
||||||
this.log(ERROR, "failed to decrypt event", e.message, e.content)
|
this.log(ERROR, "failed to decrypt event", e.message, e.content)
|
||||||
return
|
return
|
||||||
|
|
@ -179,12 +192,12 @@ export default class Handler {
|
||||||
|
|
||||||
async Send(initiator: SendInitiator, data: SendData, relays?: string[]) {
|
async Send(initiator: SendInitiator, data: SendData, relays?: string[]) {
|
||||||
const keys = this.GetSendKeys(initiator)
|
const keys = this.GetSendKeys(initiator)
|
||||||
|
const privateKey = Buffer.from(keys.privateKey, 'hex')
|
||||||
let toSign: UnsignedEvent
|
let toSign: UnsignedEvent
|
||||||
if (data.type === 'content') {
|
if (data.type === 'content') {
|
||||||
let content: string
|
let content: string
|
||||||
try {
|
try {
|
||||||
const decoded = await encryptData(data.content, getSharedSecret(keys.privateKey, data.pub))
|
content = encryptV1(data.content, getConversationKeyV1(keys.privateKey, data.pub))
|
||||||
content = encodePayload(decoded)
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.log(ERROR, "failed to encrypt content", e.message, data.content)
|
this.log(ERROR, "failed to encrypt content", e.message, data.content)
|
||||||
return
|
return
|
||||||
|
|
@ -200,8 +213,7 @@ export default class Handler {
|
||||||
toSign = data.event
|
toSign = data.event
|
||||||
if (data.encrypt) {
|
if (data.encrypt) {
|
||||||
try {
|
try {
|
||||||
const content = await encryptData(data.event.content, getSharedSecret(keys.privateKey, data.encrypt.toPub))
|
toSign.content = encryptV2(data.event.content, getConversationKeyV2(Buffer.from(keys.privateKey, 'hex'), data.encrypt.toPub))
|
||||||
toSign.content = encodePayload(content)
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.log(ERROR, "failed to encrypt content", e.message)
|
this.log(ERROR, "failed to encrypt content", e.message)
|
||||||
return
|
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
|
let sent = false
|
||||||
const log = getLogger({ appName: keys.name })
|
const log = getLogger({ appName: keys.name })
|
||||||
await Promise.all(this.pool.publish(relays || this.settings.relays, signed).map(async p => {
|
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));
|
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 nonce = randomBytes(24);
|
||||||
const plaintext = new TextEncoder().encode(content);
|
const plaintext = new TextEncoder().encode(content);
|
||||||
const ciphertext = xchacha20(sharedSecret, nonce, plaintext, plaintext);
|
const ciphertext = xchacha20(sharedSecret, nonce, plaintext, plaintext);
|
||||||
return {
|
return encodePayload({ ciphertext, nonce });
|
||||||
ciphertext: Uint8Array.from(ciphertext),
|
|
||||||
nonce: nonce,
|
|
||||||
} as EncryptedData;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 dst = xchacha20(sharedSecret, payload.nonce, payload.ciphertext, payload.ciphertext);
|
||||||
const decoded = new TextDecoder().decode(dst);
|
const decoded = new TextDecoder().decode(dst);
|
||||||
return decoded;
|
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 crypto from 'crypto';
|
||||||
import { Between, DataSource, EntityManager, FindOperator, IsNull, LessThanOrEqual, MoreThanOrEqual } from "typeorm"
|
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 { Application } from "./entity/Application.js"
|
||||||
import UserStorage from './userStorage.js';
|
import UserStorage from './userStorage.js';
|
||||||
import { ApplicationUser } from './entity/ApplicationUser.js';
|
import { ApplicationUser } from './entity/ApplicationUser.js';
|
||||||
|
|
@ -67,10 +67,11 @@ export default class {
|
||||||
}
|
}
|
||||||
|
|
||||||
async GenerateApplicationKeys(app: Application) {
|
async GenerateApplicationKeys(app: Application) {
|
||||||
const priv = generatePrivateKey()
|
const priv = generateSecretKey()
|
||||||
const pub = getPublicKey(priv)
|
const pub = getPublicKey(priv)
|
||||||
await this.UpdateApplication(app, { nostr_private_key: priv, nostr_public_key: pub })
|
const privString = Buffer.from(priv).toString('hex')
|
||||||
return { privateKey: priv, publicKey: pub, appId: app.app_id, name: app.name }
|
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) {
|
async AddApplicationUser(application: Application, userIdentifier: string, balance: number, nostrPub?: string) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue