Compare commits

...

15 Commits

Author SHA1 Message Date
fiatjaf
db6baf2e6b bump to v2.2.1 2024-02-16 07:43:38 -03:00
fiatjaf
bb1e6f4356 nip46: only handle the first auth_url for every command. 2024-02-16 07:43:20 -03:00
fiatjaf
5626d3048b nip46: remove NostrConnectAdmin wrong kind. 2024-02-16 07:40:21 -03:00
fiatjaf
058d0276e2 nip49: nfkc normalization. 2024-02-16 00:13:58 -03:00
Sepehr Safari
37b046c047 bump to v2.2.0 2024-02-14 19:48:07 -03:00
Sepehr Safari
846654b449 add exports/nip75 to package.json 2024-02-14 19:48:07 -03:00
Sepehr Safari
b676dc0987 add tests for nip75 2024-02-14 19:48:07 -03:00
Sepehr Safari
b1ce901555 implement nip75 handlers 2024-02-14 19:48:07 -03:00
fiatjaf
62e5730965 call useWebSocketImplementation() on relay and pool tests. 2024-02-14 13:26:38 -03:00
fiatjaf
01f13292bb useWebSocketImplementation() on relay.ts 2024-02-14 13:19:48 -03:00
fiatjaf
7b0458db72 make the examples on the readme use the new import method. 2024-02-14 13:17:59 -03:00
fiatjaf
3aab7121f7 use a public BunkerPointer property on BunkerSigner class. 2024-02-14 12:29:47 -03:00
fiatjaf
ce059d4608 bump to v2.1.8 2024-02-14 12:25:22 -03:00
fiatjaf
b72b0dc1f0 nip46: fix typo in fetchCustodialBunkers() function name. 2024-02-14 12:24:47 -03:00
fiatjaf
29e5b71473 nip46: implement the remaining methods. 2024-02-14 12:24:33 -03:00
12 changed files with 432 additions and 68 deletions

View File

@@ -19,7 +19,7 @@ If using TypeScript, this package requires TypeScript >= 5.0.
### Generating a private key and a public key
```js
import { generateSecretKey, getPublicKey } from 'nostr-tools'
import { generateSecretKey, getPublicKey } from 'nostr-tools/pure'
let sk = generateSecretKey() // `sk` is a Uint8Array
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
```js
import { finalizeEvent, verifyEvent } from 'nostr-tools'
import { finalizeEvent, verifyEvent } from 'nostr-tools/pure'
let event = finalizeEvent({
kind: 1,
@@ -43,7 +43,8 @@ let isGood = verifyEvent(event)
### Interacting with a relay
```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')
console.log(`connected to ${relay.url}`)
@@ -91,16 +92,17 @@ await relay.publish(signedEvent)
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
import 'websocket-polyfill'
import { useWebSocketImplementation } from 'nostr-tools/relay'
useWebSocketImplementation(require('ws'))
```
### Interacting with multiple relays
```js
import { SimplePool } from 'nostr-tools'
import { SimplePool } from 'nostr-tools/pool'
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
```js
import { parseReferences } from 'nostr-tools'
import { parseReferences } from 'nostr-tools/references'
let references = parseReferences(event)
let simpleAugmentedContent = event.content
@@ -156,9 +158,9 @@ for (let i = 0; i < references.length; i++) {
### Querying profile data from a NIP-05 address
```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)
// prints: 32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245
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:
```js
nip05.useFetchImplementation(require('node-fetch'))
import { useFetchImplementation } from 'nostr-tools/nip05'
useFetchImplementation(require('node-fetch'))
```
### Encoding and decoding NIP-19 codes
```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 nsec = nip19.nsecEncode(sk)
@@ -197,21 +201,6 @@ assert(data.pubkey === pk)
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`
[`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.

View File

@@ -7,6 +7,16 @@ import { Queue, normalizeURL } from './utils.ts'
import { makeAuthEvent } from './nip42.ts'
import { yieldThread } from './helpers.ts'
var _WebSocket: typeof WebSocket
try {
_WebSocket = WebSocket
} catch {}
export function useWebSocketImplementation(websocketImplementation: any) {
_WebSocket = websocketImplementation
}
export class AbstractRelay {
public readonly url: string
private _connected: boolean = false
@@ -74,7 +84,7 @@ export class AbstractRelay {
}, this.connectionTimeout)
try {
this.ws = new WebSocket(this.url)
this.ws = new _WebSocket(this.url)
} catch (err) {
reject(err)
return

View File

@@ -78,7 +78,6 @@ export const ClientAuth = 22242
export const NWCWalletRequest = 23194
export const NWCWalletResponse = 23195
export const NostrConnect = 24133
export const NostrConnectAdmin = 24134
export const HTTPAuth = 27235
export const Followsets = 30000
export const Genericlists = 30001

View File

@@ -4,7 +4,8 @@ import { AbstractSimplePool, SubCloser } from './abstract-pool.ts'
import { decrypt, encrypt } from './nip04.ts'
import { NIP05_REGEX } from './nip05.ts'
import { SimplePool } from './pool.ts'
import { Handlerinformation, NostrConnect, NostrConnectAdmin } from './kinds.ts'
import { Handlerinformation, NostrConnect } from './kinds.ts'
import { hexToBytes } from '@noble/hashes/utils'
var _fetch: any
@@ -73,7 +74,6 @@ export type BunkerSignerParams = {
export class BunkerSigner {
private pool: AbstractSimplePool
private subCloser: SubCloser
private relays: string[]
private isOpen: boolean
private serial: number
private idPrefix: string
@@ -83,9 +83,9 @@ export class BunkerSigner {
reject: (_: string) => void
}
}
private waitingForAuth: { [id: string]: boolean }
private secretKey: Uint8Array
private connectionSecret: string
public remotePubkey: string
public bp: BunkerPointer
/**
* Creates a new instance of the Nip46 class.
@@ -100,24 +100,26 @@ export class BunkerSigner {
this.pool = params.pool || new SimplePool()
this.secretKey = clientSecretKey
this.relays = bp.relays
this.remotePubkey = bp.pubkey
this.connectionSecret = bp.secret || ''
this.bp = bp
this.isOpen = false
this.idPrefix = Math.random().toString(36).substring(7)
this.serial = 0
this.listeners = {}
this.waitingForAuth = {}
const listeners = this.listeners
const waitingForAuth = this.waitingForAuth
this.subCloser = this.pool.subscribeMany(
this.relays,
[{ kinds: [NostrConnect, NostrConnectAdmin], '#p': [getPublicKey(this.secretKey)] }],
this.bp.relays,
[{ kinds: [NostrConnect], '#p': [getPublicKey(this.secretKey)] }],
{
async onevent(event: NostrEvent) {
const { id, result, error } = JSON.parse(await decrypt(clientSecretKey, event.pubkey, event.content))
if (result === 'auth_url') {
if (result === 'auth_url' && waitingForAuth[id]) {
delete listeners[id]
if (params.onauth) {
params.onauth(error)
} else {
@@ -153,17 +155,13 @@ export class BunkerSigner {
this.serial++
const id = `${this.idPrefix}-${this.serial}`
const encryptedContent = await encrypt(
this.secretKey,
this.remotePubkey,
JSON.stringify({ id, method, params }),
)
const encryptedContent = await encrypt(this.secretKey, this.bp.pubkey, JSON.stringify({ id, method, params }))
// the request event
const verifiedEvent: VerifiedEvent = finalizeEvent(
{
kind: method === 'create_account' ? NostrConnectAdmin : NostrConnect,
tags: [['p', this.remotePubkey]],
kind: NostrConnect,
tags: [['p', this.bp.pubkey]],
content: encryptedContent,
created_at: Math.floor(Date.now() / 1000),
},
@@ -172,9 +170,10 @@ export class BunkerSigner {
// setup callback listener
this.listeners[id] = { resolve, reject }
this.waitingForAuth[id] = true
// publish the event
await Promise.any(this.pool.publish(this.relays, verifiedEvent))
await Promise.any(this.pool.publish(this.bp.relays, verifiedEvent))
} catch (err) {
reject(err)
}
@@ -182,9 +181,8 @@ export class BunkerSigner {
}
/**
* Sends a ping request to the remote server.
* Requires permission/access rights to bunker.
* @returns "Pong" if successful. The promise will reject if the response is not "pong".
* Calls the "connect" method on the bunker.
* The promise will be rejected if the response is not "pong".
*/
async ping(): Promise<void> {
let resp = await this.sendRequest('ping', [])
@@ -192,15 +190,25 @@ export class BunkerSigner {
}
/**
* Connects to a remote server using the provided keys and remote public key.
* Optionally, a secret can be provided for additional authentication.
*
* @param remotePubkey - Optional the remote public key to connect to.
* @param secret - Optional secret for additional authentication.
* @returns "ack" if successful. The promise will reject if the response is not "ack".
* Calls the "connect" method on the bunker.
*/
async connect(): Promise<void> {
await this.sendRequest('connect', [getPublicKey(this.secretKey), this.connectionSecret])
await this.sendRequest('connect', [getPublicKey(this.secretKey), this.bp.secret || ''])
}
/**
* This was supposed to call the "get_public_key" method on the bunker,
* but instead we just returns the public key we already know.
*/
async getPublicKey(): Promise<string> {
return this.bp.pubkey
}
/**
* Calls the "get_relays" method on the bunker.
*/
async getRelays(): Promise<{ [relay: string]: { read: boolean; write: boolean } }> {
return JSON.parse(await this.sendRequest('get_relays', []))
}
/**
@@ -211,12 +219,33 @@ export class BunkerSigner {
async signEvent(event: UnsignedEvent): Promise<VerifiedEvent> {
let resp = await this.sendRequest('sign_event', [JSON.stringify(event)])
let signed: NostrEvent = JSON.parse(resp)
if (signed.pubkey === this.remotePubkey && verifyEvent(signed)) {
if (signed.pubkey === this.bp.pubkey && verifyEvent(signed)) {
return signed
} else {
throw new Error(`event returned from bunker is improperly signed: ${JSON.stringify(signed)}`)
}
}
async nip04Encrypt(thirdPartyPubkey: string, plaintext: string): Promise<string> {
return await this.sendRequest('nip04_encrypt', [thirdPartyPubkey, plaintext])
}
async nip04Decrypt(thirdPartyPubkey: string, ciphertext: string): Promise<string> {
return await this.sendRequest('nip04_decrypt', [thirdPartyPubkey, ciphertext])
}
async nip44GetKey(thirdPartyPubkey: string): Promise<Uint8Array> {
let resp = await this.sendRequest('nip44_get_key', [thirdPartyPubkey])
return hexToBytes(resp)
}
async nip44Encrypt(thirdPartyPubkey: string, plaintext: string): Promise<string> {
return await this.sendRequest('nip44_encrypt', [thirdPartyPubkey, plaintext])
}
async nip44Decrypt(thirdPartyPubkey: string, ciphertext: string): Promise<string> {
return await this.sendRequest('nip44_encrypt', [thirdPartyPubkey, ciphertext])
}
}
/**
@@ -244,7 +273,7 @@ export async function createAccount(
// 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
rpc.remotePubkey = pubkey
rpc.bp.pubkey = pubkey
await rpc.connect()
return rpc
@@ -254,7 +283,7 @@ export async function createAccount(
* Fetches info on available providers that announce themselves using NIP-89 events.
* @returns A promise that resolves to an array of available bunker objects.
*/
export async function fetchCustodialbunkers(pool: AbstractSimplePool, relays: string[]): Promise<BunkerProfile[]> {
export async function fetchCustodialBunkers(pool: AbstractSimplePool, relays: string[]): Promise<BunkerProfile[]> {
const events = await pool.querySync(relays, {
kinds: [Handlerinformation],
'#k': [NostrConnect.toString()],

View File

@@ -79,10 +79,17 @@ const vectors: [string, string, number, 0x00 | 0x01 | 0x02, string][] = [
'ncryptsec1qgqnx59n7duv6ec3hhrvn33q25u2qfd7m69vv6plsg7spnw6d4r9hq0ayjsnlw99eghqqzj8ps7vfwx40nqp9gpw7yzyy09jmwkq3a3z8q0ph5jahs2hap5k6h2wfrme7w2nuek4jnwpzfht4q3u79ra',
],
[
'',
'ÅΩẛ̣',
'11b25a101667dd9208db93c0827c6bdad66729a5b521156a7e9d3b22b3ae8944',
9,
0x01,
'ncryptsec1qgylzyeunu2j85au05ae0g0sly03xu54tgnjemr6g9w0eqwuuczya7k0f4juqve64vzsrlxqxmcekzrpvg2a8qu4q6wtjxe0dvy3xkjh5smmz4uy59jg0jay9vnf28e3rc6jq2kd26h6g3fejyw6cype',
'ncryptsec1qgy5kwr5v8p206vwaflp4g6r083kwts6q5sh8m4d0q56edpxwhrly78ema2z7jpdeldsz7u5wpxpyhs6m0405skdsep9n37uncw7xlc8q8meyw6d6ky47vcl0guhqpt5dx8ejxc8hvzf6y2gwsl5s0nw',
],
[
'ÅΩṩ',
'11b25a101667dd9208db93c0827c6bdad66729a5b521156a7e9d3b22b3ae8944',
9,
0x01,
'ncryptsec1qgy5f4lcx873yarkfpngaudarxfj4wj939xn4azmd66j6jrwcml6av87d6vnelzn70kszgkg4lj9rsdjlqz0wn7m7456sr2q5yjpy72ykgkdwckevl857hpcfnwzswj9lajxtln0tsr9h7xdwqm6pqzf',
],
]

View File

@@ -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 {
let salt = randomBytes(16)
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 aad = Uint8Array.from([ksb])
let xc2p1 = xchacha20poly1305(key, nonce, aad)
@@ -37,7 +37,7 @@ export function decrypt(ncryptsec: string, password: string): Uint8Array {
let aad = Uint8Array.from([ksb])
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 sec = xc2p1.decrypt(ciphertext)

203
nip75.test.ts Normal file
View 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
View 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
}

View File

@@ -1,7 +1,7 @@
{
"type": "module",
"name": "nostr-tools",
"version": "2.1.7",
"version": "2.2.1",
"description": "Tools for making a Nostr client.",
"repository": {
"type": "git",
@@ -170,6 +170,11 @@
"require": "./lib/cjs/nip57.js",
"types": "./lib/types/nip57.d.ts"
},
"./nip75": {
"import": "./lib/esm/nip75.js",
"require": "./lib/cjs/nip75.js",
"types": "./lib/types/nip75.d.ts"
},
"./nip94": {
"import": "./lib/esm/nip94.js",
"require": "./lib/cjs/nip94.js",

View File

@@ -2,7 +2,10 @@ import { afterEach, beforeEach, expect, test } from 'bun:test'
import { SimplePool } from './pool.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 mockRelays: MockRelay[]

View File

@@ -1,8 +1,10 @@
import { expect, test } from 'bun:test'
import { finalizeEvent, generateSecretKey, getPublicKey } from './pure.ts'
import { Relay } from './relay.ts'
import { MockRelay } from './test-helpers.ts'
import { Relay, useWebSocketImplementation } from './relay.ts'
import { MockRelay, MockWebSocketClient } from './test-helpers.ts'
useWebSocketImplementation(MockWebSocketClient)
test('connectivity', async () => {
const mockRelay = new MockRelay()

View File

@@ -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 { matchFilters, type Filter } from './filter.ts'
export const MockWebSocketClient = WebSocket
export function buildEvent(params: Partial<Event>): Event {
return {
id: '',