- Introduced comprehensive documentation for the Nostr Development Kit (NDK) including an overview, quick reference, and troubleshooting guide. - Added detailed examples covering initialization, authentication, event publishing, querying, and user profile management. - Structured the documentation to facilitate quick lookups and deep learning, based on real-world usage patterns from the Plebeian Market application. - Created an index for examples to enhance usability and navigation. - Bumped version to 1.0.0 to reflect the addition of this new skill set.
377 lines
8.8 KiB
TypeScript
377 lines
8.8 KiB
TypeScript
/**
|
|
* 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
|
|
}
|
|
|