You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
376 lines
8.8 KiB
376 lines
8.8 KiB
/** |
|
* NDK Event Publishing Patterns |
|
* |
|
* Examples from: src/publish/orders.tsx, scripts/gen_products.ts |
|
*/ |
|
|
|
import NDK, { NDKEvent, NDKTag } from '@nostr-dev-kit/ndk' |
|
|
|
// ============================================================ |
|
// BASIC EVENT PUBLISHING |
|
// ============================================================ |
|
|
|
const publishBasicNote = async (ndk: NDK, content: string) => { |
|
// Create event |
|
const event = new NDKEvent(ndk) |
|
event.kind = 1 // Text note |
|
event.content = content |
|
event.tags = [] |
|
|
|
// Sign and publish |
|
await event.sign() |
|
await event.publish() |
|
|
|
console.log('✅ Published note:', event.id) |
|
return event.id |
|
} |
|
|
|
// ============================================================ |
|
// EVENT WITH TAGS |
|
// ============================================================ |
|
|
|
const publishNoteWithTags = async ( |
|
ndk: NDK, |
|
content: string, |
|
options: { |
|
mentions?: string[] // pubkeys to mention |
|
hashtags?: string[] |
|
replyTo?: string // event ID |
|
} |
|
) => { |
|
const event = new NDKEvent(ndk) |
|
event.kind = 1 |
|
event.content = content |
|
event.tags = [] |
|
|
|
// Add mentions |
|
if (options.mentions) { |
|
options.mentions.forEach(pubkey => { |
|
event.tags.push(['p', pubkey]) |
|
}) |
|
} |
|
|
|
// Add hashtags |
|
if (options.hashtags) { |
|
options.hashtags.forEach(tag => { |
|
event.tags.push(['t', tag]) |
|
}) |
|
} |
|
|
|
// Add reply |
|
if (options.replyTo) { |
|
event.tags.push(['e', options.replyTo, '', 'reply']) |
|
} |
|
|
|
await event.sign() |
|
await event.publish() |
|
|
|
return event.id |
|
} |
|
|
|
// ============================================================ |
|
// PRODUCT LISTING (PARAMETERIZED REPLACEABLE EVENT) |
|
// ============================================================ |
|
|
|
interface ProductData { |
|
slug: string // Unique identifier |
|
title: string |
|
description: string |
|
price: number |
|
currency: string |
|
images: string[] |
|
shippingRefs?: string[] |
|
category?: string |
|
} |
|
|
|
const publishProduct = async (ndk: NDK, product: ProductData) => { |
|
const event = new NDKEvent(ndk) |
|
event.kind = 30402 // Product listing kind |
|
event.content = product.description |
|
|
|
// Build tags |
|
event.tags = [ |
|
['d', product.slug], // Unique identifier (required for replaceable) |
|
['title', product.title], |
|
['price', product.price.toString(), product.currency], |
|
] |
|
|
|
// Add images |
|
product.images.forEach(image => { |
|
event.tags.push(['image', image]) |
|
}) |
|
|
|
// Add shipping options |
|
if (product.shippingRefs) { |
|
product.shippingRefs.forEach(ref => { |
|
event.tags.push(['shipping', ref]) |
|
}) |
|
} |
|
|
|
// Add category |
|
if (product.category) { |
|
event.tags.push(['t', product.category]) |
|
} |
|
|
|
// Optional: set custom timestamp |
|
event.created_at = Math.floor(Date.now() / 1000) |
|
|
|
await event.sign() |
|
await event.publish() |
|
|
|
console.log('✅ Published product:', product.title) |
|
return event.id |
|
} |
|
|
|
// ============================================================ |
|
// ORDER CREATION EVENT |
|
// ============================================================ |
|
|
|
interface OrderData { |
|
orderId: string |
|
sellerPubkey: string |
|
productRef: string |
|
quantity: number |
|
totalAmount: string |
|
currency: string |
|
shippingRef?: string |
|
shippingAddress?: string |
|
email?: string |
|
phone?: string |
|
notes?: string |
|
} |
|
|
|
const createOrder = async (ndk: NDK, order: OrderData) => { |
|
const event = new NDKEvent(ndk) |
|
event.kind = 16 // Order processing kind |
|
event.content = order.notes || '' |
|
|
|
// Required tags per spec |
|
event.tags = [ |
|
['p', order.sellerPubkey], |
|
['subject', `Order ${order.orderId.substring(0, 8)}`], |
|
['type', 'order-creation'], |
|
['order', order.orderId], |
|
['amount', order.totalAmount], |
|
['item', order.productRef, order.quantity.toString()], |
|
] |
|
|
|
// Optional tags |
|
if (order.shippingRef) { |
|
event.tags.push(['shipping', order.shippingRef]) |
|
} |
|
|
|
if (order.shippingAddress) { |
|
event.tags.push(['address', order.shippingAddress]) |
|
} |
|
|
|
if (order.email) { |
|
event.tags.push(['email', order.email]) |
|
} |
|
|
|
if (order.phone) { |
|
event.tags.push(['phone', order.phone]) |
|
} |
|
|
|
try { |
|
await event.sign() |
|
await event.publish() |
|
|
|
console.log('✅ Order created:', order.orderId) |
|
return { success: true, eventId: event.id } |
|
} catch (error) { |
|
console.error('❌ Failed to create order:', error) |
|
return { success: false, error } |
|
} |
|
} |
|
|
|
// ============================================================ |
|
// STATUS UPDATE EVENT |
|
// ============================================================ |
|
|
|
const publishStatusUpdate = async ( |
|
ndk: NDK, |
|
orderId: string, |
|
recipientPubkey: string, |
|
status: 'pending' | 'paid' | 'shipped' | 'delivered' | 'cancelled', |
|
notes?: string |
|
) => { |
|
const event = new NDKEvent(ndk) |
|
event.kind = 16 |
|
event.content = notes || `Order status updated to ${status}` |
|
event.tags = [ |
|
['p', recipientPubkey], |
|
['subject', 'order-info'], |
|
['type', 'status-update'], |
|
['order', orderId], |
|
['status', status], |
|
] |
|
|
|
await event.sign() |
|
await event.publish() |
|
|
|
return event.id |
|
} |
|
|
|
// ============================================================ |
|
// BATCH PUBLISHING |
|
// ============================================================ |
|
|
|
const publishMultipleEvents = async ( |
|
ndk: NDK, |
|
events: Array<{ kind: number; content: string; tags: NDKTag[] }> |
|
) => { |
|
const results = [] |
|
|
|
for (const eventData of events) { |
|
try { |
|
const event = new NDKEvent(ndk) |
|
event.kind = eventData.kind |
|
event.content = eventData.content |
|
event.tags = eventData.tags |
|
|
|
await event.sign() |
|
await event.publish() |
|
|
|
results.push({ success: true, eventId: event.id }) |
|
} catch (error) { |
|
results.push({ success: false, error }) |
|
} |
|
} |
|
|
|
return results |
|
} |
|
|
|
// ============================================================ |
|
// PUBLISH WITH CUSTOM SIGNER |
|
// ============================================================ |
|
|
|
import { NDKSigner } from '@nostr-dev-kit/ndk' |
|
|
|
const publishWithCustomSigner = async ( |
|
ndk: NDK, |
|
signer: NDKSigner, |
|
eventData: { kind: number; content: string; tags: NDKTag[] } |
|
) => { |
|
const event = new NDKEvent(ndk) |
|
event.kind = eventData.kind |
|
event.content = eventData.content |
|
event.tags = eventData.tags |
|
|
|
// Sign with specific signer (not ndk.signer) |
|
await event.sign(signer) |
|
await event.publish() |
|
|
|
return event.id |
|
} |
|
|
|
// ============================================================ |
|
// ERROR HANDLING PATTERN |
|
// ============================================================ |
|
|
|
const publishWithErrorHandling = async ( |
|
ndk: NDK, |
|
eventData: { kind: number; content: string; tags: NDKTag[] } |
|
) => { |
|
// Validate NDK |
|
if (!ndk) { |
|
throw new Error('NDK not initialized') |
|
} |
|
|
|
// Validate signer |
|
if (!ndk.signer) { |
|
throw new Error('No active signer. Please login first.') |
|
} |
|
|
|
try { |
|
const event = new NDKEvent(ndk) |
|
event.kind = eventData.kind |
|
event.content = eventData.content |
|
event.tags = eventData.tags |
|
|
|
// Sign |
|
await event.sign() |
|
|
|
// Verify signature |
|
if (!event.sig) { |
|
throw new Error('Event signing failed') |
|
} |
|
|
|
// Publish |
|
await event.publish() |
|
|
|
// Verify event ID |
|
if (!event.id) { |
|
throw new Error('Event ID not generated') |
|
} |
|
|
|
return { |
|
success: true, |
|
eventId: event.id, |
|
pubkey: event.pubkey |
|
} |
|
} catch (error) { |
|
console.error('Publishing failed:', error) |
|
|
|
if (error instanceof Error) { |
|
// Handle specific error types |
|
if (error.message.includes('relay')) { |
|
throw new Error('Failed to publish to relays. Check connection.') |
|
} |
|
if (error.message.includes('sign')) { |
|
throw new Error('Failed to sign event. Check signer.') |
|
} |
|
} |
|
|
|
throw error |
|
} |
|
} |
|
|
|
// ============================================================ |
|
// USAGE EXAMPLE |
|
// ============================================================ |
|
|
|
async function publishingExample(ndk: NDK) { |
|
// Simple note |
|
await publishBasicNote(ndk, 'Hello Nostr!') |
|
|
|
// Note with tags |
|
await publishNoteWithTags(ndk, 'Check out this product!', { |
|
hashtags: ['marketplace', 'nostr'], |
|
mentions: ['pubkey123...'] |
|
}) |
|
|
|
// Product listing |
|
await publishProduct(ndk, { |
|
slug: 'bitcoin-tshirt', |
|
title: 'Bitcoin T-Shirt', |
|
description: 'High quality Bitcoin t-shirt', |
|
price: 25, |
|
currency: 'USD', |
|
images: ['https://example.com/image.jpg'], |
|
category: 'clothing' |
|
}) |
|
|
|
// Order |
|
await createOrder(ndk, { |
|
orderId: 'order-123', |
|
sellerPubkey: 'seller-pubkey', |
|
productRef: '30402:pubkey:bitcoin-tshirt', |
|
quantity: 1, |
|
totalAmount: '25.00', |
|
currency: 'USD', |
|
email: 'customer@example.com' |
|
}) |
|
} |
|
|
|
export { |
|
publishBasicNote, |
|
publishNoteWithTags, |
|
publishProduct, |
|
createOrder, |
|
publishStatusUpdate, |
|
publishMultipleEvents, |
|
publishWithCustomSigner, |
|
publishWithErrorHandling |
|
} |
|
|
|
|