feat: Add market integration roadmap to NOSTR architecture documentation
- Introduce a comprehensive roadmap for integrating nostr-market-app purchasing functionality into the web-app. - Outline key components of the shopping cart system, checkout process, and order management. - Detail phased implementation strategy, including enhanced user experience and advanced features. - Include security, performance, and testing considerations to ensure robust integration. feat: Enhance market store with new order and cart management features - Introduce new interfaces for Order, OrderItem, ContactInfo, and ShippingZone to support enhanced order management. - Update Stall and Product interfaces to include currency and shipping details. - Implement a comprehensive shopping cart system with stall-specific carts, including methods for adding, removing, and updating items. - Add payment-related interfaces and methods for managing payment requests and statuses. - Enhance filter options to include in-stock status and payment methods, improving product filtering capabilities. - Refactor computed properties and methods for better cart management and checkout processes. feat: Implement shopping cart functionality with new components and routing - Add ShoppingCart, CartItem, and CartSummary components to manage cart items and display summaries. - Introduce Cart.vue page to serve as the main shopping cart interface, integrating cart and summary components. - Update Navbar.vue to include a cart icon with item count, enhancing user navigation. - Implement cart management features in the market store, including item addition, quantity updates, and removal. - Establish routing for the cart page, ensuring seamless navigation for users. - Enhance ProductCard.vue to support adding items to the cart directly from the product listing. feat: Update cart and checkout functionality with improved navigation and button labels - Change "Proceed to Checkout" button text to dynamic "Place Order" based on context in CartSummary.vue. - Update "Continue Shopping" button to "Back to Cart" in CartSummary.vue for clearer navigation. - Modify routing for checkout to include stall ID in ShoppingCart.vue, enhancing checkout process. - Simplify Cart.vue by removing CartSummary component and focusing on ShoppingCart display. - Add new route for checkout with stall ID in router configuration for better handling of checkout flows. feat: Enhance cart and checkout components with improved shipping address handling - Update CartSummary.vue to use readonly types for cart items and shipping zones, ensuring immutability. - Modify Checkout.vue to conditionally display the shipping address field based on the selected shipping zone's requirements for physical shipping. - Add a digital delivery note for products that do not require a shipping address. - Introduce a computed property to determine if a shipping address is required, improving validation logic during checkout. - Update market store to include a new property for shipping zones indicating if physical shipping is required. feat: Implement order placement functionality in checkout process - Add a "Place Order" button in Checkout.vue that triggers the order placement process. - Introduce loading state during order placement to enhance user experience. - Implement createAndPlaceOrder method in market store to handle order creation and status updates. - Include error handling for order placement failures, providing user feedback on errors. - Update checkout logic to validate shipping zone and contact information before proceeding. feat: Add Order History page and update Navbar for order tracking - Introduce a new OrderHistory.vue page to display users' past orders with filtering and sorting options. - Update Navbar.vue to include an "Order History" option with a badge showing the count of orders. - Implement computed properties for order count and enhance user navigation experience. feat: Integrate Nostr functionality for order management and user notifications - Add NostrExtensionGuide component to inform users about the required Nostr extension for order transmission. - Implement useNostrOrders composable to manage Nostr connection, event creation, and order sending. - Update Checkout.vue to display Nostr connection status and provide feedback on order transmission. - Enhance OrderHistory.vue to show Nostr transmission status and details for each order. - Modify market store to handle Nostr event details and errors during order placement, ensuring local fallback. - Introduce types for Nostr events to improve type safety and integration with the existing order management system. refactor: Update Nostr relay configuration to use environment variable - Change DEFAULT_RELAYS to dynamically retrieve relay URLs from the VITE_MARKET_RELAYS environment variable. - Add error handling to ensure relays are configured before establishing a connection. - Modify createBlankEvent function to return a more precise type. - Update event signing process to ensure the event ID is generated correctly before signing. refactor: useAuth switch Enhance Nostr order management with authentication checks - Integrate user authentication checks to ensure Nostr features are only accessible to authenticated users. - Replace direct window.nostr calls with auth store methods for retrieving public and private keys. - Implement a helper function for signing events and mock encryption for order content. - Remove obsolete Nostr type definitions to streamline the codebase. feat: Enhance Checkout.vue with Nostr processing feedback and cleanup - Update the checkout button to disable based on order placement state. - Simplify order placement feedback by removing unnecessary Nostr processing checks. - Introduce a new visual indicator for Nostr order processing status. - Refactor computed properties for better clarity and efficiency in shipping zone handling. refactor: Streamline Nostr order handling and integrate buyer public key retrieval - Remove redundant Nostr relay tag from order event creation in useNostrOrders. - Update Checkout.vue to retrieve the buyer's public key from the auth store, enhancing order placement logic. - Modify createAndPlaceOrder method in market store to accept an optional Nostr orders instance for improved flexibility in order processing. refactor: Remove Nostr-related components and streamline order processing - Delete NostrExtensionGuide.vue and associated type definitions to simplify the codebase. - Remove unused useNostr.ts file and related logic from useNostrOrders.ts. - Update order handling in market store to directly integrate Nostr publishing without relying on external components. - Enhance Checkout.vue and Cart.vue to reflect changes in Nostr integration and provide clearer order status feedback. feat: Enhance Nostr chat functionality with malformed message handling - Introduce tracking for malformed message IDs to prevent repeated processing attempts. - Implement functions to mark messages as malformed, clean up old entries, and retrieve statistics on malformed messages. - Add periodic cleanup of malformed messages to manage memory usage. - Enhance message processing logic to skip previously identified malformed messages and provide detailed error handling for decryption failures. - Update the return object to include new functions for managing malformed messages. ZZ feat: Implement Lightning invoice management in market store - Add functionality to create and manage Lightning invoices for orders. - Introduce payment monitoring and status updates for invoices. - Implement payment confirmation messaging via Nostr upon successful payment. - Enhance order interface to include new fields for Lightning invoice details and payment status. ZZ feat: Enhance OrderHistory.vue with payment status indicators and invoice management - Add visual indicators for payment status, including 'Paid' and 'Payment Pending' badges. - Implement expandable payment display for orders with Lightning invoices. - Introduce functionality to toggle payment display and generate Lightning invoices. - Update order status messaging to reflect payment requirements and invoice generation status. ZZ feat: Enhance OrderHistory.vue with payment status indicators and invoice management - Add visual indicators for payment status, including 'Paid' and 'Payment Pending' badges. - Implement expandable payment display for orders with Lightning invoices. - Introduce functionality to toggle payment display and generate Lightning invoices. - Update order status messaging to reflect payment requirements and invoice generation status. feat: Implement order event handling in useOrderEvents composable - Introduce useOrderEvents composable to manage subscription and processing of order-related events. - Define order event types and interfaces for better type safety and clarity. - Implement methods to handle payment requests, order status updates, and invoice generation. - Enhance OrderHistory.vue to display order event subscription status and last update timestamp. - Update market store to include order update functionality for better integration with order events. FIX: Build errors refactor: Update component styles and improve UI consistency across market pages - Replace various color classes with updated design tokens for better consistency. - Change background colors of components to align with the new design system. - Update text colors to enhance readability and maintain a cohesive look. - Refactor class names in CartItem.vue, CartSummary.vue, DashboardOverview.vue, and other components to use the new color scheme. - Ensure all components reflect the updated design guidelines for a unified user experience. refactor: Remove Order History references from Navbar component - Eliminate order count computation and related UI elements from the Navbar. - Streamline the Navbar by removing the Order History button and badge. - Maintain existing functionality for other menu items, ensuring a cleaner user interface. feat: Implement QR code generation and download functionality in PaymentDisplay component - Add QR code generation for payment requests using the qrcode library. - Enhance UI to display loading states and error messages during QR code generation. - Introduce a download button for users to save the generated QR code. - Implement logic to regenerate QR code when the invoice changes. refactor: Replace useRelayHub with relayHubComposable across components - Update imports in multiple components and composables to use the new relayHubComposable for better consistency and maintainability. - Enhance OrderHistory.vue with debug information for development, displaying key states related to orders, authentication, and relay hub connectivity. - Remove unnecessary reconnect button from RelayHubStatus.vue to streamline user interactions. - Improve logging in useOrderEvents for better debugging and monitoring of order event subscriptions. refactor: Update OrderHistory.vue styles for improved UI consistency - Replace color classes with updated design tokens for better alignment with the new design system. - Enhance readability by adjusting text colors and background styles for payment status indicators. - Ensure a cohesive look across the component by standardizing class names and styles. refactor: Update component styles for improved UI consistency across checkout pages - Replace color classes with updated design tokens for better alignment with the new design system. - Enhance readability by adjusting text colors and background styles in CartSummary.vue, PaymentDisplay.vue, Checkout.vue, and OrderHistory.vue. - Standardize class names and styles to ensure a cohesive look across all components. feat: Implement invoice generation and Nostr integration in MerchantStore component - Add functionality to generate Lightning invoices for orders and send them to customers via Nostr. - Introduce a new sendInvoiceToCustomer method to update order details and publish invoice information. - Enhance order event handling in useOrderEvents to update existing orders with new invoice data. - Improve error handling and logging for invoice generation and sending processes. feat: Enhance MerchantStore and PaymentDisplay components for improved invoice handling - Add wallet indicator in MerchantStore to display the selected wallet name during pending orders. - Implement temporary fixes for missing buyer and seller public keys when generating invoices. - Update invoice generation logic to utilize the first available wallet and improve error handling. - Modify PaymentDisplay to use the new bolt11 field for payment requests and enhance date formatting. - Refactor order event handling to ensure accurate updates and invoice management across components. feat: Enhance order event processing in useOrderEvents composable - Refactor processOrderEvent to handle incoming Nostr market order events with improved validation and logging. - Implement logic to update existing orders or create new ones based on event data, ensuring accurate order management. - Add detailed console logging for better debugging and tracking of order events and their statuses. - Ensure compatibility with market order structure and invoice details for seamless integration with payment processing. feat: Enhance order management with localStorage persistence - Update createOrder method to optionally accept an order ID from events, improving order tracking. - Convert items from readonly to mutable for better manipulation. - Implement localStorage persistence for orders, ensuring data is saved and loaded across sessions. - Add methods to save and load orders from localStorage, enhancing user experience and data reliability. feat: Update invoice creation to support additional metadata and nostrmarket compatibility - Modify createInvoice method to accept an optional extra parameter for additional metadata. - Change invoice tag to 'nostrmarket' for improved compatibility with Nostr market. - Include merchant and buyer public keys in the invoice data for better integration. - Update invoice creation in market store to utilize new parameters for enhanced functionality. feat: Enhance order and invoice handling for Nostr market compatibility - Add originalOrderId to order events for tracking Nostr order IDs. - Update invoice creation to utilize original Nostr order ID when generating invoices. - Improve logging for invoice requests to LNBits, providing better visibility into the data being sent. - Ensure compatibility with nostrmarket by adjusting order ID handling in the market store. fix: Refine invoice creation logic for Nostr market compatibility - Adjust order ID handling in invoice creation to prioritize originalOrderId for better compatibility with nostrmarket. - Enhance logging to provide clearer insights into the order ID being used during invoice generation. feat: Integrate nostrmarket service for order publishing and merchant catalog management - Implement functionality to publish orders via the nostrmarket protocol, replacing the previous Nostr integration. - Add methods to publish merchant catalogs, including stalls and products, to nostrmarket with event ID tracking. - Enhance order interface to include nostrEventId for better integration with nostrmarket. - Improve error handling and logging for nostrmarket publishing processes. refactor: Simplify order creation logic in useOrderEvents and update contact structure in nostrmarketService - Streamline order creation by using event.id and defaulting to 'unknown' for stallId. - Update contact structure to include address and message, removing optional email and phone fields for clarity. - Ensure compatibility with new order data structure for improved integration with nostrmarket. feat: Add bech32 to hex conversion utility and integrate into nostrmarketService - Implement a new utility function to convert bech32 keys to hex format, enhancing key handling. - Update nostrmarketService to utilize the new conversion function for user public and private keys. - Modify contact structure to include additional fields for improved order information management. feat: Add nostrclient configuration to AppConfig for enhanced Nostr integration - Introduce a new nostrclient property in AppConfig to manage Nostr client settings. - Include url and enabled fields to configure the Nostr client connection dynamically. - Ensure compatibility with environment variables for flexible deployment configurations. feat: Introduce comprehensive order management and fulfillment documentation - Add ORDER_MANAGEMENT_FULFILLMENT.md to detail the complete order lifecycle, including order states, data models, and merchant/customer interfaces. - Implement test scripts for verifying order and payment request formats in test-nostrmarket-format.js. - Create PaymentRequestDialog.vue for handling payment requests with dynamic options and QR code generation. - Enhance useOrderEvents.ts to process nostrmarket protocol messages for order management. - Update nostrmarketService.ts to handle payment requests and order status updates, ensuring seamless integration with the marketplace. - Integrate payment request dialog in Market.vue and manage its state in the market store. refactor: Remove obsolete test script for nostrmarket order format - Delete test-nostrmarket-format.js as it is no longer needed for verifying order and payment request formats. - Update PaymentRequestDialog.vue to enhance UI components and integrate QR code generation for payment requests. - Refactor payment handling and notification logic to utilize toast notifications instead of Quasar's notify system. feat: Enhance OrderHistory component with payment request handling and QR code generation - Add UI elements to display payment request status and options in OrderHistory.vue. - Implement functions to copy payment requests, open Lightning wallets, and download QR codes. - Update nostrmarketService to generate QR codes for payment requests and manage order statuses effectively. - Remove obsolete PaymentRequestDialog integration from Market.vue for a cleaner UI. feat: Add debug information and toast notifications in OrderHistory component - Introduce debug info display for payment requests and hashes in OrderHistory.vue. - Implement toast notifications for actions like copying payment requests, opening wallets, and downloading QR codes. - Enhance error handling with user feedback for various order-related actions. - Remove obsolete payment request dialog methods from market store for cleaner code. feat: Revamp CartItem and ShoppingCart components for improved layout and functionality - Enhance CartItem.vue with responsive design for desktop and mobile views, including better organization of product details, price, quantity controls, and remove button. - Update ShoppingCart.vue to separate desktop and mobile layouts, improving the user experience with clearer action buttons and cart summary display. - Implement consistent styling and layout adjustments for better visual coherence across different screen sizes.
This commit is contained in:
parent
93ffb8bf32
commit
ea5a2380f1
43 changed files with 8983 additions and 146 deletions
|
|
@ -1,7 +1,7 @@
|
|||
import { ref, computed, onMounted, onUnmounted, readonly } from 'vue'
|
||||
import { useNostrStore } from '@/stores/nostr'
|
||||
import { useMarketStore } from '@/stores/market'
|
||||
import { useRelayHub } from '@/composables/useRelayHub'
|
||||
import { relayHubComposable } from './useRelayHub'
|
||||
import { config } from '@/lib/config'
|
||||
|
||||
// Nostr event kinds for market functionality
|
||||
|
|
@ -15,7 +15,7 @@ const MARKET_EVENT_KINDS = {
|
|||
export function useMarket() {
|
||||
const nostrStore = useNostrStore()
|
||||
const marketStore = useMarketStore()
|
||||
const relayHub = useRelayHub()
|
||||
const relayHub = relayHubComposable
|
||||
|
||||
// State
|
||||
const isLoading = ref(false)
|
||||
|
|
@ -420,23 +420,23 @@ export function useMarket() {
|
|||
}
|
||||
|
||||
// Handle order events
|
||||
const handleOrderEvent = (event: any) => {
|
||||
const handleOrderEvent = (_event: any) => {
|
||||
try {
|
||||
const orderData = JSON.parse(event.content)
|
||||
const order = {
|
||||
id: event.id,
|
||||
stall_id: orderData.stall_id || 'unknown',
|
||||
product_id: orderData.product_id || 'unknown',
|
||||
buyer_pubkey: event.pubkey,
|
||||
seller_pubkey: orderData.seller_pubkey || '',
|
||||
quantity: orderData.quantity || 1,
|
||||
total_price: orderData.total_price || 0,
|
||||
currency: orderData.currency || 'sats',
|
||||
status: orderData.status || 'pending',
|
||||
payment_request: orderData.payment_request,
|
||||
created_at: event.created_at,
|
||||
updated_at: event.created_at
|
||||
}
|
||||
// const orderData = JSON.parse(event.content)
|
||||
// const order = {
|
||||
// id: event.id,
|
||||
// stall_id: orderData.stall_id || 'unknown',
|
||||
// product_id: orderData.product_id || 'unknown',
|
||||
// buyer_pubkey: event.pubkey,
|
||||
// seller_pubkey: orderData.seller_pubkey || '',
|
||||
// quantity: orderData.quantity || 1,
|
||||
// total_price: orderData.total_price || 0,
|
||||
// currency: orderData.currency || 'sats',
|
||||
// status: orderData.status || 'pending',
|
||||
// payment_request: orderData.payment_request,
|
||||
// created_at: event.created_at,
|
||||
// updated_at: event.created_at
|
||||
// }
|
||||
|
||||
// Note: addOrder method doesn't exist in the store, so we'll just handle it silently
|
||||
} catch (err) {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { nip04, finalizeEvent, type EventTemplate } from 'nostr-tools'
|
|||
import { hexToBytes } from '@/lib/utils/crypto'
|
||||
import { getAuthToken } from '@/lib/config/lnbits'
|
||||
import { config } from '@/lib/config'
|
||||
import { useRelayHub } from './useRelayHub'
|
||||
import { relayHubComposable } from './useRelayHub'
|
||||
import { useAuth } from './useAuth'
|
||||
|
||||
// Types
|
||||
|
|
@ -66,7 +66,7 @@ const saveUnreadData = (peerPubkey: string, data: UnreadMessageData): void => {
|
|||
|
||||
export function useNostrChat() {
|
||||
// Use the centralized relay hub
|
||||
const relayHub = useRelayHub()
|
||||
const relayHub = relayHubComposable
|
||||
|
||||
// Use the main authentication system
|
||||
const auth = useAuth()
|
||||
|
|
@ -82,9 +82,60 @@ export function useNostrChat() {
|
|||
// Track latest message timestamp for each peer (for sorting)
|
||||
const latestMessageTimestamps = ref<Map<string, number>>(new Map())
|
||||
|
||||
// Store peers globally
|
||||
// Track peers globally
|
||||
const peers = ref<any[]>([])
|
||||
|
||||
// Track malformed message IDs to prevent repeated processing attempts
|
||||
const malformedMessageIds = ref(new Set<string>())
|
||||
|
||||
// Mark a message as malformed to prevent future processing attempts
|
||||
const markMessageAsMalformed = (eventId: string) => {
|
||||
malformedMessageIds.value.add(eventId)
|
||||
// Also mark as processed to prevent retries
|
||||
processedMessageIds.value.add(eventId)
|
||||
}
|
||||
|
||||
// Clean up old malformed messages (call this periodically)
|
||||
const cleanupMalformedMessages = () => {
|
||||
// const now = Math.floor(Date.now() / 1000)
|
||||
// const maxAge = 24 * 60 * 60 // 24 hours
|
||||
|
||||
// Clear old malformed message IDs to free memory
|
||||
// This is a simple cleanup - in production you might want more sophisticated tracking
|
||||
if (malformedMessageIds.value.size > 1000) {
|
||||
console.log('Cleaning up malformed message tracking (clearing all)')
|
||||
malformedMessageIds.value.clear()
|
||||
}
|
||||
}
|
||||
|
||||
// Set up periodic cleanup
|
||||
let cleanupInterval: NodeJS.Timeout | null = null
|
||||
|
||||
// Clean up resources
|
||||
const cleanup = () => {
|
||||
if (cleanupInterval) {
|
||||
clearInterval(cleanupInterval)
|
||||
cleanupInterval = null
|
||||
console.log('Cleaned up malformed message tracking interval')
|
||||
}
|
||||
}
|
||||
|
||||
// Manually clear all malformed message tracking
|
||||
const clearAllMalformedMessages = () => {
|
||||
const count = malformedMessageIds.value.size
|
||||
malformedMessageIds.value.clear()
|
||||
console.log(`Cleared ${count} malformed message IDs from tracking`)
|
||||
}
|
||||
|
||||
// Get statistics about malformed messages
|
||||
const getMalformedMessageStats = () => {
|
||||
return {
|
||||
totalMalformed: malformedMessageIds.value.size,
|
||||
totalProcessed: processedMessageIds.value.size,
|
||||
malformedIds: Array.from(malformedMessageIds.value).slice(0, 10) // First 10 for debugging
|
||||
}
|
||||
}
|
||||
|
||||
// Computed - use relay hub's connection status and auth system
|
||||
const isConnected = computed(() => relayHub.isConnected.value)
|
||||
|
||||
|
|
@ -322,6 +373,11 @@ export function useNostrChat() {
|
|||
await relayHub.connect()
|
||||
}
|
||||
|
||||
// Set up periodic cleanup of malformed messages
|
||||
if (!cleanupInterval) {
|
||||
cleanupInterval = setInterval(cleanupMalformedMessages, 5 * 60 * 1000) // Every 5 minutes
|
||||
console.log('Set up periodic cleanup of malformed messages')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to connect to relays:', error)
|
||||
|
|
@ -495,13 +551,45 @@ export function useNostrChat() {
|
|||
return
|
||||
}
|
||||
|
||||
// Check if this message was previously identified as malformed
|
||||
if (malformedMessageIds.value.has(event.id)) {
|
||||
console.log('Skipping previously identified malformed message:', event.id)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// For NIP-04 direct messages, always use peerPubkey as the second argument
|
||||
// This is the public key of the other party in the conversation
|
||||
const isSentByMe = event.pubkey === currentUser.value.pubkey
|
||||
|
||||
// Check for malformed messages before attempting decryption
|
||||
if (typeof event.content !== 'string' || event.content.length === 0) {
|
||||
console.warn('Skipping message with invalid content format:', {
|
||||
eventId: event.id,
|
||||
contentType: typeof event.content,
|
||||
contentLength: event.content?.length
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Check for our old placeholder encryption format
|
||||
if (event.content.includes('[ENCRYPTED]') && event.content.includes('[ENCRYPTED]')) {
|
||||
console.warn('Skipping message with old placeholder encryption format:', {
|
||||
eventId: event.id,
|
||||
content: event.content.substring(0, 100) + '...'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Check for other common malformed patterns
|
||||
if (event.content.startsWith('[') || event.content.includes('ENCRYPTED')) {
|
||||
console.warn('Skipping message with suspicious encryption format:', {
|
||||
eventId: event.id,
|
||||
content: event.content.substring(0, 100) + '...'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
const decryptedContent = await nip04.decrypt(
|
||||
currentUser.value.prvkey,
|
||||
peerPubkey, // Always use peerPubkey for shared secret derivation
|
||||
|
|
@ -556,7 +644,40 @@ export function useNostrChat() {
|
|||
onMessageAdded.value(peerPubkey)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to decrypt message:', error)
|
||||
// Provide more specific error handling for different types of failures
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
|
||||
|
||||
// Check for specific error patterns that indicate malformed messages
|
||||
if (errorMessage.includes('join.decode') || errorMessage.includes('input should be string')) {
|
||||
console.warn('Skipping malformed message (invalid NIP-04 format):', {
|
||||
eventId: event.id,
|
||||
pubkey: event.pubkey,
|
||||
error: errorMessage,
|
||||
contentPreview: typeof event.content === 'string' ? event.content.substring(0, 100) + '...' : 'Invalid content type'
|
||||
})
|
||||
markMessageAsMalformed(event.id)
|
||||
return
|
||||
}
|
||||
|
||||
if (errorMessage.includes('Invalid byte sequence') || errorMessage.includes('hex string')) {
|
||||
console.warn('Skipping message with invalid hex encoding:', {
|
||||
eventId: event.id,
|
||||
pubkey: event.pubkey,
|
||||
error: errorMessage
|
||||
})
|
||||
markMessageAsMalformed(event.id)
|
||||
return
|
||||
}
|
||||
|
||||
// For other decryption errors, log with more context
|
||||
console.error('Failed to decrypt message:', {
|
||||
eventId: event.id,
|
||||
pubkey: event.pubkey,
|
||||
error: errorMessage,
|
||||
contentType: typeof event.content,
|
||||
contentLength: event.content?.length,
|
||||
contentPreview: typeof event.content === 'string' ? event.content.substring(0, 100) + '...' : 'Invalid content type'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -794,7 +915,12 @@ export function useNostrChat() {
|
|||
subscribeToAllPeersForNotifications,
|
||||
currentUser,
|
||||
hasNostrKeys,
|
||||
getNostrKeyStatus
|
||||
getNostrKeyStatus,
|
||||
markMessageAsMalformed,
|
||||
cleanupMalformedMessages,
|
||||
clearAllMalformedMessages, // Add the new function to the return object
|
||||
cleanup, // Add the cleanup function to the return object
|
||||
getMalformedMessageStats // Add the new function to the return object
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
248
src/composables/useNostrOrders.ts
Normal file
248
src/composables/useNostrOrders.ts
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
import { ref, computed, readonly } from 'vue'
|
||||
import { finalizeEvent, type EventTemplate, nip04 } from 'nostr-tools'
|
||||
import { relayHub } from '@/lib/nostr/relayHub'
|
||||
import { auth } from '@/composables/useAuth'
|
||||
import { hexToBytes } from '@/lib/utils/crypto'
|
||||
import type { Order } from '@/stores/market'
|
||||
|
||||
export function useNostrOrders() {
|
||||
// State
|
||||
const isPublishing = ref(false)
|
||||
const lastError = ref<string | null>(null)
|
||||
const publishedEvents = ref<Record<string, string>>({}) // orderId -> eventId
|
||||
|
||||
// Computed
|
||||
const isReady = computed(() => {
|
||||
return auth.isAuthenticated.value &&
|
||||
!!auth.currentUser.value?.pubkey &&
|
||||
!!auth.currentUser.value?.prvkey
|
||||
})
|
||||
|
||||
const currentUserPubkey = computed(() => auth.currentUser.value?.pubkey || '')
|
||||
const currentUserPrvkey = computed(() => auth.currentUser.value?.prvkey || '')
|
||||
|
||||
// Methods
|
||||
const validateAuth = (): { valid: boolean; error?: string } => {
|
||||
if (!auth.isAuthenticated.value) {
|
||||
return { valid: false, error: 'User not authenticated' }
|
||||
}
|
||||
|
||||
if (!currentUserPubkey.value) {
|
||||
return { valid: false, error: 'User public key not available' }
|
||||
}
|
||||
|
||||
if (!currentUserPrvkey.value) {
|
||||
return { valid: false, error: 'User private key not available' }
|
||||
}
|
||||
|
||||
// Validate key formats
|
||||
if (currentUserPubkey.value.length !== 64) {
|
||||
return { valid: false, error: 'Invalid public key format' }
|
||||
}
|
||||
|
||||
if (currentUserPrvkey.value.length !== 64) {
|
||||
return { valid: false, error: 'Invalid private key format' }
|
||||
}
|
||||
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
const createEventTemplate = (recipientPubkey: string, content: string): EventTemplate => {
|
||||
return {
|
||||
kind: 4, // Encrypted Direct Message
|
||||
tags: [['p', recipientPubkey]], // Recipient tag
|
||||
content: content,
|
||||
created_at: Math.floor(Date.now() / 1000)
|
||||
}
|
||||
}
|
||||
|
||||
const encryptOrderContent = async (order: Order, recipientPubkey: string): Promise<string> => {
|
||||
try {
|
||||
console.log('Encrypting order content:', {
|
||||
orderId: order.id,
|
||||
recipientPubkey,
|
||||
hasPrivateKey: !!currentUserPrvkey.value,
|
||||
privateKeyLength: currentUserPrvkey.value?.length
|
||||
})
|
||||
|
||||
// Validate keys
|
||||
if (!currentUserPrvkey.value || !recipientPubkey) {
|
||||
throw new Error('Missing private key or recipient public key')
|
||||
}
|
||||
|
||||
if (currentUserPrvkey.value.length !== 64) {
|
||||
throw new Error(`Invalid private key length: ${currentUserPrvkey.value.length} (expected 64)`)
|
||||
}
|
||||
|
||||
if (recipientPubkey.length !== 64) {
|
||||
throw new Error(`Invalid recipient public key length: ${recipientPubkey.length} (expected 64)`)
|
||||
}
|
||||
|
||||
// Create the order payload
|
||||
const orderPayload = {
|
||||
type: 'market_order',
|
||||
orderId: order.id,
|
||||
items: order.items,
|
||||
contactInfo: order.contactInfo,
|
||||
shippingZone: order.shippingZone,
|
||||
paymentMethod: order.paymentMethod,
|
||||
subtotal: order.subtotal,
|
||||
shippingCost: order.shippingCost,
|
||||
total: order.total,
|
||||
currency: order.currency,
|
||||
createdAt: order.createdAt,
|
||||
buyerPubkey: order.buyerPubkey
|
||||
}
|
||||
|
||||
// Convert to JSON string
|
||||
const orderJson = JSON.stringify(orderPayload)
|
||||
console.log('Order payload created:', orderPayload)
|
||||
|
||||
// Encrypt the order content using NIP-04
|
||||
const encryptedContent = await nip04.encrypt(
|
||||
hexToBytes(currentUserPrvkey.value),
|
||||
recipientPubkey,
|
||||
orderJson
|
||||
)
|
||||
|
||||
console.log('Order content encrypted successfully:', {
|
||||
originalLength: orderJson.length,
|
||||
encryptedLength: encryptedContent.length,
|
||||
encryptedPreview: encryptedContent.substring(0, 50) + '...'
|
||||
})
|
||||
|
||||
return encryptedContent
|
||||
} catch (error) {
|
||||
console.error('Failed to encrypt order content:', error)
|
||||
throw new Error('Failed to encrypt order content')
|
||||
}
|
||||
}
|
||||
|
||||
const publishOrderEvent = async (order: Order, recipientPubkey: string): Promise<{ id: string; sig: string }> => {
|
||||
try {
|
||||
// Validate authentication
|
||||
const authValidation = validateAuth()
|
||||
if (!authValidation.valid) {
|
||||
throw new Error(authValidation.error)
|
||||
}
|
||||
|
||||
// Set publishing state
|
||||
isPublishing.value = true
|
||||
lastError.value = null
|
||||
|
||||
// Encrypt the order content
|
||||
const encryptedContent = await encryptOrderContent(order, recipientPubkey)
|
||||
|
||||
// Create event template
|
||||
const eventTemplate = createEventTemplate(recipientPubkey, encryptedContent)
|
||||
|
||||
// Finalize the event (sign and generate ID)
|
||||
const event = finalizeEvent(eventTemplate, hexToBytes(currentUserPrvkey.value))
|
||||
|
||||
// Publish via relay hub
|
||||
await relayHub.publishEvent(event)
|
||||
|
||||
// Store the published event
|
||||
publishedEvents.value[order.id] = event.id
|
||||
|
||||
console.log('Order event published successfully:', {
|
||||
orderId: order.id,
|
||||
eventId: event.id,
|
||||
recipient: recipientPubkey,
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
|
||||
return { id: event.id, sig: event.sig }
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
lastError.value = errorMessage
|
||||
console.error('Failed to publish order event:', error)
|
||||
throw new Error(`Failed to publish order: ${errorMessage}`)
|
||||
} finally {
|
||||
isPublishing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const getPublishedEventId = (orderId: string): string | undefined => {
|
||||
return publishedEvents.value[orderId]
|
||||
}
|
||||
|
||||
const clearError = () => {
|
||||
lastError.value = null
|
||||
}
|
||||
|
||||
const reset = () => {
|
||||
isPublishing.value = false
|
||||
lastError.value = null
|
||||
publishedEvents.value = {}
|
||||
}
|
||||
|
||||
const testEncryption = async (): Promise<boolean> => {
|
||||
try {
|
||||
if (!isReady.value) {
|
||||
console.log('Nostr not ready for testing')
|
||||
return false
|
||||
}
|
||||
|
||||
const testMessage = 'Hello, this is a test message for NIP-04 encryption!'
|
||||
const testRecipient = currentUserPubkey.value // Encrypt to ourselves for testing
|
||||
|
||||
console.log('Testing NIP-04 encryption with:', {
|
||||
message: testMessage,
|
||||
recipient: testRecipient,
|
||||
sender: currentUserPubkey.value
|
||||
})
|
||||
|
||||
// Encrypt
|
||||
const encrypted = await nip04.encrypt(
|
||||
hexToBytes(currentUserPrvkey.value),
|
||||
testRecipient,
|
||||
testMessage
|
||||
)
|
||||
|
||||
console.log('Test message encrypted:', encrypted)
|
||||
|
||||
// Decrypt
|
||||
const decrypted = await nip04.decrypt(
|
||||
currentUserPrvkey.value,
|
||||
currentUserPubkey.value,
|
||||
encrypted
|
||||
)
|
||||
|
||||
console.log('Test message decrypted:', decrypted)
|
||||
|
||||
const success = decrypted === testMessage
|
||||
console.log('NIP-04 test result:', success ? 'PASSED' : 'FAILED')
|
||||
|
||||
return success
|
||||
} catch (error) {
|
||||
console.error('NIP-04 test failed:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
isPublishing: readonly(isPublishing),
|
||||
lastError: readonly(lastError),
|
||||
publishedEvents: readonly(publishedEvents),
|
||||
|
||||
// Computed
|
||||
isReady,
|
||||
currentUserPubkey,
|
||||
currentUserPrvkey,
|
||||
|
||||
// Methods
|
||||
validateAuth,
|
||||
createEventTemplate,
|
||||
encryptOrderContent,
|
||||
publishOrderEvent,
|
||||
getPublishedEventId,
|
||||
clearError,
|
||||
reset,
|
||||
testEncryption
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const nostrOrders = useNostrOrders()
|
||||
198
src/composables/useNostrclientHub.ts
Normal file
198
src/composables/useNostrclientHub.ts
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
import { ref, computed, onMounted, onUnmounted, readonly } from 'vue'
|
||||
import { nostrclientHub, type SubscriptionConfig } from '../lib/nostr/nostrclientHub'
|
||||
|
||||
export function useNostrclientHub() {
|
||||
// Reactive state
|
||||
const isConnected = ref(false)
|
||||
const isConnecting = ref(false)
|
||||
const connectionStatus = ref<'connecting' | 'connected' | 'disconnected' | 'error'>('disconnected')
|
||||
const error = ref<Error | null>(null)
|
||||
const activeSubscriptions = ref<Set<string>>(new Set())
|
||||
|
||||
// Reactive counts
|
||||
const totalSubscriptionCount = ref(0)
|
||||
const subscriptionDetails = ref<Array<{ id: string; filters: any[] }>>([])
|
||||
|
||||
// Computed properties
|
||||
const connectionHealth = computed(() => {
|
||||
return isConnected.value ? 100 : 0
|
||||
})
|
||||
|
||||
// Initialize nostrclient hub
|
||||
const initialize = async (): Promise<void> => {
|
||||
try {
|
||||
connectionStatus.value = 'connecting'
|
||||
error.value = null
|
||||
|
||||
console.log('🔧 NostrclientHub: Initializing...')
|
||||
await nostrclientHub.initialize()
|
||||
console.log('🔧 NostrclientHub: Initialization successful')
|
||||
|
||||
// Set up event listeners
|
||||
setupEventListeners()
|
||||
|
||||
connectionStatus.value = 'connected'
|
||||
isConnected.value = true
|
||||
console.log('🔧 NostrclientHub: Connection status set to connected')
|
||||
|
||||
} catch (err) {
|
||||
const errorObj = err instanceof Error ? err : new Error('Failed to initialize NostrclientHub')
|
||||
error.value = errorObj
|
||||
connectionStatus.value = 'error'
|
||||
isConnected.value = false
|
||||
console.error('🔧 NostrclientHub: Failed to initialize:', errorObj)
|
||||
throw errorObj
|
||||
}
|
||||
}
|
||||
|
||||
// Connect to nostrclient
|
||||
const connect = async (): Promise<void> => {
|
||||
try {
|
||||
connectionStatus.value = 'connecting'
|
||||
error.value = null
|
||||
|
||||
await nostrclientHub.connect()
|
||||
|
||||
connectionStatus.value = 'connected'
|
||||
isConnected.value = true
|
||||
} catch (err) {
|
||||
const errorObj = err instanceof Error ? err : new Error('Failed to connect')
|
||||
error.value = errorObj
|
||||
connectionStatus.value = 'error'
|
||||
isConnected.value = false
|
||||
throw errorObj
|
||||
}
|
||||
}
|
||||
|
||||
// Disconnect from nostrclient
|
||||
const disconnect = (): void => {
|
||||
nostrclientHub.disconnect()
|
||||
connectionStatus.value = 'disconnected'
|
||||
isConnected.value = false
|
||||
error.value = null
|
||||
}
|
||||
|
||||
// Subscribe to events
|
||||
const subscribe = (config: SubscriptionConfig): (() => void) => {
|
||||
try {
|
||||
const unsubscribe = nostrclientHub.subscribe(config)
|
||||
activeSubscriptions.value.add(config.id)
|
||||
|
||||
// Update reactive state
|
||||
totalSubscriptionCount.value = nostrclientHub.totalSubscriptionCount
|
||||
subscriptionDetails.value = nostrclientHub.subscriptionDetails
|
||||
|
||||
return () => {
|
||||
unsubscribe()
|
||||
activeSubscriptions.value.delete(config.id)
|
||||
totalSubscriptionCount.value = nostrclientHub.totalSubscriptionCount
|
||||
subscriptionDetails.value = nostrclientHub.subscriptionDetails
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to subscribe:', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Publish an event
|
||||
const publishEvent = async (event: any): Promise<void> => {
|
||||
try {
|
||||
await nostrclientHub.publishEvent(event)
|
||||
} catch (err) {
|
||||
console.error('Failed to publish event:', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Query events
|
||||
const queryEvents = async (filters: any[]): Promise<any[]> => {
|
||||
try {
|
||||
return await nostrclientHub.queryEvents(filters)
|
||||
} catch (err) {
|
||||
console.error('Failed to query events:', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Set up event listeners
|
||||
const setupEventListeners = () => {
|
||||
nostrclientHub.on('connected', () => {
|
||||
isConnected.value = true
|
||||
isConnecting.value = false
|
||||
connectionStatus.value = 'connected'
|
||||
error.value = null
|
||||
})
|
||||
|
||||
nostrclientHub.on('disconnected', () => {
|
||||
isConnected.value = false
|
||||
isConnecting.value = false
|
||||
connectionStatus.value = 'disconnected'
|
||||
})
|
||||
|
||||
nostrclientHub.on('error', (err) => {
|
||||
error.value = err
|
||||
connectionStatus.value = 'error'
|
||||
})
|
||||
|
||||
nostrclientHub.on('connectionError', (err) => {
|
||||
error.value = err
|
||||
connectionStatus.value = 'error'
|
||||
})
|
||||
|
||||
nostrclientHub.on('maxReconnectionAttemptsReached', () => {
|
||||
error.value = new Error('Max reconnection attempts reached')
|
||||
connectionStatus.value = 'error'
|
||||
})
|
||||
|
||||
nostrclientHub.on('event', ({ subscriptionId, event }) => {
|
||||
console.log('Received event for subscription:', subscriptionId, event.id)
|
||||
})
|
||||
|
||||
nostrclientHub.on('eose', ({ subscriptionId }) => {
|
||||
console.log('EOSE received for subscription:', subscriptionId)
|
||||
})
|
||||
|
||||
nostrclientHub.on('notice', ({ message }) => {
|
||||
console.log('Notice from nostrclient:', message)
|
||||
})
|
||||
|
||||
nostrclientHub.on('eventPublished', ({ eventId }) => {
|
||||
console.log('Event published successfully:', eventId)
|
||||
})
|
||||
}
|
||||
|
||||
// Clean up event listeners
|
||||
const cleanup = () => {
|
||||
nostrclientHub.removeAllListeners()
|
||||
}
|
||||
|
||||
// Auto-cleanup on unmount
|
||||
onUnmounted(() => {
|
||||
cleanup()
|
||||
})
|
||||
|
||||
return {
|
||||
// State
|
||||
isConnected: readonly(isConnected),
|
||||
isConnecting: readonly(isConnecting),
|
||||
connectionStatus: readonly(connectionStatus),
|
||||
error: readonly(error),
|
||||
activeSubscriptions: readonly(activeSubscriptions),
|
||||
totalSubscriptionCount: readonly(totalSubscriptionCount),
|
||||
subscriptionDetails: readonly(subscriptionDetails),
|
||||
|
||||
// Computed
|
||||
connectionHealth: readonly(connectionHealth),
|
||||
|
||||
// Methods
|
||||
initialize,
|
||||
connect,
|
||||
disconnect,
|
||||
subscribe,
|
||||
publishEvent,
|
||||
queryEvents,
|
||||
|
||||
// Internal
|
||||
cleanup
|
||||
}
|
||||
}
|
||||
585
src/composables/useOrderEvents.ts
Normal file
585
src/composables/useOrderEvents.ts
Normal file
|
|
@ -0,0 +1,585 @@
|
|||
import { ref, computed, watch } 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'
|
||||
|
||||
// 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'
|
||||
}
|
||||
|
||||
export interface OrderEvent {
|
||||
type: OrderEventType
|
||||
orderId: string
|
||||
data: any
|
||||
timestamp: number
|
||||
senderPubkey: string
|
||||
}
|
||||
|
||||
export interface PaymentRequestEvent {
|
||||
type: OrderEventType.PAYMENT_REQUEST
|
||||
orderId: string
|
||||
paymentRequest: string
|
||||
amount: number
|
||||
currency: string
|
||||
memo: string
|
||||
expiresAt: number
|
||||
}
|
||||
|
||||
export interface OrderStatusEvent {
|
||||
type: OrderEventType.ORDER_PAID | OrderEventType.ORDER_SHIPPED | OrderEventType.ORDER_DELIVERED
|
||||
orderId: string
|
||||
status: OrderStatus
|
||||
timestamp: number
|
||||
additionalData?: any
|
||||
}
|
||||
|
||||
export function useOrderEvents() {
|
||||
const relayHub = relayHubComposable
|
||||
const auth = useAuth()
|
||||
const marketStore = useMarketStore()
|
||||
|
||||
// State
|
||||
const isSubscribed = ref(false)
|
||||
const lastEventTimestamp = ref(0)
|
||||
const processedEventIds = ref(new Set<string>())
|
||||
const subscriptionId = ref<string | null>(null)
|
||||
|
||||
// Computed
|
||||
const currentUserPubkey = computed(() => auth.currentUser?.value?.pubkey)
|
||||
const isReady = computed(() => {
|
||||
const isAuth = auth.isAuthenticated
|
||||
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
|
||||
since: lastEventTimestamp.value
|
||||
}
|
||||
]
|
||||
|
||||
console.log('Using filters:', filters)
|
||||
|
||||
const unsubscribe = relayHub.subscribe({
|
||||
id: `order-events-${currentUserPubkey.value}-${Date.now()}`,
|
||||
filters,
|
||||
relays: config.market.supportedRelays,
|
||||
onEvent: (event: any) => {
|
||||
console.log('Received event in order subscription:', event.id)
|
||||
handleOrderEvent(event)
|
||||
},
|
||||
onEose: () => {
|
||||
console.log('Order events subscription EOSE')
|
||||
}
|
||||
})
|
||||
|
||||
subscriptionId.value = `order-events-${currentUserPubkey.value}-${Date.now()}`
|
||||
isSubscribed.value = true
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
try {
|
||||
// Decrypt the message content
|
||||
const decryptedContent = await nip04.decrypt(
|
||||
auth.currentUser.value.prvkey,
|
||||
event.pubkey, // Sender's pubkey
|
||||
event.content
|
||||
)
|
||||
|
||||
// Parse the decrypted content
|
||||
const orderEvent = 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')
|
||||
break
|
||||
|
||||
case 1:
|
||||
// Payment request from merchant
|
||||
console.log('Received payment request from merchant')
|
||||
await nostrmarketService.handlePaymentRequest(message)
|
||||
break
|
||||
|
||||
case 2:
|
||||
// Order status update from merchant
|
||||
console.log('Received order status update from merchant')
|
||||
await nostrmarketService.handleOrderStatusUpdate(message)
|
||||
break
|
||||
|
||||
default:
|
||||
console.warn('Unknown nostrmarket message type:', message.type)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to handle nostrmarket message:', 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
|
||||
})
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
if (existingOrder) {
|
||||
console.log('Order already exists, updating with new information:', existingOrder.id)
|
||||
|
||||
// Update the existing order with any new information
|
||||
const updatedOrder = {
|
||||
...existingOrder,
|
||||
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
|
||||
}
|
||||
|
||||
// Create a basic order object from the event data
|
||||
const orderData: Partial<Order> = {
|
||||
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 || '',
|
||||
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
|
||||
}
|
||||
|
||||
// Create a basic order object from the event data
|
||||
const orderData: Partial<Order> = {
|
||||
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
|
||||
}
|
||||
|
||||
try {
|
||||
await subscribeToOrderEvents()
|
||||
console.log('Started listening for order events')
|
||||
} catch (error) {
|
||||
console.error('Failed to start listening for order events:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop listening for order events
|
||||
const stopListening = () => {
|
||||
if (subscriptionId.value) {
|
||||
// Use the cleanup method from relayHub
|
||||
relayHub.cleanup()
|
||||
subscriptionId.value = null
|
||||
}
|
||||
isSubscribed.value = false
|
||||
console.log('Stopped listening for 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')
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
isSubscribed,
|
||||
lastEventTimestamp,
|
||||
|
||||
// Methods
|
||||
startListening,
|
||||
stopListening,
|
||||
subscribeToOrderEvents,
|
||||
cleanupProcessedEvents
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const orderEvents = useOrderEvents()
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { ref, computed, onMounted, onUnmounted, readonly } from 'vue'
|
||||
import { relayHub, type SubscriptionConfig, type RelayStatus } from '../lib/nostr/relayHub'
|
||||
import { config } from '../lib/config'
|
||||
|
||||
|
|
@ -31,19 +31,24 @@ export function useRelayHub() {
|
|||
error.value = null
|
||||
|
||||
// Get relay URLs from config
|
||||
const relayUrls = config.nostr.relays
|
||||
const relayUrls = config.market.supportedRelays
|
||||
console.log('🔧 RelayHub: Initializing with relay URLs:', relayUrls)
|
||||
|
||||
if (!relayUrls || relayUrls.length === 0) {
|
||||
throw new Error('No relay URLs configured')
|
||||
}
|
||||
|
||||
// Initialize the relay hub
|
||||
console.log('🔧 RelayHub: Calling relayHub.initialize...')
|
||||
await relayHub.initialize(relayUrls)
|
||||
console.log('🔧 RelayHub: Initialization successful')
|
||||
|
||||
// Set up event listeners
|
||||
setupEventListeners()
|
||||
|
||||
connectionStatus.value = 'connected'
|
||||
isConnected.value = true
|
||||
console.log('🔧 RelayHub: Connection status set to connected')
|
||||
|
||||
|
||||
} catch (err) {
|
||||
|
|
@ -51,7 +56,7 @@ export function useRelayHub() {
|
|||
error.value = errorObj
|
||||
connectionStatus.value = 'error'
|
||||
isConnected.value = false
|
||||
console.error('Failed to initialize RelayHub:', errorObj)
|
||||
console.error('🔧 RelayHub: Failed to initialize RelayHub:', errorObj)
|
||||
throw errorObj
|
||||
}
|
||||
}
|
||||
|
|
@ -128,35 +133,11 @@ export function useRelayHub() {
|
|||
}
|
||||
}
|
||||
|
||||
// Force reconnection
|
||||
const reconnect = async (): Promise<void> => {
|
||||
try {
|
||||
connectionStatus.value = 'connecting'
|
||||
error.value = null
|
||||
|
||||
await relayHub.reconnect()
|
||||
|
||||
connectionStatus.value = 'connected'
|
||||
isConnected.value = true
|
||||
} catch (err) {
|
||||
const errorObj = err instanceof Error ? err : new Error('Failed to reconnect')
|
||||
error.value = errorObj
|
||||
connectionStatus.value = 'error'
|
||||
isConnected.value = false
|
||||
throw errorObj
|
||||
}
|
||||
}
|
||||
|
||||
// Get relay status
|
||||
const getRelayStatus = (url: string): RelayStatus | undefined => {
|
||||
return relayStatuses.value.find(status => status.url === url)
|
||||
}
|
||||
|
||||
// Check if a specific relay is connected
|
||||
const isRelayConnected = (url: string): boolean => {
|
||||
return relayHub.isRelayConnected(url)
|
||||
}
|
||||
|
||||
// Set up event listeners for relay hub events
|
||||
const setupEventListeners = (): void => {
|
||||
relayHub.on('connected', (count: number) => {
|
||||
|
|
@ -274,18 +255,16 @@ export function useRelayHub() {
|
|||
|
||||
return {
|
||||
// State
|
||||
isConnected,
|
||||
connectionStatus,
|
||||
relayStatuses,
|
||||
error,
|
||||
activeSubscriptions,
|
||||
|
||||
// Computed
|
||||
connectedRelayCount,
|
||||
totalRelayCount,
|
||||
totalSubscriptionCount,
|
||||
subscriptionDetails,
|
||||
connectionHealth,
|
||||
isConnected: readonly(isConnected),
|
||||
connectionStatus: readonly(connectionStatus),
|
||||
relayStatuses: readonly(relayStatuses),
|
||||
error: readonly(error),
|
||||
activeSubscriptions: readonly(activeSubscriptions),
|
||||
connectedRelayCount: readonly(connectedRelayCount),
|
||||
totalRelayCount: readonly(totalRelayCount),
|
||||
totalSubscriptionCount: readonly(totalSubscriptionCount),
|
||||
subscriptionDetails: readonly(subscriptionDetails),
|
||||
connectionHealth: readonly(connectionHealth),
|
||||
|
||||
// Methods
|
||||
initialize,
|
||||
|
|
@ -294,9 +273,11 @@ export function useRelayHub() {
|
|||
subscribe,
|
||||
publishEvent,
|
||||
queryEvents,
|
||||
reconnect,
|
||||
getRelayStatus,
|
||||
isRelayConnected,
|
||||
getConnectionHealth: connectionHealth,
|
||||
cleanup
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance for global state
|
||||
export const relayHubComposable = useRelayHub()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue