-
-
+
+
-
No Payment Invoice
-
+
No Payment Invoice
+
A Lightning invoice will be sent by the merchant once they process your order.
-
+
You'll receive the invoice via Nostr when it's ready.
@@ -193,10 +185,8 @@ import { Badge } from '@/components/ui/badge'
import {
Copy,
Wallet,
-
Info,
- CheckCircle,
- Download
+ CheckCircle
} from 'lucide-vue-next'
import { useMarketStore } from '@/stores/market'
import QRCode from 'qrcode'
@@ -266,9 +256,9 @@ const formatPaymentStatus = (status: string) => {
const getStatusColor = (status: string) => {
switch (status) {
case 'paid': return 'text-green-600'
- case 'pending': return 'text-yellow-600'
- case 'expired': return 'text-red-600'
- default: return 'text-gray-600'
+ case 'pending': return 'text-amber-600'
+ case 'expired': return 'text-destructive'
+ default: return 'text-muted-foreground'
}
}
@@ -277,9 +267,20 @@ const formatHash = (hash: string) => {
return `${hash.substring(0, 8)}...${hash.substring(hash.length - 8)}`
}
-const formatDate = (timestamp: number) => {
- if (!timestamp) return 'N/A'
- return new Date(timestamp * 1000).toLocaleString()
+const formatDate = (dateValue: string | number | undefined) => {
+ if (!dateValue) return 'N/A'
+
+ let timestamp: number
+
+ if (typeof dateValue === 'string') {
+ // Handle ISO date strings from LNBits API
+ timestamp = new Date(dateValue).getTime()
+ } else {
+ // Handle Unix timestamps (seconds) from our store
+ timestamp = dateValue * 1000
+ }
+
+ return new Date(timestamp).toLocaleString()
}
const copyPaymentRequest = async () => {
@@ -302,17 +303,6 @@ const openInWallet = () => {
window.open(walletUrl, '_blank')
}
-const downloadQRCode = () => {
- if (!qrCodeDataUrl.value) return
-
- const link = document.createElement('a')
- link.href = qrCodeDataUrl.value
- link.download = `qr-code-${invoice.value?.amount}-${currency.value}.png`
- document.body.appendChild(link)
- link.click()
- document.body.removeChild(link)
-}
-
const retryQRCode = () => {
if (invoice.value?.bolt11) {
generateQRCode(invoice.value.bolt11)
diff --git a/src/components/market/PaymentRequestDialog.vue b/src/components/market/PaymentRequestDialog.vue
index 1516f56..eb43b1f 100644
--- a/src/components/market/PaymentRequestDialog.vue
+++ b/src/components/market/PaymentRequestDialog.vue
@@ -255,7 +255,7 @@ const payWithWallet = async () => {
const { useAuth } = await import('@/composables/useAuth')
const auth = useAuth()
- if (!auth.currentUser.value?.walletId || !auth.currentUser.value?.adminKey) {
+ if (!auth.currentUser.value?.wallets?.[0]?.id || !auth.currentUser.value?.wallets?.[0]?.adminkey) {
toast.error('Please connect your wallet to pay')
return
}
@@ -263,8 +263,8 @@ const payWithWallet = async () => {
// Pay the invoice
const result = await payInvoiceWithWallet(
lightningInvoice.value,
- auth.currentUser.value.walletId,
- auth.currentUser.value.adminKey
+ auth.currentUser.value.wallets[0].id,
+ auth.currentUser.value.wallets[0].adminkey
)
console.log('Payment result:', result)
diff --git a/src/components/ui/input/Input.vue b/src/components/ui/input/Input.vue
index 243a497..08479ed 100644
--- a/src/components/ui/input/Input.vue
+++ b/src/components/ui/input/Input.vue
@@ -20,5 +20,11 @@ const modelValue = useVModel(props, 'modelValue', emits, {
-
+
diff --git a/src/composables/useNostrclientHub.ts b/src/composables/useNostrclientHub.ts
index 1c5c269..f675a0a 100644
--- a/src/composables/useNostrclientHub.ts
+++ b/src/composables/useNostrclientHub.ts
@@ -1,4 +1,4 @@
-import { ref, computed, onMounted, onUnmounted, readonly } from 'vue'
+import { ref, computed, onUnmounted, readonly } from 'vue'
import { nostrclientHub, type SubscriptionConfig } from '../lib/nostr/nostrclientHub'
export function useNostrclientHub() {
diff --git a/src/composables/useOrderEvents.ts b/src/composables/useOrderEvents.ts
index 0439508..e8c2a4f 100644
--- a/src/composables/useOrderEvents.ts
+++ b/src/composables/useOrderEvents.ts
@@ -1,47 +1,84 @@
-import { ref, computed, watch } from 'vue'
+import { ref, computed } from 'vue'
import { nip04 } from 'nostr-tools'
import { relayHubComposable } from './useRelayHub'
import { useAuth } from './useAuth'
import { useMarketStore } from '@/stores/market'
-import { config } from '@/lib/config'
-import type { Order, OrderStatus } from '@/stores/market'
-import type { LightningInvoice } from '@/lib/services/invoiceService'
+import { decode } from 'light-bolt11-decoder'
-// Order event types based on NIP-69 and nostrmarket patterns
-export enum OrderEventType {
- CUSTOMER_ORDER = 'customer_order',
- PAYMENT_REQUEST = 'payment_request',
- ORDER_PAID = 'order_paid',
- ORDER_SHIPPED = 'order_shipped',
- ORDER_DELIVERED = 'order_delivered',
- ORDER_CANCELLED = 'order_cancelled',
- INVOICE_GENERATED = 'invoice_generated'
+// Nostrmarket Order interfaces based on the actual implementation
+
+// Nostrmarket Order interfaces based on the actual implementation
+interface OrderItem {
+ product_id: string
+ quantity: number
}
-export interface OrderEvent {
- type: OrderEventType
- orderId: string
- data: any
- timestamp: number
- senderPubkey: string
+interface OrderContact {
+ nostr?: string
+ phone?: string
+ email?: string
}
-export interface PaymentRequestEvent {
- type: OrderEventType.PAYMENT_REQUEST
- orderId: string
- paymentRequest: string
- amount: number
- currency: string
- memo: string
- expiresAt: number
+// Direct message types from nostrmarket
+enum DirectMessageType {
+ PLAIN_TEXT = -1,
+ CUSTOMER_ORDER = 0,
+ PAYMENT_REQUEST = 1,
+ ORDER_PAID_OR_SHIPPED = 2
}
-export interface OrderStatusEvent {
- type: OrderEventType.ORDER_PAID | OrderEventType.ORDER_SHIPPED | OrderEventType.ORDER_DELIVERED
- orderId: string
- status: OrderStatus
- timestamp: number
- additionalData?: any
+// Event types for nostrmarket protocol
+interface CustomerOrderEvent {
+ type: DirectMessageType.CUSTOMER_ORDER
+ id: string
+ items: OrderItem[]
+ contact?: OrderContact
+ shipping_id: string
+ message?: string
+}
+
+interface PaymentRequestEvent {
+ type: DirectMessageType.PAYMENT_REQUEST
+ id: string
+ message?: string
+ payment_options: Array<{
+ type: string
+ link: string
+ }>
+}
+
+interface OrderStatusEvent {
+ type: DirectMessageType.ORDER_PAID_OR_SHIPPED
+ id: string
+ message?: string
+ paid?: boolean
+ shipped?: boolean
+}
+
+// Helper function to extract expiry from bolt11 invoice
+function extractExpiryFromBolt11(bolt11String: string): string | undefined {
+ try {
+ const decoded = decode(bolt11String)
+ console.log('Decoded bolt11 invoice:', {
+ amount: decoded.sections.find(section => section.name === 'amount')?.value,
+ expiry: decoded.expiry,
+ timestamp: decoded.sections.find(section => section.name === 'timestamp')?.value
+ })
+
+ // Calculate expiry date from timestamp + expiry seconds
+ const timestamp = decoded.sections.find(section => section.name === 'timestamp')?.value as number
+ const expirySeconds = decoded.expiry as number
+
+ if (timestamp && expirySeconds) {
+ const expiryDate = new Date((timestamp + expirySeconds) * 1000)
+ return expiryDate.toISOString()
+ }
+
+ return undefined
+ } catch (error) {
+ console.warn('Failed to extract expiry from bolt11:', error)
+ return undefined
+ }
}
export function useOrderEvents() {
@@ -62,524 +99,216 @@ export function useOrderEvents() {
const isConnected = relayHub.isConnected.value
const hasPubkey = !!currentUserPubkey.value
- console.log('OrderEvents isReady check:', { isAuth, isConnected, hasPubkey })
return isAuth && isConnected && hasPubkey
})
// Subscribe to order events
const subscribeToOrderEvents = async () => {
- console.log('subscribeToOrderEvents called with:', {
- isReady: isReady.value,
- isSubscribed: isSubscribed.value,
- currentUserPubkey: currentUserPubkey.value,
- relayHubConnected: relayHub.isConnected.value,
- authStatus: auth.isAuthenticated
- })
-
if (!isReady.value || isSubscribed.value) {
- console.warn('Cannot subscribe to order events: not ready or already subscribed', {
- isReady: isReady.value,
- isSubscribed: isSubscribed.value
- })
return
}
try {
- console.log('Subscribing to order events for user:', currentUserPubkey.value)
-
// Subscribe to direct messages (kind 4) that contain order information
const filters = [
{
kinds: [4], // NIP-04 encrypted direct messages
- '#p': [currentUserPubkey.value].filter(Boolean) as string[], // Messages to us, filter out undefined
+ '#p': [currentUserPubkey.value].filter(Boolean) as string[],
since: lastEventTimestamp.value
}
]
- console.log('Using filters:', filters)
-
- const unsubscribe = relayHub.subscribe({
- id: `order-events-${currentUserPubkey.value}-${Date.now()}`,
+ relayHub.subscribe({
+ id: 'order-events',
filters,
- relays: config.market.supportedRelays,
- onEvent: (event: any) => {
- console.log('Received event in order subscription:', event.id)
- handleOrderEvent(event)
- },
+ onEvent: handleOrderEvent,
onEose: () => {
- console.log('Order events subscription EOSE')
+ console.log('Order events subscription ended')
}
})
- subscriptionId.value = `order-events-${currentUserPubkey.value}-${Date.now()}`
+ subscriptionId.value = 'order-events'
isSubscribed.value = true
+ console.log('Successfully subscribed to order events')
- console.log('Successfully subscribed to order events with ID:', subscriptionId.value)
- return unsubscribe
} catch (error) {
console.error('Failed to subscribe to order events:', error)
- throw error
}
}
// Handle incoming order events
const handleOrderEvent = async (event: any) => {
- if (!auth.currentUser?.value?.prvkey) {
- console.warn('Cannot decrypt order event: no private key available')
- return
- }
-
- // Check if we've already processed this event
if (processedEventIds.value.has(event.id)) {
return
}
+ processedEventIds.value.add(event.id)
+ lastEventTimestamp.value = Math.max(lastEventTimestamp.value, event.created_at)
+
try {
// Decrypt the message content
const decryptedContent = await nip04.decrypt(
- auth.currentUser.value.prvkey,
- event.pubkey, // Sender's pubkey
+ auth.currentUser.value?.prvkey || '',
+ event.pubkey,
event.content
)
- // Parse the decrypted content
- const orderEvent = JSON.parse(decryptedContent)
+ // Parse the JSON content
+ const jsonData = JSON.parse(decryptedContent)
- console.log('Received order event:', {
- eventId: event.id,
- type: orderEvent.type,
- orderId: orderEvent.orderId,
- sender: event.pubkey
- })
-
- // Handle nostrmarket protocol messages
- if (orderEvent.type === 0 || orderEvent.type === 1 || orderEvent.type === 2) {
- await handleNostrmarketMessage(orderEvent, event.pubkey)
- return
- }
-
- // Process the order event based on type
- await processOrderEvent(orderEvent, event.pubkey)
-
- // Mark as processed
- processedEventIds.value.add(event.id)
- lastEventTimestamp.value = Math.max(lastEventTimestamp.value, event.created_at)
-
- } catch (error) {
- console.error('Failed to process order event:', {
- eventId: event.id,
- error: error instanceof Error ? error.message : 'Unknown error'
- })
- }
- }
-
- // Handle nostrmarket protocol messages (type 0, 1, 2)
- const handleNostrmarketMessage = async (message: any, senderPubkey: string) => {
- try {
- console.log('Processing nostrmarket message:', {
- type: message.type,
- orderId: message.id,
- sender: senderPubkey
- })
-
- // Import nostrmarket service
- const { nostrmarketService } = await import('@/lib/services/nostrmarketService')
-
- switch (message.type) {
- case 0:
- // Customer order - this should be handled by the merchant side
- console.log('Received customer order (type 0) - this should be handled by merchant')
+ // Handle different message types
+ switch (jsonData.type) {
+ case DirectMessageType.CUSTOMER_ORDER:
+ await handleCustomerOrder(jsonData as CustomerOrderEvent, event.pubkey)
break
-
- case 1:
- // Payment request from merchant
- console.log('Received payment request from merchant')
- await nostrmarketService.handlePaymentRequest(message)
+ case DirectMessageType.PAYMENT_REQUEST:
+ await handlePaymentRequest(jsonData as PaymentRequestEvent, event.pubkey)
break
-
- case 2:
- // Order status update from merchant
- console.log('Received order status update from merchant')
- await nostrmarketService.handleOrderStatusUpdate(message)
+ case DirectMessageType.ORDER_PAID_OR_SHIPPED:
+ await handleOrderStatusUpdate(jsonData as OrderStatusEvent, event.pubkey)
break
-
default:
- console.warn('Unknown nostrmarket message type:', message.type)
+ console.log('Unknown message type:', jsonData.type)
}
} catch (error) {
- console.error('Failed to handle nostrmarket message:', error)
+ console.error('Error processing order event:', error)
}
}
- // Process incoming Nostr events
- const processOrderEvent = async (event: any, senderPubkey: string) => {
- try {
- console.log('Received order event:', {
- eventId: event.id || 'unknown',
- type: event.type,
- orderId: event.orderId,
- sender: senderPubkey
- })
+ // Handle customer order (type 0)
+ const handleCustomerOrder = async (orderData: CustomerOrderEvent, _senderPubkey: string) => {
+ console.log('Received customer order:', orderData)
- // Only process events that have the required market order structure
- if (!event.type || event.type !== 'market_order') {
- console.log('Skipping non-market order event:', event.type)
- return
- }
+ // Create a basic order object from the event data
+ const order = {
+ id: orderData.id,
+ type: DirectMessageType.CUSTOMER_ORDER,
+ items: orderData.items,
+ contact: orderData.contact,
+ shipping_id: orderData.shipping_id,
+ message: orderData.message,
+ createdAt: Date.now(),
+ updatedAt: Date.now()
+ }
- // Validate that this is actually a market order event
- if (!event.orderId || !event.items || !Array.isArray(event.items)) {
- console.log('Skipping invalid market order event - missing required fields')
- return
- }
+ // Store the order in our local state
+ // Note: We're not using the complex Order interface from market store
+ // Instead, we're using the simple nostrmarket format
+ console.log('Processed customer order:', order)
+ }
- console.log('Processing market order:', event)
-
- // Check if this order already exists - use the orderId as the primary key
- const existingOrder = Object.values(marketStore.orders).find(
- order => order.id === event.orderId
- )
+ // Handle payment request (type 1)
+ const handlePaymentRequest = async (paymentData: PaymentRequestEvent, _senderPubkey: string) => {
+ console.log('Received payment request:', paymentData)
+
+ // Find the lightning payment option
+ const lightningOption = paymentData.payment_options?.find(opt => opt.type === 'ln')
+ if (lightningOption) {
+ console.log('Lightning payment request:', lightningOption.link)
+ // Find the existing order by ID
+ const existingOrder = marketStore.orders[paymentData.id]
if (existingOrder) {
- console.log('Order already exists, updating with new information:', existingOrder.id)
+ console.log('Found existing order, updating with payment request:', existingOrder.id)
- // Update the existing order with any new information
- const updatedOrder = {
- ...existingOrder,
- updatedAt: Math.floor(Date.now() / 1000)
+ // Try to extract actual expiry from bolt11
+ const actualExpiry = extractExpiryFromBolt11(lightningOption.link)
+
+ // Create lightning invoice object
+ const lightningInvoice = {
+ checking_id: '', // Will be extracted from bolt11 if needed
+ payment_hash: '', // Will be extracted from bolt11 if needed
+ wallet_id: '', // Not available from payment request
+ amount: existingOrder.total,
+ fee: 0, // Not available from payment request
+ bolt11: lightningOption.link,
+ status: 'pending',
+ memo: paymentData.message || 'Payment for order',
+ created_at: new Date().toISOString(),
+ expiry: actualExpiry // Use actual expiry from bolt11 decoding
}
- // If there's invoice information, update it
- if (event.lightningInvoice) {
- updatedOrder.lightningInvoice = event.lightningInvoice
- updatedOrder.paymentHash = event.paymentHash
- updatedOrder.paymentStatus = event.paymentStatus || 'pending'
- updatedOrder.paymentRequest = event.paymentRequest
- }
-
- // Update the order in the store
- marketStore.updateOrder(existingOrder.id, updatedOrder)
-
- console.log('Updated existing order:', {
- orderId: existingOrder.id,
- hasInvoice: !!updatedOrder.lightningInvoice,
- paymentStatus: updatedOrder.paymentStatus
- })
-
- return
- }
-
- // Create a basic order object from the event data
- const orderData: Partial
= {
- id: event.orderId,
- nostrEventId: event.id || 'unknown',
- buyerPubkey: senderPubkey,
- sellerPubkey: event.sellerPubkey || '',
- items: event.items || [],
- total: event.total || 0,
- currency: event.currency || 'sat',
- status: 'pending' as OrderStatus,
- createdAt: event.createdAt || Date.now(),
- updatedAt: Date.now(),
- // Add invoice details if present
- ...(event.lightningInvoice && {
- lightningInvoice: {
- checking_id: event.lightningInvoice.checking_id || event.lightningInvoice.payment_hash || '',
- payment_hash: event.lightningInvoice.payment_hash || '',
- wallet_id: event.lightningInvoice.wallet_id || '',
- amount: event.lightningInvoice.amount || 0,
- fee: event.lightningInvoice.fee || 0,
- bolt11: event.lightningInvoice.bolt11 || event.lightningInvoice.payment_request || '',
- status: 'pending',
- memo: event.lightningInvoice.memo || '',
- expiry: event.lightningInvoice.expiry || '',
- preimage: event.lightningInvoice.preimage || '',
- extra: event.lightningInvoice.extra || {},
- created_at: event.lightningInvoice.created_at || '',
- updated_at: event.lightningInvoice.updated_at || ''
- },
- paymentHash: event.lightningInvoice.payment_hash || '',
- paymentStatus: 'pending',
- paymentRequest: event.lightningInvoice.bolt11 || event.lightningInvoice.payment_request || '',
+ // Update the order with the lightning invoice
+ marketStore.updateOrder(existingOrder.id, {
+ lightningInvoice,
+ status: 'pending',
+ paymentRequest: lightningOption.link,
updatedAt: Date.now()
})
- }
-
- // Create the order using the store method
- const order = marketStore.createOrder({
- id: event.id,
- cartId: event.id,
- stallId: 'unknown', // We'll need to determine this from the items
- buyerPubkey: senderPubkey,
- sellerPubkey: '', // Will be set when we know the merchant
- status: 'pending',
- items: Array.from(orderData.items || []), // Convert readonly to mutable
- contactInfo: orderData.contactInfo || {},
- shippingZone: orderData.shippingZone || {
- id: 'online',
- name: 'Online',
- cost: 0,
- currency: 'sat',
- description: 'Online delivery'
- },
- paymentMethod: 'lightning',
- subtotal: 0,
- shippingCost: 0,
- total: 0,
- currency: 'sat',
- originalOrderId: event.id
- })
-
- console.log('Created order from market event:', {
- orderId: order.id,
- total: order.total,
- status: order.status
- })
-
- } catch (error) {
- console.error('Failed to handle market order:', error)
- }
- }
-
- // Handle payment request events
- const handlePaymentRequest = async (event: PaymentRequestEvent, _senderPubkey: string) => {
- try {
- // Find the order in our store
- const order = marketStore.orders[event.orderId]
- if (!order) {
- console.warn('Payment request received for unknown order:', event.orderId)
- return
- }
-
- // Update order with payment request (excluding readonly items)
- const { items, ...orderWithoutItems } = order
- const updatedOrder = {
- ...orderWithoutItems,
- paymentRequest: event.paymentRequest,
- paymentStatus: 'pending' as const,
- updatedAt: Date.now() / 1000
- }
-
- // Update the order in the store
- marketStore.updateOrder(event.orderId, updatedOrder)
-
- console.log('Order updated with payment request:', {
- orderId: event.orderId,
- amount: event.amount,
- currency: event.currency
- })
-
- } catch (error) {
- console.error('Failed to handle payment request:', error)
- }
- }
-
- // Handle order status updates
- const handleOrderStatusUpdate = async (event: OrderStatusEvent, _senderPubkey: string) => {
- try {
- // Find the order in our store
- const order = marketStore.orders[event.orderId]
- if (!order) {
- console.warn('Status update received for unknown order:', event.orderId)
- return
- }
-
- // Update order status
- marketStore.updateOrderStatus(event.orderId, event.status)
-
- console.log('Order status updated:', {
- orderId: event.orderId,
- newStatus: event.status,
- timestamp: event.timestamp
- })
-
- } catch (error) {
- console.error('Failed to handle order status update:', error)
- }
- }
-
- // Handle invoice generation events
- const handleInvoiceGenerated = async (event: any, _senderPubkey: string) => {
- try {
- // Find the order in our store
- const order = marketStore.orders[event.orderId]
- if (!order) {
- console.warn('Invoice generated for unknown order:', event.orderId)
- return
- }
-
- // Update order with invoice details (excluding readonly items)
- const { items, ...orderWithoutItems } = order
- const updatedOrder = {
- ...orderWithoutItems,
- lightningInvoice: {
- payment_hash: event.paymentHash,
- payment_request: event.paymentRequest,
- amount: event.amount,
- memo: event.memo,
- expiry: event.expiresAt,
- created_at: event.timestamp,
- status: 'pending' as const
- },
- paymentHash: event.paymentHash,
- paymentStatus: 'pending' as const,
- updatedAt: Date.now() / 1000
- }
-
- // Update the order in the store
- marketStore.updateOrder(event.orderId, updatedOrder)
-
- console.log('Order updated with invoice details:', {
- orderId: event.orderId,
- paymentHash: event.paymentHash,
- amount: event.amount
- })
-
- } catch (error) {
- console.error('Failed to handle invoice generation:', error)
- }
- }
-
- // Handle market order events (new orders)
- const handleMarketOrder = async (event: any, senderPubkey: string) => {
- try {
- console.log('Processing market order:', event)
-
- // Check if this order already exists
- const existingOrder = Object.values(marketStore.orders).find(
- order => order.id === event.orderId || order.nostrEventId === event.id
- )
-
- if (existingOrder) {
- console.log('Order already exists, updating with new information:', existingOrder.id)
- // Update the existing order with any new information
- const updatedOrder = {
- ...existingOrder,
- ...event,
- updatedAt: Math.floor(Date.now() / 1000)
- }
-
- // If there's invoice information, update it
- if (event.lightningInvoice) {
- updatedOrder.lightningInvoice = event.lightningInvoice
- updatedOrder.paymentHash = event.paymentHash
- updatedOrder.paymentStatus = event.paymentStatus || 'pending'
- updatedOrder.paymentRequest = event.paymentRequest
- }
-
- // Update the order in the store
- marketStore.updateOrder(existingOrder.id, updatedOrder)
-
- console.log('Updated existing order:', {
- orderId: existingOrder.id,
- hasInvoice: !!updatedOrder.lightningInvoice,
- paymentStatus: updatedOrder.paymentStatus
- })
-
- return
+ console.log('Order updated with payment request:', existingOrder.id)
+ } else {
+ console.warn('Order not found for payment request:', paymentData.id)
}
-
- // Create a basic order object from the event data
- const orderData: Partial = {
- id: event.orderId,
- nostrEventId: event.id,
- buyerPubkey: event.pubkey || '',
- sellerPubkey: event.sellerPubkey || '',
- items: event.items || [],
- total: event.total || 0,
- currency: event.currency || 'sat',
- status: 'pending' as OrderStatus,
- createdAt: event.createdAt || Date.now(),
- updatedAt: Date.now(),
- // Add invoice details if present
- ...(event.lightningInvoice && {
- lightningInvoice: {
- checking_id: event.lightningInvoice.checking_id || event.lightningInvoice.payment_hash || '',
- payment_hash: event.lightningInvoice.payment_hash || '',
- wallet_id: event.lightningInvoice.wallet_id || '',
- amount: event.lightningInvoice.amount || 0,
- fee: event.lightningInvoice.fee || 0,
- bolt11: event.lightningInvoice.bolt11 || event.lightningInvoice.payment_request || '',
- status: 'pending',
- memo: event.lightningInvoice.memo || '',
- expiry: event.lightningInvoice.expiry || '',
- preimage: event.lightningInvoice.preimage || '',
- extra: event.lightningInvoice.extra || {},
- created_at: event.lightningInvoice.created_at || '',
- updated_at: event.lightningInvoice.updated_at || ''
- },
- paymentHash: event.lightningInvoice.payment_hash || '',
- paymentStatus: 'pending',
- paymentRequest: event.lightningInvoice.bolt11 || event.lightningInvoice.payment_request || '',
- updatedAt: Date.now()
- })
- }
-
- // Create the order using the store method
- const order = marketStore.createOrder(orderData)
-
- console.log('Created order from market event:', {
- orderId: order.id,
- total: order.total,
- status: order.status
- })
-
- } catch (error) {
- console.error('Failed to handle market order:', error)
}
}
- // Start listening for order events
- const startListening = async () => {
- if (!isReady.value) {
- console.warn('Cannot start listening: not ready')
- return
- }
+ // Handle order status update (type 2)
+ const handleOrderStatusUpdate = async (statusData: OrderStatusEvent, _senderPubkey: string) => {
+ console.log('Received order status update:', statusData)
- try {
- await subscribeToOrderEvents()
- console.log('Started listening for order events')
- } catch (error) {
- console.error('Failed to start listening for order events:', error)
+ // Update order status in local state
+ if (statusData.paid !== undefined) {
+ console.log(`Order ${statusData.id} payment status: ${statusData.paid}`)
+ }
+ if (statusData.shipped !== undefined) {
+ console.log(`Order ${statusData.id} shipping status: ${statusData.shipped}`)
}
}
- // Stop listening for order events
- const stopListening = () => {
+ // Unsubscribe from order events
+ const unsubscribeFromOrderEvents = () => {
if (subscriptionId.value) {
- // Use the cleanup method from relayHub
relayHub.cleanup()
subscriptionId.value = null
}
isSubscribed.value = false
- console.log('Stopped listening for order events')
+ console.log('Unsubscribed from order events')
}
- // Clean up old processed events
- const cleanupProcessedEvents = () => {
- // const now = Date.now()
- // const cutoff = now - (24 * 60 * 60 * 1000) // 24 hours ago
-
- // Remove old event IDs (this is a simple cleanup, could be more sophisticated)
- if (processedEventIds.value.size > 1000) {
- processedEventIds.value.clear()
- console.log('Cleaned up processed event IDs')
+ // Watch for ready state changes
+ const watchReadyState = () => {
+ if (isReady.value && !isSubscribed.value) {
+ subscribeToOrderEvents()
+ } else if (!isReady.value && isSubscribed.value) {
+ unsubscribeFromOrderEvents()
}
}
+ // Watch for authentication changes
+ const watchAuthChanges = () => {
+ if (auth.isAuthenticated && relayHub.isConnected.value) {
+ subscribeToOrderEvents()
+ } else {
+ unsubscribeFromOrderEvents()
+ }
+ }
+
+ // Initialize subscription when ready
+ const initialize = () => {
+ if (isReady.value) {
+ subscribeToOrderEvents()
+ }
+ }
+
+ // Cleanup
+ const cleanup = () => {
+ unsubscribeFromOrderEvents()
+ processedEventIds.value.clear()
+ }
+
return {
// State
- isSubscribed,
- lastEventTimestamp,
+ isSubscribed: computed(() => isSubscribed.value),
+ isReady: computed(() => isReady.value),
+ lastEventTimestamp: computed(() => lastEventTimestamp.value),
// Methods
- startListening,
- stopListening,
subscribeToOrderEvents,
- cleanupProcessedEvents
+ unsubscribeFromOrderEvents,
+ initialize,
+ cleanup,
+ watchReadyState,
+ watchAuthChanges
}
}
-
-// Export singleton instance
-export const orderEvents = useOrderEvents()
diff --git a/src/lib/nostr/utils.ts b/src/lib/nostr/utils.ts
deleted file mode 100644
index 5ed6c1f..0000000
--- a/src/lib/nostr/utils.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-// Helper function to convert bech32 to hex
-export function bech32ToHex(bech32Key: string): string {
- if (bech32Key.startsWith('npub1') || bech32Key.startsWith('nsec1')) {
- // Import bech32 conversion dynamically to avoid bundling issues
- const { bech32Decode, convertbits } = require('bech32')
- const [, data] = bech32Decode(bech32Key)
- if (!data) {
- throw new Error(`Invalid bech32 key: ${bech32Key}`)
- }
- const converted = convertbits(data, 5, 8, false)
- if (!converted) {
- throw new Error(`Failed to convert bech32 key: ${bech32Key}`)
- }
- return Buffer.from(converted).toString('hex')
- }
- // Already hex format
- return bech32Key
-}
diff --git a/src/lib/services/invoiceService.ts b/src/lib/services/invoiceService.ts
index b0a586e..f6dfab5 100644
--- a/src/lib/services/invoiceService.ts
+++ b/src/lib/services/invoiceService.ts
@@ -82,7 +82,7 @@ class InvoiceService {
amount: order.total,
unit: 'sat',
memo: `Order ${order.id} - ${order.items.length} items`,
- expiry: 3600, // 1 hour
+ expiry: extra?.expiry || 3600, // Allow configurable expiry, default 1 hour
extra: {
tag: 'nostrmarket', // Use nostrmarket tag for compatibility
order_id: extra?.order_id || order.id, // Use passed order_id or fallback to order.id
@@ -113,6 +113,8 @@ class InvoiceService {
console.log('Full LNBits response:', response)
console.log('Response type:', typeof response)
console.log('Response keys:', Object.keys(response))
+ console.log('Response expiry field:', response.expiry)
+ console.log('Response created_at field:', response.created_at)
// Check if we have the expected fields
if (!response.bolt11) {
diff --git a/src/lib/services/nostrmarketService.ts b/src/lib/services/nostrmarketService.ts
index 016cbc2..51a6a8a 100644
--- a/src/lib/services/nostrmarketService.ts
+++ b/src/lib/services/nostrmarketService.ts
@@ -2,7 +2,6 @@ import { finalizeEvent, type EventTemplate, nip04 } from 'nostr-tools'
import { relayHub } from '@/lib/nostr/relayHub'
import { auth } from '@/composables/useAuth'
import type { Stall, Product, Order } from '@/stores/market'
-import { bech32ToHex } from '@/lib/utils/bech32'
export interface NostrmarketStall {
id: string
@@ -69,24 +68,49 @@ export interface NostrmarketOrderStatus {
}
export class NostrmarketService {
+ /**
+ * Convert hex string to Uint8Array (browser-compatible)
+ */
+ private hexToUint8Array(hex: string): Uint8Array {
+ const bytes = new Uint8Array(hex.length / 2)
+ for (let i = 0; i < hex.length; i += 2) {
+ bytes[i / 2] = parseInt(hex.substr(i, 2), 16)
+ }
+ return bytes
+ }
+
private getAuth() {
if (!auth.isAuthenticated.value || !auth.currentUser.value?.prvkey) {
throw new Error('User not authenticated or private key not available')
}
- // Convert bech32 keys to hex format if needed
- const originalPubkey = auth.currentUser.value.pubkey
- const originalPrvkey = auth.currentUser.value.prvkey
- const pubkey = bech32ToHex(originalPubkey)
- const prvkey = bech32ToHex(originalPrvkey)
+ const pubkey = auth.currentUser.value.pubkey
+ const prvkey = auth.currentUser.value.prvkey
- console.log('🔑 Key conversion debug:', {
- originalPubkey: originalPubkey?.substring(0, 10) + '...',
- originalPrvkey: originalPrvkey?.substring(0, 10) + '...',
- convertedPubkey: pubkey.substring(0, 10) + '...',
- convertedPrvkey: prvkey.substring(0, 10) + '...',
+ if (!pubkey || !prvkey) {
+ throw new Error('Public key or private key is missing')
+ }
+
+ // Validate that we have proper hex strings
+ if (!/^[0-9a-fA-F]{64}$/.test(pubkey)) {
+ throw new Error(`Invalid public key format: ${pubkey.substring(0, 10)}...`)
+ }
+
+ if (!/^[0-9a-fA-F]{64}$/.test(prvkey)) {
+ throw new Error(`Invalid private key format: ${prvkey.substring(0, 10)}...`)
+ }
+
+ console.log('🔑 Key debug:', {
+ pubkey: pubkey.substring(0, 10) + '...',
+ prvkey: prvkey.substring(0, 10) + '...',
pubkeyIsHex: /^[0-9a-fA-F]{64}$/.test(pubkey),
- prvkeyIsHex: /^[0-9a-fA-F]{64}$/.test(prvkey)
+ prvkeyIsHex: /^[0-9a-fA-F]{64}$/.test(prvkey),
+ pubkeyLength: pubkey.length,
+ prvkeyLength: prvkey.length,
+ pubkeyType: typeof pubkey,
+ prvkeyType: typeof prvkey,
+ pubkeyIsString: typeof pubkey === 'string',
+ prvkeyIsString: typeof prvkey === 'string'
})
return {
@@ -99,7 +123,7 @@ export class NostrmarketService {
* Publish a stall event (kind 30017) to Nostr
*/
async publishStall(stall: Stall): Promise {
- const { pubkey, prvkey } = this.getAuth()
+ const { prvkey } = this.getAuth()
const stallData: NostrmarketStall = {
id: stall.id,
@@ -124,23 +148,24 @@ export class NostrmarketService {
created_at: Math.floor(Date.now() / 1000)
}
- const event = finalizeEvent(eventTemplate, prvkey)
- const eventId = await relayHub.publishEvent(event)
+ const prvkeyBytes = this.hexToUint8Array(prvkey)
+ const event = finalizeEvent(eventTemplate, prvkeyBytes)
+ const result = await relayHub.publishEvent(event)
console.log('Stall published to nostrmarket:', {
stallId: stall.id,
- eventId: eventId,
+ eventId: result,
content: stallData
})
- return eventId
+ return result.success.toString()
}
/**
* Publish a product event (kind 30018) to Nostr
*/
async publishProduct(product: Product): Promise {
- const { pubkey, prvkey } = this.getAuth()
+ const { prvkey } = this.getAuth()
const productData: NostrmarketProduct = {
id: product.id,
@@ -166,23 +191,24 @@ export class NostrmarketService {
created_at: Math.floor(Date.now() / 1000)
}
- const event = finalizeEvent(eventTemplate, prvkey)
- const eventId = await relayHub.publishEvent(event)
+ const prvkeyBytes = this.hexToUint8Array(prvkey)
+ const event = finalizeEvent(eventTemplate, prvkeyBytes)
+ const result = await relayHub.publishEvent(event)
console.log('Product published to nostrmarket:', {
productId: product.id,
- eventId: eventId,
+ eventId: result,
content: productData
})
- return eventId
+ return result.success.toString()
}
/**
* Publish an order event (kind 4 encrypted DM) to nostrmarket
*/
async publishOrder(order: Order, merchantPubkey: string): Promise {
- const { pubkey, prvkey } = this.getAuth()
+ const { prvkey } = this.getAuth()
// Convert order to nostrmarket format - exactly matching the specification
const orderData = {
@@ -205,7 +231,27 @@ export class NostrmarketService {
}
// Encrypt the message using NIP-04
- const encryptedContent = await nip04.encrypt(prvkey, merchantPubkey, JSON.stringify(orderData))
+ console.log('🔐 NIP-04 encryption debug:', {
+ prvkeyType: typeof prvkey,
+ prvkeyIsString: typeof prvkey === 'string',
+ prvkeyLength: prvkey.length,
+ prvkeySample: prvkey.substring(0, 10) + '...',
+ merchantPubkeyType: typeof merchantPubkey,
+ merchantPubkeyLength: merchantPubkey.length,
+ orderDataString: JSON.stringify(orderData).substring(0, 50) + '...'
+ })
+
+ let encryptedContent: string
+ try {
+ encryptedContent = await nip04.encrypt(prvkey, merchantPubkey, JSON.stringify(orderData))
+ console.log('🔐 NIP-04 encryption successful:', {
+ encryptedContentLength: encryptedContent.length,
+ encryptedContentSample: encryptedContent.substring(0, 50) + '...'
+ })
+ } catch (error) {
+ console.error('🔐 NIP-04 encryption failed:', error)
+ throw error
+ }
const eventTemplate: EventTemplate = {
kind: 4, // Encrypted DM
@@ -214,18 +260,36 @@ export class NostrmarketService {
created_at: Math.floor(Date.now() / 1000)
}
- const event = finalizeEvent(eventTemplate, prvkey)
- const eventId = await relayHub.publishEvent(event)
+ console.log('🔧 finalizeEvent debug:', {
+ prvkeyType: typeof prvkey,
+ prvkeyIsString: typeof prvkey === 'string',
+ prvkeyLength: prvkey.length,
+ prvkeySample: prvkey.substring(0, 10) + '...',
+ encodedPrvkeyType: typeof new TextEncoder().encode(prvkey),
+ encodedPrvkeyLength: new TextEncoder().encode(prvkey).length,
+ eventTemplate
+ })
+
+ // Convert hex string to Uint8Array properly
+ const prvkeyBytes = this.hexToUint8Array(prvkey)
+ console.log('🔧 prvkeyBytes debug:', {
+ prvkeyBytesType: typeof prvkeyBytes,
+ prvkeyBytesLength: prvkeyBytes.length,
+ prvkeyBytesIsUint8Array: prvkeyBytes instanceof Uint8Array
+ })
+
+ const event = finalizeEvent(eventTemplate, prvkeyBytes)
+ const result = await relayHub.publishEvent(event)
console.log('Order published to nostrmarket:', {
orderId: order.id,
- eventId: eventId,
+ eventId: result,
merchantPubkey,
content: orderData,
encryptedContent: encryptedContent.substring(0, 50) + '...'
})
- return eventId
+ return result.success.toString()
}
/**
diff --git a/src/lib/utils/bech32.ts b/src/lib/utils/bech32.ts
deleted file mode 100644
index 249c1e8..0000000
--- a/src/lib/utils/bech32.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { nip19 } from 'nostr-tools'
-
-// Helper function to convert bech32 to hex using nostr-tools
-export function bech32ToHex(bech32Key: string): string {
- if (bech32Key.startsWith('npub1') || bech32Key.startsWith('nsec1')) {
- const { type, data } = nip19.decode(bech32Key)
- return data as string
- }
- // Already hex format
- return bech32Key
-}
diff --git a/src/pages/OrderHistory.vue b/src/pages/OrderHistory.vue
index 72818b3..377e9ea 100644
--- a/src/pages/OrderHistory.vue
+++ b/src/pages/OrderHistory.vue
@@ -307,10 +307,11 @@ import { Badge } from '@/components/ui/badge'
import { Package, Wallet } from 'lucide-vue-next'
import type { OrderStatus } from '@/stores/market'
import PaymentDisplay from '@/components/market/PaymentDisplay.vue'
-import { orderEvents } from '@/composables/useOrderEvents'
+import { useOrderEvents } from '@/composables/useOrderEvents'
const router = useRouter()
const marketStore = useMarketStore()
+const orderEvents = useOrderEvents()
// Local state
const statusFilter = ref('')
@@ -419,7 +420,7 @@ onMounted(() => {
// Start listening for order events if not already listening
if (!orderEvents.isSubscribed.value) {
- orderEvents.startListening()
+ orderEvents.initialize()
}
})
diff --git a/src/stores/market.ts b/src/stores/market.ts
index 27b7001..798a8cd 100644
--- a/src/stores/market.ts
+++ b/src/stores/market.ts
@@ -1,9 +1,10 @@
import { defineStore } from 'pinia'
-import { ref, computed, readonly } from 'vue'
+import { ref, computed, readonly, watch } from 'vue'
import { nostrOrders } from '@/composables/useNostrOrders'
import { invoiceService } from '@/lib/services/invoiceService'
import { paymentMonitor } from '@/lib/services/paymentMonitor'
import { nostrmarketService } from '@/lib/services/nostrmarketService'
+import { useAuth } from '@/composables/useAuth'
import type { LightningInvoice } from '@/lib/services/invoiceService'
@@ -85,6 +86,7 @@ export interface Order {
qrCodeDataUrl?: string
qrCodeLoading?: boolean
qrCodeError?: string | null
+ showQRCode?: boolean // Toggle QR code visibility
}
export interface OrderItem {
@@ -166,6 +168,13 @@ export interface PaymentStatus {
}
export const useMarketStore = defineStore('market', () => {
+ const auth = useAuth()
+
+ // Helper function to get user-specific storage key
+ const getUserStorageKey = (baseKey: string) => {
+ const userPubkey = auth.currentUser?.value?.pubkey
+ return userPubkey ? `${baseKey}_${userPubkey}` : baseKey
+ }
// Core market state
const markets = ref([])
const stalls = ref([])
@@ -807,7 +816,9 @@ export const useMarketStore = defineStore('market', () => {
// Persistence methods
const saveOrdersToStorage = () => {
try {
- localStorage.setItem('market_orders', JSON.stringify(orders.value))
+ const storageKey = getUserStorageKey('market_orders')
+ localStorage.setItem(storageKey, JSON.stringify(orders.value))
+ console.log('Saved orders to localStorage with key:', storageKey)
} catch (error) {
console.warn('Failed to save orders to localStorage:', error)
}
@@ -815,17 +826,30 @@ export const useMarketStore = defineStore('market', () => {
const loadOrdersFromStorage = () => {
try {
- const stored = localStorage.getItem('market_orders')
+ const storageKey = getUserStorageKey('market_orders')
+ const stored = localStorage.getItem(storageKey)
if (stored) {
const parsedOrders = JSON.parse(stored)
orders.value = parsedOrders
- console.log('Loaded orders from localStorage:', Object.keys(parsedOrders).length)
+ console.log('Loaded orders from localStorage with key:', storageKey, 'Orders count:', Object.keys(parsedOrders).length)
+ } else {
+ console.log('No orders found in localStorage for key:', storageKey)
+ // Clear any existing orders when switching users
+ orders.value = {}
}
} catch (error) {
console.warn('Failed to load orders from localStorage:', error)
+ // Clear orders on error
+ orders.value = {}
}
}
+ // Clear orders when user changes
+ const clearOrdersForUserChange = () => {
+ orders.value = {}
+ console.log('Cleared orders for user change')
+ }
+
// Payment utility methods
const calculateOrderTotal = (cart: StallCart, shippingZone?: ShippingZone) => {
const subtotal = cart.subtotal
@@ -918,6 +942,15 @@ export const useMarketStore = defineStore('market', () => {
// Initialize orders from localStorage
loadOrdersFromStorage()
+ // Watch for user changes and reload orders
+ watch(() => auth.currentUser?.value?.pubkey, (newPubkey, oldPubkey) => {
+ if (newPubkey !== oldPubkey) {
+ console.log('User changed, clearing and reloading orders. Old:', oldPubkey, 'New:', newPubkey)
+ clearOrdersForUserChange()
+ loadOrdersFromStorage()
+ }
+ })
+
return {
// State
markets: readonly(markets),
@@ -997,6 +1030,7 @@ export const useMarketStore = defineStore('market', () => {
updateOrder,
saveOrdersToStorage,
loadOrdersFromStorage,
+ clearOrdersForUserChange,
publishToNostrmarket
}
})
\ No newline at end of file