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