mirror of
https://github.com/nbd-wtf/nostr-tools.git
synced 2025-12-09 16:48:50 +00:00
Compare commits
39 Commits
alexgleaso
...
v2.5.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4f9bece6e | ||
|
|
e217f751da | ||
|
|
d0ae8b36a2 | ||
|
|
fd945757be | ||
|
|
c12ddd3c53 | ||
|
|
1e9f828e3e | ||
|
|
0a5eaac088 | ||
|
|
e858698cb9 | ||
|
|
b349ee577d | ||
|
|
849a2ac3f3 | ||
|
|
c18b94677c | ||
|
|
f306cec716 | ||
|
|
5c7e9c8f36 | ||
|
|
1d7620a057 | ||
|
|
88247e56c1 | ||
|
|
e5cda3509c | ||
|
|
02da1dc036 | ||
|
|
7aed747bb2 | ||
|
|
747a7944d7 | ||
|
|
9f8b7274b3 | ||
|
|
ee565db7f5 | ||
|
|
e9ee8258e7 | ||
|
|
ad07d260ab | ||
|
|
632184afb8 | ||
|
|
d7d5d30f41 | ||
|
|
387ce2c335 | ||
|
|
b62b8f88af | ||
|
|
6b43533f2e | ||
|
|
e30e08d8e2 | ||
|
|
59426d9f35 | ||
|
|
5429142858 | ||
|
|
564c9bca17 | ||
|
|
0190ae94a7 | ||
|
|
e1bde08ff3 | ||
|
|
71456feb20 | ||
|
|
ca928c697b | ||
|
|
7b98cae7fa | ||
|
|
db53f37161 | ||
|
|
1691f0b51d |
18
.github/workflows/publish.yml
vendored
18
.github/workflows/publish.yml
vendored
@@ -1,18 +0,0 @@
|
|||||||
name: Publish
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
publish:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
id-token: write # The OIDC ID token is used for authentication with JSR.
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: oven-sh/setup-bun@v1
|
|
||||||
- run: bun i
|
|
||||||
- run: npx jsr publish
|
|
||||||
11
README.md
11
README.md
@@ -25,6 +25,15 @@ let sk = generateSecretKey() // `sk` is a Uint8Array
|
|||||||
let pk = getPublicKey(sk) // `pk` is a hex string
|
let pk = getPublicKey(sk) // `pk` is a hex string
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To get the secret key in hex format, use
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { bytesToHex, hexToBytes } from '@noble/hashes/utils' // already an installed dependency
|
||||||
|
|
||||||
|
let skHex = bytesToHex(sk)
|
||||||
|
let backToBytes = hexToBytes(skHex)
|
||||||
|
```
|
||||||
|
|
||||||
### Creating, signing and verifying events
|
### Creating, signing and verifying events
|
||||||
|
|
||||||
```js
|
```js
|
||||||
@@ -129,7 +138,7 @@ let h = pool.subscribeMany(
|
|||||||
await Promise.any(pool.publish(relays, newEvent))
|
await Promise.any(pool.publish(relays, newEvent))
|
||||||
console.log('published to at least one relay!')
|
console.log('published to at least one relay!')
|
||||||
|
|
||||||
let events = await pool.querySync(relays, [{ kinds: [0, 1] }])
|
let events = await pool.querySync(relays, { kinds: [0, 1] })
|
||||||
let event = await pool.get(relays, {
|
let event = await pool.get(relays, {
|
||||||
ids: ['44e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245'],
|
ids: ['44e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245'],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -48,6 +48,10 @@ export class AbstractSimplePool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
subscribeMany(relays: string[], filters: Filter[], params: SubscribeManyParams): SubCloser {
|
subscribeMany(relays: string[], filters: Filter[], params: SubscribeManyParams): SubCloser {
|
||||||
|
return this.subscribeManyMap(Object.fromEntries(relays.map(url => [url, filters])), params)
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribeManyMap(requests: { [relay: string]: Filter[] }, params: SubscribeManyParams): SubCloser {
|
||||||
if (this.trackRelays) {
|
if (this.trackRelays) {
|
||||||
params.receivedEvent = (relay: AbstractRelay, id: string) => {
|
params.receivedEvent = (relay: AbstractRelay, id: string) => {
|
||||||
let set = this.seenOn.get(id)
|
let set = this.seenOn.get(id)
|
||||||
@@ -61,12 +65,13 @@ export class AbstractSimplePool {
|
|||||||
|
|
||||||
const _knownIds = new Set<string>()
|
const _knownIds = new Set<string>()
|
||||||
const subs: Subscription[] = []
|
const subs: Subscription[] = []
|
||||||
|
const relaysLength = Object.keys(requests).length
|
||||||
|
|
||||||
// batch all EOSEs into a single
|
// batch all EOSEs into a single
|
||||||
const eosesReceived: boolean[] = []
|
const eosesReceived: boolean[] = []
|
||||||
let handleEose = (i: number) => {
|
let handleEose = (i: number) => {
|
||||||
eosesReceived[i] = true
|
eosesReceived[i] = true
|
||||||
if (eosesReceived.filter(a => a).length === relays.length) {
|
if (eosesReceived.filter(a => a).length === relaysLength) {
|
||||||
params.oneose?.()
|
params.oneose?.()
|
||||||
handleEose = () => {}
|
handleEose = () => {}
|
||||||
}
|
}
|
||||||
@@ -76,7 +81,7 @@ export class AbstractSimplePool {
|
|||||||
let handleClose = (i: number, reason: string) => {
|
let handleClose = (i: number, reason: string) => {
|
||||||
handleEose(i)
|
handleEose(i)
|
||||||
closesReceived[i] = reason
|
closesReceived[i] = reason
|
||||||
if (closesReceived.filter(a => a).length === relays.length) {
|
if (closesReceived.filter(a => a).length === relaysLength) {
|
||||||
params.onclose?.(closesReceived)
|
params.onclose?.(closesReceived)
|
||||||
handleClose = () => {}
|
handleClose = () => {}
|
||||||
}
|
}
|
||||||
@@ -93,13 +98,16 @@ export class AbstractSimplePool {
|
|||||||
|
|
||||||
// open a subscription in all given relays
|
// open a subscription in all given relays
|
||||||
const allOpened = Promise.all(
|
const allOpened = Promise.all(
|
||||||
relays.map(normalizeURL).map(async (url, i, arr) => {
|
Object.entries(requests).map(async (req, i, arr) => {
|
||||||
if (arr.indexOf(url) !== i) {
|
if (arr.indexOf(req) !== i) {
|
||||||
// duplicate
|
// duplicate
|
||||||
handleClose(i, 'duplicate url')
|
handleClose(i, 'duplicate url')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let [url, filters] = req
|
||||||
|
url = normalizeURL(url)
|
||||||
|
|
||||||
let relay: AbstractRelay
|
let relay: AbstractRelay
|
||||||
try {
|
try {
|
||||||
relay = await this.ensureRelay(url, {
|
relay = await this.ensureRelay(url, {
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ export class AbstractRelay {
|
|||||||
public onclose: (() => void) | null = null
|
public onclose: (() => void) | null = null
|
||||||
public onnotice: (msg: string) => void = msg => console.debug(`NOTICE from ${this.url}: ${msg}`)
|
public onnotice: (msg: string) => void = msg => console.debug(`NOTICE from ${this.url}: ${msg}`)
|
||||||
|
|
||||||
|
// this is exposed just to help in ndk migration, shouldn't be relied upon
|
||||||
|
public _onauth: ((challenge: string) => void) | null = null
|
||||||
|
|
||||||
public baseEoseTimeout: number = 4400
|
public baseEoseTimeout: number = 4400
|
||||||
public connectionTimeout: number = 4400
|
public connectionTimeout: number = 4400
|
||||||
public openSubs: Map<string, Subscription> = new Map()
|
public openSubs: Map<string, Subscription> = new Map()
|
||||||
@@ -99,17 +102,20 @@ export class AbstractRelay {
|
|||||||
this.ws.onerror = ev => {
|
this.ws.onerror = ev => {
|
||||||
reject((ev as any).message)
|
reject((ev as any).message)
|
||||||
if (this._connected) {
|
if (this._connected) {
|
||||||
|
this._connected = false
|
||||||
|
this.connectionPromise = undefined
|
||||||
this.onclose?.()
|
this.onclose?.()
|
||||||
this.closeAllSubscriptions('relay connection errored')
|
this.closeAllSubscriptions('relay connection errored')
|
||||||
this._connected = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.ws.onclose = async () => {
|
this.ws.onclose = async () => {
|
||||||
|
if (this._connected) {
|
||||||
|
this._connected = false
|
||||||
this.connectionPromise = undefined
|
this.connectionPromise = undefined
|
||||||
this.onclose?.()
|
this.onclose?.()
|
||||||
this.closeAllSubscriptions('relay connection closed')
|
this.closeAllSubscriptions('relay connection closed')
|
||||||
this._connected = false
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.ws.onmessage = this._onmessage.bind(this)
|
this.ws.onmessage = this._onmessage.bind(this)
|
||||||
@@ -212,6 +218,7 @@ export class AbstractRelay {
|
|||||||
return
|
return
|
||||||
case 'AUTH': {
|
case 'AUTH': {
|
||||||
this.challenge = data[1] as string
|
this.challenge = data[1] as string
|
||||||
|
this._onauth?.(data[1] as string)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -252,7 +259,7 @@ export class AbstractRelay {
|
|||||||
const ret = new Promise<number>((resolve, reject) => {
|
const ret = new Promise<number>((resolve, reject) => {
|
||||||
this.openCountRequests.set(id, { resolve, reject })
|
this.openCountRequests.set(id, { resolve, reject })
|
||||||
})
|
})
|
||||||
this.send('["COUNT","' + id + '",' + JSON.stringify(filters) + ']')
|
this.send('["COUNT","' + id + '",' + JSON.stringify(filters).substring(1))
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,7 +345,7 @@ export class Subscription {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public close(reason: string = 'closed by caller') {
|
public close(reason: string = 'closed by caller') {
|
||||||
if (!this.closed) {
|
if (!this.closed && this.relay.connected) {
|
||||||
// if the connection was closed by the user calling .close() we will send a CLOSE message
|
// if the connection was closed by the user calling .close() we will send a CLOSE message
|
||||||
// otherwise this._open will be already set to false so we will skip this
|
// otherwise this._open will be already set to false so we will skip this
|
||||||
this.relay.send('["CLOSE",' + JSON.stringify(this.id) + ']')
|
this.relay.send('["CLOSE",' + JSON.stringify(this.id) + ']')
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { run, bench, group, baseline } from 'mitata'
|
import { run, bench, group, baseline } from 'mitata'
|
||||||
import { initNostrWasm } from 'nostr-wasm'
|
import { initNostrWasm } from 'nostr-wasm'
|
||||||
import { NostrEvent } from './core'
|
import { NostrEvent } from './core.ts'
|
||||||
import { finalizeEvent, generateSecretKey } from './pure'
|
import { finalizeEvent, generateSecretKey } from './pure.ts'
|
||||||
import { setNostrWasm, verifyEvent } from './wasm'
|
import { setNostrWasm, verifyEvent } from './wasm.ts'
|
||||||
import { AbstractRelay } from './abstract-relay.ts'
|
import { AbstractRelay } from './abstract-relay.ts'
|
||||||
import { Relay as PureRelay } from './relay.ts'
|
import { Relay as PureRelay } from './relay.ts'
|
||||||
import { alwaysTrue } from './helpers.ts'
|
import { alwaysTrue } from './helpers.ts'
|
||||||
|
|||||||
306
core.test.ts
306
core.test.ts
@@ -1,293 +1,19 @@
|
|||||||
import { describe, test, expect } from 'bun:test'
|
import { test, expect } from 'bun:test'
|
||||||
|
|
||||||
import {
|
import { sortEvents } from './core.ts'
|
||||||
finalizeEvent,
|
|
||||||
serializeEvent,
|
|
||||||
getEventHash,
|
|
||||||
validateEvent,
|
|
||||||
verifyEvent,
|
|
||||||
verifiedSymbol,
|
|
||||||
getPublicKey,
|
|
||||||
generateSecretKey,
|
|
||||||
} from './pure.ts'
|
|
||||||
import { ShortTextNote } from './kinds.ts'
|
|
||||||
import { bytesToHex, hexToBytes } from '@noble/hashes/utils'
|
|
||||||
|
|
||||||
test('private key generation', () => {
|
test('sortEvents', () => {
|
||||||
expect(bytesToHex(generateSecretKey())).toMatch(/[a-f0-9]{64}/)
|
const events = [
|
||||||
})
|
{ id: 'abc123', pubkey: 'key1', created_at: 1610000000, kind: 1, tags: [], content: 'Hello', sig: 'sig1' },
|
||||||
|
{ id: 'abc124', pubkey: 'key2', created_at: 1620000000, kind: 1, tags: [], content: 'World', sig: 'sig2' },
|
||||||
test('public key generation', () => {
|
{ id: 'abc125', pubkey: 'key3', created_at: 1620000000, kind: 1, tags: [], content: '!', sig: 'sig3' },
|
||||||
expect(getPublicKey(generateSecretKey())).toMatch(/[a-f0-9]{64}/)
|
]
|
||||||
})
|
|
||||||
|
const sortedEvents = sortEvents(events)
|
||||||
test('public key from private key deterministic', () => {
|
|
||||||
let sk = generateSecretKey()
|
expect(sortedEvents).toEqual([
|
||||||
let pk = getPublicKey(sk)
|
{ id: 'abc124', pubkey: 'key2', created_at: 1620000000, kind: 1, tags: [], content: 'World', sig: 'sig2' },
|
||||||
|
{ id: 'abc125', pubkey: 'key3', created_at: 1620000000, kind: 1, tags: [], content: '!', sig: 'sig3' },
|
||||||
for (let i = 0; i < 5; i++) {
|
{ id: 'abc123', pubkey: 'key1', created_at: 1610000000, kind: 1, tags: [], content: 'Hello', sig: 'sig1' },
|
||||||
expect(getPublicKey(sk)).toEqual(pk)
|
])
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('finalizeEvent', () => {
|
|
||||||
test('should create a signed event from a template', () => {
|
|
||||||
const privateKey = hexToBytes('d217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf')
|
|
||||||
const publicKey = getPublicKey(privateKey)
|
|
||||||
|
|
||||||
const template = {
|
|
||||||
kind: ShortTextNote,
|
|
||||||
tags: [],
|
|
||||||
content: 'Hello, world!',
|
|
||||||
created_at: 1617932115,
|
|
||||||
}
|
|
||||||
|
|
||||||
const event = finalizeEvent(template, privateKey)
|
|
||||||
|
|
||||||
expect(event.kind).toEqual(template.kind)
|
|
||||||
expect(event.tags).toEqual(template.tags)
|
|
||||||
expect(event.content).toEqual(template.content)
|
|
||||||
expect(event.created_at).toEqual(template.created_at)
|
|
||||||
expect(event.pubkey).toEqual(publicKey)
|
|
||||||
expect(typeof event.id).toEqual('string')
|
|
||||||
expect(typeof event.sig).toEqual('string')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('serializeEvent', () => {
|
|
||||||
test('should serialize a valid event object', () => {
|
|
||||||
const privateKey = hexToBytes('d217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf')
|
|
||||||
const publicKey = getPublicKey(privateKey)
|
|
||||||
|
|
||||||
const unsignedEvent = {
|
|
||||||
pubkey: publicKey,
|
|
||||||
created_at: 1617932115,
|
|
||||||
kind: ShortTextNote,
|
|
||||||
tags: [],
|
|
||||||
content: 'Hello, world!',
|
|
||||||
}
|
|
||||||
|
|
||||||
const serializedEvent = serializeEvent(unsignedEvent)
|
|
||||||
|
|
||||||
expect(serializedEvent).toEqual(
|
|
||||||
JSON.stringify([
|
|
||||||
0,
|
|
||||||
publicKey,
|
|
||||||
unsignedEvent.created_at,
|
|
||||||
unsignedEvent.kind,
|
|
||||||
unsignedEvent.tags,
|
|
||||||
unsignedEvent.content,
|
|
||||||
]),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('should throw an error for an invalid event object', () => {
|
|
||||||
const privateKey = hexToBytes('d217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf')
|
|
||||||
const publicKey = getPublicKey(privateKey)
|
|
||||||
|
|
||||||
const invalidEvent = {
|
|
||||||
kind: ShortTextNote,
|
|
||||||
tags: [],
|
|
||||||
created_at: 1617932115,
|
|
||||||
pubkey: publicKey, // missing content
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(() => {
|
|
||||||
// @ts-expect-error
|
|
||||||
serializeEvent(invalidEvent)
|
|
||||||
}).toThrow("can't serialize event with wrong or missing properties")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('getEventHash', () => {
|
|
||||||
test('should return the correct event hash', () => {
|
|
||||||
const privateKey = hexToBytes('d217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf')
|
|
||||||
const publicKey = getPublicKey(privateKey)
|
|
||||||
|
|
||||||
const unsignedEvent = {
|
|
||||||
kind: ShortTextNote,
|
|
||||||
tags: [],
|
|
||||||
content: 'Hello, world!',
|
|
||||||
created_at: 1617932115,
|
|
||||||
pubkey: publicKey,
|
|
||||||
}
|
|
||||||
|
|
||||||
const eventHash = getEventHash(unsignedEvent)
|
|
||||||
|
|
||||||
expect(typeof eventHash).toEqual('string')
|
|
||||||
expect(eventHash.length).toEqual(64)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('validateEvent', () => {
|
|
||||||
test('should return true for a valid event object', () => {
|
|
||||||
const privateKey = hexToBytes('d217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf')
|
|
||||||
const publicKey = getPublicKey(privateKey)
|
|
||||||
|
|
||||||
const unsignedEvent = {
|
|
||||||
kind: ShortTextNote,
|
|
||||||
tags: [],
|
|
||||||
content: 'Hello, world!',
|
|
||||||
created_at: 1617932115,
|
|
||||||
pubkey: publicKey,
|
|
||||||
}
|
|
||||||
|
|
||||||
const isValid = validateEvent(unsignedEvent)
|
|
||||||
|
|
||||||
expect(isValid).toEqual(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('should return false for a non object event', () => {
|
|
||||||
const nonObjectEvent = ''
|
|
||||||
const isValid = validateEvent(nonObjectEvent)
|
|
||||||
expect(isValid).toEqual(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('should return false for an event object with missing properties', () => {
|
|
||||||
const invalidEvent = {
|
|
||||||
kind: ShortTextNote,
|
|
||||||
tags: [],
|
|
||||||
created_at: 1617932115, // missing content and pubkey
|
|
||||||
}
|
|
||||||
|
|
||||||
const isValid = validateEvent(invalidEvent)
|
|
||||||
|
|
||||||
expect(isValid).toEqual(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('should return false for an empty object', () => {
|
|
||||||
const emptyObj = {}
|
|
||||||
|
|
||||||
const isValid = validateEvent(emptyObj)
|
|
||||||
|
|
||||||
expect(isValid).toEqual(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('should return false for an object with invalid properties', () => {
|
|
||||||
const privateKey = hexToBytes('d217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf')
|
|
||||||
const publicKey = getPublicKey(privateKey)
|
|
||||||
|
|
||||||
const invalidEvent = {
|
|
||||||
kind: 1,
|
|
||||||
tags: [],
|
|
||||||
created_at: '1617932115', // should be a number
|
|
||||||
pubkey: publicKey,
|
|
||||||
}
|
|
||||||
|
|
||||||
const isValid = validateEvent(invalidEvent)
|
|
||||||
|
|
||||||
expect(isValid).toEqual(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('should return false for an object with an invalid public key', () => {
|
|
||||||
const invalidEvent = {
|
|
||||||
kind: 1,
|
|
||||||
tags: [],
|
|
||||||
content: 'Hello, world!',
|
|
||||||
created_at: 1617932115,
|
|
||||||
pubkey: 'invalid_pubkey',
|
|
||||||
}
|
|
||||||
|
|
||||||
const isValid = validateEvent(invalidEvent)
|
|
||||||
|
|
||||||
expect(isValid).toEqual(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('should return false for an object with invalid tags', () => {
|
|
||||||
const privateKey = hexToBytes('d217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf')
|
|
||||||
const publicKey = getPublicKey(privateKey)
|
|
||||||
|
|
||||||
const invalidEvent = {
|
|
||||||
kind: 1,
|
|
||||||
tags: {}, // should be an array
|
|
||||||
content: 'Hello, world!',
|
|
||||||
created_at: 1617932115,
|
|
||||||
pubkey: publicKey,
|
|
||||||
}
|
|
||||||
|
|
||||||
const isValid = validateEvent(invalidEvent)
|
|
||||||
|
|
||||||
expect(isValid).toEqual(false)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('verifyEvent', () => {
|
|
||||||
test('should return true for a valid event signature', () => {
|
|
||||||
const privateKey = hexToBytes('d217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf')
|
|
||||||
const event = finalizeEvent(
|
|
||||||
{
|
|
||||||
kind: ShortTextNote,
|
|
||||||
tags: [],
|
|
||||||
content: 'Hello, world!',
|
|
||||||
created_at: 1617932115,
|
|
||||||
},
|
|
||||||
privateKey,
|
|
||||||
)
|
|
||||||
|
|
||||||
const isValid = verifyEvent(event)
|
|
||||||
expect(isValid).toEqual(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('should return false for an invalid event signature', () => {
|
|
||||||
const privateKey = hexToBytes('d217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf')
|
|
||||||
const { [verifiedSymbol]: _, ...event } = finalizeEvent(
|
|
||||||
{
|
|
||||||
kind: ShortTextNote,
|
|
||||||
tags: [],
|
|
||||||
content: 'Hello, world!',
|
|
||||||
created_at: 1617932115,
|
|
||||||
},
|
|
||||||
privateKey,
|
|
||||||
)
|
|
||||||
|
|
||||||
// tamper with the signature
|
|
||||||
event.sig = event.sig.replace(/^.{3}/g, '666')
|
|
||||||
|
|
||||||
const isValid = verifyEvent(event)
|
|
||||||
expect(isValid).toEqual(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('should return false when verifying an event with a different private key', () => {
|
|
||||||
const privateKey1 = hexToBytes('d217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf')
|
|
||||||
|
|
||||||
const privateKey2 = hexToBytes('5b4a34f4e4b23c63ad55a35e3f84a3b53d96dbf266edf521a8358f71d19cbf67')
|
|
||||||
const publicKey2 = getPublicKey(privateKey2)
|
|
||||||
|
|
||||||
const { [verifiedSymbol]: _, ...event } = finalizeEvent(
|
|
||||||
{
|
|
||||||
kind: ShortTextNote,
|
|
||||||
tags: [],
|
|
||||||
content: 'Hello, world!',
|
|
||||||
created_at: 1617932115,
|
|
||||||
},
|
|
||||||
privateKey1,
|
|
||||||
)
|
|
||||||
|
|
||||||
// verify with different private key
|
|
||||||
const isValid = verifyEvent({
|
|
||||||
...event,
|
|
||||||
pubkey: publicKey2,
|
|
||||||
})
|
|
||||||
expect(isValid).toEqual(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('should return false for an invalid event id', () => {
|
|
||||||
const privateKey = hexToBytes('d217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf')
|
|
||||||
|
|
||||||
const { [verifiedSymbol]: _, ...event } = finalizeEvent(
|
|
||||||
{
|
|
||||||
kind: 1,
|
|
||||||
tags: [],
|
|
||||||
content: 'Hello, world!',
|
|
||||||
created_at: 1617932115,
|
|
||||||
},
|
|
||||||
privateKey,
|
|
||||||
)
|
|
||||||
|
|
||||||
// tamper with the id
|
|
||||||
event.id = event.id.replace(/^.{3}/g, '666')
|
|
||||||
|
|
||||||
const isValid = verifyEvent(event)
|
|
||||||
expect(isValid).toEqual(false)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|||||||
14
core.ts
14
core.ts
@@ -49,3 +49,17 @@ export function validateEvent<T>(event: T): event is T & UnsignedEvent {
|
|||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sort events in reverse-chronological order by the `created_at` timestamp,
|
||||||
|
* and then by the event `id` (lexicographically) in case of ties.
|
||||||
|
* This mutates the array.
|
||||||
|
*/
|
||||||
|
export function sortEvents(events: Event[]): Event[] {
|
||||||
|
return events.sort((a: NostrEvent, b: NostrEvent): number => {
|
||||||
|
if (a.created_at !== b.created_at) {
|
||||||
|
return b.created_at - a.created_at
|
||||||
|
}
|
||||||
|
return a.id.localeCompare(b.id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -41,7 +41,9 @@ export function matchFilter(filter: Filter, event: Event): boolean {
|
|||||||
|
|
||||||
export function matchFilters(filters: Filter[], event: Event): boolean {
|
export function matchFilters(filters: Filter[], event: Event): boolean {
|
||||||
for (let i = 0; i < filters.length; i++) {
|
for (let i = 0; i < filters.length; i++) {
|
||||||
if (matchFilter(filters[i], event)) return true
|
if (matchFilter(filters[i], event)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
43
jsr.json
43
jsr.json
@@ -1,5 +1,44 @@
|
|||||||
{
|
{
|
||||||
"name": "@nostr/tools",
|
"name": "@nostr/tools",
|
||||||
"version": "2.3.1",
|
"version": "2.3.2",
|
||||||
"exports": "./index.ts"
|
"exports": {
|
||||||
|
".": "./index.ts",
|
||||||
|
"./core": "./core.ts",
|
||||||
|
"./pure": "./pure.ts",
|
||||||
|
"./wasm": "./wasm.ts",
|
||||||
|
"./kinds": "./kinds.ts",
|
||||||
|
"./filter": "./filter.ts",
|
||||||
|
"./abstract-relay": "./abstract-relay.ts",
|
||||||
|
"./relay": "./relay.ts",
|
||||||
|
"./abstract-pool": "./abstract-pool.ts",
|
||||||
|
"./pool": "./pool.ts",
|
||||||
|
"./references": "./references.ts",
|
||||||
|
"./nip04": "./nip04.ts",
|
||||||
|
"./nip05": "./nip05.ts",
|
||||||
|
"./nip06": "./nip06.ts",
|
||||||
|
"./nip10": "./nip10.ts",
|
||||||
|
"./nip11": "./nip11.ts",
|
||||||
|
"./nip13": "./nip13.ts",
|
||||||
|
"./nip18": "./nip18.ts",
|
||||||
|
"./nip19": "./nip19.ts",
|
||||||
|
"./nip21": "./nip21.ts",
|
||||||
|
"./nip25": "./nip25.ts",
|
||||||
|
"./nip27": "./nip27.ts",
|
||||||
|
"./nip28": "./nip28.ts",
|
||||||
|
"./nip29": "./nip29.ts",
|
||||||
|
"./nip30": "./nip30.ts",
|
||||||
|
"./nip39": "./nip39.ts",
|
||||||
|
"./nip42": "./nip42.ts",
|
||||||
|
"./nip44": "./nip44.ts",
|
||||||
|
"./nip46": "./nip46.ts",
|
||||||
|
"./nip49": "./nip49.ts",
|
||||||
|
"./nip57": "./nip57.ts",
|
||||||
|
"./nip75": "./nip75.ts",
|
||||||
|
"./nip94": "./nip94.ts",
|
||||||
|
"./nip96": "./nip96.ts",
|
||||||
|
"./nip98": "./nip98.ts",
|
||||||
|
"./nip99": "./nip99.ts",
|
||||||
|
"./fakejson": "./fakejson.ts",
|
||||||
|
"./utils": "./utils.ts"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,17 +1,16 @@
|
|||||||
import { describe, test, expect } from 'bun:test'
|
import { describe, test, expect } from 'bun:test'
|
||||||
import fetch from 'node-fetch'
|
import fetch from 'node-fetch'
|
||||||
import { useFetchImplementation, fetchRelayInformation } from './nip11'
|
import { useFetchImplementation, fetchRelayInformation } from './nip11.ts'
|
||||||
|
|
||||||
// TODO: replace with a mock
|
// TODO: replace with a mock
|
||||||
describe('requesting relay as for NIP11', () => {
|
describe('requesting relay as for NIP11', () => {
|
||||||
useFetchImplementation(fetch)
|
useFetchImplementation(fetch)
|
||||||
|
|
||||||
test('testing a relay', async () => {
|
test('testing a relay', async () => {
|
||||||
const info = await fetchRelayInformation('wss://atlas.nostr.land')
|
const info = await fetchRelayInformation('wss://nos.lol')
|
||||||
expect(info.name).toEqual('nostr.land')
|
expect(info.name).toEqual('nos.lol')
|
||||||
expect(info.description).toContain('nostr.land family')
|
expect(info.description).toContain('Generally accepts notes, except spammy ones.')
|
||||||
expect(info.fees).toBeTruthy()
|
|
||||||
expect(info.supported_nips).toEqual([1, 2, 4, 9, 11, 12, 16, 20, 22, 28, 33, 40])
|
expect(info.supported_nips).toEqual([1, 2, 4, 9, 11, 12, 16, 20, 22, 28, 33, 40])
|
||||||
expect(info.software).toEqual('custom')
|
expect(info.software).toEqual('git+https://github.com/hoytech/strfry.git')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
14
nip29.ts
14
nip29.ts
@@ -1,10 +1,10 @@
|
|||||||
import { AbstractSimplePool } from './abstract-pool'
|
import { AbstractSimplePool } from './abstract-pool.ts'
|
||||||
import { Subscription } from './abstract-relay'
|
import { Subscription } from './abstract-relay.ts'
|
||||||
import { decode } from './nip19'
|
import { decode } from './nip19.ts'
|
||||||
import type { Event } from './core'
|
import type { Event } from './core.ts'
|
||||||
import { fetchRelayInformation } from './nip11'
|
import { fetchRelayInformation } from './nip11.ts'
|
||||||
import { normalizeURL } from './utils'
|
import { normalizeURL } from './utils.ts'
|
||||||
import { AddressPointer } from './nip19'
|
import { AddressPointer } from './nip19.ts'
|
||||||
|
|
||||||
export function subscribeRelayGroups(
|
export function subscribeRelayGroups(
|
||||||
pool: AbstractSimplePool,
|
pool: AbstractSimplePool,
|
||||||
|
|||||||
10
nip46.ts
10
nip46.ts
@@ -17,7 +17,7 @@ export function useFetchImplementation(fetchImplementation: any) {
|
|||||||
_fetch = fetchImplementation
|
_fetch = fetchImplementation
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BUNKER_REGEX = /^bunker:\/\/([0-9a-f]{64})\??([?\/\w:.=&%]*)$/
|
export const BUNKER_REGEX = /^bunker:\/\/([0-9a-f]{64})\??([?\/\w:.=&%-]*)$/
|
||||||
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||||
|
|
||||||
export type BunkerPointer = {
|
export type BunkerPointer = {
|
||||||
@@ -193,7 +193,7 @@ export class BunkerSigner {
|
|||||||
* Calls the "connect" method on the bunker.
|
* Calls the "connect" method on the bunker.
|
||||||
*/
|
*/
|
||||||
async connect(): Promise<void> {
|
async connect(): Promise<void> {
|
||||||
await this.sendRequest('connect', [getPublicKey(this.secretKey), this.bp.secret || ''])
|
await this.sendRequest('connect', [this.bp.pubkey, this.bp.secret || ''])
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -254,6 +254,8 @@ export class BunkerSigner {
|
|||||||
* @param username - The username for the account.
|
* @param username - The username for the account.
|
||||||
* @param domain - The domain for the account.
|
* @param domain - The domain for the account.
|
||||||
* @param email - The optional email for the account.
|
* @param email - The optional email for the account.
|
||||||
|
* @param localSecretKey - Optionally pass a local secret key that will be used to communicate with the bunker,
|
||||||
|
this will default to generating a random key.
|
||||||
* @throws Error if the email is present but invalid.
|
* @throws Error if the email is present but invalid.
|
||||||
* @returns A Promise that resolves to the auth_url that the client should follow to create an account.
|
* @returns A Promise that resolves to the auth_url that the client should follow to create an account.
|
||||||
*/
|
*/
|
||||||
@@ -263,11 +265,11 @@ export async function createAccount(
|
|||||||
username: string,
|
username: string,
|
||||||
domain: string,
|
domain: string,
|
||||||
email?: string,
|
email?: string,
|
||||||
|
localSecretKey: Uint8Array = generateSecretKey()
|
||||||
): Promise<BunkerSigner> {
|
): Promise<BunkerSigner> {
|
||||||
if (email && !EMAIL_REGEX.test(email)) throw new Error('Invalid email')
|
if (email && !EMAIL_REGEX.test(email)) throw new Error('Invalid email')
|
||||||
|
|
||||||
let sk = generateSecretKey()
|
let rpc = new BunkerSigner(localSecretKey, bunker.bunkerPointer, params)
|
||||||
let rpc = new BunkerSigner(sk, bunker.bunkerPointer, params)
|
|
||||||
|
|
||||||
let pubkey = await rpc.sendRequest('create_account', [username, domain, email || ''])
|
let pubkey = await rpc.sendRequest('create_account', [username, domain, email || ''])
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, test, expect } from 'bun:test'
|
import { describe, test, expect } from 'bun:test'
|
||||||
import { hexToBytes } from '@noble/hashes/utils'
|
import { hexToBytes } from '@noble/hashes/utils'
|
||||||
import { makeNwcRequestEvent, parseConnectionString } from './nip47'
|
import { makeNwcRequestEvent, parseConnectionString } from './nip47.ts'
|
||||||
import { decrypt } from './nip04.ts'
|
import { decrypt } from './nip04.ts'
|
||||||
import { NWCWalletRequest } from './kinds.ts'
|
import { NWCWalletRequest } from './kinds.ts'
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { test, expect } from 'bun:test'
|
import { test, expect } from 'bun:test'
|
||||||
import { decrypt, encrypt } from './nip49'
|
import { decrypt, encrypt } from './nip49.ts'
|
||||||
import { hexToBytes } from '@noble/hashes/utils'
|
import { hexToBytes } from '@noble/hashes/utils'
|
||||||
|
|
||||||
test('encrypt and decrypt', () => {
|
test('encrypt and decrypt', () => {
|
||||||
|
|||||||
2
nip49.ts
2
nip49.ts
@@ -1,7 +1,7 @@
|
|||||||
import { scrypt } from '@noble/hashes/scrypt'
|
import { scrypt } from '@noble/hashes/scrypt'
|
||||||
import { xchacha20poly1305 } from '@noble/ciphers/chacha'
|
import { xchacha20poly1305 } from '@noble/ciphers/chacha'
|
||||||
import { concatBytes, randomBytes } from '@noble/hashes/utils'
|
import { concatBytes, randomBytes } from '@noble/hashes/utils'
|
||||||
import { Bech32MaxSize, encodeBytes } from './nip19'
|
import { Bech32MaxSize, encodeBytes } from './nip19.ts'
|
||||||
import { bech32 } from '@scure/base'
|
import { bech32 } from '@scure/base'
|
||||||
|
|
||||||
export function encrypt(sec: Uint8Array, password: string, logn: number = 16, ksb: 0x00 | 0x01 | 0x02 = 0x02): string {
|
export function encrypt(sec: Uint8Array, password: string, logn: number = 16, ksb: 0x00 | 0x01 | 0x02 = 0x02): string {
|
||||||
|
|||||||
357
nip58.test.ts
Normal file
357
nip58.test.ts
Normal file
@@ -0,0 +1,357 @@
|
|||||||
|
import { expect, test } from 'bun:test'
|
||||||
|
|
||||||
|
import { EventTemplate } from './core.ts'
|
||||||
|
import {
|
||||||
|
BadgeAward as BadgeAwardKind,
|
||||||
|
BadgeDefinition as BadgeDefinitionKind,
|
||||||
|
ProfileBadges as ProfileBadgesKind,
|
||||||
|
} from './kinds.ts'
|
||||||
|
import { finalizeEvent, generateSecretKey } from './pure.ts'
|
||||||
|
|
||||||
|
import {
|
||||||
|
BadgeAward,
|
||||||
|
BadgeDefinition,
|
||||||
|
ProfileBadges,
|
||||||
|
generateBadgeAwardEventTemplate,
|
||||||
|
generateBadgeDefinitionEventTemplate,
|
||||||
|
generateProfileBadgesEventTemplate,
|
||||||
|
validateBadgeAwardEvent,
|
||||||
|
validateBadgeDefinitionEvent,
|
||||||
|
validateProfileBadgesEvent,
|
||||||
|
} from './nip58.ts'
|
||||||
|
|
||||||
|
test('BadgeDefinition has required property "d"', () => {
|
||||||
|
const badge: BadgeDefinition = {
|
||||||
|
d: 'badge-id',
|
||||||
|
}
|
||||||
|
expect(badge.d).toEqual('badge-id')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('BadgeDefinition has optional property "name"', () => {
|
||||||
|
const badge: BadgeDefinition = {
|
||||||
|
d: 'badge-id',
|
||||||
|
name: 'Badge Name',
|
||||||
|
}
|
||||||
|
expect(badge.name).toEqual('Badge Name')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('BadgeDefinition has optional property "description"', () => {
|
||||||
|
const badge: BadgeDefinition = {
|
||||||
|
d: 'badge-id',
|
||||||
|
description: 'Badge Description',
|
||||||
|
}
|
||||||
|
expect(badge.description).toEqual('Badge Description')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('BadgeDefinition has optional property "image"', () => {
|
||||||
|
const badge: BadgeDefinition = {
|
||||||
|
d: 'badge-id',
|
||||||
|
image: ['https://example.com/badge.png', '1024x1024'],
|
||||||
|
}
|
||||||
|
expect(badge.image).toEqual(['https://example.com/badge.png', '1024x1024'])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('BadgeDefinition has optional property "thumbs"', () => {
|
||||||
|
const badge: BadgeDefinition = {
|
||||||
|
d: 'badge-id',
|
||||||
|
thumbs: [
|
||||||
|
['https://example.com/thumb.png', '100x100'],
|
||||||
|
['https://example.com/thumb2.png', '200x200'],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
expect(badge.thumbs).toEqual([
|
||||||
|
['https://example.com/thumb.png', '100x100'],
|
||||||
|
['https://example.com/thumb2.png', '200x200'],
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('BadgeAward has required property "a"', () => {
|
||||||
|
const badgeAward: BadgeAward = {
|
||||||
|
a: 'badge-definition-address',
|
||||||
|
p: [
|
||||||
|
['pubkey1', 'relay1'],
|
||||||
|
['pubkey2', 'relay2'],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
expect(badgeAward.a).toEqual('badge-definition-address')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('BadgeAward has required property "p"', () => {
|
||||||
|
const badgeAward: BadgeAward = {
|
||||||
|
a: 'badge-definition-address',
|
||||||
|
p: [
|
||||||
|
['pubkey1', 'relay1'],
|
||||||
|
['pubkey2', 'relay2'],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
expect(badgeAward.p).toEqual([
|
||||||
|
['pubkey1', 'relay1'],
|
||||||
|
['pubkey2', 'relay2'],
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('ProfileBadges has required property "d"', () => {
|
||||||
|
const profileBadges: ProfileBadges = {
|
||||||
|
d: 'profile_badges',
|
||||||
|
badges: [],
|
||||||
|
}
|
||||||
|
expect(profileBadges.d).toEqual('profile_badges')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('ProfileBadges has required property "badges"', () => {
|
||||||
|
const profileBadges: ProfileBadges = {
|
||||||
|
d: 'profile_badges',
|
||||||
|
badges: [],
|
||||||
|
}
|
||||||
|
expect(profileBadges.badges).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('ProfileBadges badges array contains objects with required properties "a" and "e"', () => {
|
||||||
|
const profileBadges: ProfileBadges = {
|
||||||
|
d: 'profile_badges',
|
||||||
|
badges: [
|
||||||
|
{
|
||||||
|
a: 'badge-definition-address',
|
||||||
|
e: ['badge-award-event-id'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
expect(profileBadges.badges[0].a).toEqual('badge-definition-address')
|
||||||
|
expect(profileBadges.badges[0].e).toEqual(['badge-award-event-id'])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('generateBadgeDefinitionEventTemplate generates EventTemplate with mandatory tags', () => {
|
||||||
|
const badge: BadgeDefinition = {
|
||||||
|
d: 'badge-id',
|
||||||
|
}
|
||||||
|
const eventTemplate = generateBadgeDefinitionEventTemplate(badge)
|
||||||
|
expect(eventTemplate.tags).toEqual([['d', 'badge-id']])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('generateBadgeDefinitionEventTemplate generates EventTemplate with optional tags', () => {
|
||||||
|
const badge: BadgeDefinition = {
|
||||||
|
d: 'badge-id',
|
||||||
|
name: 'Badge Name',
|
||||||
|
description: 'Badge Description',
|
||||||
|
image: ['https://example.com/badge.png', '1024x1024'],
|
||||||
|
thumbs: [
|
||||||
|
['https://example.com/thumb.png', '100x100'],
|
||||||
|
['https://example.com/thumb2.png', '200x200'],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
const eventTemplate = generateBadgeDefinitionEventTemplate(badge)
|
||||||
|
expect(eventTemplate.tags).toEqual([
|
||||||
|
['d', 'badge-id'],
|
||||||
|
['name', 'Badge Name'],
|
||||||
|
['description', 'Badge Description'],
|
||||||
|
['image', 'https://example.com/badge.png', '1024x1024'],
|
||||||
|
['thumb', 'https://example.com/thumb.png', '100x100'],
|
||||||
|
['thumb', 'https://example.com/thumb2.png', '200x200'],
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('generateBadgeDefinitionEventTemplate generates EventTemplate without optional tags', () => {
|
||||||
|
const badge: BadgeDefinition = {
|
||||||
|
d: 'badge-id',
|
||||||
|
}
|
||||||
|
const eventTemplate = generateBadgeDefinitionEventTemplate(badge)
|
||||||
|
expect(eventTemplate.tags).toEqual([['d', 'badge-id']])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('validateBadgeDefinitionEvent returns true for valid BadgeDefinition event', () => {
|
||||||
|
const sk = generateSecretKey()
|
||||||
|
const eventTemplate: EventTemplate = {
|
||||||
|
content: '',
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
kind: BadgeDefinitionKind,
|
||||||
|
tags: [
|
||||||
|
['d', 'badge-id'],
|
||||||
|
['name', 'Badge Name'],
|
||||||
|
['description', 'Badge Description'],
|
||||||
|
['image', 'https://example.com/badge.png', '1024x1024'],
|
||||||
|
['thumb', 'https://example.com/thumb.png', '100x100'],
|
||||||
|
['thumb', 'https://example.com/thumb2.png', '200x200'],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
const event = finalizeEvent(eventTemplate, sk)
|
||||||
|
const isValid = validateBadgeDefinitionEvent(event)
|
||||||
|
|
||||||
|
expect(isValid).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('validateBadgeDefinitionEvent returns false for invalid BadgeDefinition event', () => {
|
||||||
|
const sk = generateSecretKey()
|
||||||
|
const eventTemplate: EventTemplate = {
|
||||||
|
content: '',
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
kind: BadgeDefinitionKind,
|
||||||
|
tags: [],
|
||||||
|
}
|
||||||
|
const event = finalizeEvent(eventTemplate, sk)
|
||||||
|
const isValid = validateBadgeDefinitionEvent(event)
|
||||||
|
|
||||||
|
expect(isValid).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('generateBadgeAwardEventTemplate generates EventTemplate with mandatory tags', () => {
|
||||||
|
const badgeAward: BadgeAward = {
|
||||||
|
a: 'badge-definition-address',
|
||||||
|
p: [
|
||||||
|
['pubkey1', 'relay1'],
|
||||||
|
['pubkey2', 'relay2'],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
const eventTemplate = generateBadgeAwardEventTemplate(badgeAward)
|
||||||
|
expect(eventTemplate.tags).toEqual([
|
||||||
|
['a', 'badge-definition-address'],
|
||||||
|
['p', 'pubkey1', 'relay1'],
|
||||||
|
['p', 'pubkey2', 'relay2'],
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('generateBadgeAwardEventTemplate generates EventTemplate without optional tags', () => {
|
||||||
|
const badgeAward: BadgeAward = {
|
||||||
|
a: 'badge-definition-address',
|
||||||
|
p: [
|
||||||
|
['pubkey1', 'relay1'],
|
||||||
|
['pubkey2', 'relay2'],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
const eventTemplate = generateBadgeAwardEventTemplate(badgeAward)
|
||||||
|
expect(eventTemplate.tags).toEqual([
|
||||||
|
['a', 'badge-definition-address'],
|
||||||
|
['p', 'pubkey1', 'relay1'],
|
||||||
|
['p', 'pubkey2', 'relay2'],
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('generateBadgeAwardEventTemplate generates EventTemplate with optional tags', () => {
|
||||||
|
const badgeAward: BadgeAward = {
|
||||||
|
a: 'badge-definition-address',
|
||||||
|
p: [
|
||||||
|
['pubkey1', 'relay1'],
|
||||||
|
['pubkey2', 'relay2'],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
const eventTemplate = generateBadgeAwardEventTemplate(badgeAward)
|
||||||
|
expect(eventTemplate.tags).toEqual([
|
||||||
|
['a', 'badge-definition-address'],
|
||||||
|
['p', 'pubkey1', 'relay1'],
|
||||||
|
['p', 'pubkey2', 'relay2'],
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('validateBadgeAwardEvent returns true for valid BadgeAward event', () => {
|
||||||
|
const sk = generateSecretKey()
|
||||||
|
const eventTemplate: EventTemplate = {
|
||||||
|
content: '',
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
kind: BadgeAwardKind,
|
||||||
|
tags: [
|
||||||
|
['a', 'badge-definition-address'],
|
||||||
|
['p', 'pubkey1', 'relay1'],
|
||||||
|
['p', 'pubkey2', 'relay2'],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
const event = finalizeEvent(eventTemplate, sk)
|
||||||
|
const isValid = validateBadgeAwardEvent(event)
|
||||||
|
|
||||||
|
expect(isValid).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('validateBadgeAwardEvent returns false for invalid BadgeAward event', () => {
|
||||||
|
const sk = generateSecretKey()
|
||||||
|
const eventTemplate: EventTemplate = {
|
||||||
|
content: '',
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
kind: BadgeAwardKind,
|
||||||
|
tags: [],
|
||||||
|
}
|
||||||
|
const event = finalizeEvent(eventTemplate, sk)
|
||||||
|
const isValid = validateBadgeAwardEvent(event)
|
||||||
|
|
||||||
|
expect(isValid).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('generateProfileBadgesEventTemplate generates EventTemplate with mandatory tags', () => {
|
||||||
|
const profileBadges: ProfileBadges = {
|
||||||
|
d: 'profile_badges',
|
||||||
|
badges: [],
|
||||||
|
}
|
||||||
|
const eventTemplate = generateProfileBadgesEventTemplate(profileBadges)
|
||||||
|
expect(eventTemplate.tags).toEqual([['d', 'profile_badges']])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('generateProfileBadgesEventTemplate generates EventTemplate with optional tags', () => {
|
||||||
|
const profileBadges: ProfileBadges = {
|
||||||
|
d: 'profile_badges',
|
||||||
|
badges: [
|
||||||
|
{
|
||||||
|
a: 'badge-definition-address',
|
||||||
|
e: ['badge-award-event-id'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
const eventTemplate = generateProfileBadgesEventTemplate(profileBadges)
|
||||||
|
expect(eventTemplate.tags).toEqual([
|
||||||
|
['d', 'profile_badges'],
|
||||||
|
['a', 'badge-definition-address'],
|
||||||
|
['e', 'badge-award-event-id'],
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('generateProfileBadgesEventTemplate generates EventTemplate with multiple optional tags', () => {
|
||||||
|
const profileBadges: ProfileBadges = {
|
||||||
|
d: 'profile_badges',
|
||||||
|
badges: [
|
||||||
|
{
|
||||||
|
a: 'badge-definition-address1',
|
||||||
|
e: ['badge-award-event-id1', 'badge-award-event-id2'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
a: 'badge-definition-address2',
|
||||||
|
e: ['badge-award-event-id3'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
const eventTemplate = generateProfileBadgesEventTemplate(profileBadges)
|
||||||
|
expect(eventTemplate.tags).toEqual([
|
||||||
|
['d', 'profile_badges'],
|
||||||
|
['a', 'badge-definition-address1'],
|
||||||
|
['e', 'badge-award-event-id1', 'badge-award-event-id2'],
|
||||||
|
['a', 'badge-definition-address2'],
|
||||||
|
['e', 'badge-award-event-id3'],
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('validateProfileBadgesEvent returns true for valid ProfileBadges event', () => {
|
||||||
|
const sk = generateSecretKey()
|
||||||
|
const eventTemplate: EventTemplate = {
|
||||||
|
content: '',
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
kind: ProfileBadgesKind,
|
||||||
|
tags: [
|
||||||
|
['d', 'profile_badges'],
|
||||||
|
['a', 'badge-definition-address'],
|
||||||
|
['e', 'badge-award-event-id'],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
const event = finalizeEvent(eventTemplate, sk)
|
||||||
|
const isValid = validateProfileBadgesEvent(event)
|
||||||
|
|
||||||
|
expect(isValid).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('validateProfileBadgesEvent returns false for invalid ProfileBadges event', () => {
|
||||||
|
const sk = generateSecretKey()
|
||||||
|
const eventTemplate: EventTemplate = {
|
||||||
|
content: '',
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
kind: ProfileBadgesKind,
|
||||||
|
tags: [],
|
||||||
|
}
|
||||||
|
const event = finalizeEvent(eventTemplate, sk)
|
||||||
|
const isValid = validateProfileBadgesEvent(event)
|
||||||
|
|
||||||
|
expect(isValid).toBe(false)
|
||||||
|
})
|
||||||
245
nip58.ts
Normal file
245
nip58.ts
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
import { Event, EventTemplate } from './core.ts'
|
||||||
|
import {
|
||||||
|
BadgeAward as BadgeAwardKind,
|
||||||
|
BadgeDefinition as BadgeDefinitionKind,
|
||||||
|
ProfileBadges as ProfileBadgesKind,
|
||||||
|
} from './kinds.ts'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the structure for defining a badge within the Nostr network.
|
||||||
|
* This structure is used to create templates for badge definition events,
|
||||||
|
* facilitating the recognition and awarding of badges to users for various achievements.
|
||||||
|
*/
|
||||||
|
export type BadgeDefinition = {
|
||||||
|
/**
|
||||||
|
* A unique identifier for the badge. This is used to distinguish badges
|
||||||
|
* from one another and should be unique across all badge definitions.
|
||||||
|
* Typically, this could be a short, descriptive string.
|
||||||
|
*/
|
||||||
|
d: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An optional short name for the badge. This provides a human-readable
|
||||||
|
* title for the badge, making it easier to recognize and refer to.
|
||||||
|
*/
|
||||||
|
name?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An optional description for the badge. This field can be used to
|
||||||
|
* provide more detailed information about the badge, such as the criteria
|
||||||
|
* for its awarding or its significance.
|
||||||
|
*/
|
||||||
|
description?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An optional image URL and dimensions for the badge. The first element
|
||||||
|
* of the tuple is the URL pointing to a high-resolution image representing
|
||||||
|
* the badge, and the second element specifies the image's dimensions in
|
||||||
|
* the format "widthxheight". The recommended dimensions are 1024x1024 pixels.
|
||||||
|
*/
|
||||||
|
image?: [string, string]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An optional list of thumbnail images for the badge. Each element in the
|
||||||
|
* array is a tuple, where the first element is the URL pointing to a thumbnail
|
||||||
|
* version of the badge image, and the second element specifies the thumbnail's
|
||||||
|
* dimensions in the format "widthxheight". Multiple thumbnails can be provided
|
||||||
|
* to support different display sizes.
|
||||||
|
*/
|
||||||
|
thumbs?: Array<[string, string]>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the structure for awarding a badge to one or more recipients
|
||||||
|
* within the Nostr network. This structure is used to create templates for
|
||||||
|
* badge award events, which are immutable and signify the recognition of
|
||||||
|
* individuals' achievements or contributions.
|
||||||
|
*/
|
||||||
|
export type BadgeAward = {
|
||||||
|
/**
|
||||||
|
* A reference to the Badge Definition event. This is typically composed
|
||||||
|
* of the event ID of the badge definition. It establishes a clear linkage
|
||||||
|
* between the badge being awarded and its original definition, ensuring
|
||||||
|
* that recipients are awarded the correct badge.
|
||||||
|
*/
|
||||||
|
a: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array of p tags, each containing a pubkey and its associated relays.
|
||||||
|
*/
|
||||||
|
p: string[][]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the collection of badges a user chooses to display on their profile.
|
||||||
|
* This structure is crucial for applications that allow users to showcase achievements
|
||||||
|
* or recognitions in the form of badges, following the specifications of NIP-58.
|
||||||
|
*/
|
||||||
|
export type ProfileBadges = {
|
||||||
|
/**
|
||||||
|
* A unique identifier for the profile badges collection. According to NIP-58,
|
||||||
|
* this should be set to "profile_badges" to differentiate it from other event types.
|
||||||
|
*/
|
||||||
|
d: 'profile_badges'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list of badges that the user has elected to display on their profile. Each item
|
||||||
|
* in the array represents a specific badge, including references to both its definition
|
||||||
|
* and the award event.
|
||||||
|
*/
|
||||||
|
badges: Array<{
|
||||||
|
/**
|
||||||
|
* The event address of the badge definition. This is a reference to the specific badge
|
||||||
|
* being displayed, linking back to the badge's original definition event. It allows
|
||||||
|
* clients to fetch and display the badge's details, such as its name, description,
|
||||||
|
* and image.
|
||||||
|
*/
|
||||||
|
a: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The event id of the badge award with corresponding relays. This references the event
|
||||||
|
* in which the badge was awarded to the user. It is crucial for verifying the
|
||||||
|
* authenticity of the badge display, ensuring that the user was indeed awarded the
|
||||||
|
* badge they are choosing to display.
|
||||||
|
*/
|
||||||
|
e: string[]
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates an EventTemplate based on the provided BadgeDefinition.
|
||||||
|
*
|
||||||
|
* @param {BadgeDefinition} badgeDefinition - The BadgeDefinition object.
|
||||||
|
* @returns {EventTemplate} - The generated EventTemplate object.
|
||||||
|
*/
|
||||||
|
export function generateBadgeDefinitionEventTemplate({
|
||||||
|
d,
|
||||||
|
description,
|
||||||
|
image,
|
||||||
|
name,
|
||||||
|
thumbs,
|
||||||
|
}: BadgeDefinition): EventTemplate {
|
||||||
|
// Mandatory tags
|
||||||
|
const tags: string[][] = [['d', d]]
|
||||||
|
|
||||||
|
// Append optional tags
|
||||||
|
name && tags.push(['name', name])
|
||||||
|
description && tags.push(['description', description])
|
||||||
|
image && tags.push(['image', ...image])
|
||||||
|
if (thumbs) {
|
||||||
|
for (const thumb of thumbs) {
|
||||||
|
tags.push(['thumb', ...thumb])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct the EventTemplate object
|
||||||
|
const eventTemplate: EventTemplate = {
|
||||||
|
content: '',
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
kind: BadgeDefinitionKind,
|
||||||
|
tags,
|
||||||
|
}
|
||||||
|
|
||||||
|
return eventTemplate
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates a badge definition event.
|
||||||
|
*
|
||||||
|
* @param event - The event to validate.
|
||||||
|
* @returns A boolean indicating whether the event is a valid badge definition event.
|
||||||
|
*/
|
||||||
|
export function validateBadgeDefinitionEvent(event: Event): boolean {
|
||||||
|
if (event.kind !== BadgeDefinitionKind) return false
|
||||||
|
|
||||||
|
const requiredTags = ['d'] as const
|
||||||
|
for (const tag of requiredTags) {
|
||||||
|
if (!event.tags.find(([t]) => t == tag)) return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates an EventTemplate based on the provided BadgeAward.
|
||||||
|
*
|
||||||
|
* @param {BadgeAward} badgeAward - The BadgeAward object.
|
||||||
|
* @returns {EventTemplate} - The generated EventTemplate object.
|
||||||
|
*/
|
||||||
|
export function generateBadgeAwardEventTemplate({ a, p }: BadgeAward): EventTemplate {
|
||||||
|
// Mandatory tags
|
||||||
|
const tags: string[][] = [['a', a]]
|
||||||
|
for (const _p of p) {
|
||||||
|
tags.push(['p', ..._p])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct the EventTemplate object
|
||||||
|
const eventTemplate: EventTemplate = {
|
||||||
|
content: '',
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
kind: BadgeAwardKind,
|
||||||
|
tags,
|
||||||
|
}
|
||||||
|
|
||||||
|
return eventTemplate
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates a badge award event.
|
||||||
|
*
|
||||||
|
* @param event - The event to validate.
|
||||||
|
* @returns A boolean indicating whether the event is a valid badge award event.
|
||||||
|
*/
|
||||||
|
export function validateBadgeAwardEvent(event: Event): boolean {
|
||||||
|
if (event.kind !== BadgeAwardKind) return false
|
||||||
|
|
||||||
|
const requiredTags = ['a', 'p'] as const
|
||||||
|
for (const tag of requiredTags) {
|
||||||
|
if (!event.tags.find(([t]) => t == tag)) return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates an EventTemplate based on the provided ProfileBadges.
|
||||||
|
*
|
||||||
|
* @param {ProfileBadges} profileBadges - The ProfileBadges object.
|
||||||
|
* @returns {EventTemplate} - The generated EventTemplate object.
|
||||||
|
*/
|
||||||
|
export function generateProfileBadgesEventTemplate({ badges }: ProfileBadges): EventTemplate {
|
||||||
|
// Mandatory tags
|
||||||
|
const tags: string[][] = [['d', 'profile_badges']]
|
||||||
|
|
||||||
|
// Append optional tags
|
||||||
|
for (const badge of badges) {
|
||||||
|
tags.push(['a', badge.a], ['e', ...badge.e])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct the EventTemplate object
|
||||||
|
const eventTemplate: EventTemplate = {
|
||||||
|
content: '',
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
kind: ProfileBadgesKind,
|
||||||
|
tags,
|
||||||
|
}
|
||||||
|
|
||||||
|
return eventTemplate
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates a profile badges event.
|
||||||
|
*
|
||||||
|
* @param event - The event to validate.
|
||||||
|
* @returns A boolean indicating whether the event is a valid profile badges event.
|
||||||
|
*/
|
||||||
|
export function validateProfileBadgesEvent(event: Event): boolean {
|
||||||
|
if (event.kind !== ProfileBadgesKind) return false
|
||||||
|
|
||||||
|
const requiredTags = ['d'] as const
|
||||||
|
for (const tag of requiredTags) {
|
||||||
|
if (!event.tags.find(([t]) => t == tag)) return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
4
nip75.ts
4
nip75.ts
@@ -1,5 +1,5 @@
|
|||||||
import { Event, EventTemplate } from './core'
|
import { Event, EventTemplate } from './core.ts'
|
||||||
import { ZapGoal } from './kinds'
|
import { ZapGoal } from './kinds.ts'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a fundraising goal in the Nostr network as defined by NIP-75.
|
* Represents a fundraising goal in the Nostr network as defined by NIP-75.
|
||||||
|
|||||||
2
nip94.ts
2
nip94.ts
@@ -1,4 +1,4 @@
|
|||||||
import { Event, EventTemplate } from './core'
|
import { Event, EventTemplate } from './core.ts'
|
||||||
import { FileMetadata as FileMetadataKind } from './kinds.ts'
|
import { FileMetadata as FileMetadataKind } from './kinds.ts'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
4
nip96.ts
4
nip96.ts
@@ -1,6 +1,6 @@
|
|||||||
import { sha256 } from '@noble/hashes/sha256'
|
import { sha256 } from '@noble/hashes/sha256'
|
||||||
import { EventTemplate } from './core'
|
import { EventTemplate } from './core.ts'
|
||||||
import { FileServerPreference } from './kinds'
|
import { FileServerPreference } from './kinds.ts'
|
||||||
import { bytesToHex } from '@noble/hashes/utils'
|
import { bytesToHex } from '@noble/hashes/utils'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { describe, expect, test } from 'bun:test'
|
import { describe, expect, test } from 'bun:test'
|
||||||
|
|
||||||
import { Event } from './core'
|
import { Event } from './core.ts'
|
||||||
import { ClassifiedListing, DraftClassifiedListing } from './kinds'
|
import { ClassifiedListing, DraftClassifiedListing } from './kinds.ts'
|
||||||
import { ClassifiedListingObject, generateEventTemplate, parseEvent, validateEvent } from './nip99'
|
import { ClassifiedListingObject, generateEventTemplate, parseEvent, validateEvent } from './nip99.ts'
|
||||||
import { finalizeEvent, generateSecretKey } from './pure'
|
import { finalizeEvent, generateSecretKey } from './pure.ts'
|
||||||
|
|
||||||
describe('validateEvent', () => {
|
describe('validateEvent', () => {
|
||||||
test('should return true for a valid classified listing event', () => {
|
test('should return true for a valid classified listing event', () => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"name": "nostr-tools",
|
"name": "nostr-tools",
|
||||||
"version": "2.3.1",
|
"version": "2.5.2",
|
||||||
"description": "Tools for making a Nostr client.",
|
"description": "Tools for making a Nostr client.",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -170,6 +170,11 @@
|
|||||||
"require": "./lib/cjs/nip57.js",
|
"require": "./lib/cjs/nip57.js",
|
||||||
"types": "./lib/types/nip57.d.ts"
|
"types": "./lib/types/nip57.d.ts"
|
||||||
},
|
},
|
||||||
|
"./nip58": {
|
||||||
|
"import": "./lib/esm/nip58.js",
|
||||||
|
"require": "./lib/cjs/nip58.js",
|
||||||
|
"types": "./lib/types/nip58.d.ts"
|
||||||
|
},
|
||||||
"./nip75": {
|
"./nip75": {
|
||||||
"import": "./lib/esm/nip75.js",
|
"import": "./lib/esm/nip75.js",
|
||||||
"require": "./lib/cjs/nip75.js",
|
"require": "./lib/cjs/nip75.js",
|
||||||
|
|||||||
81
pool.test.ts
81
pool.test.ts
@@ -4,6 +4,7 @@ import { SimplePool } from './pool.ts'
|
|||||||
import { finalizeEvent, generateSecretKey, getPublicKey, type Event } from './pure.ts'
|
import { finalizeEvent, generateSecretKey, getPublicKey, type Event } from './pure.ts'
|
||||||
import { useWebSocketImplementation } from './relay.ts'
|
import { useWebSocketImplementation } from './relay.ts'
|
||||||
import { MockRelay, MockWebSocketClient } from './test-helpers.ts'
|
import { MockRelay, MockWebSocketClient } from './test-helpers.ts'
|
||||||
|
import { hexToBytes } from '@noble/hashes/utils'
|
||||||
|
|
||||||
useWebSocketImplementation(MockWebSocketClient)
|
useWebSocketImplementation(MockWebSocketClient)
|
||||||
|
|
||||||
@@ -84,6 +85,86 @@ test('same with double subs', async () => {
|
|||||||
expect(received).toHaveLength(2)
|
expect(received).toHaveLength(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('subscribe many map', async () => {
|
||||||
|
let priv = hexToBytes('8ea002840d413ccdd5be98df5dd89d799eaa566355ede83ca0bbdbb4b145e0d3')
|
||||||
|
let pub = getPublicKey(priv)
|
||||||
|
|
||||||
|
let received: Event[] = []
|
||||||
|
let event1 = finalizeEvent(
|
||||||
|
{
|
||||||
|
created_at: Math.round(Date.now() / 1000),
|
||||||
|
content: 'test1',
|
||||||
|
kind: 20001,
|
||||||
|
tags: [],
|
||||||
|
},
|
||||||
|
priv,
|
||||||
|
)
|
||||||
|
let event2 = finalizeEvent(
|
||||||
|
{
|
||||||
|
created_at: Math.round(Date.now() / 1000),
|
||||||
|
content: 'test2',
|
||||||
|
kind: 20002,
|
||||||
|
tags: [['t', 'biloba']],
|
||||||
|
},
|
||||||
|
priv,
|
||||||
|
)
|
||||||
|
let event3 = finalizeEvent(
|
||||||
|
{
|
||||||
|
created_at: Math.round(Date.now() / 1000),
|
||||||
|
content: 'test3',
|
||||||
|
kind: 20003,
|
||||||
|
tags: [['t', 'biloba']],
|
||||||
|
},
|
||||||
|
priv,
|
||||||
|
)
|
||||||
|
|
||||||
|
const [relayA, relayB, relayC] = relayURLs
|
||||||
|
|
||||||
|
pool.subscribeManyMap(
|
||||||
|
{
|
||||||
|
[relayA]: [{ authors: [pub], kinds: [20001] }],
|
||||||
|
[relayB]: [{ authors: [pub], kinds: [20002] }],
|
||||||
|
[relayC]: [{ kinds: [20003], '#t': ['biloba'] }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onevent(event: Event) {
|
||||||
|
received.push(event)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// publish the first
|
||||||
|
await Promise.all(pool.publish([relayA, relayB], event1))
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100))
|
||||||
|
|
||||||
|
expect(received).toHaveLength(1)
|
||||||
|
expect(received[0]).toEqual(event1)
|
||||||
|
|
||||||
|
// publish the second
|
||||||
|
await pool.publish([relayB], event2)[0]
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100))
|
||||||
|
|
||||||
|
expect(received).toHaveLength(2)
|
||||||
|
expect(received[1]).toEqual(event2)
|
||||||
|
|
||||||
|
// publish a events that shouldn't match our filters
|
||||||
|
await Promise.all([
|
||||||
|
...pool.publish([relayA, relayB], event3),
|
||||||
|
...pool.publish([relayA, relayB, relayC], event1),
|
||||||
|
pool.publish([relayA, relayB, relayC], event2),
|
||||||
|
])
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100))
|
||||||
|
|
||||||
|
expect(received).toHaveLength(2)
|
||||||
|
|
||||||
|
// publsih the third
|
||||||
|
await pool.publish([relayC], event3)[0]
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100))
|
||||||
|
|
||||||
|
expect(received).toHaveLength(3)
|
||||||
|
expect(received[2]).toEqual(event3)
|
||||||
|
})
|
||||||
|
|
||||||
test('query a bunch of events and cancel on eose', async () => {
|
test('query a bunch of events and cancel on eose', async () => {
|
||||||
let events = new Set<string>()
|
let events = new Set<string>()
|
||||||
|
|
||||||
|
|||||||
293
pure.test.ts
Normal file
293
pure.test.ts
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
import { describe, test, expect } from 'bun:test'
|
||||||
|
|
||||||
|
import {
|
||||||
|
finalizeEvent,
|
||||||
|
serializeEvent,
|
||||||
|
getEventHash,
|
||||||
|
validateEvent,
|
||||||
|
verifyEvent,
|
||||||
|
verifiedSymbol,
|
||||||
|
getPublicKey,
|
||||||
|
generateSecretKey,
|
||||||
|
} from './pure.ts'
|
||||||
|
import { ShortTextNote } from './kinds.ts'
|
||||||
|
import { bytesToHex, hexToBytes } from '@noble/hashes/utils'
|
||||||
|
|
||||||
|
test('private key generation', () => {
|
||||||
|
expect(bytesToHex(generateSecretKey())).toMatch(/[a-f0-9]{64}/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('public key generation', () => {
|
||||||
|
expect(getPublicKey(generateSecretKey())).toMatch(/[a-f0-9]{64}/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('public key from private key deterministic', () => {
|
||||||
|
let sk = generateSecretKey()
|
||||||
|
let pk = getPublicKey(sk)
|
||||||
|
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
expect(getPublicKey(sk)).toEqual(pk)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('finalizeEvent', () => {
|
||||||
|
test('should create a signed event from a template', () => {
|
||||||
|
const privateKey = hexToBytes('d217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf')
|
||||||
|
const publicKey = getPublicKey(privateKey)
|
||||||
|
|
||||||
|
const template = {
|
||||||
|
kind: ShortTextNote,
|
||||||
|
tags: [],
|
||||||
|
content: 'Hello, world!',
|
||||||
|
created_at: 1617932115,
|
||||||
|
}
|
||||||
|
|
||||||
|
const event = finalizeEvent(template, privateKey)
|
||||||
|
|
||||||
|
expect(event.kind).toEqual(template.kind)
|
||||||
|
expect(event.tags).toEqual(template.tags)
|
||||||
|
expect(event.content).toEqual(template.content)
|
||||||
|
expect(event.created_at).toEqual(template.created_at)
|
||||||
|
expect(event.pubkey).toEqual(publicKey)
|
||||||
|
expect(typeof event.id).toEqual('string')
|
||||||
|
expect(typeof event.sig).toEqual('string')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('serializeEvent', () => {
|
||||||
|
test('should serialize a valid event object', () => {
|
||||||
|
const privateKey = hexToBytes('d217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf')
|
||||||
|
const publicKey = getPublicKey(privateKey)
|
||||||
|
|
||||||
|
const unsignedEvent = {
|
||||||
|
pubkey: publicKey,
|
||||||
|
created_at: 1617932115,
|
||||||
|
kind: ShortTextNote,
|
||||||
|
tags: [],
|
||||||
|
content: 'Hello, world!',
|
||||||
|
}
|
||||||
|
|
||||||
|
const serializedEvent = serializeEvent(unsignedEvent)
|
||||||
|
|
||||||
|
expect(serializedEvent).toEqual(
|
||||||
|
JSON.stringify([
|
||||||
|
0,
|
||||||
|
publicKey,
|
||||||
|
unsignedEvent.created_at,
|
||||||
|
unsignedEvent.kind,
|
||||||
|
unsignedEvent.tags,
|
||||||
|
unsignedEvent.content,
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should throw an error for an invalid event object', () => {
|
||||||
|
const privateKey = hexToBytes('d217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf')
|
||||||
|
const publicKey = getPublicKey(privateKey)
|
||||||
|
|
||||||
|
const invalidEvent = {
|
||||||
|
kind: ShortTextNote,
|
||||||
|
tags: [],
|
||||||
|
created_at: 1617932115,
|
||||||
|
pubkey: publicKey, // missing content
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
// @ts-expect-error
|
||||||
|
serializeEvent(invalidEvent)
|
||||||
|
}).toThrow("can't serialize event with wrong or missing properties")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('getEventHash', () => {
|
||||||
|
test('should return the correct event hash', () => {
|
||||||
|
const privateKey = hexToBytes('d217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf')
|
||||||
|
const publicKey = getPublicKey(privateKey)
|
||||||
|
|
||||||
|
const unsignedEvent = {
|
||||||
|
kind: ShortTextNote,
|
||||||
|
tags: [],
|
||||||
|
content: 'Hello, world!',
|
||||||
|
created_at: 1617932115,
|
||||||
|
pubkey: publicKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventHash = getEventHash(unsignedEvent)
|
||||||
|
|
||||||
|
expect(typeof eventHash).toEqual('string')
|
||||||
|
expect(eventHash.length).toEqual(64)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('validateEvent', () => {
|
||||||
|
test('should return true for a valid event object', () => {
|
||||||
|
const privateKey = hexToBytes('d217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf')
|
||||||
|
const publicKey = getPublicKey(privateKey)
|
||||||
|
|
||||||
|
const unsignedEvent = {
|
||||||
|
kind: ShortTextNote,
|
||||||
|
tags: [],
|
||||||
|
content: 'Hello, world!',
|
||||||
|
created_at: 1617932115,
|
||||||
|
pubkey: publicKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValid = validateEvent(unsignedEvent)
|
||||||
|
|
||||||
|
expect(isValid).toEqual(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should return false for a non object event', () => {
|
||||||
|
const nonObjectEvent = ''
|
||||||
|
const isValid = validateEvent(nonObjectEvent)
|
||||||
|
expect(isValid).toEqual(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should return false for an event object with missing properties', () => {
|
||||||
|
const invalidEvent = {
|
||||||
|
kind: ShortTextNote,
|
||||||
|
tags: [],
|
||||||
|
created_at: 1617932115, // missing content and pubkey
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValid = validateEvent(invalidEvent)
|
||||||
|
|
||||||
|
expect(isValid).toEqual(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should return false for an empty object', () => {
|
||||||
|
const emptyObj = {}
|
||||||
|
|
||||||
|
const isValid = validateEvent(emptyObj)
|
||||||
|
|
||||||
|
expect(isValid).toEqual(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should return false for an object with invalid properties', () => {
|
||||||
|
const privateKey = hexToBytes('d217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf')
|
||||||
|
const publicKey = getPublicKey(privateKey)
|
||||||
|
|
||||||
|
const invalidEvent = {
|
||||||
|
kind: 1,
|
||||||
|
tags: [],
|
||||||
|
created_at: '1617932115', // should be a number
|
||||||
|
pubkey: publicKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValid = validateEvent(invalidEvent)
|
||||||
|
|
||||||
|
expect(isValid).toEqual(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should return false for an object with an invalid public key', () => {
|
||||||
|
const invalidEvent = {
|
||||||
|
kind: 1,
|
||||||
|
tags: [],
|
||||||
|
content: 'Hello, world!',
|
||||||
|
created_at: 1617932115,
|
||||||
|
pubkey: 'invalid_pubkey',
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValid = validateEvent(invalidEvent)
|
||||||
|
|
||||||
|
expect(isValid).toEqual(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should return false for an object with invalid tags', () => {
|
||||||
|
const privateKey = hexToBytes('d217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf')
|
||||||
|
const publicKey = getPublicKey(privateKey)
|
||||||
|
|
||||||
|
const invalidEvent = {
|
||||||
|
kind: 1,
|
||||||
|
tags: {}, // should be an array
|
||||||
|
content: 'Hello, world!',
|
||||||
|
created_at: 1617932115,
|
||||||
|
pubkey: publicKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValid = validateEvent(invalidEvent)
|
||||||
|
|
||||||
|
expect(isValid).toEqual(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('verifyEvent', () => {
|
||||||
|
test('should return true for a valid event signature', () => {
|
||||||
|
const privateKey = hexToBytes('d217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf')
|
||||||
|
const event = finalizeEvent(
|
||||||
|
{
|
||||||
|
kind: ShortTextNote,
|
||||||
|
tags: [],
|
||||||
|
content: 'Hello, world!',
|
||||||
|
created_at: 1617932115,
|
||||||
|
},
|
||||||
|
privateKey,
|
||||||
|
)
|
||||||
|
|
||||||
|
const isValid = verifyEvent(event)
|
||||||
|
expect(isValid).toEqual(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should return false for an invalid event signature', () => {
|
||||||
|
const privateKey = hexToBytes('d217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf')
|
||||||
|
const { [verifiedSymbol]: _, ...event } = finalizeEvent(
|
||||||
|
{
|
||||||
|
kind: ShortTextNote,
|
||||||
|
tags: [],
|
||||||
|
content: 'Hello, world!',
|
||||||
|
created_at: 1617932115,
|
||||||
|
},
|
||||||
|
privateKey,
|
||||||
|
)
|
||||||
|
|
||||||
|
// tamper with the signature
|
||||||
|
event.sig = event.sig.replace(/^.{3}/g, '666')
|
||||||
|
|
||||||
|
const isValid = verifyEvent(event)
|
||||||
|
expect(isValid).toEqual(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should return false when verifying an event with a different private key', () => {
|
||||||
|
const privateKey1 = hexToBytes('d217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf')
|
||||||
|
|
||||||
|
const privateKey2 = hexToBytes('5b4a34f4e4b23c63ad55a35e3f84a3b53d96dbf266edf521a8358f71d19cbf67')
|
||||||
|
const publicKey2 = getPublicKey(privateKey2)
|
||||||
|
|
||||||
|
const { [verifiedSymbol]: _, ...event } = finalizeEvent(
|
||||||
|
{
|
||||||
|
kind: ShortTextNote,
|
||||||
|
tags: [],
|
||||||
|
content: 'Hello, world!',
|
||||||
|
created_at: 1617932115,
|
||||||
|
},
|
||||||
|
privateKey1,
|
||||||
|
)
|
||||||
|
|
||||||
|
// verify with different private key
|
||||||
|
const isValid = verifyEvent({
|
||||||
|
...event,
|
||||||
|
pubkey: publicKey2,
|
||||||
|
})
|
||||||
|
expect(isValid).toEqual(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should return false for an invalid event id', () => {
|
||||||
|
const privateKey = hexToBytes('d217c1ff2f8a65c3e3a1740db3b9f58b8c848bb45e26d00ed4714e4a0f4ceecf')
|
||||||
|
|
||||||
|
const { [verifiedSymbol]: _, ...event } = finalizeEvent(
|
||||||
|
{
|
||||||
|
kind: 1,
|
||||||
|
tags: [],
|
||||||
|
content: 'Hello, world!',
|
||||||
|
created_at: 1617932115,
|
||||||
|
},
|
||||||
|
privateKey,
|
||||||
|
)
|
||||||
|
|
||||||
|
// tamper with the id
|
||||||
|
event.id = event.id.replace(/^.{3}/g, '666')
|
||||||
|
|
||||||
|
const isValid = verifyEvent(event)
|
||||||
|
expect(isValid).toEqual(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -35,9 +35,9 @@ export class MockRelay {
|
|||||||
finalizeEvent(
|
finalizeEvent(
|
||||||
{
|
{
|
||||||
kind: 1,
|
kind: 1,
|
||||||
content: '',
|
content: 'autogenerated by relay',
|
||||||
created_at: Math.floor(Date.now() / 1000),
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
tags: [],
|
tags: [['t', 'auto']],
|
||||||
},
|
},
|
||||||
sk,
|
sk,
|
||||||
),
|
),
|
||||||
@@ -68,9 +68,9 @@ export class MockRelay {
|
|||||||
const event = finalizeEvent(
|
const event = finalizeEvent(
|
||||||
{
|
{
|
||||||
kind,
|
kind,
|
||||||
content: '',
|
content: 'kind-aware autogenerated by relay',
|
||||||
created_at: Math.floor(Date.now() / 1000),
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
tags: [],
|
tags: [['t', 'auto']],
|
||||||
},
|
},
|
||||||
sk,
|
sk,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "esnext",
|
"module": "NodeNext",
|
||||||
"target": "esnext",
|
"target": "esnext",
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "NodeNext",
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"emitDeclarationOnly": true,
|
"emitDeclarationOnly": true,
|
||||||
|
|||||||
4
wasm.ts
4
wasm.ts
@@ -1,6 +1,6 @@
|
|||||||
import { bytesToHex } from '@noble/hashes/utils'
|
import { bytesToHex } from '@noble/hashes/utils'
|
||||||
import { Nostr as NostrWasm } from 'nostr-wasm'
|
import { Nostr as NostrWasm } from 'nostr-wasm'
|
||||||
import { EventTemplate, Event, Nostr, VerifiedEvent, verifiedSymbol } from './core'
|
import { EventTemplate, Event, Nostr, VerifiedEvent, verifiedSymbol } from './core.ts'
|
||||||
|
|
||||||
let nw: NostrWasm
|
let nw: NostrWasm
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ class Wasm implements Nostr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const i = new Wasm()
|
const i: Wasm = new Wasm()
|
||||||
export const generateSecretKey = i.generateSecretKey
|
export const generateSecretKey = i.generateSecretKey
|
||||||
export const getPublicKey = i.getPublicKey
|
export const getPublicKey = i.getPublicKey
|
||||||
export const finalizeEvent = i.finalizeEvent
|
export const finalizeEvent = i.finalizeEvent
|
||||||
|
|||||||
Reference in New Issue
Block a user