- Update imports in DashboardOverview.vue to use relative paths for better module organization. - Modify OrderHistory.vue to replace 'lightningInvoice' with 'paymentRequest' for consistency in payment handling. - Enhance order event handling in useMarket.ts by adding subscription and decryption logic for order-related DMs. - Update nostrmarketService.ts to use relative imports, ensuring consistency across the module. - Introduce error handling and logging for order updates, improving the robustness of the market module.
635 lines
20 KiB
TypeScript
635 lines
20 KiB
TypeScript
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<Error | null>(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<string, any[]>()
|
|
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<string, any[]>()
|
|
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
|
|
}
|
|
}
|