import { ref, computed, onMounted, onUnmounted, readonly } from 'vue' import { useNostrStore } from '@/stores/nostr' import { useMarketStore } from '../stores/market' import { injectService, SERVICE_TOKENS } from '@/core/di-container' import { config } from '@/lib/config' import { nostrmarketService } from '../services/nostrmarketService' import { nip04 } from 'nostr-tools' // Nostr event kinds for market functionality const MARKET_EVENT_KINDS = { MARKET: 30019, STALL: 30017, PRODUCT: 30018, ORDER: 30020 } as const export function useMarket() { const nostrStore = useNostrStore() const marketStore = useMarketStore() const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB) as any const authService = injectService(SERVICE_TOKENS.AUTH_SERVICE) as any if (!relayHub) { throw new Error('RelayHub not available. Make sure base module is installed.') } if (!authService) { throw new Error('AuthService not available. Make sure base module is installed.') } // State const isLoading = ref(false) const error = ref(null) const isConnected = ref(false) const activeMarket = computed(() => marketStore.activeMarket) const markets = computed(() => marketStore.markets) const stalls = computed(() => marketStore.stalls) const products = computed(() => marketStore.products) const orders = computed(() => marketStore.orders) // Connection state const connectionStatus = computed(() => { if (isConnected.value) return 'connected' if (nostrStore.isConnecting) return 'connecting' if (nostrStore.error) return 'error' return 'disconnected' }) // Load market from naddr const loadMarket = async (naddr: string) => { try { isLoading.value = true error.value = null // Load market from naddr // Parse naddr to get market data // TODO: Confirm if this should use nostrStore.account?.pubkey or authService.user.value?.pubkey const marketData = { identifier: naddr.split(':')[2] || 'default', pubkey: naddr.split(':')[1] || nostrStore.account?.pubkey || '' } if (!marketData.pubkey) { throw new Error('No pubkey available for market') } await loadMarketData(marketData) } catch (err) { error.value = err instanceof Error ? err : new Error('Failed to load market') throw err } finally { isLoading.value = false } } // Load market data from Nostr events const loadMarketData = async (marketData: any) => { try { console.log('πŸ›’ Loading market data for:', { identifier: marketData.identifier, pubkey: marketData.pubkey?.slice(0, 8) }) // Check if we can query events (relays are connected) if (!isConnected.value) { console.log('πŸ›’ Not connected to relays, creating default market') // Create default market without trying to fetch from Nostr const market = { d: marketData.identifier, pubkey: marketData.pubkey, relays: config.nostr.relays, selected: true, opts: { name: 'Demo Market (Offline)', description: 'Demo market running in offline mode', merchants: [], ui: {} } } marketStore.addMarket(market) marketStore.setActiveMarket(market) return } // Load market data from Nostr events // Fetch market configuration event const events = await relayHub.queryEvents([ { kinds: [MARKET_EVENT_KINDS.MARKET], authors: [marketData.pubkey], '#d': [marketData.identifier] } ]) console.log('πŸ›’ Found', events.length, 'market events') // Process market events if (events.length > 0) { const marketEvent = events[0] // Process market event const market = { d: marketData.identifier, pubkey: marketData.pubkey, relays: config.nostr.relays, selected: true, opts: JSON.parse(marketEvent.content) } marketStore.addMarket(market) marketStore.setActiveMarket(market) } else { // No market events found, create default // Create a default market if none exists const market = { d: marketData.identifier, pubkey: marketData.pubkey, relays: config.nostr.relays, selected: true, opts: { name: 'AriΓ¨ge Market', description: 'A communal market to sell your goods', merchants: [], ui: {} } } marketStore.addMarket(market) marketStore.setActiveMarket(market) } } catch (err) { // Don't throw error, create default market instead const market = { d: marketData.identifier, pubkey: marketData.pubkey, relays: config.nostr.relays, selected: true, opts: { name: 'Default Market', description: 'A default market', merchants: [], ui: {} } } marketStore.addMarket(market) marketStore.setActiveMarket(market) } } // Load stalls from market merchants const loadStalls = async () => { try { // Get the active market to filter by its merchants const activeMarket = marketStore.activeMarket if (!activeMarket) { return } const merchants = [...(activeMarket.opts.merchants || [])] if (merchants.length === 0) { return } // Fetch stall events from market merchants only const events = await relayHub.queryEvents([ { kinds: [MARKET_EVENT_KINDS.STALL], authors: merchants } ]) console.log('πŸ›’ Found', events.length, 'stall events for', merchants.length, 'merchants') // Process stall events // Group events by stall ID and keep only the most recent version const stallGroups = new Map() events.forEach((event: any) => { const stallId = event.tags.find((tag: any) => tag[0] === 'd')?.[1] if (stallId) { if (!stallGroups.has(stallId)) { stallGroups.set(stallId, []) } stallGroups.get(stallId)!.push(event) } }) // Process each stall group stallGroups.forEach((stallEvents, stallId) => { // Sort by created_at and take the most recent const latestEvent = stallEvents.sort((a: any, b: any) => b.created_at - a.created_at)[0] try { const stallData = JSON.parse(latestEvent.content) const stall = { id: stallId, pubkey: latestEvent.pubkey, name: stallData.name || 'Unnamed Stall', description: stallData.description || '', created_at: latestEvent.created_at, ...stallData } marketStore.addStall(stall) } catch (err) { // Silently handle parse errors } }) } catch (err) { // Silently handle stall loading errors } } // Load products from market stalls const loadProducts = async () => { try { const activeMarket = marketStore.activeMarket if (!activeMarket) { return } const merchants = [...(activeMarket.opts.merchants || [])] if (merchants.length === 0) { return } // Fetch product events from market merchants const events = await relayHub.queryEvents([ { kinds: [MARKET_EVENT_KINDS.PRODUCT], authors: merchants } ]) console.log('πŸ›’ Found', events.length, 'product events for', merchants.length, 'merchants') // Process product events // Group events by product ID and keep only the most recent version const productGroups = new Map() events.forEach((event: any) => { const productId = event.tags.find((tag: any) => tag[0] === 'd')?.[1] if (productId) { if (!productGroups.has(productId)) { productGroups.set(productId, []) } productGroups.get(productId)!.push(event) } }) // Process each product group productGroups.forEach((productEvents, productId) => { // Sort by created_at and take the most recent const latestEvent = productEvents.sort((a: any, b: any) => b.created_at - a.created_at)[0] try { const productData = JSON.parse(latestEvent.content) const product = { id: productId, stall_id: productData.stall_id || 'unknown', stallName: productData.stallName || 'Unknown Stall', name: productData.name || 'Unnamed Product', description: productData.description || '', price: productData.price || 0, currency: productData.currency || 'sats', quantity: productData.quantity || 1, images: productData.images || [], categories: productData.categories || [], createdAt: latestEvent.created_at, updatedAt: latestEvent.created_at } marketStore.addProduct(product) } catch (err) { // Silently handle parse errors } }) } catch (err) { // Silently handle product loading errors } } // Subscribe to market updates const subscribeToMarketUpdates = (): (() => void) | null => { try { const activeMarket = marketStore.activeMarket if (!activeMarket) { return null } // Subscribe to market events const unsubscribe = relayHub.subscribe({ id: `market-${activeMarket.d}`, filters: [ { kinds: [MARKET_EVENT_KINDS.MARKET] }, { kinds: [MARKET_EVENT_KINDS.STALL] }, { kinds: [MARKET_EVENT_KINDS.PRODUCT] }, { kinds: [MARKET_EVENT_KINDS.ORDER] } ], onEvent: (event: any) => { handleMarketEvent(event) } }) return unsubscribe } catch (error) { return null } } // Subscribe to order-related DMs (payment requests, status updates) const subscribeToOrderUpdates = (): (() => void) | null => { try { // TODO: Confirm if this should use nostrStore.account?.pubkey or authService.user.value?.pubkey const userPubkey = nostrStore.account?.pubkey || authService.user.value?.pubkey if (!userPubkey) { console.warn('Cannot subscribe to order updates: no user pubkey available', { nostrStorePubkey: nostrStore.account?.pubkey, authServicePubkey: authService.user.value?.pubkey, isAuthenticated: authService.isAuthenticated.value }) return null } console.log('πŸ”” Setting up order updates subscription for user:', userPubkey.slice(0, 8)) // Subscribe to encrypted DMs directed to this user (payment requests, status updates) const unsubscribe = relayHub.subscribe({ id: `order-updates-${userPubkey}`, filters: [ { kinds: [4], // Encrypted DMs '#p': [userPubkey], // Messages directed to this user since: Math.floor(Date.now() / 1000) - 3600 // Last hour to avoid old messages } ], onEvent: async (event: any) => { await handleOrderDM(event) } }) return unsubscribe } catch (error) { console.error('Failed to subscribe to order updates:', error) return null } } // Handle incoming order DMs (payment requests, status updates) const handleOrderDM = async (event: any) => { try { console.log('πŸ”” Received order-related DM:', event.id, 'from:', event.pubkey.slice(0, 8)) // TODO: Confirm if this should use nostrStore.account?.pubkey or authService.user.value?.pubkey const userPubkey = nostrStore.account?.pubkey || authService.user.value?.pubkey const userPrivkey = nostrStore.account?.privkey || authService.user.value?.prvkey if (!userPrivkey) { console.warn('Cannot decrypt DM: no user private key available', { nostrStorePrivkey: !!nostrStore.account?.privkey, authServicePrivkey: !!authService.user.value?.prvkey }) return } console.log('πŸ”“ Attempting to decrypt DM with private key available') // Decrypt the DM content const decryptedContent = await nip04.decrypt(userPrivkey, event.pubkey, event.content) console.log('πŸ”“ Decrypted DM content:', decryptedContent) // Parse the decrypted content as JSON const messageData = JSON.parse(decryptedContent) console.log('πŸ“¨ Parsed message data:', messageData) // Handle different types of messages switch (messageData.type) { case 1: // Payment request console.log('πŸ’° Processing payment request') await nostrmarketService.handlePaymentRequest(messageData) break case 2: // Order status update console.log('πŸ“¦ Processing order status update') await nostrmarketService.handleOrderStatusUpdate(messageData) break default: console.log('❓ Unknown message type:', messageData.type) } } catch (error) { console.error('Failed to handle order DM:', error) } } // Handle incoming market events const handleMarketEvent = (event: any) => { // Process market event switch (event.kind) { case MARKET_EVENT_KINDS.MARKET: // Handle market updates break case MARKET_EVENT_KINDS.STALL: // Handle stall updates handleStallEvent(event) break case MARKET_EVENT_KINDS.PRODUCT: // Handle product updates handleProductEvent(event) break case MARKET_EVENT_KINDS.ORDER: // Handle order updates handleOrderEvent(event) break } } // Process pending products (products without stalls) const processPendingProducts = () => { const productsWithoutStalls = products.value.filter(product => { // Check if product has a stall tag return !product.stall_id }) if (productsWithoutStalls.length > 0) { // You could create default stalls or handle this as needed } } // Handle stall events const handleStallEvent = (event: any) => { try { const stallId = event.tags.find((tag: any) => tag[0] === 'd')?.[1] if (stallId) { const stallData = JSON.parse(event.content) const stall = { id: stallId, pubkey: event.pubkey, name: stallData.name || 'Unnamed Stall', description: stallData.description || '', created_at: event.created_at, ...stallData } marketStore.addStall(stall) } } catch (err) { // Silently handle stall event errors } } // Handle product events const handleProductEvent = (event: any) => { try { const productId = event.tags.find((tag: any) => tag[0] === 'd')?.[1] if (productId) { const productData = JSON.parse(event.content) const product = { id: productId, stall_id: productData.stall_id || 'unknown', stallName: productData.stallName || 'Unknown Stall', pubkey: event.pubkey, name: productData.name || 'Unnamed Product', description: productData.description || '', price: productData.price || 0, currency: productData.currency || 'sats', quantity: productData.quantity || 1, images: productData.images || [], categories: productData.categories || [], createdAt: event.created_at, updatedAt: event.created_at } marketStore.addProduct(product) } } catch (err) { // Silently handle product event errors } } // Handle order events 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 // } // Note: addOrder method doesn't exist in the store, so we'll just handle it silently } catch (err) { // Silently handle order event errors } } // Publish a product const publishProduct = async (_productData: any) => { // Implementation would depend on your event creation logic // TODO: Implement product publishing } // Publish a stall const publishStall = async (_stallData: any) => { // Implementation would depend on your event creation logic // TODO: Implement stall publishing } // Connect to market const connectToMarket = async () => { try { console.log('πŸ›’ Checking RelayHub connection...') // Use existing relay hub connection (should already be connected by base module) isConnected.value = relayHub.isConnected.value console.log('πŸ›’ RelayHub connected:', isConnected.value) if (!isConnected.value) { console.warn('πŸ›’ RelayHub not connected - this is expected if authentication is not complete') // Don't try to connect here - let the base module handle connections // Just proceed with offline/demo mode console.log('πŸ›’ Proceeding in offline mode') } console.log('πŸ›’ Market connected successfully') // Load market data console.log('πŸ›’ Loading basic market data...') // TODO: Confirm if this should use nostrStore.account?.pubkey or authService.user.value?.pubkey await loadMarketData({ identifier: 'default', pubkey: nostrStore.account?.pubkey || '' }) // Load stalls and products only if connected if (isConnected.value) { console.log('πŸ›’ Loading stalls...') await loadStalls() console.log('πŸ›’ Loading products...') await loadProducts() } else { console.log('πŸ›’ Skipping stalls/products loading - not connected to relays') } // Subscribe to updates console.log('πŸ›’ Subscribing to market updates...') subscribeToMarketUpdates() // Subscribe to order-related DMs console.log('πŸ›’ Subscribing to order updates...') subscribeToOrderUpdates() } catch (err) { console.error('πŸ›’ Failed to connect to market:', err) error.value = err instanceof Error ? err : new Error('Failed to connect to market') throw err } } // Disconnect from market const disconnectFromMarket = () => { isConnected.value = false error.value = null // Market disconnected } // Initialize market on mount onMounted(async () => { if (nostrStore.isConnected) { await connectToMarket() } }) // Cleanup on unmount onUnmounted(() => { disconnectFromMarket() }) return { // State isLoading: readonly(isLoading), error: readonly(error), isConnected: readonly(isConnected), connectionStatus: readonly(connectionStatus), activeMarket: readonly(activeMarket), markets: readonly(markets), stalls: readonly(stalls), products: readonly(products), orders: readonly(orders), // Actions loadMarket, connectToMarket, disconnectFromMarket, processPendingProducts, publishProduct, publishStall, subscribeToMarketUpdates, subscribeToOrderUpdates } }