Compare commits

...

33 Commits

Author SHA1 Message Date
fiatjaf
24f5068fdb bump to v1.7.5 2023-03-19 09:09:19 -03:00
fiatjaf
5733f9c4e4 reject promise on WebSocket initiation failure. 2023-03-19 09:07:16 -03:00
BilligsterUser
6b73bbf8a3 Type sub event handler (#156)
* RelayEvent allow Promises

* type Sub EventHandler

* Update relay.ts
2023-03-12 08:51:38 -03:00
BilligsterUser
d244b62c7a type Relay EventHandler (#121)
* type Relay EventHandler

* Update relay.ts
2023-03-11 15:24:52 -03:00
BilligsterUser
b00af9a30a call ensurerelay() before calling pool.publish()
fixes #153
2023-03-11 14:22:11 -03:00
BilligsterUser
be7c981c14 NIP-39: validate github 2023-03-11 08:33:36 -03:00
BilligsterUser
5539e5cf89 Pool: Sub use provided alreadyHaveEvent Fn 2023-03-06 13:27:24 -03:00
Egge
73decbc8e0 added connect to ensureRelay if status != 1 2023-03-06 11:13:08 -03:00
BilligsterUser
b3d95cecdd add search field to filter (NIP-50) 2023-03-05 08:08:36 -03:00
ramigs
82228036ef close relay's websocket only if it's in state OPEN 2023-03-04 12:57:50 -03:00
fiatjaf
01435ab9f5 test decode nip19 with relays.
closes https://github.com/nbd-wtf/nostr-tools/issues/147
2023-03-02 21:32:53 -03:00
fiatjaf
63cbc4133a nip05 domain must have a dot. 2023-03-02 21:32:53 -03:00
BilligsterUser
049f183d27 update actions/checkout to v3
fixes ci warning

Node.js 12 actions are deprecated. Please update the following actions to use Node.js 16: actions/checkout@v2. For more information see: https://github.blog/changelog/2022-09-22-github-actions-all-actions-will-begin-running-on-node16-instead-of-node12/.
2023-03-02 09:38:10 -03:00
BilligsterUser
f9e3119ab4 package.json add License field 2023-03-02 09:34:54 -03:00
fiatjaf
f992c9c967 cleanup lib/ so we can only publish the esm file under esm/ 2023-03-02 08:36:43 -03:00
fiatjaf
dbf625d6ac fix justfile emit-types. 2023-03-02 08:27:44 -03:00
fiatjaf
8622bd11dd replace test relays. 2023-03-02 08:24:50 -03:00
fiatjaf
0970eee70f remove log from test. 2023-03-02 08:22:30 -03:00
fiatjaf
086f8830e3 catch fetch error on nip05. 2023-03-02 08:21:17 -03:00
Egge
e48d722227 Fixed readme for publishing with pool 2023-03-01 15:29:24 -03:00
Fernando López Guevara
0d77013aab chore(dx): add format script 💅 (#128) 2023-02-28 12:19:10 -03:00
BilligsterUser
4c415280aa ci emit types on publish 2023-02-27 22:17:52 -03:00
fiatjaf
4188aaf7c8 just type-check 2023-02-27 19:53:12 -03:00
BilligsterUser
673f4abab8 add type definition
fixes #138
2023-02-27 19:51:19 -03:00
BilligsterUser
bcefaa0757 update repo url 2023-02-27 19:49:00 -03:00
fiatjaf
649af36a86 one more nip19 test. 2023-02-27 16:10:26 -03:00
Simon
96a6f7af87 ensure kind has type 'number' in validateEvent 2023-02-27 14:22:36 -03:00
fiatjaf
a4c713efcb fix readme example.
fixes https://github.com/nbd-wtf/nostr-tools/issues/136
2023-02-27 12:43:24 -03:00
fiatjaf
9d345a8f01 configurable list and get timeout on relay. 2023-02-26 21:23:09 -03:00
fiatjaf
c362212778 make pool.publish() return a single Pub object. 2023-02-26 17:44:51 -03:00
fiatjaf
a8938a3a0f wait a second before failing to send on a not yet connected websocket. 2023-02-26 16:53:03 -03:00
fiatjaf
a21329da3f make timeouts configurable for pool. 2023-02-26 16:50:49 -03:00
fiatjaf
63f4a49a69 increase pool timeouts. 2023-02-26 15:05:26 -03:00
21 changed files with 250 additions and 226 deletions

View File

@@ -16,6 +16,7 @@ jobs:
- run: just install-dependencies - run: just install-dependencies
- run: just build - run: just build
- run: just test - run: just test
- run: just emit-types
- uses: JS-DevTools/npm-publish@v1 - uses: JS-DevTools/npm-publish@v1
with: with:
token: ${{ secrets.NPM_TOKEN }} token: ${{ secrets.NPM_TOKEN }}

View File

@@ -9,7 +9,7 @@ jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
node-version: 18 node-version: 18

View File

@@ -10,7 +10,6 @@ Only depends on _@scure_ and _@noble_ packages.
npm install nostr-tools # or yarn add nostr-tools npm install nostr-tools # or yarn add nostr-tools
``` ```
## Usage ## Usage
### Generating a private key and a public key ### Generating a private key and a public key
@@ -138,11 +137,16 @@ const pool = new SimplePool()
let relays = ['wss://relay.example.com', 'wss://relay.example2.com'] let relays = ['wss://relay.example.com', 'wss://relay.example2.com']
let relay = await pool.ensureRelay('wss://relay.example3.com') let sub = pool.sub(
[...relays, 'wss://relay.example3.com'],
let sub = pool.sub([...relays, relay], [{ [
authors: ['32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245'] {
}]) authors: [
'32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245'
]
}
]
)
sub.on('event', event => { sub.on('event', event => {
// this will only be called once the first time the event is received // this will only be called once the first time the event is received
@@ -150,11 +154,10 @@ sub.on('event', event => {
}) })
let pubs = pool.publish(relays, newEvent) let pubs = pool.publish(relays, newEvent)
pubs.forEach(pub => pubs.on('ok', () => {
pub.on('ok', () => { // this may be called multiple times, once for every relay that accepts the event
// ... // ...
}) })
)
let events = await pool.list(relays, [{kinds: [0, 1]}]) let events = await pool.list(relays, [{kinds: [0, 1]}])
let event = await pool.get(relays, { let event = await pool.get(relays, {

View File

@@ -17,7 +17,7 @@ esbuild
packages: 'external' packages: 'external'
}) })
.then(() => { .then(() => {
const packageJson = JSON.stringify({ type: 'module' }) const packageJson = JSON.stringify({type: 'module'})
fs.writeFileSync(`${__dirname}/lib/esm/package.json`, packageJson, 'utf8') fs.writeFileSync(`${__dirname}/lib/esm/package.json`, packageJson, 'utf8')
console.log('esm build success.') console.log('esm build success.')

View File

@@ -80,6 +80,7 @@ export function getEventHash(event: UnsignedEvent): string {
export function validateEvent(event: UnsignedEvent): boolean { export function validateEvent(event: UnsignedEvent): boolean {
if (typeof event !== 'object') return false if (typeof event !== 'object') return false
if (typeof event.kind !== 'number') return false
if (typeof event.content !== 'string') return false if (typeof event.content !== 'string') return false
if (typeof event.created_at !== 'number') return false if (typeof event.created_at !== 'number') return false
if (typeof event.pubkey !== 'string') return false if (typeof event.pubkey !== 'string') return false

View File

@@ -7,6 +7,7 @@ export type Filter = {
since?: number since?: number
until?: number until?: number
limit?: number limit?: number
search?: string
[key: `#${string}`]: string[] [key: `#${string}`]: string[]
} }

View File

@@ -9,6 +9,7 @@ export * as nip05 from './nip05'
export * as nip06 from './nip06' export * as nip06 from './nip06'
export * as nip19 from './nip19' export * as nip19 from './nip19'
export * as nip26 from './nip26' export * as nip26 from './nip26'
export * as nip39 from './nip39'
export * as nip57 from './nip57' export * as nip57 from './nip57'
export * as fj from './fakejson' export * as fj from './fakejson'

View File

@@ -4,13 +4,20 @@ install-dependencies:
yarn --ignore-engines yarn --ignore-engines
build: build:
rm -rf lib
node build.js node build.js
test: build test: build
jest jest
testOnly file: build test-only file: build
jest {{file}} jest {{file}}
publish: build emit-types:
tsc # see tsconfig.json
publish: build emit-types
npm publish npm publish
format:
prettier --plugin-search-dir . --write .

140
magic.ts
View File

@@ -1,140 +0,0 @@
import {Relay, relayInit} from './relay'
import {Event} from './event'
import {normalizeURL} from './utils'
export default function (
writeableRelays: string[],
fallbackRelays: string[],
safeRelays: string[]
) {
return new MagicPool(fallbackRelays, writeableRelays, safeRelays)
}
class MagicPool {
private _conn: {[url: string]: Relay}
private _fallback: {[url: string]: Relay}
private _write: {[url: string]: Relay}
private _safe: {[url: string]: Relay}
private _profileRelays: {[pubkey: string]: RelayTableScore}
private _tempCache: {[id: string]: Event}
constructor(
fallbackRelays: string[],
writeableRelays: string[],
safeRelays: string[] = [
'wss://eden.nostr.land',
'wss://nostr.milou.lol',
'wss://relay.minds.com/nostr/v1/ws'
]
) {
this._conn = {}
this._write = {}
this._fallback = {}
this._profileRelays = {}
this._tempCache = {}
const hasEventId = (id: string): boolean => id in this._tempCache
const init = (url: string) => {
this._conn[normalizeURL(url)] = relayInit(normalizeURL(url), hasEventId)
}
fallbackRelays.forEach(init)
writeableRelays.forEach(init)
safeRelays.forEach(init)
this._write = Object.fromEntries(
writeableRelays.map(url => [
normalizeURL(url),
this._conn[normalizeURL(url)]
])
)
this._fallback = Object.fromEntries(
fallbackRelays.map(url => [
normalizeURL(url),
this._conn[normalizeURL(url)]
])
)
this._safe = Object.fromEntries(
safeRelays.map(url => [normalizeURL(url), this._conn[normalizeURL(url)]])
)
}
publish(event: Event) {
return Promise.all(
Object.entries(this._write).map(
([url, relay]) =>
new Promise(async resolve => {
await relay.connect()
let pub = relay.publish(event)
let to = setTimeout(() => {
let end = setTimeout(() => {
resolve({url, success: false, reason: 'timeout'})
}, 2500)
pub.on('seen', () => {
clearTimeout(end)
resolve({url, success: true, reason: 'seen'})
})
}, 2500)
pub.on('ok', () => {
clearTimeout(to)
resolve({url, success: true, reason: 'ok'})
})
pub.on('failed', (reason: string) => {
clearTimeout(to)
resolve({url, success: false, reason})
})
})
)
)
}
profile(
pubkey: string,
onUpdate: (events: Event[]) => void
): {
page(n: number): void
} {
var relays = new Set()
let rts = this._profileRelays[pubkey]
if (rts) {
relays = rts.get(3)
}
let fallback = Object.values(this._fallback)
for (let i = 0; i < fallback.length; i++) {
if (relays.size < 3) {
relays.add(fallback[Math.floor(Math.random() * fallback.length)])
} else break
}
// start subscription
for (let r in relays) {
r.
}
return {
page(n: number) {}
}
}
}
class RelayTableScore {
seen: string[] = []
hinted: string[] = []
explicit: string[] = []
get(n: number): Set<string> {
let relays = new Set<string>()
for (let i = 0; i < n; i++) {
for (let j = 0; j < 3; j++) {
let v = [this.seen, this.explicit, this.hinted][j][i]
if (v) {
relays.add(v)
if (relays.size >= n) return relays
}
}
}
return relays
}
}

View File

@@ -37,10 +37,16 @@ export async function queryProfile(
} }
if (!name.match(/^[A-Za-z0-9-_]+$/)) return null if (!name.match(/^[A-Za-z0-9-_]+$/)) return null
if (!domain.includes('.')) return null
let res = await ( let res
try {
res = await (
await _fetch(`https://${domain}/.well-known/nostr.json?name=${name}`) await _fetch(`https://${domain}/.well-known/nostr.json?name=${name}`)
).json() ).json()
} catch (err) {
return null
}
if (!res?.names?.[name]) return null if (!res?.names?.[name]) return null

View File

@@ -4,12 +4,16 @@ const {nip06} = require('./lib/nostr.cjs')
test('generate private key from a mnemonic', async () => { test('generate private key from a mnemonic', async () => {
const mnemonic = 'zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong' const mnemonic = 'zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong'
const privateKey = nip06.privateKeyFromSeedWords(mnemonic) const privateKey = nip06.privateKeyFromSeedWords(mnemonic)
expect(privateKey).toEqual('c26cf31d8ba425b555ca27d00ca71b5008004f2f662470f8c8131822ec129fe2') expect(privateKey).toEqual(
'c26cf31d8ba425b555ca27d00ca71b5008004f2f662470f8c8131822ec129fe2'
)
}) })
test('generate private key from a mnemonic and passphrase', async () => { test('generate private key from a mnemonic and passphrase', async () => {
const mnemonic = 'zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong' const mnemonic = 'zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong'
const passphrase = '123' const passphrase = '123'
const privateKey = nip06.privateKeyFromSeedWords(mnemonic, passphrase) const privateKey = nip06.privateKeyFromSeedWords(mnemonic, passphrase)
expect(privateKey).toEqual('55a22b8203273d0aaf24c22c8fbe99608e70c524b17265641074281c8b978ae4') expect(privateKey).toEqual(
'55a22b8203273d0aaf24c22c8fbe99608e70c524b17265641074281c8b978ae4'
)
}) })

View File

@@ -7,7 +7,10 @@ import {
} from '@scure/bip39' } from '@scure/bip39'
import {HDKey} from '@scure/bip32' import {HDKey} from '@scure/bip32'
export function privateKeyFromSeedWords(mnemonic: string, passphrase?: string): string { export function privateKeyFromSeedWords(
mnemonic: string,
passphrase?: string
): string {
let root = HDKey.fromMasterSeed(mnemonicToSeedSync(mnemonic, passphrase)) let root = HDKey.fromMasterSeed(mnemonicToSeedSync(mnemonic, passphrase))
let privateKey = root.derive(`m/44'/1237'/0'/0/0`).privateKey let privateKey = root.derive(`m/44'/1237'/0'/0/0`).privateKey
if (!privateKey) throw new Error('could not derive private key') if (!privateKey) throw new Error('could not derive private key')

View File

@@ -35,6 +35,21 @@ test('encode and decode nprofile', () => {
expect(data.relays).toContain(relays[1]) expect(data.relays).toContain(relays[1])
}) })
test('decode nprofile without relays', () => {
expect(
nip19.decode(
nip19.nprofileEncode({
pubkey:
'97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322',
relays: []
})
).data
).toHaveProperty(
'pubkey',
'97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322'
)
})
test('encode and decode naddr', () => { test('encode and decode naddr', () => {
let pk = getPublicKey(generatePrivateKey()) let pk = getPublicKey(generatePrivateKey())
let relays = [ let relays = [
@@ -57,7 +72,7 @@ test('encode and decode naddr', () => {
expect(data.identifier).toEqual('banana') expect(data.identifier).toEqual('banana')
}) })
test('encode and decode naddr from habla.news', () => { test('decode naddr from habla.news', () => {
let {type, data} = nip19.decode( let {type, data} = nip19.decode(
'naddr1qq98yetxv4ex2mnrv4esygrl54h466tz4v0re4pyuavvxqptsejl0vxcmnhfl60z3rth2xkpjspsgqqqw4rsf34vl5' 'naddr1qq98yetxv4ex2mnrv4esygrl54h466tz4v0re4pyuavvxqptsejl0vxcmnhfl60z3rth2xkpjspsgqqqw4rsf34vl5'
) )
@@ -68,3 +83,20 @@ test('encode and decode naddr from habla.news', () => {
expect(data.kind).toEqual(30023) expect(data.kind).toEqual(30023)
expect(data.identifier).toEqual('references') expect(data.identifier).toEqual('references')
}) })
test('decode naddr from go-nostr with different TLV ordering', () => {
let {type, data} = nip19.decode(
'naddr1qqrxyctwv9hxzq3q80cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsxpqqqp65wqfwwaehxw309aex2mrp0yhxummnw3ezuetcv9khqmr99ekhjer0d4skjm3wv4uxzmtsd3jjucm0d5q3vamnwvaz7tmwdaehgu3wvfskuctwvyhxxmmd0zfmwx'
)
expect(type).toEqual('naddr')
expect(data.pubkey).toEqual(
'3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d'
)
expect(data.relays).toContain(
'wss://relay.nostr.example.mydomain.example.com'
)
expect(data.relays).toContain('wss://nostr.banana.com')
expect(data.kind).toEqual(30023)
expect(data.identifier).toEqual('banana')
})

15
nip39.test.js Normal file
View File

@@ -0,0 +1,15 @@
/* eslint-env jest */
const fetch = require('node-fetch')
const {nip39} = require('./lib/nostr.cjs.js')
test('validate github claim', async () => {
nip39.useFetchImplementation(fetch)
let result = await nip39.validateGithub(
'npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z',
'vitorpamplona',
'cf19e2d1d7f8dac6348ad37b35ec8421'
)
expect(result).toBe(true)
})

27
nip39.ts Normal file
View File

@@ -0,0 +1,27 @@
var _fetch: any
try {
_fetch = fetch
} catch {}
export function useFetchImplementation(fetchImplementation: any) {
_fetch = fetchImplementation
}
export async function validateGithub(
pubkey: string,
username: string,
proof: string
): Promise<boolean> {
try {
let res = await (
await _fetch(`https://gist.github.com/${username}/${proof}/raw`)
).text()
return (
res ===
`Verifying that I control the following Nostr public key: ${pubkey}`
)
} catch (_) {
return false
}
}

View File

@@ -1,17 +1,22 @@
{ {
"name": "nostr-tools", "name": "nostr-tools",
"version": "1.6.1", "version": "1.7.5",
"description": "Tools for making a Nostr client.", "description": "Tools for making a Nostr client.",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/fiatjaf/nostr-tools.git" "url": "https://github.com/nbd-wtf/nostr-tools.git"
}, },
"files": [
"./lib/**/*"
],
"types": "./lib/index.d.ts",
"main": "lib/nostr.cjs.js", "main": "lib/nostr.cjs.js",
"module": "lib/esm/nostr.mjs", "module": "lib/esm/nostr.mjs",
"exports": { "exports": {
"import": "./lib/esm/nostr.mjs", "import": "./lib/esm/nostr.mjs",
"require": "./lib/nostr.cjs.js" "require": "./lib/nostr.cjs.js"
}, },
"license": "Public domain",
"dependencies": { "dependencies": {
"@noble/hashes": "1.0.0", "@noble/hashes": "1.0.0",
"@noble/secp256k1": "^1.7.1", "@noble/secp256k1": "^1.7.1",

View File

@@ -12,17 +12,17 @@ const {
let pool = new SimplePool() let pool = new SimplePool()
let relays = [ let relays = [
'wss://nostr-dev.wellorder.net/', 'wss://relay.damus.io/',
'wss://relay.nostr.bg/', 'wss://relay.nostr.bg/',
'wss://nostr.fmt.wiz.biz/', 'wss://nostr.fmt.wiz.biz/',
'wss://relay.nostr.band/', 'wss://relay.nostr.band/',
'wss://nostr.zebedee.cloud/' 'wss://nos.lol/'
] ]
afterAll(() => { afterAll(() => {
pool.close([ pool.close([
...relays, ...relays,
'wss://nostr-relay.untethr.me', 'wss://nostr.wine',
'wss://offchain.pub', 'wss://offchain.pub',
'wss://eden.nostr.land' 'wss://eden.nostr.land'
]) ])

73
pool.ts
View File

@@ -8,8 +8,13 @@ export class SimplePool {
private _conn: {[url: string]: Relay} private _conn: {[url: string]: Relay}
private _seenOn: {[id: string]: Set<string>} = {} // a map of all events we've seen in each relay private _seenOn: {[id: string]: Set<string>} = {} // a map of all events we've seen in each relay
constructor() { private eoseSubTimeout: number
private getTimeout: number
constructor(options: {eoseSubTimeout?: number; getTimeout?: number} = {}) {
this._conn = {} this._conn = {}
this.eoseSubTimeout = options.eoseSubTimeout || 3400
this.getTimeout = options.getTimeout || 3400
} }
close(relays: string[]): void { close(relays: string[]): void {
@@ -22,9 +27,17 @@ export class SimplePool {
async ensureRelay(url: string): Promise<Relay> { async ensureRelay(url: string): Promise<Relay> {
const nm = normalizeURL(url) const nm = normalizeURL(url)
const existing = this._conn[nm] const existing = this._conn[nm]
if (existing) return existing if (existing && existing.status === 1) return existing
const relay = relayInit(nm) if (existing) {
await existing.connect()
return existing
}
const relay = relayInit(nm, {
getTimeout: this.getTimeout * 0.9,
listTimeout: this.getTimeout * 0.9
})
this._conn[nm] = relay this._conn[nm] = relay
await relay.connect() await relay.connect()
@@ -34,8 +47,11 @@ export class SimplePool {
sub(relays: string[], filters: Filter[], opts?: SubscriptionOptions): Sub { sub(relays: string[], filters: Filter[], opts?: SubscriptionOptions): Sub {
let _knownIds: Set<string> = new Set() let _knownIds: Set<string> = new Set()
let modifiedOpts = opts || {} let modifiedOpts = {...(opts || {})}
modifiedOpts.alreadyHaveEvent = (id, url) => { modifiedOpts.alreadyHaveEvent = (id, url) => {
if (opts?.alreadyHaveEvent?.(id, url)) {
return true
}
let set = this._seenOn[id] || new Set() let set = this._seenOn[id] || new Set()
set.add(url) set.add(url)
this._seenOn[id] = set this._seenOn[id] = set
@@ -51,7 +67,7 @@ export class SimplePool {
let eoseTimeout = setTimeout(() => { let eoseTimeout = setTimeout(() => {
eoseSent = true eoseSent = true
for (let cb of eoseListeners.values()) cb() for (let cb of eoseListeners.values()) cb()
}, 2400) }, this.eoseSubTimeout)
relays.forEach(async relay => { relays.forEach(async relay => {
let r let r
@@ -91,19 +107,17 @@ export class SimplePool {
subs.forEach(sub => sub.unsub()) subs.forEach(sub => sub.unsub())
}, },
on(type, cb) { on(type, cb) {
switch (type) { if (type === 'event') {
case 'event':
eventListeners.add(cb) eventListeners.add(cb)
break } else if (type === 'eose') {
case 'eose': eoseListeners.add(cb as () => void | Promise<void>)
eoseListeners.add(cb)
break
} }
}, },
off(type, cb) { off(type, cb) {
if (type === 'event') { if (type === 'event') {
eventListeners.delete(cb) eventListeners.delete(cb)
} else if (type === 'eose') eoseListeners.delete(cb) } else if (type === 'eose')
eoseListeners.delete(cb as () => void | Promise<void>)
} }
} }
@@ -120,7 +134,7 @@ export class SimplePool {
let timeout = setTimeout(() => { let timeout = setTimeout(() => {
sub.unsub() sub.unsub()
resolve(null) resolve(null)
}, 1500) }, this.getTimeout)
sub.on('event', (event: Event) => { sub.on('event', (event: Event) => {
resolve(event) resolve(event)
clearTimeout(timeout) clearTimeout(timeout)
@@ -150,25 +164,28 @@ export class SimplePool {
}) })
} }
publish(relays: string[], event: Event): Pub[] { publish(relays: string[], event: Event): Pub {
return relays.map(relay => { const pubs: Pub[] = []
let r = this._conn[normalizeURL(relay)] relays.forEach(async relay => {
if (!r) return badPub(relay) let r
let s = r.publish(event) try {
return s r = await this.ensureRelay(relay)
pubs.push(r.publish(event))
} catch (_) {}
}) })
return {
on(type, cb) {
pubs.forEach((pub, i) => {
pub.on(type, () => cb(relays[i]))
})
},
off() {
// do nothing here, FIXME
}
}
} }
seenOn(id: string): string[] { seenOn(id: string): string[] {
return Array.from(this._seenOn[id]?.values?.() || []) return Array.from(this._seenOn[id]?.values?.() || [])
} }
} }
function badPub(relay: string): Pub {
return {
on(typ, cb) {
if (typ === 'failed') cb(`relay ${relay} not connected`)
},
off() {}
}
}

View File

@@ -9,7 +9,7 @@ const {
signEvent signEvent
} = require('./lib/nostr.cjs') } = require('./lib/nostr.cjs')
let relay = relayInit('wss://nostr-dev.wellorder.net/') let relay = relayInit('wss://relay.damus.io/')
beforeAll(() => { beforeAll(() => {
relay.connect() relay.connect()

View File

@@ -4,8 +4,16 @@ import {Event, verifySignature, validateEvent} from './event'
import {Filter, matchFilters} from './filter' import {Filter, matchFilters} from './filter'
import {getHex64, getSubscriptionId} from './fakejson' import {getHex64, getSubscriptionId} from './fakejson'
type RelayEvent = 'connect' | 'disconnect' | 'error' | 'notice' type RelayEvent = {
connect: () => void | Promise<void>
disconnect: () => void | Promise<void>
error: () => void | Promise<void>
notice: (msg: string) => void | Promise<void>
}
type SubEvent = {
event: (event: Event) => void | Promise<void>
eose: () => void | Promise<void>
}
export type Relay = { export type Relay = {
url: string url: string
status: number status: number
@@ -15,8 +23,14 @@ export type Relay = {
list: (filters: Filter[], opts?: SubscriptionOptions) => Promise<Event[]> list: (filters: Filter[], opts?: SubscriptionOptions) => Promise<Event[]>
get: (filter: Filter, opts?: SubscriptionOptions) => Promise<Event | null> get: (filter: Filter, opts?: SubscriptionOptions) => Promise<Event | null>
publish: (event: Event) => Pub publish: (event: Event) => Pub
on: (type: RelayEvent, cb: any) => void off: <T extends keyof RelayEvent, U extends RelayEvent[T]>(
off: (type: RelayEvent, cb: any) => void event: T,
listener: U
) => void
on: <T extends keyof RelayEvent, U extends RelayEvent[T]>(
event: T,
listener: U
) => void
} }
export type Pub = { export type Pub = {
on: (type: 'ok' | 'failed', cb: any) => void on: (type: 'ok' | 'failed', cb: any) => void
@@ -25,8 +39,14 @@ export type Pub = {
export type Sub = { export type Sub = {
sub: (filters: Filter[], opts: SubscriptionOptions) => Sub sub: (filters: Filter[], opts: SubscriptionOptions) => Sub
unsub: () => void unsub: () => void
on: (type: 'event' | 'eose', cb: any) => void on: <T extends keyof SubEvent, U extends SubEvent[T]>(
off: (type: 'event' | 'eose', cb: any) => void event: T,
listener: U
) => void
off: <T extends keyof SubEvent, U extends SubEvent[T]>(
event: T,
listener: U
) => void
} }
export type SubscriptionOptions = { export type SubscriptionOptions = {
@@ -35,25 +55,25 @@ export type SubscriptionOptions = {
alreadyHaveEvent?: null | ((id: string, relay: string) => boolean) alreadyHaveEvent?: null | ((id: string, relay: string) => boolean)
} }
export function relayInit(url: string): Relay { export function relayInit(
url: string,
options: {
getTimeout?: number
listTimeout?: number
} = {}
): Relay {
let {listTimeout = 3000, getTimeout = 3000} = options
var ws: WebSocket var ws: WebSocket
var openSubs: {[id: string]: {filters: Filter[]} & SubscriptionOptions} = {} var openSubs: {[id: string]: {filters: Filter[]} & SubscriptionOptions} = {}
var listeners: { var listeners: {[TK in keyof RelayEvent]: RelayEvent[TK][]} = {
connect: Array<() => void>
disconnect: Array<() => void>
error: Array<() => void>
notice: Array<(msg: string) => void>
} = {
connect: [], connect: [],
disconnect: [], disconnect: [],
error: [], error: [],
notice: [] notice: []
} }
var subListeners: { var subListeners: {
[subid: string]: { [subid: string]: {[TK in keyof SubEvent]: SubEvent[TK][]}
event: Array<(event: Event) => void>
eose: Array<() => void>
}
} = {} } = {}
var pubListeners: { var pubListeners: {
[eventid: string]: { [eventid: string]: {
@@ -65,7 +85,11 @@ export function relayInit(url: string): Relay {
async function connectRelay(): Promise<void> { async function connectRelay(): Promise<void> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
try {
ws = new WebSocket(url) ws = new WebSocket(url)
} catch (err) {
reject(err)
}
ws.onopen = () => { ws.onopen = () => {
listeners.connect.forEach(cb => cb()) listeners.connect.forEach(cb => cb())
@@ -174,9 +198,12 @@ export function relayInit(url: string): Relay {
async function trySend(params: [string, ...any]) { async function trySend(params: [string, ...any]) {
let msg = JSON.stringify(params) let msg = JSON.stringify(params)
if (!connected()) {
await new Promise(resolve => setTimeout(resolve, 1000))
if (!connected()) { if (!connected()) {
return return
} }
}
try { try {
ws.send(msg) ws.send(msg)
} catch (err) { } catch (err) {
@@ -214,14 +241,20 @@ export function relayInit(url: string): Relay {
delete subListeners[subid] delete subListeners[subid]
trySend(['CLOSE', subid]) trySend(['CLOSE', subid])
}, },
on: (type: 'event' | 'eose', cb: any): void => { on: <T extends keyof SubEvent, U extends SubEvent[T]>(
type: T,
cb: U
): void => {
subListeners[subid] = subListeners[subid] || { subListeners[subid] = subListeners[subid] || {
event: [], event: [],
eose: [] eose: []
} }
subListeners[subid][type].push(cb) subListeners[subid][type].push(cb)
}, },
off: (type: 'event' | 'eose', cb: any): void => { off: <T extends keyof SubEvent, U extends SubEvent[T]>(
type: T,
cb: U
): void => {
let listeners = subListeners[subid] let listeners = subListeners[subid]
let idx = listeners[type].indexOf(cb) let idx = listeners[type].indexOf(cb)
if (idx >= 0) listeners[type].splice(idx, 1) if (idx >= 0) listeners[type].splice(idx, 1)
@@ -232,13 +265,20 @@ export function relayInit(url: string): Relay {
return { return {
url, url,
sub, sub,
on: (type: RelayEvent, cb: any): void => { on: <T extends keyof RelayEvent, U extends RelayEvent[T]>(
type: T,
cb: U
): void => {
listeners[type].push(cb) listeners[type].push(cb)
if (type === 'connect' && ws?.readyState === 1) { if (type === 'connect' && ws?.readyState === 1) {
cb() // i would love to know why we need this
;(cb as () => void)()
} }
}, },
off: (type: RelayEvent, cb: any): void => { off: <T extends keyof RelayEvent, U extends RelayEvent[T]>(
type: T,
cb: U
): void => {
let index = listeners[type].indexOf(cb) let index = listeners[type].indexOf(cb)
if (index !== -1) listeners[type].splice(index, 1) if (index !== -1) listeners[type].splice(index, 1)
}, },
@@ -249,7 +289,7 @@ export function relayInit(url: string): Relay {
let timeout = setTimeout(() => { let timeout = setTimeout(() => {
s.unsub() s.unsub()
resolve(events) resolve(events)
}, 1500) }, listTimeout)
s.on('eose', () => { s.on('eose', () => {
s.unsub() s.unsub()
clearTimeout(timeout) clearTimeout(timeout)
@@ -265,7 +305,7 @@ export function relayInit(url: string): Relay {
let timeout = setTimeout(() => { let timeout = setTimeout(() => {
s.unsub() s.unsub()
resolve(null) resolve(null)
}, 1500) }, getTimeout)
s.on('event', (event: Event) => { s.on('event', (event: Event) => {
s.unsub() s.unsub()
clearTimeout(timeout) clearTimeout(timeout)
@@ -299,8 +339,9 @@ export function relayInit(url: string): Relay {
listeners = {connect: [], disconnect: [], error: [], notice: []} listeners = {connect: [], disconnect: [], error: [], notice: []}
subListeners = {} subListeners = {}
pubListeners = {} pubListeners = {}
if (ws.readyState === WebSocket.OPEN) {
ws?.close() ws?.close()
}
}, },
get status() { get status() {
return ws?.readyState ?? 3 return ws?.readyState ?? 3

View File

@@ -9,7 +9,7 @@
"skipLibCheck": true, "skipLibCheck": true,
"esModuleInterop": true, "esModuleInterop": true,
"emitDeclarationOnly": true, "emitDeclarationOnly": true,
"outDir": "dist", "outDir": "lib",
"rootDir": "." "rootDir": "."
} }
} }