nostr-tools 2.11.0 → 2.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +117 -86
  2. package/lib/cjs/abstract-pool.js +139 -5
  3. package/lib/cjs/abstract-pool.js.map +3 -3
  4. package/lib/cjs/abstract-relay.js +7 -2
  5. package/lib/cjs/abstract-relay.js.map +2 -2
  6. package/lib/cjs/index.js +141 -7
  7. package/lib/cjs/index.js.map +3 -3
  8. package/lib/cjs/nip04.js +2 -2
  9. package/lib/cjs/nip04.js.map +2 -2
  10. package/lib/cjs/nip46.js +165 -46
  11. package/lib/cjs/nip46.js.map +4 -4
  12. package/lib/cjs/nip47.js +1 -1
  13. package/lib/cjs/nip47.js.map +2 -2
  14. package/lib/cjs/pool.js +139 -5
  15. package/lib/cjs/pool.js.map +3 -3
  16. package/lib/cjs/relay.js +7 -2
  17. package/lib/cjs/relay.js.map +2 -2
  18. package/lib/esm/abstract-pool.js +139 -5
  19. package/lib/esm/abstract-pool.js.map +3 -3
  20. package/lib/esm/abstract-relay.js +7 -2
  21. package/lib/esm/abstract-relay.js.map +2 -2
  22. package/lib/esm/index.js +141 -7
  23. package/lib/esm/index.js.map +3 -3
  24. package/lib/esm/nip04.js +2 -2
  25. package/lib/esm/nip04.js.map +2 -2
  26. package/lib/esm/nip46.js +161 -42
  27. package/lib/esm/nip46.js.map +4 -4
  28. package/lib/esm/nip47.js +1 -1
  29. package/lib/esm/nip47.js.map +2 -2
  30. package/lib/esm/pool.js +139 -5
  31. package/lib/esm/pool.js.map +3 -3
  32. package/lib/esm/relay.js +7 -2
  33. package/lib/esm/relay.js.map +2 -2
  34. package/lib/nostr.bundle.js +141 -7
  35. package/lib/nostr.bundle.js.map +3 -3
  36. package/lib/types/abstract-pool.d.ts +12 -2
  37. package/lib/types/abstract-relay.d.ts +1 -0
  38. package/lib/types/nip04.d.ts +2 -2
  39. package/lib/types/nip05.d.ts +1 -1
  40. package/lib/types/nip19.d.ts +7 -7
  41. package/lib/types/nip46.d.ts +1 -0
  42. package/lib/types/test-helpers.d.ts +0 -1
  43. package/package.json +2 -2
package/README.md CHANGED
@@ -57,43 +57,43 @@ let event = finalizeEvent({
57
57
  let isGood = verifyEvent(event)
58
58
  ```
59
59
 
60
- ### Interacting with a relay
60
+ ### Interacting with one or multiple relays
61
+
62
+ Doesn't matter what you do, you always should be using a `SimplePool`:
61
63
 
62
64
  ```js
63
65
  import { finalizeEvent, generateSecretKey, getPublicKey } from 'nostr-tools/pure'
64
- import { Relay } from 'nostr-tools/relay'
66
+ import { SimplePool } from 'nostr-tools/pool'
65
67
 
66
- const relay = await Relay.connect('wss://relay.example.com')
67
- console.log(`connected to ${relay.url}`)
68
+ const pool = new SimplePool()
68
69
 
69
70
  // let's query for an event that exists
70
- const sub = relay.subscribe([
71
+ const event = relay.get(
72
+ ['wss://relay.example.com'],
71
73
  {
72
74
  ids: ['d7dd5eb3ab747e16f8d0212d53032ea2a7cadef53837e5a6c66d42849fcb9027'],
73
75
  },
74
- ], {
75
- onevent(event) {
76
- console.log('we got the event we wanted:', event)
77
- },
78
- oneose() {
79
- sub.close()
80
- }
81
- })
76
+ )
77
+ if (event) {
78
+ console.log('it exists indeed on this relay:', event)
79
+ }
82
80
 
83
81
  // let's publish a new event while simultaneously monitoring the relay for it
84
82
  let sk = generateSecretKey()
85
83
  let pk = getPublicKey(sk)
86
84
 
87
- relay.subscribe([
85
+ pool.subscribe(
86
+ ['wss://a.com', 'wss://b.com', 'wss://c.com'],
88
87
  {
89
88
  kinds: [1],
90
89
  authors: [pk],
91
90
  },
92
- ], {
93
- onevent(event) {
94
- console.log('got event:', event)
91
+ {
92
+ onevent(event) {
93
+ console.log('got event:', event)
94
+ }
95
95
  }
96
- })
96
+ )
97
97
 
98
98
  let eventTemplate = {
99
99
  kind: 1,
@@ -104,7 +104,7 @@ let eventTemplate = {
104
104
 
105
105
  // this assigns the pubkey, calculates the event id and signs the event in a single step
106
106
  const signedEvent = finalizeEvent(eventTemplate, sk)
107
- await relay.publish(signedEvent)
107
+ await pool.publish(['wss://a.com', 'wss://b.com'], signedEvent)
108
108
 
109
109
  relay.close()
110
110
  ```
@@ -119,59 +119,116 @@ import WebSocket from 'ws'
119
119
  useWebSocketImplementation(WebSocket)
120
120
  ```
121
121
 
122
- ### Interacting with multiple relays
122
+ ### Parsing references (mentions) from a content based on NIP-27
123
123
 
124
124
  ```js
125
- import { SimplePool } from 'nostr-tools/pool'
125
+ import * as nip27 from '@nostr/tools/nip27'
126
+
127
+ for (let block of nip27.parse(evt.content)) {
128
+ switch (block.type) {
129
+ case 'text':
130
+ console.log(block.text)
131
+ break
132
+ case 'reference': {
133
+ if ('id' in block.pointer) {
134
+ console.log("it's a nevent1 uri", block.pointer)
135
+ } else if ('identifier' in block.pointer) {
136
+ console.log("it's a naddr1 uri", block.pointer)
137
+ } else {
138
+ console.log("it's an npub1 or nprofile1 uri", block.pointer)
139
+ }
140
+ break
141
+ }
142
+ case 'url': {
143
+ console.log("it's a normal url:", block.url)
144
+ break
145
+ }
146
+ case 'image':
147
+ case 'video':
148
+ case 'audio':
149
+ console.log("it's a media url:", block.url)
150
+ case 'relay':
151
+ console.log("it's a websocket url, probably a relay address:", block.url)
152
+ default:
153
+ break
154
+ }
155
+ }
156
+ ```
126
157
 
127
- const pool = new SimplePool()
158
+ ### Connecting to a bunker using NIP-46
128
159
 
129
- let relays = ['wss://relay.example.com', 'wss://relay.example2.com']
160
+ ```js
161
+ import { generateSecretKey, getPublicKey } from '@nostr/tools/pure'
162
+ import { BunkerSigner, parseBunkerInput } from '@nostr/tools/nip46'
163
+ import { SimplePool } from '@nostr/tools/pool'
130
164
 
131
- let h = pool.subscribeMany(
132
- [...relays, 'wss://relay.example3.com'],
133
- [
134
- {
135
- authors: ['32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245'],
136
- },
137
- ],
138
- {
139
- onevent(event) {
140
- // this will only be called once the first time the event is received
141
- // ...
142
- },
143
- oneose() {
144
- h.close()
145
- }
146
- }
147
- )
165
+ // the client needs a local secret key (which is generally persisted) for communicating with the bunker
166
+ const localSecretKey = generateSecretKey()
148
167
 
149
- await Promise.any(pool.publish(relays, newEvent))
150
- console.log('published to at least one relay!')
168
+ // parse a bunker URI
169
+ const bunkerPointer = await parseBunkerInput('bunker://abcd...?relay=wss://relay.example.com')
170
+ if (!bunkerPointer) {
171
+ throw new Error('Invalid bunker input')
172
+ }
151
173
 
152
- let events = await pool.querySync(relays, { kinds: [0, 1] })
153
- let event = await pool.get(relays, {
154
- ids: ['44e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245'],
174
+ // create the bunker instance
175
+ const pool = new SimplePool()
176
+ const bunker = new BunkerSigner(localSecretKey, bunkerPointer, { pool })
177
+ await bunker.connect()
178
+
179
+ // and use it
180
+ const pubkey = await bunker.getPublicKey()
181
+ const event = await bunker.signEvent({
182
+ kind: 1,
183
+ created_at: Math.floor(Date.now() / 1000),
184
+ tags: [],
185
+ content: 'Hello from bunker!'
155
186
  })
187
+
188
+ // cleanup
189
+ await signer.close()
190
+ pool.close([])
156
191
  ```
157
192
 
158
- ### Parsing references (mentions) from a content using NIP-10 and NIP-27
193
+ ### Parsing thread from any note based on NIP-10
159
194
 
160
195
  ```js
161
- import { parseReferences } from 'nostr-tools/references'
162
-
163
- let references = parseReferences(event)
164
- let simpleAugmentedContent = event.content
165
- for (let i = 0; i < references.length; i++) {
166
- let { text, profile, event, address } = references[i]
167
- let augmentedReference = profile
168
- ? `<strong>@${profilesCache[profile.pubkey].name}</strong>`
169
- : event
170
- ? `<em>${eventsCache[event.id].content.slice(0, 5)}</em>`
171
- : address
172
- ? `<a href="${text}">[link]</a>`
173
- : text
174
- simpleAugmentedContent.replaceAll(text, augmentedReference)
196
+ import * as nip10 from '@nostr/tools/nip10'
197
+
198
+ // event is a nostr event with tags
199
+ const refs = nip10.parse(event)
200
+
201
+ // get the root event of the thread
202
+ if (refs.root) {
203
+ console.log('root event:', refs.root.id)
204
+ console.log('root event relay hints:', refs.root.relays)
205
+ console.log('root event author:', refs.root.author)
206
+ }
207
+
208
+ // get the immediate parent being replied to
209
+ if (refs.reply) {
210
+ console.log('reply to:', refs.reply.id)
211
+ console.log('reply relay hints:', refs.reply.relays)
212
+ console.log('reply author:', refs.reply.author)
213
+ }
214
+
215
+ // get any mentioned events
216
+ for (let mention of refs.mentions) {
217
+ console.log('mentioned event:', mention.id)
218
+ console.log('mention relay hints:', mention.relays)
219
+ console.log('mention author:', mention.author)
220
+ }
221
+
222
+ // get any quoted events
223
+ for (let quote of refs.quotes) {
224
+ console.log('quoted event:', quote.id)
225
+ console.log('quote relay hints:', quote.relays)
226
+ }
227
+
228
+ // get any referenced profiles
229
+ for (let profile of refs.profiles) {
230
+ console.log('referenced profile:', profile.pubkey)
231
+ console.log('profile relay hints:', profile.relays)
175
232
  }
176
233
  ```
177
234
 
@@ -205,32 +262,6 @@ declare global {
205
262
  }
206
263
  ```
207
264
 
208
-
209
- ### Generating NIP-06 keys
210
- ```js
211
- import {
212
- privateKeyFromSeedWords,
213
- accountFromSeedWords,
214
- extendedKeysFromSeedWords,
215
- accountFromExtendedKey
216
- } from 'nostr-tools/nip06'
217
-
218
- const mnemonic = 'zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong'
219
- const passphrase = '123' // optional
220
- const accountIndex = 0
221
- const sk0 = privateKeyFromSeedWords(mnemonic, passphrase, accountIndex)
222
-
223
- const { privateKey: sk1, publicKey: pk1 } = accountFromSeedWords(mnemonic, passphrase, accountIndex)
224
-
225
- const extendedAccountIndex = 0
226
-
227
- const { privateExtendedKey, publicExtendedKey } = extendedKeysFromSeedWords(mnemonic, passphrase, extendedAccountIndex)
228
-
229
- const { privateKey: sk2, publicKey: pk2 } = accountFromExtendedKey(privateExtendedKey)
230
-
231
- const { publicKey: pk3 } = accountFromExtendedKey(publicExtendedKey)
232
- ```
233
-
234
265
  ### Encoding and decoding NIP-19 codes
235
266
 
236
267
  ```js
@@ -198,6 +198,7 @@ var AbstractRelay = class {
198
198
  incomingMessageQueue = new Queue();
199
199
  queueRunning = false;
200
200
  challenge;
201
+ authPromise;
201
202
  serial = 0;
202
203
  verifyEvent;
203
204
  _WebSocket;
@@ -232,6 +233,7 @@ var AbstractRelay = class {
232
233
  if (this.connectionPromise)
233
234
  return this.connectionPromise;
234
235
  this.challenge = void 0;
236
+ this.authPromise = void 0;
235
237
  this.connectionPromise = new Promise((resolve, reject) => {
236
238
  this.connectionTimeoutHandle = setTimeout(() => {
237
239
  reject("connection timed out");
@@ -356,6 +358,7 @@ var AbstractRelay = class {
356
358
  return;
357
359
  case "AUTH": {
358
360
  this.challenge = data[1];
361
+ this.authPromise = void 0;
359
362
  this._onauth?.(data[1]);
360
363
  return;
361
364
  }
@@ -374,8 +377,10 @@ var AbstractRelay = class {
374
377
  async auth(signAuthEvent) {
375
378
  if (!this.challenge)
376
379
  throw new Error("can't perform auth, no challenge was received");
380
+ if (this.authPromise)
381
+ return this.authPromise;
377
382
  const evt = await signAuthEvent(makeAuthEvent(this.url, this.challenge));
378
- const ret = new Promise((resolve, reject) => {
383
+ this.authPromise = new Promise((resolve, reject) => {
379
384
  const timeout = setTimeout(() => {
380
385
  const ep = this.openEventPublishes.get(evt.id);
381
386
  if (ep) {
@@ -386,7 +391,7 @@ var AbstractRelay = class {
386
391
  this.openEventPublishes.set(evt.id, { resolve, reject, timeout });
387
392
  });
388
393
  this.send('["AUTH",' + JSON.stringify(evt) + "]");
389
- return ret;
394
+ return this.authPromise;
390
395
  }
391
396
  async publish(event) {
392
397
  const ret = new Promise((resolve, reject) => {
@@ -517,8 +522,110 @@ var AbstractSimplePool = class {
517
522
  this.relays.get(url)?.close();
518
523
  });
519
524
  }
525
+ subscribe(relays, filter, params) {
526
+ return this.subscribeMap(
527
+ relays.map((url) => ({ url, filter })),
528
+ params
529
+ );
530
+ }
520
531
  subscribeMany(relays, filters, params) {
521
- return this.subscribeManyMap(Object.fromEntries(relays.map((url) => [url, filters])), params);
532
+ return this.subscribeMap(
533
+ relays.flatMap((url) => filters.map((filter) => ({ url, filter }))),
534
+ params
535
+ );
536
+ }
537
+ subscribeMap(requests, params) {
538
+ if (this.trackRelays) {
539
+ params.receivedEvent = (relay, id) => {
540
+ let set = this.seenOn.get(id);
541
+ if (!set) {
542
+ set = /* @__PURE__ */ new Set();
543
+ this.seenOn.set(id, set);
544
+ }
545
+ set.add(relay);
546
+ };
547
+ }
548
+ const _knownIds = /* @__PURE__ */ new Set();
549
+ const subs = [];
550
+ const eosesReceived = [];
551
+ let handleEose = (i) => {
552
+ if (eosesReceived[i])
553
+ return;
554
+ eosesReceived[i] = true;
555
+ if (eosesReceived.filter((a) => a).length === requests.length) {
556
+ params.oneose?.();
557
+ handleEose = () => {
558
+ };
559
+ }
560
+ };
561
+ const closesReceived = [];
562
+ let handleClose = (i, reason) => {
563
+ if (closesReceived[i])
564
+ return;
565
+ handleEose(i);
566
+ closesReceived[i] = reason;
567
+ if (closesReceived.filter((a) => a).length === requests.length) {
568
+ params.onclose?.(closesReceived);
569
+ handleClose = () => {
570
+ };
571
+ }
572
+ };
573
+ const localAlreadyHaveEventHandler = (id) => {
574
+ if (params.alreadyHaveEvent?.(id)) {
575
+ return true;
576
+ }
577
+ const have = _knownIds.has(id);
578
+ _knownIds.add(id);
579
+ return have;
580
+ };
581
+ const allOpened = Promise.all(
582
+ requests.map(async ({ url, filter }, i) => {
583
+ url = normalizeURL(url);
584
+ let relay;
585
+ try {
586
+ relay = await this.ensureRelay(url, {
587
+ connectionTimeout: params.maxWait ? Math.max(params.maxWait * 0.8, params.maxWait - 1e3) : void 0
588
+ });
589
+ } catch (err) {
590
+ handleClose(i, err?.message || String(err));
591
+ return;
592
+ }
593
+ let subscription = relay.subscribe([filter], {
594
+ ...params,
595
+ oneose: () => handleEose(i),
596
+ onclose: (reason) => {
597
+ if (reason.startsWith("auth-required:") && params.doauth) {
598
+ relay.auth(params.doauth).then(() => {
599
+ relay.subscribe([filter], {
600
+ ...params,
601
+ oneose: () => handleEose(i),
602
+ onclose: (reason2) => {
603
+ handleClose(i, reason2);
604
+ },
605
+ alreadyHaveEvent: localAlreadyHaveEventHandler,
606
+ eoseTimeout: params.maxWait
607
+ });
608
+ }).catch((err) => {
609
+ handleClose(i, `auth was required and attempted, but failed with: ${err}`);
610
+ });
611
+ } else {
612
+ handleClose(i, reason);
613
+ }
614
+ },
615
+ alreadyHaveEvent: localAlreadyHaveEventHandler,
616
+ eoseTimeout: params.maxWait
617
+ });
618
+ subs.push(subscription);
619
+ })
620
+ );
621
+ return {
622
+ async close() {
623
+ await allOpened;
624
+ subs.forEach((sub) => {
625
+ sub.close();
626
+ });
627
+ }
628
+ };
522
629
  }
523
630
  subscribeManyMap(requests, params) {
524
631
  if (this.trackRelays) {
@@ -585,7 +692,25 @@ var AbstractSimplePool = class {
585
692
  let subscription = relay.subscribe(filters, {
586
693
  ...params,
587
694
  oneose: () => handleEose(i),
588
- onclose: (reason) => handleClose(i, reason),
695
+ onclose: (reason) => {
696
+ if (reason.startsWith("auth-required:") && params.doauth) {
697
+ relay.auth(params.doauth).then(() => {
698
+ relay.subscribe(filters, {
699
+ ...params,
700
+ oneose: () => handleEose(i),
701
+ onclose: (reason2) => {
702
+ handleClose(i, reason2);
703
+ },
704
+ alreadyHaveEvent: localAlreadyHaveEventHandler,
705
+ eoseTimeout: params.maxWait
706
+ });
707
+ }).catch((err) => {
708
+ handleClose(i, `auth was required and attempted, but failed with: ${err}`);
709
+ });
710
+ } else {
711
+ handleClose(i, reason);
712
+ }
713
+ },
589
714
  alreadyHaveEvent: localAlreadyHaveEventHandler,
590
715
  eoseTimeout: params.maxWait
591
716
  });
@@ -601,6 +726,15 @@ var AbstractSimplePool = class {
601
726
  }
602
727
  };
603
728
  }
729
+ subscribeEose(relays, filter, params) {
730
+ const subcloser = this.subscribe(relays, filter, {
731
+ ...params,
732
+ oneose() {
733
+ subcloser.close();
734
+ }
735
+ });
736
+ return subcloser;
737
+ }
604
738
  subscribeManyEose(relays, filters, params) {
605
739
  const subcloser = this.subscribeMany(relays, filters, {
606
740
  ...params,
@@ -613,7 +747,7 @@ var AbstractSimplePool = class {
613
747
  async querySync(relays, filter, params) {
614
748
  return new Promise(async (resolve) => {
615
749
  const events = [];
616
- this.subscribeManyEose(relays, [filter], {
750
+ this.subscribeEose(relays, filter, {
617
751
  ...params,
618
752
  onevent(event) {
619
753
  events.push(event);