mirror of
https://github.com/nbd-wtf/nostr-tools.git
synced 2025-12-09 16:48:50 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
058d0276e2 | ||
|
|
37b046c047 | ||
|
|
846654b449 | ||
|
|
b676dc0987 | ||
|
|
b1ce901555 | ||
|
|
62e5730965 | ||
|
|
01f13292bb | ||
|
|
7b0458db72 | ||
|
|
3aab7121f7 |
41
README.md
41
README.md
@@ -19,7 +19,7 @@ If using TypeScript, this package requires TypeScript >= 5.0.
|
|||||||
### Generating a private key and a public key
|
### Generating a private key and a public key
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import { generateSecretKey, getPublicKey } from 'nostr-tools'
|
import { generateSecretKey, getPublicKey } from 'nostr-tools/pure'
|
||||||
|
|
||||||
let sk = generateSecretKey() // `sk` is a Uint8Array
|
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
|
||||||
@@ -28,7 +28,7 @@ let pk = getPublicKey(sk) // `pk` is a hex string
|
|||||||
### Creating, signing and verifying events
|
### Creating, signing and verifying events
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import { finalizeEvent, verifyEvent } from 'nostr-tools'
|
import { finalizeEvent, verifyEvent } from 'nostr-tools/pure'
|
||||||
|
|
||||||
let event = finalizeEvent({
|
let event = finalizeEvent({
|
||||||
kind: 1,
|
kind: 1,
|
||||||
@@ -43,7 +43,8 @@ let isGood = verifyEvent(event)
|
|||||||
### Interacting with a relay
|
### Interacting with a relay
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import { Relay, finalizeEvent, generateSecretKey, getPublicKey } from 'nostr-tools'
|
import { finalizeEvent, generateSecretKey, getPublicKey } from 'nostr-tools/pure'
|
||||||
|
import { Relay } from 'nostr-tools/relay'
|
||||||
|
|
||||||
const relay = await Relay.connect('wss://relay.example.com')
|
const relay = await Relay.connect('wss://relay.example.com')
|
||||||
console.log(`connected to ${relay.url}`)
|
console.log(`connected to ${relay.url}`)
|
||||||
@@ -91,16 +92,17 @@ await relay.publish(signedEvent)
|
|||||||
relay.close()
|
relay.close()
|
||||||
```
|
```
|
||||||
|
|
||||||
To use this on Node.js you first must install `websocket-polyfill` and import it:
|
To use this on Node.js you first must install `ws` and call something like this:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import 'websocket-polyfill'
|
import { useWebSocketImplementation } from 'nostr-tools/relay'
|
||||||
|
useWebSocketImplementation(require('ws'))
|
||||||
```
|
```
|
||||||
|
|
||||||
### Interacting with multiple relays
|
### Interacting with multiple relays
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import { SimplePool } from 'nostr-tools'
|
import { SimplePool } from 'nostr-tools/pool'
|
||||||
|
|
||||||
const pool = new SimplePool()
|
const pool = new SimplePool()
|
||||||
|
|
||||||
@@ -136,7 +138,7 @@ let event = await pool.get(relays, {
|
|||||||
### Parsing references (mentions) from a content using NIP-10 and NIP-27
|
### Parsing references (mentions) from a content using NIP-10 and NIP-27
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import { parseReferences } from 'nostr-tools'
|
import { parseReferences } from 'nostr-tools/references'
|
||||||
|
|
||||||
let references = parseReferences(event)
|
let references = parseReferences(event)
|
||||||
let simpleAugmentedContent = event.content
|
let simpleAugmentedContent = event.content
|
||||||
@@ -156,9 +158,9 @@ for (let i = 0; i < references.length; i++) {
|
|||||||
### Querying profile data from a NIP-05 address
|
### Querying profile data from a NIP-05 address
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import { nip05 } from 'nostr-tools'
|
import { queryProfile } from 'nostr-tools/nip05'
|
||||||
|
|
||||||
let profile = await nip05.queryProfile('jb55.com')
|
let profile = await queryProfile('jb55.com')
|
||||||
console.log(profile.pubkey)
|
console.log(profile.pubkey)
|
||||||
// prints: 32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245
|
// prints: 32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245
|
||||||
console.log(profile.relays)
|
console.log(profile.relays)
|
||||||
@@ -168,13 +170,15 @@ console.log(profile.relays)
|
|||||||
To use this on Node.js < v18, you first must install `node-fetch@2` and call something like this:
|
To use this on Node.js < v18, you first must install `node-fetch@2` and call something like this:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
nip05.useFetchImplementation(require('node-fetch'))
|
import { useFetchImplementation } from 'nostr-tools/nip05'
|
||||||
|
useFetchImplementation(require('node-fetch'))
|
||||||
```
|
```
|
||||||
|
|
||||||
### Encoding and decoding NIP-19 codes
|
### Encoding and decoding NIP-19 codes
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import { nip19, generateSecretKey, getPublicKey } from 'nostr-tools'
|
import { generateSecretKey, getPublicKey } from 'nostr-tools/pure'
|
||||||
|
import * as nip19 from 'nostr-tools/nip19'
|
||||||
|
|
||||||
let sk = generateSecretKey()
|
let sk = generateSecretKey()
|
||||||
let nsec = nip19.nsecEncode(sk)
|
let nsec = nip19.nsecEncode(sk)
|
||||||
@@ -197,21 +201,6 @@ assert(data.pubkey === pk)
|
|||||||
assert(data.relays.length === 2)
|
assert(data.relays.length === 2)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Import modes
|
|
||||||
|
|
||||||
### Using just the packages you want
|
|
||||||
|
|
||||||
Importing the entirety of `nostr-tools` may bloat your build, so you should probably import individual packages instead:
|
|
||||||
|
|
||||||
```js
|
|
||||||
import { generateSecretKey, finalizeEvent, verifyEvent } from 'nostr-tools/pure'
|
|
||||||
import { SimplePool } from 'nostr-tools/pool'
|
|
||||||
import { Relay, Subscription } from 'nostr-tools/relay'
|
|
||||||
import { matchFilter } from 'nostr-tools/filter'
|
|
||||||
import { decode, nprofileEncode, neventEncode, npubEncode } from 'nostr-tools/nip19'
|
|
||||||
// and so on and so forth
|
|
||||||
```
|
|
||||||
|
|
||||||
### Using it with `nostr-wasm`
|
### Using it with `nostr-wasm`
|
||||||
|
|
||||||
[`nostr-wasm`](https://github.com/fiatjaf/nostr-wasm) is a thin wrapper over [libsecp256k1](https://github.com/bitcoin-core/secp256k1) compiled to WASM just for hashing, signing and verifying Nostr events.
|
[`nostr-wasm`](https://github.com/fiatjaf/nostr-wasm) is a thin wrapper over [libsecp256k1](https://github.com/bitcoin-core/secp256k1) compiled to WASM just for hashing, signing and verifying Nostr events.
|
||||||
|
|||||||
@@ -7,6 +7,16 @@ import { Queue, normalizeURL } from './utils.ts'
|
|||||||
import { makeAuthEvent } from './nip42.ts'
|
import { makeAuthEvent } from './nip42.ts'
|
||||||
import { yieldThread } from './helpers.ts'
|
import { yieldThread } from './helpers.ts'
|
||||||
|
|
||||||
|
var _WebSocket: typeof WebSocket
|
||||||
|
|
||||||
|
try {
|
||||||
|
_WebSocket = WebSocket
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
export function useWebSocketImplementation(websocketImplementation: any) {
|
||||||
|
_WebSocket = websocketImplementation
|
||||||
|
}
|
||||||
|
|
||||||
export class AbstractRelay {
|
export class AbstractRelay {
|
||||||
public readonly url: string
|
public readonly url: string
|
||||||
private _connected: boolean = false
|
private _connected: boolean = false
|
||||||
@@ -74,7 +84,7 @@ export class AbstractRelay {
|
|||||||
}, this.connectionTimeout)
|
}, this.connectionTimeout)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.ws = new WebSocket(this.url)
|
this.ws = new _WebSocket(this.url)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
reject(err)
|
reject(err)
|
||||||
return
|
return
|
||||||
|
|||||||
28
nip46.ts
28
nip46.ts
@@ -74,7 +74,6 @@ export type BunkerSignerParams = {
|
|||||||
export class BunkerSigner {
|
export class BunkerSigner {
|
||||||
private pool: AbstractSimplePool
|
private pool: AbstractSimplePool
|
||||||
private subCloser: SubCloser
|
private subCloser: SubCloser
|
||||||
private relays: string[]
|
|
||||||
private isOpen: boolean
|
private isOpen: boolean
|
||||||
private serial: number
|
private serial: number
|
||||||
private idPrefix: string
|
private idPrefix: string
|
||||||
@@ -85,8 +84,7 @@ export class BunkerSigner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
private secretKey: Uint8Array
|
private secretKey: Uint8Array
|
||||||
private connectionSecret: string
|
public bp: BunkerPointer
|
||||||
public remotePubkey: string
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new instance of the Nip46 class.
|
* Creates a new instance of the Nip46 class.
|
||||||
@@ -101,9 +99,7 @@ export class BunkerSigner {
|
|||||||
|
|
||||||
this.pool = params.pool || new SimplePool()
|
this.pool = params.pool || new SimplePool()
|
||||||
this.secretKey = clientSecretKey
|
this.secretKey = clientSecretKey
|
||||||
this.relays = bp.relays
|
this.bp = bp
|
||||||
this.remotePubkey = bp.pubkey
|
|
||||||
this.connectionSecret = bp.secret || ''
|
|
||||||
this.isOpen = false
|
this.isOpen = false
|
||||||
this.idPrefix = Math.random().toString(36).substring(7)
|
this.idPrefix = Math.random().toString(36).substring(7)
|
||||||
this.serial = 0
|
this.serial = 0
|
||||||
@@ -112,7 +108,7 @@ export class BunkerSigner {
|
|||||||
const listeners = this.listeners
|
const listeners = this.listeners
|
||||||
|
|
||||||
this.subCloser = this.pool.subscribeMany(
|
this.subCloser = this.pool.subscribeMany(
|
||||||
this.relays,
|
this.bp.relays,
|
||||||
[{ kinds: [NostrConnect, NostrConnectAdmin], '#p': [getPublicKey(this.secretKey)] }],
|
[{ kinds: [NostrConnect, NostrConnectAdmin], '#p': [getPublicKey(this.secretKey)] }],
|
||||||
{
|
{
|
||||||
async onevent(event: NostrEvent) {
|
async onevent(event: NostrEvent) {
|
||||||
@@ -154,17 +150,13 @@ export class BunkerSigner {
|
|||||||
this.serial++
|
this.serial++
|
||||||
const id = `${this.idPrefix}-${this.serial}`
|
const id = `${this.idPrefix}-${this.serial}`
|
||||||
|
|
||||||
const encryptedContent = await encrypt(
|
const encryptedContent = await encrypt(this.secretKey, this.bp.pubkey, JSON.stringify({ id, method, params }))
|
||||||
this.secretKey,
|
|
||||||
this.remotePubkey,
|
|
||||||
JSON.stringify({ id, method, params }),
|
|
||||||
)
|
|
||||||
|
|
||||||
// the request event
|
// the request event
|
||||||
const verifiedEvent: VerifiedEvent = finalizeEvent(
|
const verifiedEvent: VerifiedEvent = finalizeEvent(
|
||||||
{
|
{
|
||||||
kind: method === 'create_account' ? NostrConnectAdmin : NostrConnect,
|
kind: method === 'create_account' ? NostrConnectAdmin : NostrConnect,
|
||||||
tags: [['p', this.remotePubkey]],
|
tags: [['p', this.bp.pubkey]],
|
||||||
content: encryptedContent,
|
content: encryptedContent,
|
||||||
created_at: Math.floor(Date.now() / 1000),
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
},
|
},
|
||||||
@@ -175,7 +167,7 @@ export class BunkerSigner {
|
|||||||
this.listeners[id] = { resolve, reject }
|
this.listeners[id] = { resolve, reject }
|
||||||
|
|
||||||
// publish the event
|
// publish the event
|
||||||
await Promise.any(this.pool.publish(this.relays, verifiedEvent))
|
await Promise.any(this.pool.publish(this.bp.relays, verifiedEvent))
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
reject(err)
|
reject(err)
|
||||||
}
|
}
|
||||||
@@ -195,7 +187,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.connectionSecret])
|
await this.sendRequest('connect', [getPublicKey(this.secretKey), this.bp.secret || ''])
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -203,7 +195,7 @@ export class BunkerSigner {
|
|||||||
* but instead we just returns the public key we already know.
|
* but instead we just returns the public key we already know.
|
||||||
*/
|
*/
|
||||||
async getPublicKey(): Promise<string> {
|
async getPublicKey(): Promise<string> {
|
||||||
return this.remotePubkey
|
return this.bp.pubkey
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -221,7 +213,7 @@ export class BunkerSigner {
|
|||||||
async signEvent(event: UnsignedEvent): Promise<VerifiedEvent> {
|
async signEvent(event: UnsignedEvent): Promise<VerifiedEvent> {
|
||||||
let resp = await this.sendRequest('sign_event', [JSON.stringify(event)])
|
let resp = await this.sendRequest('sign_event', [JSON.stringify(event)])
|
||||||
let signed: NostrEvent = JSON.parse(resp)
|
let signed: NostrEvent = JSON.parse(resp)
|
||||||
if (signed.pubkey === this.remotePubkey && verifyEvent(signed)) {
|
if (signed.pubkey === this.bp.pubkey && verifyEvent(signed)) {
|
||||||
return signed
|
return signed
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`event returned from bunker is improperly signed: ${JSON.stringify(signed)}`)
|
throw new Error(`event returned from bunker is improperly signed: ${JSON.stringify(signed)}`)
|
||||||
@@ -275,7 +267,7 @@ export async function createAccount(
|
|||||||
|
|
||||||
// once we get the newly created pubkey back, we hijack this signer instance
|
// once we get the newly created pubkey back, we hijack this signer instance
|
||||||
// and turn it into the main instance for this newly created pubkey
|
// and turn it into the main instance for this newly created pubkey
|
||||||
rpc.remotePubkey = pubkey
|
rpc.bp.pubkey = pubkey
|
||||||
await rpc.connect()
|
await rpc.connect()
|
||||||
|
|
||||||
return rpc
|
return rpc
|
||||||
|
|||||||
@@ -79,10 +79,17 @@ const vectors: [string, string, number, 0x00 | 0x01 | 0x02, string][] = [
|
|||||||
'ncryptsec1qgqnx59n7duv6ec3hhrvn33q25u2qfd7m69vv6plsg7spnw6d4r9hq0ayjsnlw99eghqqzj8ps7vfwx40nqp9gpw7yzyy09jmwkq3a3z8q0ph5jahs2hap5k6h2wfrme7w2nuek4jnwpzfht4q3u79ra',
|
'ncryptsec1qgqnx59n7duv6ec3hhrvn33q25u2qfd7m69vv6plsg7spnw6d4r9hq0ayjsnlw99eghqqzj8ps7vfwx40nqp9gpw7yzyy09jmwkq3a3z8q0ph5jahs2hap5k6h2wfrme7w2nuek4jnwpzfht4q3u79ra',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'',
|
'ÅΩẛ̣',
|
||||||
'11b25a101667dd9208db93c0827c6bdad66729a5b521156a7e9d3b22b3ae8944',
|
'11b25a101667dd9208db93c0827c6bdad66729a5b521156a7e9d3b22b3ae8944',
|
||||||
9,
|
9,
|
||||||
0x01,
|
0x01,
|
||||||
'ncryptsec1qgylzyeunu2j85au05ae0g0sly03xu54tgnjemr6g9w0eqwuuczya7k0f4juqve64vzsrlxqxmcekzrpvg2a8qu4q6wtjxe0dvy3xkjh5smmz4uy59jg0jay9vnf28e3rc6jq2kd26h6g3fejyw6cype',
|
'ncryptsec1qgy5kwr5v8p206vwaflp4g6r083kwts6q5sh8m4d0q56edpxwhrly78ema2z7jpdeldsz7u5wpxpyhs6m0405skdsep9n37uncw7xlc8q8meyw6d6ky47vcl0guhqpt5dx8ejxc8hvzf6y2gwsl5s0nw',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'ÅΩṩ',
|
||||||
|
'11b25a101667dd9208db93c0827c6bdad66729a5b521156a7e9d3b22b3ae8944',
|
||||||
|
9,
|
||||||
|
0x01,
|
||||||
|
'ncryptsec1qgy5f4lcx873yarkfpngaudarxfj4wj939xn4azmd66j6jrwcml6av87d6vnelzn70kszgkg4lj9rsdjlqz0wn7m7456sr2q5yjpy72ykgkdwckevl857hpcfnwzswj9lajxtln0tsr9h7xdwqm6pqzf',
|
||||||
],
|
],
|
||||||
]
|
]
|
||||||
|
|||||||
4
nip49.ts
4
nip49.ts
@@ -7,7 +7,7 @@ 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 {
|
||||||
let salt = randomBytes(16)
|
let salt = randomBytes(16)
|
||||||
let n = 2 ** logn
|
let n = 2 ** logn
|
||||||
let key = scrypt(password, salt, { N: n, r: 8, p: 1, dkLen: 32 })
|
let key = scrypt(password.normalize('NFKC'), salt, { N: n, r: 8, p: 1, dkLen: 32 })
|
||||||
let nonce = randomBytes(24)
|
let nonce = randomBytes(24)
|
||||||
let aad = Uint8Array.from([ksb])
|
let aad = Uint8Array.from([ksb])
|
||||||
let xc2p1 = xchacha20poly1305(key, nonce, aad)
|
let xc2p1 = xchacha20poly1305(key, nonce, aad)
|
||||||
@@ -37,7 +37,7 @@ export function decrypt(ncryptsec: string, password: string): Uint8Array {
|
|||||||
let aad = Uint8Array.from([ksb])
|
let aad = Uint8Array.from([ksb])
|
||||||
let ciphertext = b.slice(2 + 16 + 24 + 1)
|
let ciphertext = b.slice(2 + 16 + 24 + 1)
|
||||||
|
|
||||||
let key = scrypt(password, salt, { N: n, r: 8, p: 1, dkLen: 32 })
|
let key = scrypt(password.normalize('NFKC'), salt, { N: n, r: 8, p: 1, dkLen: 32 })
|
||||||
let xc2p1 = xchacha20poly1305(key, nonce, aad)
|
let xc2p1 = xchacha20poly1305(key, nonce, aad)
|
||||||
let sec = xc2p1.decrypt(ciphertext)
|
let sec = xc2p1.decrypt(ciphertext)
|
||||||
|
|
||||||
|
|||||||
203
nip75.test.ts
Normal file
203
nip75.test.ts
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
import { describe, expect, it } from 'bun:test'
|
||||||
|
|
||||||
|
import { ZapGoal } from './kinds.ts'
|
||||||
|
import { Goal, generateGoalEventTemplate, validateZapGoalEvent } from './nip75.ts'
|
||||||
|
import { finalizeEvent, generateSecretKey } from './pure.ts'
|
||||||
|
|
||||||
|
describe('Goal Type', () => {
|
||||||
|
it('should create a proper Goal object', () => {
|
||||||
|
const goal: Goal = {
|
||||||
|
content: 'Fundraising for a new project',
|
||||||
|
amount: '100000000',
|
||||||
|
relays: ['wss://relay1.example.com', 'wss://relay2.example.com'],
|
||||||
|
closedAt: 1671150419,
|
||||||
|
image: 'https://example.com/goal-image.jpg',
|
||||||
|
summary: 'Help us reach our fundraising goal!',
|
||||||
|
r: 'https://example.com/additional-info',
|
||||||
|
a: 'fef2a50f7d9d3d5a5f38ee761bc087ec16198d3f0140df6d1e8193abf7c2b146',
|
||||||
|
zapTags: [
|
||||||
|
['zap', 'beneficiary1'],
|
||||||
|
['zap', 'beneficiary2'],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(goal.content).toBe('Fundraising for a new project')
|
||||||
|
expect(goal.amount).toBe('100000000')
|
||||||
|
expect(goal.relays).toEqual(['wss://relay1.example.com', 'wss://relay2.example.com'])
|
||||||
|
expect(goal.closedAt).toBe(1671150419)
|
||||||
|
expect(goal.image).toBe('https://example.com/goal-image.jpg')
|
||||||
|
expect(goal.summary).toBe('Help us reach our fundraising goal!')
|
||||||
|
expect(goal.r).toBe('https://example.com/additional-info')
|
||||||
|
expect(goal.a).toBe('fef2a50f7d9d3d5a5f38ee761bc087ec16198d3f0140df6d1e8193abf7c2b146')
|
||||||
|
expect(goal.zapTags).toEqual([
|
||||||
|
['zap', 'beneficiary1'],
|
||||||
|
['zap', 'beneficiary2'],
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('generateGoalEventTemplate', () => {
|
||||||
|
it('should generate an EventTemplate for a fundraising goal', () => {
|
||||||
|
const goal: Goal = {
|
||||||
|
content: 'Fundraising for a new project',
|
||||||
|
amount: '100000000',
|
||||||
|
relays: ['wss://relay1.example.com', 'wss://relay2.example.com'],
|
||||||
|
closedAt: 1671150419,
|
||||||
|
image: 'https://example.com/goal-image.jpg',
|
||||||
|
summary: 'Help us reach our fundraising goal!',
|
||||||
|
r: 'https://example.com/additional-info',
|
||||||
|
zapTags: [
|
||||||
|
['zap', 'beneficiary1'],
|
||||||
|
['zap', 'beneficiary2'],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventTemplate = generateGoalEventTemplate(goal)
|
||||||
|
|
||||||
|
expect(eventTemplate.kind).toBe(ZapGoal)
|
||||||
|
expect(eventTemplate.content).toBe('Fundraising for a new project')
|
||||||
|
expect(eventTemplate.tags).toEqual([
|
||||||
|
['amount', '100000000'],
|
||||||
|
['relays', 'wss://relay1.example.com', 'wss://relay2.example.com'],
|
||||||
|
['closed_at', '1671150419'],
|
||||||
|
['image', 'https://example.com/goal-image.jpg'],
|
||||||
|
['summary', 'Help us reach our fundraising goal!'],
|
||||||
|
['r', 'https://example.com/additional-info'],
|
||||||
|
['zap', 'beneficiary1'],
|
||||||
|
['zap', 'beneficiary2'],
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should generate an EventTemplate for a fundraising goal without optional properties', () => {
|
||||||
|
const goal: Goal = {
|
||||||
|
content: 'Fundraising for a new project',
|
||||||
|
amount: '100000000',
|
||||||
|
relays: ['wss://relay1.example.com', 'wss://relay2.example.com'],
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventTemplate = generateGoalEventTemplate(goal)
|
||||||
|
|
||||||
|
expect(eventTemplate.kind).toBe(ZapGoal)
|
||||||
|
expect(eventTemplate.content).toBe('Fundraising for a new project')
|
||||||
|
expect(eventTemplate.tags).toEqual([
|
||||||
|
['amount', '100000000'],
|
||||||
|
['relays', 'wss://relay1.example.com', 'wss://relay2.example.com'],
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should generate an EventTemplate that is valid', () => {
|
||||||
|
const sk = generateSecretKey()
|
||||||
|
const goal: Goal = {
|
||||||
|
content: 'Fundraising for a new project',
|
||||||
|
amount: '100000000',
|
||||||
|
relays: ['wss://relay1.example.com', 'wss://relay2.example.com'],
|
||||||
|
closedAt: 1671150419,
|
||||||
|
image: 'https://example.com/goal-image.jpg',
|
||||||
|
summary: 'Help us reach our fundraising goal!',
|
||||||
|
r: 'https://example.com/additional-info',
|
||||||
|
zapTags: [
|
||||||
|
['zap', 'beneficiary1'],
|
||||||
|
['zap', 'beneficiary2'],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
const eventTemplate = generateGoalEventTemplate(goal)
|
||||||
|
const event = finalizeEvent(eventTemplate, sk)
|
||||||
|
const isValid = validateZapGoalEvent(event)
|
||||||
|
|
||||||
|
expect(isValid).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('validateZapGoalEvent', () => {
|
||||||
|
it('should validate a proper Goal event', () => {
|
||||||
|
const sk = generateSecretKey()
|
||||||
|
const eventTemplate = {
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
kind: ZapGoal,
|
||||||
|
content: 'Fundraising for a new project',
|
||||||
|
tags: [
|
||||||
|
['amount', '100000000'],
|
||||||
|
['relays', 'wss://relay1.example.com', 'wss://relay2.example.com'],
|
||||||
|
['closed_at', '1671150419'],
|
||||||
|
['image', 'https://example.com/goal-image.jpg'],
|
||||||
|
['summary', 'Help us reach our fundraising goal!'],
|
||||||
|
['r', 'https://example.com/additional-info'],
|
||||||
|
['zap', 'beneficiary1'],
|
||||||
|
['zap', 'beneficiary2'],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
const event = finalizeEvent(eventTemplate, sk)
|
||||||
|
const isValid = validateZapGoalEvent(event)
|
||||||
|
|
||||||
|
expect(isValid).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not validate an event with an incorrect kind', () => {
|
||||||
|
const sk = generateSecretKey()
|
||||||
|
const eventTemplate = {
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
kind: 0, // Incorrect kind
|
||||||
|
content: 'Fundraising for a new project',
|
||||||
|
tags: [
|
||||||
|
['amount', '100000000'],
|
||||||
|
['relays', 'wss://relay1.example.com', 'wss://relay2.example.com'],
|
||||||
|
['closed_at', '1671150419'],
|
||||||
|
['image', 'https://example.com/goal-image.jpg'],
|
||||||
|
['summary', 'Help us reach our fundraising goal!'],
|
||||||
|
['r', 'https://example.com/additional-info'],
|
||||||
|
['zap', 'beneficiary1'],
|
||||||
|
['zap', 'beneficiary2'],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
const event = finalizeEvent(eventTemplate, sk)
|
||||||
|
const isValid = validateZapGoalEvent(event)
|
||||||
|
|
||||||
|
expect(isValid).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not validate an event with missing required "amount" tag', () => {
|
||||||
|
const sk = generateSecretKey()
|
||||||
|
const eventTemplate = {
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
kind: ZapGoal,
|
||||||
|
content: 'Fundraising for a new project',
|
||||||
|
tags: [
|
||||||
|
// Missing "amount" tag
|
||||||
|
['relays', 'wss://relay1.example.com', 'wss://relay2.example.com'],
|
||||||
|
['closed_at', '1671150419'],
|
||||||
|
['image', 'https://example.com/goal-image.jpg'],
|
||||||
|
['summary', 'Help us reach our fundraising goal!'],
|
||||||
|
['r', 'https://example.com/additional-info'],
|
||||||
|
['zap', 'beneficiary1'],
|
||||||
|
['zap', 'beneficiary2'],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
const event = finalizeEvent(eventTemplate, sk)
|
||||||
|
const isValid = validateZapGoalEvent(event)
|
||||||
|
|
||||||
|
expect(isValid).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not validate an event with missing required "relays" tag', () => {
|
||||||
|
const sk = generateSecretKey()
|
||||||
|
const eventTemplate = {
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
kind: ZapGoal,
|
||||||
|
content: 'Fundraising for a new project',
|
||||||
|
tags: [
|
||||||
|
['amount', '100000000'],
|
||||||
|
// Missing "relays" tag
|
||||||
|
['closed_at', '1671150419'],
|
||||||
|
['image', 'https://example.com/goal-image.jpg'],
|
||||||
|
['summary', 'Help us reach our fundraising goal!'],
|
||||||
|
['r', 'https://example.com/additional-info'],
|
||||||
|
['zap', 'beneficiary1'],
|
||||||
|
['zap', 'beneficiary2'],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
const event = finalizeEvent(eventTemplate, sk)
|
||||||
|
const isValid = validateZapGoalEvent(event)
|
||||||
|
|
||||||
|
expect(isValid).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
115
nip75.ts
Normal file
115
nip75.ts
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import { Event, EventTemplate } from './core'
|
||||||
|
import { ZapGoal } from './kinds'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a fundraising goal in the Nostr network as defined by NIP-75.
|
||||||
|
* This type is used to structure the information needed to create a goal event (`kind:9041`).
|
||||||
|
*/
|
||||||
|
export type Goal = {
|
||||||
|
/**
|
||||||
|
* A human-readable description of the fundraising goal.
|
||||||
|
* This content should provide clear information about the purpose of the fundraising.
|
||||||
|
*/
|
||||||
|
content: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The target amount for the fundraising goal in milisats.
|
||||||
|
* This defines the financial target that the fundraiser aims to reach.
|
||||||
|
*/
|
||||||
|
amount: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list of relays where the zaps towards this goal will be sent to and tallied from.
|
||||||
|
* Each relay is represented by its WebSocket URL.
|
||||||
|
*/
|
||||||
|
relays: string[]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An optional timestamp (in seconds, UNIX epoch) indicating when the fundraising goal is considered closed.
|
||||||
|
* Zaps published after this timestamp should not count towards the goal progress.
|
||||||
|
* If not provided, the goal remains open indefinitely or until manually closed.
|
||||||
|
*/
|
||||||
|
closedAt?: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An optional URL to an image related to the goal.
|
||||||
|
* This can be used to visually represent the goal on client interfaces.
|
||||||
|
*/
|
||||||
|
image?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An optional brief description or summary of the goal.
|
||||||
|
* This can provide a quick overview of the goal, separate from the detailed `content`.
|
||||||
|
*/
|
||||||
|
summary?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An optional URL related to the goal, providing additional information or actions through an 'r' tag.
|
||||||
|
* This is a single URL, as per NIP-75 specifications for linking additional resources.
|
||||||
|
*/
|
||||||
|
r?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An optional parameterized replaceable event linked to the goal, specified through an 'a' tag.
|
||||||
|
* This is a single event id, aligning with NIP-75's allowance for linking to specific events.
|
||||||
|
*/
|
||||||
|
a?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional tags specifying multiple beneficiary pubkeys or additional criteria for zapping,
|
||||||
|
* allowing contributions to be directed towards multiple recipients or according to specific conditions.
|
||||||
|
*/
|
||||||
|
zapTags?: string[][]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates an EventTemplate for a fundraising goal based on the provided ZapGoal object.
|
||||||
|
* This function is tailored to fit the structure of EventTemplate as defined in the library.
|
||||||
|
* @param zapGoal The ZapGoal object containing the details of the fundraising goal.
|
||||||
|
* @returns An EventTemplate object structured for creating a Nostr event.
|
||||||
|
*/
|
||||||
|
export function generateGoalEventTemplate({
|
||||||
|
amount,
|
||||||
|
content,
|
||||||
|
relays,
|
||||||
|
a,
|
||||||
|
closedAt,
|
||||||
|
image,
|
||||||
|
r,
|
||||||
|
summary,
|
||||||
|
zapTags,
|
||||||
|
}: Goal): EventTemplate {
|
||||||
|
const tags: string[][] = [
|
||||||
|
['amount', amount],
|
||||||
|
['relays', ...relays],
|
||||||
|
]
|
||||||
|
|
||||||
|
// Append optional tags based on the presence of optional properties in zapGoal
|
||||||
|
closedAt && tags.push(['closed_at', closedAt.toString()])
|
||||||
|
image && tags.push(['image', image])
|
||||||
|
summary && tags.push(['summary', summary])
|
||||||
|
r && tags.push(['r', r])
|
||||||
|
a && tags.push(['a', a])
|
||||||
|
zapTags && tags.push(...zapTags)
|
||||||
|
|
||||||
|
// Construct the EventTemplate object
|
||||||
|
const eventTemplate: EventTemplate = {
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
kind: ZapGoal,
|
||||||
|
content,
|
||||||
|
tags,
|
||||||
|
}
|
||||||
|
|
||||||
|
return eventTemplate
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateZapGoalEvent(event: Event): boolean {
|
||||||
|
if (event.kind !== ZapGoal) return false
|
||||||
|
|
||||||
|
const requiredTags = ['amount', 'relays'] as const
|
||||||
|
for (const tag of requiredTags) {
|
||||||
|
if (!event.tags.find(([t]) => t == tag)) return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"name": "nostr-tools",
|
"name": "nostr-tools",
|
||||||
"version": "2.1.8",
|
"version": "2.2.0",
|
||||||
"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"
|
||||||
},
|
},
|
||||||
|
"./nip75": {
|
||||||
|
"import": "./lib/esm/nip75.js",
|
||||||
|
"require": "./lib/cjs/nip75.js",
|
||||||
|
"types": "./lib/types/nip75.d.ts"
|
||||||
|
},
|
||||||
"./nip94": {
|
"./nip94": {
|
||||||
"import": "./lib/esm/nip94.js",
|
"import": "./lib/esm/nip94.js",
|
||||||
"require": "./lib/cjs/nip94.js",
|
"require": "./lib/cjs/nip94.js",
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ import { afterEach, beforeEach, expect, test } from 'bun:test'
|
|||||||
|
|
||||||
import { SimplePool } from './pool.ts'
|
import { SimplePool } from './pool.ts'
|
||||||
import { finalizeEvent, generateSecretKey, getPublicKey, type Event } from './pure.ts'
|
import { finalizeEvent, generateSecretKey, getPublicKey, type Event } from './pure.ts'
|
||||||
import { MockRelay } from './test-helpers.ts'
|
import { useWebSocketImplementation } from './relay.ts'
|
||||||
|
import { MockRelay, MockWebSocketClient } from './test-helpers.ts'
|
||||||
|
|
||||||
|
useWebSocketImplementation(MockWebSocketClient)
|
||||||
|
|
||||||
let pool: SimplePool
|
let pool: SimplePool
|
||||||
let mockRelays: MockRelay[]
|
let mockRelays: MockRelay[]
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { expect, test } from 'bun:test'
|
import { expect, test } from 'bun:test'
|
||||||
|
|
||||||
import { finalizeEvent, generateSecretKey, getPublicKey } from './pure.ts'
|
import { finalizeEvent, generateSecretKey, getPublicKey } from './pure.ts'
|
||||||
import { Relay } from './relay.ts'
|
import { Relay, useWebSocketImplementation } from './relay.ts'
|
||||||
import { MockRelay } from './test-helpers.ts'
|
import { MockRelay, MockWebSocketClient } from './test-helpers.ts'
|
||||||
|
|
||||||
|
useWebSocketImplementation(MockWebSocketClient)
|
||||||
|
|
||||||
test('connectivity', async () => {
|
test('connectivity', async () => {
|
||||||
const mockRelay = new MockRelay()
|
const mockRelay = new MockRelay()
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { Server } from 'mock-socket'
|
import { Server, WebSocket } from 'mock-socket'
|
||||||
|
|
||||||
import { finalizeEvent, type Event, getPublicKey, generateSecretKey } from './pure.ts'
|
import { finalizeEvent, type Event, getPublicKey, generateSecretKey } from './pure.ts'
|
||||||
import { matchFilters, type Filter } from './filter.ts'
|
import { matchFilters, type Filter } from './filter.ts'
|
||||||
|
|
||||||
|
export const MockWebSocketClient = WebSocket
|
||||||
|
|
||||||
export function buildEvent(params: Partial<Event>): Event {
|
export function buildEvent(params: Partial<Event>): Event {
|
||||||
return {
|
return {
|
||||||
id: '',
|
id: '',
|
||||||
|
|||||||
Reference in New Issue
Block a user