mirror of
https://github.com/nbd-wtf/nostr-tools.git
synced 2025-12-09 16:48:50 +00:00
Compare commits
12 Commits
v1.0.0-bet
...
v1.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8262a81cb2 | ||
|
|
26e6da6ba3 | ||
|
|
8aa31bb437 | ||
|
|
4bd4469357 | ||
|
|
89ae21f796 | ||
|
|
41a1614d89 | ||
|
|
0500415a4e | ||
|
|
cee4357cab | ||
|
|
d5cf5930d1 | ||
|
|
a78e2036aa | ||
|
|
adc1854ac6 | ||
|
|
83148e8bdf |
@@ -24,6 +24,7 @@
|
|||||||
"document": false,
|
"document": false,
|
||||||
"navigator": false,
|
"navigator": false,
|
||||||
"window": false,
|
"window": false,
|
||||||
|
"crypto": false,
|
||||||
"location": false,
|
"location": false,
|
||||||
"URL": false,
|
"URL": false,
|
||||||
"URLSearchParams": false,
|
"URLSearchParams": false,
|
||||||
|
|||||||
26
README.md
26
README.md
@@ -2,15 +2,17 @@
|
|||||||
|
|
||||||
Tools for developing [Nostr](https://github.com/fiatjaf/nostr) clients.
|
Tools for developing [Nostr](https://github.com/fiatjaf/nostr) clients.
|
||||||
|
|
||||||
|
Very lean on dependencies.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
### Generating a private key and a public key
|
### Generating a private key and a public key
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import { generatePrivateKey, getPublicKey } from 'nostr-tools'
|
import {generatePrivateKey, getPublicKey} from 'nostr-tools'
|
||||||
|
|
||||||
let sk = generatePrivateKey() # `sk` is a hex string
|
let sk = generatePrivateKey() // `sk` is a hex string
|
||||||
let pk = getPublicKey(sk) # `pk` is a hex string
|
let pk = getPublicKey(sk) // `pk` is a hex string
|
||||||
```
|
```
|
||||||
|
|
||||||
### Creating, signing and verifying events
|
### Creating, signing and verifying events
|
||||||
@@ -31,7 +33,7 @@ let event = {
|
|||||||
content: 'hello'
|
content: 'hello'
|
||||||
}
|
}
|
||||||
|
|
||||||
event.id = getEventHash(event.id)
|
event.id = getEventHash(event)
|
||||||
event.pubkey = getPublicKey(privateKey)
|
event.pubkey = getPublicKey(privateKey)
|
||||||
event.sig = await signEvent(event, privateKey)
|
event.sig = await signEvent(event, privateKey)
|
||||||
|
|
||||||
@@ -112,6 +114,12 @@ pub.on('failed', reason => {
|
|||||||
await relay.close()
|
await relay.close()
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To use this on Node.js you first must install `websocket-polyfill` and import it:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import 'websocket-polyfill'
|
||||||
|
```
|
||||||
|
|
||||||
### Querying profile data from a NIP-05 address
|
### Querying profile data from a NIP-05 address
|
||||||
|
|
||||||
```js
|
```js
|
||||||
@@ -122,8 +130,11 @@ console.log(profile.pubkey)
|
|||||||
// prints: 32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245
|
// prints: 32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245
|
||||||
console.log(profile.relays)
|
console.log(profile.relays)
|
||||||
// prints: [wss://relay.damus.io]
|
// prints: [wss://relay.damus.io]
|
||||||
|
```
|
||||||
|
|
||||||
// on nodejs, install node-fetch@2 and call this first:
|
To use this on Node.js you first must install `node-fetch@2` and call something like this:
|
||||||
|
|
||||||
|
```js
|
||||||
nip05.useFetchImplementation(require('node-fetch'))
|
nip05.useFetchImplementation(require('node-fetch'))
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -171,7 +182,7 @@ let pk2 = getPublicKey(sk2)
|
|||||||
|
|
||||||
// on the sender side
|
// on the sender side
|
||||||
let message = 'hello'
|
let message = 'hello'
|
||||||
let ciphertext = nip04.encrypt(sk1, pk2, 'hello')
|
let ciphertext = await nip04.encrypt(sk1, pk2, 'hello')
|
||||||
|
|
||||||
let event = {
|
let event = {
|
||||||
kind: 4,
|
kind: 4,
|
||||||
@@ -184,11 +195,10 @@ let event = {
|
|||||||
sendEvent(event)
|
sendEvent(event)
|
||||||
|
|
||||||
// on the receiver side
|
// on the receiver side
|
||||||
|
|
||||||
sub.on('event', (event) => {
|
sub.on('event', (event) => {
|
||||||
let sender = event.tags.find(([k, v]) => k === 'p' && && v && v !== '')[1]
|
let sender = event.tags.find(([k, v]) => k === 'p' && && v && v !== '')[1]
|
||||||
pk1 === sender
|
pk1 === sender
|
||||||
let plaintext = nip04.decrypt(sk2, pk1, event.content)
|
let plaintext = await nip04.decrypt(sk2, pk1, event.content)
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
6
build.js
6
build.js
@@ -1,16 +1,10 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
const esbuild = require('esbuild')
|
const esbuild = require('esbuild')
|
||||||
const alias = require('esbuild-plugin-alias')
|
|
||||||
|
|
||||||
let common = {
|
let common = {
|
||||||
entryPoints: ['index.ts'],
|
entryPoints: ['index.ts'],
|
||||||
bundle: true,
|
bundle: true,
|
||||||
plugins: [
|
|
||||||
alias({
|
|
||||||
stream: require.resolve('readable-stream')
|
|
||||||
})
|
|
||||||
],
|
|
||||||
sourcemap: 'external'
|
sourcemap: 'external'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
12
event.ts
12
event.ts
@@ -1,8 +1,8 @@
|
|||||||
import {Buffer} from 'buffer'
|
|
||||||
// @ts-ignore
|
|
||||||
import * as secp256k1 from '@noble/secp256k1'
|
import * as secp256k1 from '@noble/secp256k1'
|
||||||
import {sha256} from '@noble/hashes/sha256'
|
import {sha256} from '@noble/hashes/sha256'
|
||||||
|
|
||||||
|
import {utf8Encoder} from './utils'
|
||||||
|
|
||||||
export type Event = {
|
export type Event = {
|
||||||
id?: string
|
id?: string
|
||||||
sig?: string
|
sig?: string
|
||||||
@@ -35,8 +35,8 @@ export function serializeEvent(evt: Event): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getEventHash(event: Event): string {
|
export function getEventHash(event: Event): string {
|
||||||
let eventHash = sha256(Buffer.from(serializeEvent(event)))
|
let eventHash = sha256(utf8Encoder.encode(serializeEvent(event)))
|
||||||
return Buffer.from(eventHash).toString('hex')
|
return secp256k1.utils.bytesToHex(eventHash)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validateEvent(event: Event): boolean {
|
export function validateEvent(event: Event): boolean {
|
||||||
@@ -63,7 +63,7 @@ export function verifySignature(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function signEvent(event: Event, key: string): Promise<string> {
|
export async function signEvent(event: Event, key: string): Promise<string> {
|
||||||
return Buffer.from(
|
return secp256k1.utils.bytesToHex(
|
||||||
await secp256k1.schnorr.sign(event.id || getEventHash(event), key)
|
await secp256k1.schnorr.sign(event.id || getEventHash(event), key)
|
||||||
).toString('hex')
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
5
keys.ts
5
keys.ts
@@ -1,10 +1,9 @@
|
|||||||
import * as secp256k1 from '@noble/secp256k1'
|
import * as secp256k1 from '@noble/secp256k1'
|
||||||
import {Buffer} from 'buffer'
|
|
||||||
|
|
||||||
export function generatePrivateKey(): string {
|
export function generatePrivateKey(): string {
|
||||||
return Buffer.from(secp256k1.utils.randomPrivateKey()).toString('hex')
|
return secp256k1.utils.bytesToHex(secp256k1.utils.randomPrivateKey())
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPublicKey(privateKey: string): string {
|
export function getPublicKey(privateKey: string): string {
|
||||||
return Buffer.from(secp256k1.schnorr.getPublicKey(privateKey)).toString('hex')
|
return secp256k1.utils.bytesToHex(secp256k1.schnorr.getPublicKey(privateKey))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
/* eslint-env jest */
|
/* eslint-env jest */
|
||||||
|
|
||||||
|
globalThis.crypto = require('crypto')
|
||||||
const {nip04, getPublicKey, generatePrivateKey} = require('./lib/nostr.cjs')
|
const {nip04, getPublicKey, generatePrivateKey} = require('./lib/nostr.cjs')
|
||||||
|
|
||||||
test('encrypt and decrypt message', () => {
|
test('encrypt and decrypt message', async () => {
|
||||||
let sk1 = generatePrivateKey()
|
let sk1 = generatePrivateKey()
|
||||||
let sk2 = generatePrivateKey()
|
let sk2 = generatePrivateKey()
|
||||||
let pk1 = getPublicKey(sk1)
|
let pk1 = getPublicKey(sk1)
|
||||||
let pk2 = getPublicKey(sk2)
|
let pk2 = getPublicKey(sk2)
|
||||||
|
|
||||||
expect(nip04.decrypt(sk2, pk1, nip04.encrypt(sk1, pk2, 'hello'))).toEqual(
|
expect(
|
||||||
'hello'
|
await nip04.decrypt(sk2, pk1, await nip04.encrypt(sk1, pk2, 'hello'))
|
||||||
)
|
).toEqual('hello')
|
||||||
})
|
})
|
||||||
|
|||||||
69
nip04.ts
69
nip04.ts
@@ -1,45 +1,66 @@
|
|||||||
import {Buffer} from 'buffer'
|
|
||||||
import {randomBytes} from '@noble/hashes/utils'
|
import {randomBytes} from '@noble/hashes/utils'
|
||||||
import * as secp256k1 from '@noble/secp256k1'
|
import * as secp256k1 from '@noble/secp256k1'
|
||||||
// @ts-ignore
|
import {encode as b64encode, decode as b64decode} from 'base64-arraybuffer'
|
||||||
import aes from 'browserify-cipher'
|
|
||||||
|
|
||||||
export function encrypt(privkey: string, pubkey: string, text: string): string {
|
import {utf8Decoder, utf8Encoder} from './utils'
|
||||||
|
|
||||||
|
export async function encrypt(
|
||||||
|
privkey: string,
|
||||||
|
pubkey: string,
|
||||||
|
text: string
|
||||||
|
): Promise<string> {
|
||||||
const key = secp256k1.getSharedSecret(privkey, '02' + pubkey)
|
const key = secp256k1.getSharedSecret(privkey, '02' + pubkey)
|
||||||
const normalizedKey = getNormalizedX(key)
|
const normalizedKey = getNormalizedX(key)
|
||||||
|
|
||||||
let iv = Uint8Array.from(randomBytes(16))
|
let iv = Uint8Array.from(randomBytes(16))
|
||||||
var cipher = aes.createCipheriv(
|
let plaintext = utf8Encoder.encode(text)
|
||||||
'aes-256-cbc',
|
let cryptoKey = await crypto.subtle.importKey(
|
||||||
Buffer.from(normalizedKey, 'hex'),
|
'raw',
|
||||||
iv
|
normalizedKey,
|
||||||
|
{name: 'AES-CBC'},
|
||||||
|
false,
|
||||||
|
['encrypt']
|
||||||
)
|
)
|
||||||
let encryptedMessage = cipher.update(text, 'utf8', 'base64')
|
let ciphertext = await crypto.subtle.encrypt(
|
||||||
encryptedMessage += cipher.final('base64')
|
{name: 'AES-CBC', iv},
|
||||||
|
cryptoKey,
|
||||||
|
plaintext
|
||||||
|
)
|
||||||
|
let ctb64 = b64encode(ciphertext)
|
||||||
|
let ivb64 = b64encode(iv.buffer)
|
||||||
|
|
||||||
return `${encryptedMessage}?iv=${Buffer.from(iv.buffer).toString('base64')}`
|
return `${ctb64}?iv=${ivb64}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export function decrypt(
|
export async function decrypt(
|
||||||
privkey: string,
|
privkey: string,
|
||||||
pubkey: string,
|
pubkey: string,
|
||||||
ciphertext: string
|
data: string
|
||||||
): string {
|
): Promise<string> {
|
||||||
let [cip, iv] = ciphertext.split('?iv=')
|
let [ctb64, ivb64] = data.split('?iv=')
|
||||||
let key = secp256k1.getSharedSecret(privkey, '02' + pubkey)
|
let key = secp256k1.getSharedSecret(privkey, '02' + pubkey)
|
||||||
let normalizedKey = getNormalizedX(key)
|
let normalizedKey = getNormalizedX(key)
|
||||||
|
|
||||||
var decipher = aes.createDecipheriv(
|
let cryptoKey = await crypto.subtle.importKey(
|
||||||
'aes-256-cbc',
|
'raw',
|
||||||
Buffer.from(normalizedKey, 'hex'),
|
normalizedKey,
|
||||||
Buffer.from(iv, 'base64')
|
{name: 'AES-CBC'},
|
||||||
|
false,
|
||||||
|
['decrypt']
|
||||||
)
|
)
|
||||||
let decryptedMessage = decipher.update(cip, 'base64', 'utf8')
|
let ciphertext = b64decode(ctb64)
|
||||||
decryptedMessage += decipher.final('utf8')
|
let iv = b64decode(ivb64)
|
||||||
|
|
||||||
return decryptedMessage
|
let plaintext = await crypto.subtle.decrypt(
|
||||||
|
{name: 'AES-CBC', iv},
|
||||||
|
cryptoKey,
|
||||||
|
ciphertext
|
||||||
|
)
|
||||||
|
|
||||||
|
let text = utf8Decoder.decode(plaintext)
|
||||||
|
return text
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNormalizedX(key: Uint8Array): string {
|
function getNormalizedX(key: Uint8Array): Uint8Array {
|
||||||
return Buffer.from(key.slice(1, 33)).toString('hex')
|
return key.slice(1, 33)
|
||||||
}
|
}
|
||||||
|
|||||||
7
nip06.ts
7
nip06.ts
@@ -1,3 +1,4 @@
|
|||||||
|
import * as secp256k1 from '@noble/secp256k1'
|
||||||
import {wordlist} from '@scure/bip39/wordlists/english.js'
|
import {wordlist} from '@scure/bip39/wordlists/english.js'
|
||||||
import {
|
import {
|
||||||
generateMnemonic,
|
generateMnemonic,
|
||||||
@@ -7,14 +8,14 @@ import {
|
|||||||
import {HDKey} from '@scure/bip32'
|
import {HDKey} from '@scure/bip32'
|
||||||
|
|
||||||
export function privateKeyFromSeed(seed: string): string {
|
export function privateKeyFromSeed(seed: string): string {
|
||||||
let root = HDKey.fromMasterSeed(Buffer.from(seed, 'hex'))
|
let root = HDKey.fromMasterSeed(secp256k1.utils.hexToBytes(seed))
|
||||||
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')
|
||||||
return Buffer.from(privateKey).toString('hex')
|
return secp256k1.utils.bytesToHex(privateKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function seedFromWords(mnemonic: string): string {
|
export function seedFromWords(mnemonic: string): string {
|
||||||
return Buffer.from(mnemonicToSeedSync(mnemonic)).toString('hex')
|
return secp256k1.utils.bytesToHex(mnemonicToSeedSync(mnemonic))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function generateSeedWords(): string {
|
export function generateSeedWords(): string {
|
||||||
|
|||||||
5
nip19.ts
5
nip19.ts
@@ -1,6 +1,8 @@
|
|||||||
import * as secp256k1 from '@noble/secp256k1'
|
import * as secp256k1 from '@noble/secp256k1'
|
||||||
import {bech32} from 'bech32'
|
import {bech32} from 'bech32'
|
||||||
|
|
||||||
|
import {utf8Decoder, utf8Encoder} from './utils'
|
||||||
|
|
||||||
export type ProfilePointer = {
|
export type ProfilePointer = {
|
||||||
pubkey: string // hex
|
pubkey: string // hex
|
||||||
relays?: string[]
|
relays?: string[]
|
||||||
@@ -11,9 +13,6 @@ export type EventPointer = {
|
|||||||
relays?: string[]
|
relays?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
let utf8Decoder = new TextDecoder('utf-8')
|
|
||||||
let utf8Encoder = new TextEncoder()
|
|
||||||
|
|
||||||
export function decode(nip19: string): {
|
export function decode(nip19: string): {
|
||||||
type: string
|
type: string
|
||||||
data: ProfilePointer | EventPointer | string
|
data: ProfilePointer | EventPointer | string
|
||||||
|
|||||||
11
package.json
11
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "nostr-tools",
|
"name": "nostr-tools",
|
||||||
"version": "1.0.0-beta",
|
"version": "1.0.0-beta2",
|
||||||
"description": "Tools for making a Nostr client.",
|
"description": "Tools for making a Nostr client.",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -13,10 +13,8 @@
|
|||||||
"@noble/secp256k1": "^1.7.0",
|
"@noble/secp256k1": "^1.7.0",
|
||||||
"@scure/bip32": "^1.1.1",
|
"@scure/bip32": "^1.1.1",
|
||||||
"@scure/bip39": "^1.1.0",
|
"@scure/bip39": "^1.1.0",
|
||||||
"bech32": "^2.0.0",
|
"base64-arraybuffer": "^1.0.2",
|
||||||
"browserify-cipher": ">=1",
|
"bech32": "^2.0.0"
|
||||||
"buffer": "^6.0.3",
|
|
||||||
"websocket-polyfill": "^0.0.3"
|
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"decentralization",
|
"decentralization",
|
||||||
@@ -39,7 +37,8 @@
|
|||||||
"node-fetch": "2",
|
"node-fetch": "2",
|
||||||
"ts-jest": "^29.0.3",
|
"ts-jest": "^29.0.3",
|
||||||
"tsd": "^0.22.0",
|
"tsd": "^0.22.0",
|
||||||
"typescript": "^4.9.4"
|
"typescript": "^4.9.4",
|
||||||
|
"websocket-polyfill": "^0.0.3"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "node build.js",
|
"build": "node build.js",
|
||||||
|
|||||||
207
relay.test.js
207
relay.test.js
@@ -1,5 +1,6 @@
|
|||||||
/* eslint-env jest */
|
/* eslint-env jest */
|
||||||
|
|
||||||
|
require('websocket-polyfill')
|
||||||
const {
|
const {
|
||||||
relayInit,
|
relayInit,
|
||||||
generatePrivateKey,
|
generatePrivateKey,
|
||||||
@@ -8,110 +9,106 @@ const {
|
|||||||
signEvent
|
signEvent
|
||||||
} = require('./lib/nostr.cjs')
|
} = require('./lib/nostr.cjs')
|
||||||
|
|
||||||
describe('relay interaction', () => {
|
let relay = relayInit('wss://nostr-pub.semisol.dev/')
|
||||||
let relay = relayInit('wss://nostr-pub.semisol.dev/')
|
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
relay.connect()
|
relay.connect()
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await relay.close()
|
await relay.close()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('connectivity', () => {
|
test('connectivity', () => {
|
||||||
return expect(
|
return expect(
|
||||||
new Promise(resolve => {
|
new Promise(resolve => {
|
||||||
relay.on('connect', () => {
|
relay.on('connect', () => {
|
||||||
resolve(true)
|
resolve(true)
|
||||||
})
|
})
|
||||||
relay.on('error', () => {
|
relay.on('error', () => {
|
||||||
resolve(false)
|
resolve(false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
).resolves.toBe(true)
|
).resolves.toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('querying', () => {
|
test('querying', () => {
|
||||||
var resolve1
|
var resolve1
|
||||||
var resolve2
|
var resolve2
|
||||||
|
|
||||||
let sub = relay.sub([
|
let sub = relay.sub([
|
||||||
{
|
{
|
||||||
ids: [
|
ids: ['d7dd5eb3ab747e16f8d0212d53032ea2a7cadef53837e5a6c66d42849fcb9027']
|
||||||
'd7dd5eb3ab747e16f8d0212d53032ea2a7cadef53837e5a6c66d42849fcb9027'
|
}
|
||||||
]
|
])
|
||||||
}
|
sub.on('event', event => {
|
||||||
])
|
expect(event).toHaveProperty(
|
||||||
sub.on('event', event => {
|
'id',
|
||||||
expect(event).toHaveProperty(
|
'd7dd5eb3ab747e16f8d0212d53032ea2a7cadef53837e5a6c66d42849fcb9027'
|
||||||
'id',
|
)
|
||||||
'd7dd5eb3ab747e16f8d0212d53032ea2a7cadef53837e5a6c66d42849fcb9027'
|
resolve1(true)
|
||||||
)
|
})
|
||||||
resolve1(true)
|
sub.on('eose', () => {
|
||||||
})
|
resolve2(true)
|
||||||
sub.on('eose', () => {
|
})
|
||||||
resolve2(true)
|
|
||||||
})
|
return expect(
|
||||||
|
Promise.all([
|
||||||
return expect(
|
new Promise(resolve => {
|
||||||
Promise.all([
|
resolve1 = resolve
|
||||||
new Promise(resolve => {
|
}),
|
||||||
resolve1 = resolve
|
new Promise(resolve => {
|
||||||
}),
|
resolve2 = resolve
|
||||||
new Promise(resolve => {
|
})
|
||||||
resolve2 = resolve
|
])
|
||||||
})
|
).resolves.toEqual([true, true])
|
||||||
])
|
})
|
||||||
).resolves.toEqual([true, true])
|
|
||||||
})
|
test('listening (twice) and publishing', async () => {
|
||||||
|
let sk = generatePrivateKey()
|
||||||
test('listening (twice) and publishing', async () => {
|
let pk = getPublicKey(sk)
|
||||||
let sk = generatePrivateKey()
|
var resolve1
|
||||||
let pk = getPublicKey(sk)
|
var resolve2
|
||||||
var resolve1
|
|
||||||
var resolve2
|
let sub = relay.sub([
|
||||||
|
{
|
||||||
let sub = relay.sub([
|
kinds: [27572],
|
||||||
{
|
authors: [pk]
|
||||||
kinds: [27572],
|
}
|
||||||
authors: [pk]
|
])
|
||||||
}
|
|
||||||
])
|
sub.on('event', event => {
|
||||||
|
expect(event).toHaveProperty('pubkey', pk)
|
||||||
sub.on('event', event => {
|
expect(event).toHaveProperty('kind', 27572)
|
||||||
expect(event).toHaveProperty('pubkey', pk)
|
expect(event).toHaveProperty('content', 'nostr-tools test suite')
|
||||||
expect(event).toHaveProperty('kind', 27572)
|
resolve1(true)
|
||||||
expect(event).toHaveProperty('content', 'nostr-tools test suite')
|
})
|
||||||
resolve1(true)
|
sub.on('event', event => {
|
||||||
})
|
expect(event).toHaveProperty('pubkey', pk)
|
||||||
sub.on('event', event => {
|
expect(event).toHaveProperty('kind', 27572)
|
||||||
expect(event).toHaveProperty('pubkey', pk)
|
expect(event).toHaveProperty('content', 'nostr-tools test suite')
|
||||||
expect(event).toHaveProperty('kind', 27572)
|
resolve2(true)
|
||||||
expect(event).toHaveProperty('content', 'nostr-tools test suite')
|
})
|
||||||
resolve2(true)
|
|
||||||
})
|
let event = {
|
||||||
|
kind: 27572,
|
||||||
let event = {
|
pubkey: pk,
|
||||||
kind: 27572,
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
pubkey: pk,
|
tags: [],
|
||||||
created_at: Math.floor(Date.now() / 1000),
|
content: 'nostr-tools test suite'
|
||||||
tags: [],
|
}
|
||||||
content: 'nostr-tools test suite'
|
event.id = getEventHash(event)
|
||||||
}
|
event.sig = await signEvent(event, sk)
|
||||||
event.id = getEventHash(event)
|
|
||||||
event.sig = await signEvent(event, sk)
|
relay.publish(event)
|
||||||
|
return expect(
|
||||||
relay.publish(event)
|
Promise.all([
|
||||||
return expect(
|
new Promise(resolve => {
|
||||||
Promise.all([
|
resolve1 = resolve
|
||||||
new Promise(resolve => {
|
}),
|
||||||
resolve1 = resolve
|
new Promise(resolve => {
|
||||||
}),
|
resolve2 = resolve
|
||||||
new Promise(resolve => {
|
})
|
||||||
resolve2 = resolve
|
])
|
||||||
})
|
).resolves.toEqual([true, true])
|
||||||
])
|
|
||||||
).resolves.toEqual([true, true])
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|||||||
73
relay.ts
73
relay.ts
@@ -1,7 +1,5 @@
|
|||||||
/* global WebSocket */
|
/* global WebSocket */
|
||||||
|
|
||||||
import 'websocket-polyfill'
|
|
||||||
|
|
||||||
import {Event, verifySignature, validateEvent} from './event'
|
import {Event, verifySignature, validateEvent} from './event'
|
||||||
import {Filter, matchFilters} from './filter'
|
import {Filter, matchFilters} from './filter'
|
||||||
|
|
||||||
@@ -33,11 +31,8 @@ type SubscriptionOptions = {
|
|||||||
|
|
||||||
export function relayInit(url: string): Relay {
|
export function relayInit(url: string): Relay {
|
||||||
var ws: WebSocket
|
var ws: WebSocket
|
||||||
var resolveOpen: () => void
|
|
||||||
var resolveClose: () => void
|
var resolveClose: () => void
|
||||||
var untilOpen: Promise<void>
|
var untilOpen: Promise<void>
|
||||||
var wasClosed: boolean
|
|
||||||
var closed: boolean
|
|
||||||
var openSubs: {[id: string]: {filters: Filter[]} & SubscriptionOptions} = {}
|
var openSubs: {[id: string]: {filters: Filter[]} & SubscriptionOptions} = {}
|
||||||
var listeners: {
|
var listeners: {
|
||||||
connect: Array<() => void>
|
connect: Array<() => void>
|
||||||
@@ -63,64 +58,19 @@ export function relayInit(url: string): Relay {
|
|||||||
failed: Array<(reason: string) => void>
|
failed: Array<(reason: string) => void>
|
||||||
}
|
}
|
||||||
} = {}
|
} = {}
|
||||||
let attemptNumber = 1
|
|
||||||
let nextAttemptSeconds = 1
|
|
||||||
let isConnected = false
|
|
||||||
|
|
||||||
function resetOpenState() {
|
|
||||||
untilOpen = new Promise(resolve => {
|
|
||||||
resolveOpen = resolve
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function connectRelay() {
|
function connectRelay() {
|
||||||
ws = new WebSocket(url)
|
ws = new WebSocket(url)
|
||||||
|
|
||||||
ws.onopen = () => {
|
ws.onopen = () => {
|
||||||
listeners.connect.forEach(cb => cb())
|
listeners.connect.forEach(cb => cb())
|
||||||
resolveOpen()
|
|
||||||
isConnected = true
|
|
||||||
|
|
||||||
// restablish old subscriptions
|
|
||||||
if (wasClosed) {
|
|
||||||
wasClosed = false
|
|
||||||
for (let id in openSubs) {
|
|
||||||
let {filters} = openSubs[id]
|
|
||||||
sub(filters, openSubs[id])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
ws.onerror = () => {
|
ws.onerror = () => {
|
||||||
isConnected = false
|
|
||||||
listeners.error.forEach(cb => cb())
|
listeners.error.forEach(cb => cb())
|
||||||
}
|
}
|
||||||
ws.onclose = async () => {
|
ws.onclose = async () => {
|
||||||
isConnected = false
|
|
||||||
listeners.disconnect.forEach(cb => cb())
|
listeners.disconnect.forEach(cb => cb())
|
||||||
|
resolveClose()
|
||||||
if (closed) {
|
|
||||||
// we've closed this because we wanted, so end everything
|
|
||||||
resolveClose()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// otherwise keep trying to reconnect
|
|
||||||
resetOpenState()
|
|
||||||
attemptNumber++
|
|
||||||
nextAttemptSeconds += attemptNumber ** 3
|
|
||||||
if (nextAttemptSeconds > 14400) {
|
|
||||||
nextAttemptSeconds = 14400 // 4 hours
|
|
||||||
}
|
|
||||||
console.log(
|
|
||||||
`relay ${url} connection closed. reconnecting in ${nextAttemptSeconds} seconds.`
|
|
||||||
)
|
|
||||||
setTimeout(async () => {
|
|
||||||
try {
|
|
||||||
connectRelay()
|
|
||||||
} catch (err) {}
|
|
||||||
}, nextAttemptSeconds * 1000)
|
|
||||||
|
|
||||||
wasClosed = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ws.onmessage = async e => {
|
ws.onmessage = async e => {
|
||||||
@@ -173,13 +123,9 @@ export function relayInit(url: string): Relay {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resetOpenState()
|
|
||||||
|
|
||||||
async function connect(): Promise<void> {
|
async function connect(): Promise<void> {
|
||||||
if (ws?.readyState && ws.readyState === 1) return // ws already open
|
if (ws?.readyState && ws.readyState === 1) return // ws already open
|
||||||
try {
|
connectRelay()
|
||||||
connectRelay()
|
|
||||||
} catch (err) {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function trySend(params: [string, ...any]) {
|
async function trySend(params: [string, ...any]) {
|
||||||
@@ -233,13 +179,19 @@ export function relayInit(url: string): Relay {
|
|||||||
return {
|
return {
|
||||||
url,
|
url,
|
||||||
sub,
|
sub,
|
||||||
on: (type: 'connect' | 'disconnect' | 'notice', cb: any): void => {
|
on: (
|
||||||
|
type: 'connect' | 'disconnect' | 'error' | 'notice',
|
||||||
|
cb: any
|
||||||
|
): void => {
|
||||||
listeners[type].push(cb)
|
listeners[type].push(cb)
|
||||||
if (type === 'connect' && isConnected) {
|
if (type === 'connect' && ws?.readyState === 1) {
|
||||||
cb()
|
cb()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
off: (type: 'connect' | 'disconnect' | 'notice', cb: any): void => {
|
off: (
|
||||||
|
type: 'connect' | 'disconnect' | 'error' | 'notice',
|
||||||
|
cb: any
|
||||||
|
): 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)
|
||||||
},
|
},
|
||||||
@@ -298,14 +250,13 @@ export function relayInit(url: string): Relay {
|
|||||||
},
|
},
|
||||||
connect,
|
connect,
|
||||||
close(): Promise<void> {
|
close(): Promise<void> {
|
||||||
closed = true // prevent ws from trying to reconnect
|
|
||||||
ws.close()
|
ws.close()
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
resolveClose = resolve
|
resolveClose = resolve
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
get status() {
|
get status() {
|
||||||
return ws.readyState
|
return ws?.readyState ?? 3
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user