03-publishing-events.ts raw
1 /**
2 * NDK Event Publishing Patterns
3 *
4 * Examples from: src/publish/orders.tsx, scripts/gen_products.ts
5 */
6
7 import NDK, { NDKEvent, NDKTag } from '@nostr-dev-kit/ndk'
8
9 // ============================================================
10 // BASIC EVENT PUBLISHING
11 // ============================================================
12
13 const publishBasicNote = async (ndk: NDK, content: string) => {
14 // Create event
15 const event = new NDKEvent(ndk)
16 event.kind = 1 // Text note
17 event.content = content
18 event.tags = []
19
20 // Sign and publish
21 await event.sign()
22 await event.publish()
23
24 console.log('✅ Published note:', event.id)
25 return event.id
26 }
27
28 // ============================================================
29 // EVENT WITH TAGS
30 // ============================================================
31
32 const publishNoteWithTags = async (
33 ndk: NDK,
34 content: string,
35 options: {
36 mentions?: string[] // pubkeys to mention
37 hashtags?: string[]
38 replyTo?: string // event ID
39 }
40 ) => {
41 const event = new NDKEvent(ndk)
42 event.kind = 1
43 event.content = content
44 event.tags = []
45
46 // Add mentions
47 if (options.mentions) {
48 options.mentions.forEach(pubkey => {
49 event.tags.push(['p', pubkey])
50 })
51 }
52
53 // Add hashtags
54 if (options.hashtags) {
55 options.hashtags.forEach(tag => {
56 event.tags.push(['t', tag])
57 })
58 }
59
60 // Add reply
61 if (options.replyTo) {
62 event.tags.push(['e', options.replyTo, '', 'reply'])
63 }
64
65 await event.sign()
66 await event.publish()
67
68 return event.id
69 }
70
71 // ============================================================
72 // PRODUCT LISTING (PARAMETERIZED REPLACEABLE EVENT)
73 // ============================================================
74
75 interface ProductData {
76 slug: string // Unique identifier
77 title: string
78 description: string
79 price: number
80 currency: string
81 images: string[]
82 shippingRefs?: string[]
83 category?: string
84 }
85
86 const publishProduct = async (ndk: NDK, product: ProductData) => {
87 const event = new NDKEvent(ndk)
88 event.kind = 30402 // Product listing kind
89 event.content = product.description
90
91 // Build tags
92 event.tags = [
93 ['d', product.slug], // Unique identifier (required for replaceable)
94 ['title', product.title],
95 ['price', product.price.toString(), product.currency],
96 ]
97
98 // Add images
99 product.images.forEach(image => {
100 event.tags.push(['image', image])
101 })
102
103 // Add shipping options
104 if (product.shippingRefs) {
105 product.shippingRefs.forEach(ref => {
106 event.tags.push(['shipping', ref])
107 })
108 }
109
110 // Add category
111 if (product.category) {
112 event.tags.push(['t', product.category])
113 }
114
115 // Optional: set custom timestamp
116 event.created_at = Math.floor(Date.now() / 1000)
117
118 await event.sign()
119 await event.publish()
120
121 console.log('✅ Published product:', product.title)
122 return event.id
123 }
124
125 // ============================================================
126 // ORDER CREATION EVENT
127 // ============================================================
128
129 interface OrderData {
130 orderId: string
131 sellerPubkey: string
132 productRef: string
133 quantity: number
134 totalAmount: string
135 currency: string
136 shippingRef?: string
137 shippingAddress?: string
138 email?: string
139 phone?: string
140 notes?: string
141 }
142
143 const createOrder = async (ndk: NDK, order: OrderData) => {
144 const event = new NDKEvent(ndk)
145 event.kind = 16 // Order processing kind
146 event.content = order.notes || ''
147
148 // Required tags per spec
149 event.tags = [
150 ['p', order.sellerPubkey],
151 ['subject', `Order ${order.orderId.substring(0, 8)}`],
152 ['type', 'order-creation'],
153 ['order', order.orderId],
154 ['amount', order.totalAmount],
155 ['item', order.productRef, order.quantity.toString()],
156 ]
157
158 // Optional tags
159 if (order.shippingRef) {
160 event.tags.push(['shipping', order.shippingRef])
161 }
162
163 if (order.shippingAddress) {
164 event.tags.push(['address', order.shippingAddress])
165 }
166
167 if (order.email) {
168 event.tags.push(['email', order.email])
169 }
170
171 if (order.phone) {
172 event.tags.push(['phone', order.phone])
173 }
174
175 try {
176 await event.sign()
177 await event.publish()
178
179 console.log('✅ Order created:', order.orderId)
180 return { success: true, eventId: event.id }
181 } catch (error) {
182 console.error('❌ Failed to create order:', error)
183 return { success: false, error }
184 }
185 }
186
187 // ============================================================
188 // STATUS UPDATE EVENT
189 // ============================================================
190
191 const publishStatusUpdate = async (
192 ndk: NDK,
193 orderId: string,
194 recipientPubkey: string,
195 status: 'pending' | 'paid' | 'shipped' | 'delivered' | 'cancelled',
196 notes?: string
197 ) => {
198 const event = new NDKEvent(ndk)
199 event.kind = 16
200 event.content = notes || `Order status updated to ${status}`
201 event.tags = [
202 ['p', recipientPubkey],
203 ['subject', 'order-info'],
204 ['type', 'status-update'],
205 ['order', orderId],
206 ['status', status],
207 ]
208
209 await event.sign()
210 await event.publish()
211
212 return event.id
213 }
214
215 // ============================================================
216 // BATCH PUBLISHING
217 // ============================================================
218
219 const publishMultipleEvents = async (
220 ndk: NDK,
221 events: Array<{ kind: number; content: string; tags: NDKTag[] }>
222 ) => {
223 const results = []
224
225 for (const eventData of events) {
226 try {
227 const event = new NDKEvent(ndk)
228 event.kind = eventData.kind
229 event.content = eventData.content
230 event.tags = eventData.tags
231
232 await event.sign()
233 await event.publish()
234
235 results.push({ success: true, eventId: event.id })
236 } catch (error) {
237 results.push({ success: false, error })
238 }
239 }
240
241 return results
242 }
243
244 // ============================================================
245 // PUBLISH WITH CUSTOM SIGNER
246 // ============================================================
247
248 import { NDKSigner } from '@nostr-dev-kit/ndk'
249
250 const publishWithCustomSigner = async (
251 ndk: NDK,
252 signer: NDKSigner,
253 eventData: { kind: number; content: string; tags: NDKTag[] }
254 ) => {
255 const event = new NDKEvent(ndk)
256 event.kind = eventData.kind
257 event.content = eventData.content
258 event.tags = eventData.tags
259
260 // Sign with specific signer (not ndk.signer)
261 await event.sign(signer)
262 await event.publish()
263
264 return event.id
265 }
266
267 // ============================================================
268 // ERROR HANDLING PATTERN
269 // ============================================================
270
271 const publishWithErrorHandling = async (
272 ndk: NDK,
273 eventData: { kind: number; content: string; tags: NDKTag[] }
274 ) => {
275 // Validate NDK
276 if (!ndk) {
277 throw new Error('NDK not initialized')
278 }
279
280 // Validate signer
281 if (!ndk.signer) {
282 throw new Error('No active signer. Please login first.')
283 }
284
285 try {
286 const event = new NDKEvent(ndk)
287 event.kind = eventData.kind
288 event.content = eventData.content
289 event.tags = eventData.tags
290
291 // Sign
292 await event.sign()
293
294 // Verify signature
295 if (!event.sig) {
296 throw new Error('Event signing failed')
297 }
298
299 // Publish
300 await event.publish()
301
302 // Verify event ID
303 if (!event.id) {
304 throw new Error('Event ID not generated')
305 }
306
307 return {
308 success: true,
309 eventId: event.id,
310 pubkey: event.pubkey
311 }
312 } catch (error) {
313 console.error('Publishing failed:', error)
314
315 if (error instanceof Error) {
316 // Handle specific error types
317 if (error.message.includes('relay')) {
318 throw new Error('Failed to publish to relays. Check connection.')
319 }
320 if (error.message.includes('sign')) {
321 throw new Error('Failed to sign event. Check signer.')
322 }
323 }
324
325 throw error
326 }
327 }
328
329 // ============================================================
330 // USAGE EXAMPLE
331 // ============================================================
332
333 async function publishingExample(ndk: NDK) {
334 // Simple note
335 await publishBasicNote(ndk, 'Hello Nostr!')
336
337 // Note with tags
338 await publishNoteWithTags(ndk, 'Check out this product!', {
339 hashtags: ['marketplace', 'nostr'],
340 mentions: ['pubkey123...']
341 })
342
343 // Product listing
344 await publishProduct(ndk, {
345 slug: 'bitcoin-tshirt',
346 title: 'Bitcoin T-Shirt',
347 description: 'High quality Bitcoin t-shirt',
348 price: 25,
349 currency: 'USD',
350 images: ['https://example.com/image.jpg'],
351 category: 'clothing'
352 })
353
354 // Order
355 await createOrder(ndk, {
356 orderId: 'order-123',
357 sellerPubkey: 'seller-pubkey',
358 productRef: '30402:pubkey:bitcoin-tshirt',
359 quantity: 1,
360 totalAmount: '25.00',
361 currency: 'USD',
362 email: 'customer@example.com'
363 })
364 }
365
366 export {
367 publishBasicNote,
368 publishNoteWithTags,
369 publishProduct,
370 createOrder,
371 publishStatusUpdate,
372 publishMultipleEvents,
373 publishWithCustomSigner,
374 publishWithErrorHandling
375 }
376
377