548 lines
No EOL
16 KiB
TypeScript
548 lines
No EOL
16 KiB
TypeScript
import { ref, readonly } from 'vue'
|
|
import { useNostrStore } from '@/stores/nostr'
|
|
import { useMarketStore, type Market, type Stall, type Product } from '@/stores/market'
|
|
import { config } from '@/lib/config'
|
|
import { nip19 } 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 isLoading = ref(false)
|
|
const isConnected = ref(false)
|
|
|
|
// Market loading state
|
|
const loadMarket = async (naddr: string) => {
|
|
try {
|
|
isLoading.value = true
|
|
marketStore.setLoading(true)
|
|
marketStore.setError(null)
|
|
|
|
console.log('Loading market with naddr:', naddr)
|
|
|
|
// Decode naddr
|
|
const { type, data } = nip19.decode(naddr)
|
|
console.log('Decoded naddr:', { type, data })
|
|
|
|
if (type !== 'naddr' || data.kind !== MARKET_EVENT_KINDS.MARKET) {
|
|
throw new Error('Invalid market naddr')
|
|
}
|
|
|
|
console.log('About to load market data...')
|
|
// Load market data from Nostr
|
|
await loadMarketData(data)
|
|
console.log('Market data loaded successfully')
|
|
|
|
} catch (err) {
|
|
console.error('Error loading market:', err)
|
|
marketStore.setError(err instanceof Error ? err.message : 'Failed to load market')
|
|
// Don't throw error, let the UI handle it gracefully
|
|
} finally {
|
|
isLoading.value = false
|
|
marketStore.setLoading(false)
|
|
}
|
|
}
|
|
|
|
const loadMarketData = async (marketData: any) => {
|
|
try {
|
|
console.log('Starting loadMarketData...')
|
|
console.log('Got Nostr client')
|
|
|
|
// Load market configuration
|
|
console.log('Loading market config...')
|
|
await loadMarketConfig(marketData)
|
|
console.log('Market config loaded')
|
|
|
|
// Load stalls for this market
|
|
console.log('Loading stalls...')
|
|
await loadStalls(marketData.pubkey)
|
|
console.log('Stalls loaded')
|
|
|
|
// Load products for all stalls
|
|
console.log('Loading products...')
|
|
await loadProducts()
|
|
console.log('Products loaded')
|
|
|
|
// Subscribe to real-time updates
|
|
console.log('Subscribing to updates...')
|
|
try {
|
|
subscribeToMarketUpdates()
|
|
console.log('Subscribed to updates')
|
|
} catch (err) {
|
|
console.warn('Failed to subscribe to updates:', err)
|
|
// Don't fail the entire load process if subscription fails
|
|
}
|
|
|
|
// Clear any error state since we successfully loaded the market data
|
|
marketStore.setError(null)
|
|
|
|
} catch (err) {
|
|
console.error('Error loading market data:', err)
|
|
throw err
|
|
}
|
|
}
|
|
|
|
const loadMarketConfig = async (marketData: any) => {
|
|
try {
|
|
const client = nostrStore.getClient()
|
|
|
|
console.log('Loading market config for:', marketData)
|
|
|
|
// Fetch market configuration event
|
|
const events = await client.fetchEvents({
|
|
kinds: [MARKET_EVENT_KINDS.MARKET],
|
|
authors: [marketData.pubkey],
|
|
'#d': [marketData.identifier]
|
|
})
|
|
|
|
console.log('Found market events:', events.length)
|
|
|
|
if (events.length > 0) {
|
|
const marketEvent = events[0]
|
|
console.log('Market event:', marketEvent)
|
|
|
|
const market: Market = {
|
|
d: marketData.identifier,
|
|
pubkey: marketData.pubkey,
|
|
relays: config.market.supportedRelays,
|
|
selected: true,
|
|
opts: JSON.parse(marketEvent.content)
|
|
}
|
|
|
|
marketStore.addMarket(market)
|
|
marketStore.setActiveMarket(market)
|
|
} else {
|
|
console.warn('No market events found')
|
|
// Create a default market if none exists
|
|
const market: Market = {
|
|
d: marketData.identifier,
|
|
pubkey: marketData.pubkey,
|
|
relays: config.market.supportedRelays,
|
|
selected: true,
|
|
opts: {
|
|
name: 'Default Market',
|
|
description: 'A default market',
|
|
merchants: [],
|
|
ui: {}
|
|
}
|
|
}
|
|
|
|
marketStore.addMarket(market)
|
|
marketStore.setActiveMarket(market)
|
|
}
|
|
|
|
} catch (err) {
|
|
console.error('Error loading market config:', err)
|
|
// Don't throw error, create default market instead
|
|
const market: Market = {
|
|
d: marketData.identifier,
|
|
pubkey: marketData.pubkey,
|
|
relays: config.market.supportedRelays,
|
|
selected: true,
|
|
opts: {
|
|
name: 'Default Market',
|
|
description: 'A default market',
|
|
merchants: [],
|
|
ui: {}
|
|
}
|
|
}
|
|
|
|
marketStore.addMarket(market)
|
|
marketStore.setActiveMarket(market)
|
|
}
|
|
}
|
|
|
|
const loadStalls = async (marketPubkey: string) => {
|
|
try {
|
|
const client = nostrStore.getClient()
|
|
|
|
console.log('Loading stalls for market pubkey:', marketPubkey)
|
|
|
|
// Fetch stall events for this market
|
|
// Note: We need to fetch all stalls and then filter by the ones that belong to this market
|
|
// since stalls don't have a direct market association in their tags
|
|
const events = await client.fetchEvents({
|
|
kinds: [MARKET_EVENT_KINDS.STALL],
|
|
authors: [marketPubkey]
|
|
})
|
|
|
|
console.log('Found stall events:', events.length)
|
|
|
|
// Group events by stall ID and keep only the most recent version
|
|
const stallGroups = new Map<string, any[]>()
|
|
|
|
events.forEach(event => {
|
|
try {
|
|
const stallData = JSON.parse(event.content)
|
|
const stallId = stallData.id
|
|
|
|
if (!stallGroups.has(stallId)) {
|
|
stallGroups.set(stallId, [])
|
|
}
|
|
stallGroups.get(stallId)!.push({ event, stallData })
|
|
} catch (err) {
|
|
console.warn('Failed to parse stall event:', err)
|
|
}
|
|
})
|
|
|
|
// Process each stall group, keeping only the most recent version
|
|
stallGroups.forEach((stallEvents, _stallId) => {
|
|
// Sort by created_at timestamp (most recent first)
|
|
stallEvents.sort((a, b) => b.event.created_at - a.event.created_at)
|
|
|
|
// Take the most recent version
|
|
const { event, stallData } = stallEvents[0]
|
|
|
|
console.log('Processing most recent stall event:', event)
|
|
console.log('Parsed stall data:', stallData)
|
|
|
|
const stall: Stall = {
|
|
id: stallData.id, // Use the stall's unique ID from content, not the Nostr event ID
|
|
pubkey: event.pubkey,
|
|
name: stallData.name,
|
|
description: stallData.description,
|
|
logo: stallData.logo,
|
|
categories: stallData.categories,
|
|
shipping: stallData.shipping
|
|
}
|
|
|
|
console.log('Created stall (most recent version):', stall)
|
|
marketStore.addStall(stall)
|
|
})
|
|
|
|
} catch (err) {
|
|
console.error('Error loading stalls:', err)
|
|
// Don't throw error, continue without stalls
|
|
}
|
|
}
|
|
|
|
const loadProducts = async () => {
|
|
try {
|
|
const client = nostrStore.getClient()
|
|
|
|
// Get all stall pubkeys
|
|
const stallPubkeys = marketStore.stalls.map(stall => stall.pubkey)
|
|
|
|
console.log('Loading products for stall pubkeys:', stallPubkeys)
|
|
|
|
if (stallPubkeys.length === 0) {
|
|
console.log('No stalls found, skipping product loading')
|
|
return
|
|
}
|
|
|
|
// Fetch product events from all stalls
|
|
const events = await client.fetchEvents({
|
|
kinds: [MARKET_EVENT_KINDS.PRODUCT],
|
|
authors: stallPubkeys
|
|
})
|
|
|
|
console.log('Found product events:', events.length)
|
|
|
|
// Group events by product ID and keep only the most recent version
|
|
const productGroups = new Map<string, any[]>()
|
|
|
|
events.forEach(event => {
|
|
try {
|
|
const productData = JSON.parse(event.content)
|
|
const productId = productData.id
|
|
|
|
if (!productGroups.has(productId)) {
|
|
productGroups.set(productId, [])
|
|
}
|
|
productGroups.get(productId)!.push({ event, productData })
|
|
} catch (err) {
|
|
console.warn('Failed to parse product event:', err)
|
|
}
|
|
})
|
|
|
|
// Process each product group, keeping only the most recent version
|
|
productGroups.forEach((productEvents, _productId) => {
|
|
// Sort by created_at timestamp (most recent first)
|
|
productEvents.sort((a, b) => b.event.created_at - a.event.created_at)
|
|
|
|
// Take the most recent version
|
|
const { event, productData } = productEvents[0]
|
|
|
|
console.log('Processing most recent product event:', event)
|
|
console.log('Parsed product data:', productData)
|
|
|
|
const stall = marketStore.stalls.find(s => s.pubkey === event.pubkey)
|
|
console.log('Found stall for product:', stall)
|
|
|
|
if (stall) {
|
|
const product: Product = {
|
|
id: productData.id, // Use the product's unique ID from content, not the Nostr event ID
|
|
stall_id: stall.id,
|
|
stallName: stall.name,
|
|
name: productData.name,
|
|
description: productData.description,
|
|
price: productData.price,
|
|
currency: productData.currency,
|
|
quantity: productData.quantity,
|
|
images: productData.images,
|
|
categories: productData.categories,
|
|
createdAt: event.created_at,
|
|
updatedAt: event.created_at
|
|
}
|
|
|
|
console.log('Created product (most recent version):', product)
|
|
marketStore.addProduct(product)
|
|
} else {
|
|
console.warn('No stall found for product pubkey:', event.pubkey)
|
|
}
|
|
})
|
|
|
|
} catch (err) {
|
|
console.error('Error loading products:', err)
|
|
// Don't throw error, continue without products
|
|
}
|
|
|
|
// If no products found, add some sample products for testing
|
|
if (marketStore.products.length === 0) {
|
|
console.log('No products found, adding sample products for testing')
|
|
addSampleProducts()
|
|
}
|
|
}
|
|
|
|
const addSampleProducts = () => {
|
|
// Create a sample stall if none exists
|
|
if (marketStore.stalls.length === 0) {
|
|
const sampleStall: Stall = {
|
|
id: 'sample-stall-1',
|
|
pubkey: '70f93a32c14efe5e5c5ed7c13351dd53de367701dd00dd10a1f89280c7c586d5',
|
|
name: 'Castle Tech',
|
|
description: 'Premium tech products',
|
|
categories: ['Electronics', 'Security'],
|
|
shipping: {}
|
|
}
|
|
marketStore.addStall(sampleStall)
|
|
}
|
|
|
|
const sampleProducts: Product[] = [
|
|
{
|
|
id: 'sample-product-1',
|
|
stall_id: 'sample-stall-1',
|
|
stallName: 'Castle Tech',
|
|
name: 'Seed Signer',
|
|
description: 'Your Cyberpunk Cold Wallet',
|
|
price: 100000,
|
|
currency: 'sat',
|
|
quantity: 15,
|
|
images: [],
|
|
categories: ['Hardware', 'Security'],
|
|
createdAt: Date.now() / 1000,
|
|
updatedAt: Date.now() / 1000
|
|
},
|
|
{
|
|
id: 'sample-product-2',
|
|
stall_id: 'sample-stall-1',
|
|
stallName: 'Castle Tech',
|
|
name: 'Bitcoin Node',
|
|
description: 'Full Bitcoin node for maximum privacy',
|
|
price: 50000,
|
|
currency: 'sat',
|
|
quantity: 10,
|
|
images: [],
|
|
categories: ['Hardware', 'Networking'],
|
|
createdAt: Date.now() / 1000,
|
|
updatedAt: Date.now() / 1000
|
|
}
|
|
]
|
|
|
|
sampleProducts.forEach(product => {
|
|
marketStore.addProduct(product)
|
|
})
|
|
|
|
console.log('Added sample products:', sampleProducts.length)
|
|
}
|
|
|
|
const subscribeToMarketUpdates = () => {
|
|
try {
|
|
const client = nostrStore.getClient()
|
|
|
|
// Subscribe to real-time market updates
|
|
const filters = [
|
|
{
|
|
kinds: [MARKET_EVENT_KINDS.STALL, MARKET_EVENT_KINDS.PRODUCT],
|
|
since: Math.floor(Date.now() / 1000)
|
|
}
|
|
]
|
|
|
|
// Subscribe to each relay individually
|
|
const unsubscribes = config.market.supportedRelays.map(relay => {
|
|
const sub = client.poolInstance.subscribeMany(
|
|
[relay],
|
|
filters,
|
|
{
|
|
onevent: (event: any) => {
|
|
handleMarketEvent(event)
|
|
}
|
|
}
|
|
)
|
|
return () => sub.close()
|
|
})
|
|
|
|
// Return a function that unsubscribes from all relays
|
|
return () => {
|
|
unsubscribes.forEach(unsub => unsub())
|
|
}
|
|
|
|
} catch (err) {
|
|
console.error('Error subscribing to market updates:', err)
|
|
// Return a no-op function if subscription fails
|
|
return () => {}
|
|
}
|
|
}
|
|
|
|
const handleMarketEvent = (event: any) => {
|
|
try {
|
|
switch (event.kind) {
|
|
case MARKET_EVENT_KINDS.STALL:
|
|
handleStallEvent(event)
|
|
break
|
|
case MARKET_EVENT_KINDS.PRODUCT:
|
|
handleProductEvent(event)
|
|
break
|
|
case MARKET_EVENT_KINDS.ORDER:
|
|
handleOrderEvent(event)
|
|
break
|
|
}
|
|
} catch (err) {
|
|
console.error('Error handling market event:', err)
|
|
}
|
|
}
|
|
|
|
const handleStallEvent = (event: any) => {
|
|
try {
|
|
const stallData = JSON.parse(event.content)
|
|
const stall: Stall = {
|
|
id: event.id,
|
|
pubkey: event.pubkey,
|
|
name: stallData.name,
|
|
description: stallData.description,
|
|
logo: stallData.logo,
|
|
categories: stallData.categories,
|
|
shipping: stallData.shipping
|
|
}
|
|
|
|
marketStore.addStall(stall)
|
|
} catch (err) {
|
|
console.warn('Failed to parse stall event:', err)
|
|
}
|
|
}
|
|
|
|
const handleProductEvent = (event: any) => {
|
|
try {
|
|
const productData = JSON.parse(event.content)
|
|
const stall = marketStore.stalls.find(s => s.pubkey === event.pubkey)
|
|
|
|
if (stall) {
|
|
const product: Product = {
|
|
id: event.id,
|
|
stall_id: stall.id,
|
|
stallName: stall.name,
|
|
name: productData.name,
|
|
description: productData.description,
|
|
price: productData.price,
|
|
currency: productData.currency,
|
|
quantity: productData.quantity,
|
|
images: productData.images,
|
|
categories: productData.categories,
|
|
createdAt: event.created_at,
|
|
updatedAt: event.created_at
|
|
}
|
|
|
|
marketStore.addProduct(product)
|
|
}
|
|
} catch (err) {
|
|
console.warn('Failed to parse product event:', err)
|
|
}
|
|
}
|
|
|
|
const handleOrderEvent = (event: any) => {
|
|
try {
|
|
const orderData = JSON.parse(event.content)
|
|
// Handle order events (for future order management)
|
|
console.log('Order event received:', orderData)
|
|
} catch (err) {
|
|
console.warn('Failed to parse order event:', err)
|
|
}
|
|
}
|
|
|
|
const publishProduct = async (_productData: any) => {
|
|
try {
|
|
|
|
// Note: This would need to be signed with the user's private key
|
|
// For now, we'll just log that this function needs to be implemented
|
|
console.log('Product publishing not yet implemented - needs private key signing')
|
|
|
|
} catch (err) {
|
|
console.error('Error publishing product:', err)
|
|
throw err
|
|
}
|
|
}
|
|
|
|
const publishStall = async (_stallData: any) => {
|
|
try {
|
|
|
|
// Note: This would need to be signed with the user's private key
|
|
// For now, we'll just log that this function needs to be implemented
|
|
console.log('Stall publishing not yet implemented - needs private key signing')
|
|
|
|
} catch (err) {
|
|
console.error('Error publishing stall:', err)
|
|
throw err
|
|
}
|
|
}
|
|
|
|
const connectToMarket = async () => {
|
|
try {
|
|
console.log('Checking Nostr connection...')
|
|
console.log('Current connection state:', nostrStore.isConnected)
|
|
|
|
if (!nostrStore.isConnected) {
|
|
console.log('Connecting to Nostr relays...')
|
|
await nostrStore.connect()
|
|
console.log('Connected to Nostr relays')
|
|
}
|
|
|
|
isConnected.value = nostrStore.isConnected
|
|
console.log('Final connection state:', isConnected.value)
|
|
|
|
if (!isConnected.value) {
|
|
throw new Error('Failed to connect to Nostr relays')
|
|
}
|
|
|
|
} catch (err) {
|
|
console.error('Error connecting to market:', err)
|
|
throw err
|
|
}
|
|
}
|
|
|
|
const disconnectFromMarket = () => {
|
|
// Cleanup subscriptions and connections
|
|
isConnected.value = false
|
|
}
|
|
|
|
return {
|
|
// State
|
|
isLoading: readonly(isLoading),
|
|
isConnected: readonly(isConnected),
|
|
|
|
// Actions
|
|
loadMarket,
|
|
connectToMarket,
|
|
disconnectFromMarket,
|
|
publishProduct,
|
|
publishStall,
|
|
subscribeToMarketUpdates
|
|
}
|
|
}
|