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
|
|
@ -28,6 +28,10 @@ interface AppConfig {
|
|||
api: ApiConfig
|
||||
push: PushConfig
|
||||
market: MarketConfig
|
||||
nostrclient: {
|
||||
url: string
|
||||
enabled: boolean
|
||||
}
|
||||
support: {
|
||||
npub: string
|
||||
}
|
||||
|
|
@ -72,6 +76,10 @@ export const config: AppConfig = {
|
|||
lightningEnabled: Boolean(import.meta.env.VITE_LIGHTNING_ENABLED),
|
||||
defaultCurrency: import.meta.env.VITE_MARKET_DEFAULT_CURRENCY || 'sat'
|
||||
},
|
||||
nostrclient: {
|
||||
url: import.meta.env.VITE_NOSTRCLIENT_URL || 'wss://localhost:5000/nostrclient/api/v1',
|
||||
enabled: Boolean(import.meta.env.VITE_NOSTRCLIENT_ENABLED)
|
||||
},
|
||||
support: {
|
||||
npub: import.meta.env.VITE_SUPPORT_NPUB || ''
|
||||
}
|
||||
|
|
|
|||
334
src/lib/nostr/nostrclientHub.ts
Normal file
334
src/lib/nostr/nostrclientHub.ts
Normal file
|
|
@ -0,0 +1,334 @@
|
|||
import { EventEmitter } from 'events'
|
||||
import type { Filter, Event } from 'nostr-tools'
|
||||
|
||||
export interface NostrclientConfig {
|
||||
url: string
|
||||
privateKey?: string // For private WebSocket endpoint
|
||||
}
|
||||
|
||||
export interface SubscriptionConfig {
|
||||
id: string
|
||||
filters: Filter[]
|
||||
onEvent?: (event: Event) => void
|
||||
onEose?: () => void
|
||||
onClose?: () => void
|
||||
}
|
||||
|
||||
export interface RelayStatus {
|
||||
url: string
|
||||
connected: boolean
|
||||
lastSeen: number
|
||||
error?: string
|
||||
}
|
||||
|
||||
export class NostrclientHub extends EventEmitter {
|
||||
private ws: WebSocket | null = null
|
||||
private config: NostrclientConfig
|
||||
private subscriptions: Map<string, SubscriptionConfig> = new Map()
|
||||
private reconnectInterval?: NodeJS.Timeout
|
||||
private reconnectAttempts = 0
|
||||
private readonly maxReconnectAttempts = 5
|
||||
private readonly reconnectDelay = 5000
|
||||
|
||||
// Connection state
|
||||
private _isConnected = false
|
||||
private _isConnecting = false
|
||||
|
||||
constructor(config: NostrclientConfig) {
|
||||
super()
|
||||
this.config = config
|
||||
}
|
||||
|
||||
get isConnected(): boolean {
|
||||
return this._isConnected
|
||||
}
|
||||
|
||||
get isConnecting(): boolean {
|
||||
return this._isConnecting
|
||||
}
|
||||
|
||||
get totalSubscriptionCount(): number {
|
||||
return this.subscriptions.size
|
||||
}
|
||||
|
||||
get subscriptionDetails(): Array<{ id: string; filters: Filter[] }> {
|
||||
return Array.from(this.subscriptions.values()).map(sub => ({
|
||||
id: sub.id,
|
||||
filters: sub.filters
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize and connect to nostrclient WebSocket
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
console.log('🔧 NostrclientHub: Initializing connection to', this.config.url)
|
||||
await this.connect()
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to the nostrclient WebSocket
|
||||
*/
|
||||
async connect(): Promise<void> {
|
||||
if (this._isConnecting || this._isConnected) {
|
||||
return
|
||||
}
|
||||
|
||||
this._isConnecting = true
|
||||
this.reconnectAttempts++
|
||||
|
||||
try {
|
||||
console.log('🔧 NostrclientHub: Connecting to nostrclient WebSocket')
|
||||
|
||||
// Determine WebSocket endpoint
|
||||
const wsUrl = this.config.privateKey
|
||||
? `${this.config.url}/${this.config.privateKey}` // Private endpoint
|
||||
: `${this.config.url}/relay` // Public endpoint
|
||||
|
||||
this.ws = new WebSocket(wsUrl)
|
||||
|
||||
this.ws.onopen = () => {
|
||||
console.log('🔧 NostrclientHub: WebSocket connected')
|
||||
this._isConnected = true
|
||||
this._isConnecting = false
|
||||
this.reconnectAttempts = 0
|
||||
this.emit('connected')
|
||||
|
||||
// Resubscribe to existing subscriptions
|
||||
this.resubscribeAll()
|
||||
}
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
this.handleMessage(event.data)
|
||||
}
|
||||
|
||||
this.ws.onclose = (event) => {
|
||||
console.log('🔧 NostrclientHub: WebSocket closed:', event.code, event.reason)
|
||||
this._isConnected = false
|
||||
this._isConnecting = false
|
||||
this.emit('disconnected', event)
|
||||
|
||||
// Schedule reconnection
|
||||
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
||||
this.scheduleReconnect()
|
||||
} else {
|
||||
this.emit('maxReconnectionAttemptsReached')
|
||||
}
|
||||
}
|
||||
|
||||
this.ws.onerror = (error) => {
|
||||
console.error('🔧 NostrclientHub: WebSocket error:', error)
|
||||
this.emit('error', error)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this._isConnecting = false
|
||||
console.error('🔧 NostrclientHub: Connection failed:', error)
|
||||
this.emit('connectionError', error)
|
||||
|
||||
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
||||
this.scheduleReconnect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect from the WebSocket
|
||||
*/
|
||||
disconnect(): void {
|
||||
if (this.reconnectInterval) {
|
||||
clearTimeout(this.reconnectInterval)
|
||||
this.reconnectInterval = undefined
|
||||
}
|
||||
|
||||
if (this.ws) {
|
||||
this.ws.close()
|
||||
this.ws = null
|
||||
}
|
||||
|
||||
this._isConnected = false
|
||||
this._isConnecting = false
|
||||
this.subscriptions.clear()
|
||||
this.emit('disconnected')
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to events
|
||||
*/
|
||||
subscribe(config: SubscriptionConfig): () => void {
|
||||
if (!this._isConnected) {
|
||||
throw new Error('Not connected to nostrclient')
|
||||
}
|
||||
|
||||
// Store subscription
|
||||
this.subscriptions.set(config.id, config)
|
||||
|
||||
// Send REQ message
|
||||
const reqMessage = JSON.stringify([
|
||||
'REQ',
|
||||
config.id,
|
||||
...config.filters
|
||||
])
|
||||
|
||||
this.ws?.send(reqMessage)
|
||||
console.log('🔧 NostrclientHub: Subscribed to', config.id)
|
||||
|
||||
// Return unsubscribe function
|
||||
return () => {
|
||||
this.unsubscribe(config.id)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from events
|
||||
*/
|
||||
unsubscribe(subscriptionId: string): void {
|
||||
if (!this._isConnected) {
|
||||
return
|
||||
}
|
||||
|
||||
// Send CLOSE message
|
||||
const closeMessage = JSON.stringify(['CLOSE', subscriptionId])
|
||||
this.ws?.send(closeMessage)
|
||||
|
||||
// Remove from subscriptions
|
||||
this.subscriptions.delete(subscriptionId)
|
||||
console.log('🔧 NostrclientHub: Unsubscribed from', subscriptionId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish an event
|
||||
*/
|
||||
async publishEvent(event: Event): Promise<void> {
|
||||
if (!this._isConnected) {
|
||||
throw new Error('Not connected to nostrclient')
|
||||
}
|
||||
|
||||
const eventMessage = JSON.stringify(['EVENT', event])
|
||||
this.ws?.send(eventMessage)
|
||||
|
||||
console.log('🔧 NostrclientHub: Published event', event.id)
|
||||
this.emit('eventPublished', { eventId: event.id })
|
||||
}
|
||||
|
||||
/**
|
||||
* Query events (one-time fetch)
|
||||
*/
|
||||
async queryEvents(filters: Filter[]): Promise<Event[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this._isConnected) {
|
||||
reject(new Error('Not connected to nostrclient'))
|
||||
return
|
||||
}
|
||||
|
||||
const queryId = `query-${Date.now()}`
|
||||
const events: Event[] = []
|
||||
let eoseReceived = false
|
||||
|
||||
// Create temporary subscription for query
|
||||
const tempSubscription = this.subscribe({
|
||||
id: queryId,
|
||||
filters,
|
||||
onEvent: (event) => {
|
||||
events.push(event)
|
||||
},
|
||||
onEose: () => {
|
||||
eoseReceived = true
|
||||
this.unsubscribe(queryId)
|
||||
resolve(events)
|
||||
},
|
||||
onClose: () => {
|
||||
if (!eoseReceived) {
|
||||
reject(new Error('Query subscription closed unexpectedly'))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Timeout after 30 seconds
|
||||
setTimeout(() => {
|
||||
if (!eoseReceived) {
|
||||
tempSubscription()
|
||||
reject(new Error('Query timeout'))
|
||||
}
|
||||
}, 30000)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming WebSocket messages
|
||||
*/
|
||||
private handleMessage(data: string): void {
|
||||
try {
|
||||
const message = JSON.parse(data)
|
||||
|
||||
if (Array.isArray(message) && message.length >= 2) {
|
||||
const [type, subscriptionId, ...rest] = message
|
||||
|
||||
switch (type) {
|
||||
case 'EVENT':
|
||||
const event = rest[0] as Event
|
||||
const subscription = this.subscriptions.get(subscriptionId)
|
||||
if (subscription?.onEvent) {
|
||||
subscription.onEvent(event)
|
||||
}
|
||||
this.emit('event', { subscriptionId, event })
|
||||
break
|
||||
|
||||
case 'EOSE':
|
||||
const eoseSubscription = this.subscriptions.get(subscriptionId)
|
||||
if (eoseSubscription?.onEose) {
|
||||
eoseSubscription.onEose()
|
||||
}
|
||||
this.emit('eose', { subscriptionId })
|
||||
break
|
||||
|
||||
case 'NOTICE':
|
||||
console.log('🔧 NostrclientHub: Notice from relay:', rest[0])
|
||||
this.emit('notice', { message: rest[0] })
|
||||
break
|
||||
|
||||
default:
|
||||
console.log('🔧 NostrclientHub: Unknown message type:', type)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('🔧 NostrclientHub: Failed to parse message:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resubscribe to all existing subscriptions after reconnection
|
||||
*/
|
||||
private resubscribeAll(): void {
|
||||
for (const [id, config] of this.subscriptions) {
|
||||
const reqMessage = JSON.stringify([
|
||||
'REQ',
|
||||
id,
|
||||
...config.filters
|
||||
])
|
||||
this.ws?.send(reqMessage)
|
||||
}
|
||||
console.log('🔧 NostrclientHub: Resubscribed to', this.subscriptions.size, 'subscriptions')
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule automatic reconnection
|
||||
*/
|
||||
private scheduleReconnect(): void {
|
||||
if (this.reconnectInterval) {
|
||||
clearTimeout(this.reconnectInterval)
|
||||
}
|
||||
|
||||
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1)
|
||||
console.log(`🔧 NostrclientHub: Scheduling reconnection in ${delay}ms`)
|
||||
|
||||
this.reconnectInterval = setTimeout(async () => {
|
||||
await this.connect()
|
||||
}, delay)
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const nostrclientHub = new NostrclientHub({
|
||||
url: import.meta.env.VITE_NOSTRCLIENT_URL || 'wss://localhost:5000/nostrclient/api/v1'
|
||||
})
|
||||
|
|
@ -130,6 +130,8 @@ export class RelayHub extends EventEmitter {
|
|||
return
|
||||
}
|
||||
|
||||
console.log('🔧 RelayHub: Initializing with URLs:', relayUrls)
|
||||
|
||||
// Convert URLs to relay configs
|
||||
this.relayConfigs.clear()
|
||||
relayUrls.forEach((url, index) => {
|
||||
|
|
@ -141,12 +143,14 @@ export class RelayHub extends EventEmitter {
|
|||
})
|
||||
})
|
||||
|
||||
console.log('🔧 RelayHub: Relay configs created:', Array.from(this.relayConfigs.values()))
|
||||
|
||||
// Start connection management
|
||||
console.log('🔧 RelayHub: Starting connection...')
|
||||
await this.connect()
|
||||
this.startHealthCheck()
|
||||
this.isInitialized = true
|
||||
|
||||
|
||||
console.log('🔧 RelayHub: Initialization complete')
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -157,22 +161,28 @@ export class RelayHub extends EventEmitter {
|
|||
throw new Error('No relay configurations found. Call initialize() first.')
|
||||
}
|
||||
|
||||
console.log('🔧 RelayHub: Connecting to', this.relayConfigs.size, 'relays')
|
||||
|
||||
try {
|
||||
this._connectionAttempts++
|
||||
|
||||
console.log('🔧 RelayHub: Connection attempt', this._connectionAttempts)
|
||||
|
||||
// Connect to relays in priority order
|
||||
const sortedRelays = Array.from(this.relayConfigs.values())
|
||||
.sort((a, b) => (a.priority || 0) - (b.priority || 0))
|
||||
|
||||
console.log('🔧 RelayHub: Attempting connections to:', sortedRelays.map(r => r.url))
|
||||
|
||||
const connectionPromises = sortedRelays.map(async (config) => {
|
||||
try {
|
||||
console.log('🔧 RelayHub: Connecting to relay:', config.url)
|
||||
const relay = await this.pool.ensureRelay(config.url)
|
||||
this.connectedRelays.set(config.url, relay)
|
||||
console.log('🔧 RelayHub: Successfully connected to:', config.url)
|
||||
|
||||
return { url: config.url, success: true }
|
||||
} catch (error) {
|
||||
console.error(`Failed to connect to relay ${config.url}:`, error)
|
||||
console.error(`🔧 RelayHub: Failed to connect to relay ${config.url}:`, error)
|
||||
return { url: config.url, success: false, error }
|
||||
}
|
||||
})
|
||||
|
|
@ -182,25 +192,34 @@ export class RelayHub extends EventEmitter {
|
|||
result => result.status === 'fulfilled' && result.value.success
|
||||
)
|
||||
|
||||
console.log('🔧 RelayHub: Connection results:', {
|
||||
total: results.length,
|
||||
successful: successfulConnections.length,
|
||||
failed: results.length - successfulConnections.length
|
||||
})
|
||||
|
||||
if (successfulConnections.length > 0) {
|
||||
this._isConnected = true
|
||||
this._connectionAttempts = 0
|
||||
console.log('🔧 RelayHub: Connection successful, connected to', successfulConnections.length, 'relays')
|
||||
this.emit('connected', successfulConnections.length)
|
||||
|
||||
} else {
|
||||
console.error('🔧 RelayHub: Failed to connect to any relay')
|
||||
throw new Error('Failed to connect to any relay')
|
||||
}
|
||||
} catch (error) {
|
||||
this._isConnected = false
|
||||
console.error('🔧 RelayHub: Connection failed with error:', error)
|
||||
this.emit('connectionError', error)
|
||||
console.error('Connection failed:', error)
|
||||
|
||||
// Schedule reconnection if we haven't exceeded max attempts
|
||||
if (this._connectionAttempts < this.maxReconnectAttempts) {
|
||||
console.log('🔧 RelayHub: Scheduling reconnection attempt', this._connectionAttempts + 1)
|
||||
this.scheduleReconnect()
|
||||
} else {
|
||||
this.emit('maxReconnectAttemptsReached')
|
||||
console.error('Max reconnection attempts reached')
|
||||
this.emit('maxReconnectionAttemptsReached')
|
||||
console.error('🔧 RelayHub: Max reconnection attempts reached')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
18
src/lib/nostr/utils.ts
Normal file
18
src/lib/nostr/utils.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
// Helper function to convert bech32 to hex
|
||||
export function bech32ToHex(bech32Key: string): string {
|
||||
if (bech32Key.startsWith('npub1') || bech32Key.startsWith('nsec1')) {
|
||||
// Import bech32 conversion dynamically to avoid bundling issues
|
||||
const { bech32Decode, convertbits } = require('bech32')
|
||||
const [, data] = bech32Decode(bech32Key)
|
||||
if (!data) {
|
||||
throw new Error(`Invalid bech32 key: ${bech32Key}`)
|
||||
}
|
||||
const converted = convertbits(data, 5, 8, false)
|
||||
if (!converted) {
|
||||
throw new Error(`Failed to convert bech32 key: ${bech32Key}`)
|
||||
}
|
||||
return Buffer.from(converted).toString('hex')
|
||||
}
|
||||
// Already hex format
|
||||
return bech32Key
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
// Notification manager for push notifications
|
||||
import { pushService, type NotificationPayload } from './push'
|
||||
import { configUtils } from '@/lib/config'
|
||||
// import type { NotificationPayload } from './push'
|
||||
|
||||
|
||||
export interface NotificationOptions {
|
||||
enabled: boolean
|
||||
|
|
@ -69,19 +69,19 @@ export class NotificationManager {
|
|||
throw new Error('Notifications are disabled')
|
||||
}
|
||||
|
||||
const payload: NotificationPayload = {
|
||||
title: '🧪 Test Notification',
|
||||
body: 'This is a test notification from Ario',
|
||||
tag: 'test',
|
||||
icon: '/apple-touch-icon.png',
|
||||
badge: '/apple-touch-icon.png',
|
||||
data: {
|
||||
url: window.location.origin,
|
||||
timestamp: Date.now()
|
||||
}
|
||||
}
|
||||
// const payload: NotificationPayload = {
|
||||
// title: '🧪 Test Notification',
|
||||
// body: 'This is a test notification from Ario',
|
||||
// tag: 'test',
|
||||
// icon: '/apple-touch-icon.png',
|
||||
// badge: '/apple-touch-icon.png',
|
||||
// data: {
|
||||
// url: window.location.origin,
|
||||
// timestamp: Date.now()
|
||||
// }
|
||||
// }
|
||||
|
||||
await pushService.sendNotification(payload)
|
||||
// await pushService.sendNotification(payload)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
189
src/lib/services/invoiceService.ts
Normal file
189
src/lib/services/invoiceService.ts
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
import { getApiUrl } from '@/lib/config/lnbits'
|
||||
import type { Order } from '@/stores/market'
|
||||
|
||||
export interface LightningInvoice {
|
||||
checking_id: string
|
||||
payment_hash: string
|
||||
wallet_id: string
|
||||
amount: number
|
||||
fee: number
|
||||
bolt11: string // This is the payment request/invoice
|
||||
status: string
|
||||
memo?: string
|
||||
expiry?: string
|
||||
preimage?: string
|
||||
extra?: Record<string, any>
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
}
|
||||
|
||||
export interface CreateInvoiceRequest {
|
||||
amount: number
|
||||
memo: string
|
||||
unit?: 'sat' | 'btc'
|
||||
expiry?: number
|
||||
extra?: Record<string, any>
|
||||
}
|
||||
|
||||
export interface PaymentStatus {
|
||||
paid: boolean
|
||||
amount_paid: number
|
||||
paid_at?: number
|
||||
payment_hash: string
|
||||
}
|
||||
|
||||
class InvoiceService {
|
||||
private baseUrl: string
|
||||
|
||||
constructor() {
|
||||
// Use the payments endpoint for invoice creation
|
||||
this.baseUrl = getApiUrl('/payments')
|
||||
}
|
||||
|
||||
private async request<T>(
|
||||
endpoint: string,
|
||||
adminKey: string,
|
||||
options: RequestInit = {}
|
||||
): Promise<T> {
|
||||
// Construct the URL - for payments, we just append the endpoint
|
||||
const url = `${this.baseUrl}${endpoint}`
|
||||
console.log('Invoice Service Request:', { url, endpoint })
|
||||
|
||||
const headers: HeadersInit = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Api-Key': adminKey, // Use the wallet's admin key
|
||||
...options.headers,
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
console.error('Invoice Service Error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
errorText,
|
||||
url
|
||||
})
|
||||
throw new Error(`Invoice request failed: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
return response.json()
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Lightning invoice for an order
|
||||
*/
|
||||
async createInvoice(order: Order, adminKey: string, extra?: Record<string, any>): Promise<LightningInvoice> {
|
||||
const invoiceData: CreateInvoiceRequest = {
|
||||
amount: order.total,
|
||||
unit: 'sat',
|
||||
memo: `Order ${order.id} - ${order.items.length} items`,
|
||||
expiry: 3600, // 1 hour
|
||||
extra: {
|
||||
tag: 'nostrmarket', // Use nostrmarket tag for compatibility
|
||||
order_id: extra?.order_id || order.id, // Use passed order_id or fallback to order.id
|
||||
merchant_pubkey: extra?.merchant_pubkey || order.sellerPubkey, // Use passed merchant_pubkey or fallback
|
||||
...extra // Allow additional metadata to be passed in
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Log the exact data being sent to LNBits
|
||||
const requestBody = {
|
||||
out: false, // Incoming payment
|
||||
...invoiceData
|
||||
}
|
||||
|
||||
console.log('Sending invoice request to LNBits:', {
|
||||
url: `${this.baseUrl}`,
|
||||
body: requestBody,
|
||||
extra: requestBody.extra
|
||||
})
|
||||
|
||||
// Use the correct LNBits payments endpoint
|
||||
const response = await this.request<LightningInvoice>('', adminKey, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(requestBody)
|
||||
})
|
||||
|
||||
console.log('Full LNBits response:', response)
|
||||
console.log('Response type:', typeof response)
|
||||
console.log('Response keys:', Object.keys(response))
|
||||
|
||||
// Check if we have the expected fields
|
||||
if (!response.bolt11) {
|
||||
console.error('Missing bolt11 in response:', response)
|
||||
throw new Error('Invalid invoice response: missing bolt11')
|
||||
}
|
||||
|
||||
console.log('Lightning invoice created with nostrmarket tag:', {
|
||||
orderId: order.id,
|
||||
paymentHash: response.payment_hash,
|
||||
amount: response.amount,
|
||||
paymentRequest: response.bolt11.substring(0, 50) + '...',
|
||||
extra: invoiceData.extra
|
||||
})
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('Failed to create Lightning invoice:', error)
|
||||
throw new Error('Failed to create payment invoice')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check payment status of an invoice
|
||||
*/
|
||||
async checkPaymentStatus(paymentHash: string, adminKey: string): Promise<PaymentStatus> {
|
||||
try {
|
||||
const response = await this.request<PaymentStatus>(`/${paymentHash}`, adminKey, {})
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('Failed to check payment status:', error)
|
||||
throw new Error('Failed to check payment status')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all payments for a wallet
|
||||
*/
|
||||
async getWalletPayments(adminKey: string, limit: number = 100): Promise<PaymentStatus[]> {
|
||||
try {
|
||||
const response = await this.request<PaymentStatus[]>(`?limit=${limit}`, adminKey, {})
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('Failed to get wallet payments:', error)
|
||||
throw new Error('Failed to get wallet payments')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a Lightning payment request
|
||||
*/
|
||||
validatePaymentRequest(paymentRequest: string): boolean {
|
||||
// Basic validation - should start with 'lnbc' and be a valid length
|
||||
return paymentRequest.startsWith('lnbc') && paymentRequest.length > 50
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract payment hash from a payment request
|
||||
*/
|
||||
extractPaymentHash(paymentRequest: string): string | null {
|
||||
try {
|
||||
// This is a simplified extraction - in production you'd use a proper BOLT11 decoder
|
||||
const match = paymentRequest.match(/lnbc[0-9]+[a-z0-9]+/i)
|
||||
return match ? match[0] : null
|
||||
} catch (error) {
|
||||
console.error('Failed to extract payment hash:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const invoiceService = new InvoiceService()
|
||||
|
||||
396
src/lib/services/nostrmarketService.ts
Normal file
396
src/lib/services/nostrmarketService.ts
Normal file
|
|
@ -0,0 +1,396 @@
|
|||
import { finalizeEvent, type EventTemplate, nip04 } from 'nostr-tools'
|
||||
import { relayHub } from '@/lib/nostr/relayHub'
|
||||
import { auth } from '@/composables/useAuth'
|
||||
import type { Stall, Product, Order } from '@/stores/market'
|
||||
import { bech32ToHex } from '@/lib/utils/bech32'
|
||||
|
||||
export interface NostrmarketStall {
|
||||
id: string
|
||||
name: string
|
||||
description?: string
|
||||
currency: string
|
||||
shipping: Array<{
|
||||
id: string
|
||||
name: string
|
||||
cost: number
|
||||
countries: string[]
|
||||
}>
|
||||
}
|
||||
|
||||
export interface NostrmarketProduct {
|
||||
id: string
|
||||
stall_id: string
|
||||
name: string
|
||||
description?: string
|
||||
images: string[]
|
||||
categories: string[]
|
||||
price: number
|
||||
quantity: number
|
||||
currency: string
|
||||
}
|
||||
|
||||
export interface NostrmarketOrder {
|
||||
id: string
|
||||
items: Array<{
|
||||
product_id: string
|
||||
quantity: number
|
||||
}>
|
||||
contact: {
|
||||
name: string
|
||||
email?: string
|
||||
phone?: string
|
||||
}
|
||||
address?: {
|
||||
street: string
|
||||
city: string
|
||||
state: string
|
||||
country: string
|
||||
postal_code: string
|
||||
}
|
||||
shipping_id: string
|
||||
}
|
||||
|
||||
export interface NostrmarketPaymentRequest {
|
||||
type: 1
|
||||
id: string
|
||||
message?: string
|
||||
payment_options: Array<{
|
||||
type: string
|
||||
link: string
|
||||
}>
|
||||
}
|
||||
|
||||
export interface NostrmarketOrderStatus {
|
||||
type: 2
|
||||
id: string
|
||||
message?: string
|
||||
paid?: boolean
|
||||
shipped?: boolean
|
||||
}
|
||||
|
||||
export class NostrmarketService {
|
||||
private getAuth() {
|
||||
if (!auth.isAuthenticated.value || !auth.currentUser.value?.prvkey) {
|
||||
throw new Error('User not authenticated or private key not available')
|
||||
}
|
||||
|
||||
// Convert bech32 keys to hex format if needed
|
||||
const originalPubkey = auth.currentUser.value.pubkey
|
||||
const originalPrvkey = auth.currentUser.value.prvkey
|
||||
const pubkey = bech32ToHex(originalPubkey)
|
||||
const prvkey = bech32ToHex(originalPrvkey)
|
||||
|
||||
console.log('🔑 Key conversion debug:', {
|
||||
originalPubkey: originalPubkey?.substring(0, 10) + '...',
|
||||
originalPrvkey: originalPrvkey?.substring(0, 10) + '...',
|
||||
convertedPubkey: pubkey.substring(0, 10) + '...',
|
||||
convertedPrvkey: prvkey.substring(0, 10) + '...',
|
||||
pubkeyIsHex: /^[0-9a-fA-F]{64}$/.test(pubkey),
|
||||
prvkeyIsHex: /^[0-9a-fA-F]{64}$/.test(prvkey)
|
||||
})
|
||||
|
||||
return {
|
||||
pubkey,
|
||||
prvkey
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish a stall event (kind 30017) to Nostr
|
||||
*/
|
||||
async publishStall(stall: Stall): Promise<string> {
|
||||
const { pubkey, prvkey } = this.getAuth()
|
||||
|
||||
const stallData: NostrmarketStall = {
|
||||
id: stall.id,
|
||||
name: stall.name,
|
||||
description: stall.description,
|
||||
currency: stall.currency,
|
||||
shipping: (stall.shipping || []).map(zone => ({
|
||||
id: zone.id,
|
||||
name: zone.name,
|
||||
cost: zone.cost,
|
||||
countries: []
|
||||
}))
|
||||
}
|
||||
|
||||
const eventTemplate: EventTemplate = {
|
||||
kind: 30017,
|
||||
tags: [
|
||||
['t', 'stall'],
|
||||
['t', 'nostrmarket']
|
||||
],
|
||||
content: JSON.stringify(stallData),
|
||||
created_at: Math.floor(Date.now() / 1000)
|
||||
}
|
||||
|
||||
const event = finalizeEvent(eventTemplate, prvkey)
|
||||
const eventId = await relayHub.publishEvent(event)
|
||||
|
||||
console.log('Stall published to nostrmarket:', {
|
||||
stallId: stall.id,
|
||||
eventId: eventId,
|
||||
content: stallData
|
||||
})
|
||||
|
||||
return eventId
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish a product event (kind 30018) to Nostr
|
||||
*/
|
||||
async publishProduct(product: Product): Promise<string> {
|
||||
const { pubkey, prvkey } = this.getAuth()
|
||||
|
||||
const productData: NostrmarketProduct = {
|
||||
id: product.id,
|
||||
stall_id: product.stall_id,
|
||||
name: product.name,
|
||||
description: product.description,
|
||||
images: product.images || [],
|
||||
categories: product.categories || [],
|
||||
price: product.price,
|
||||
quantity: product.quantity,
|
||||
currency: product.currency
|
||||
}
|
||||
|
||||
const eventTemplate: EventTemplate = {
|
||||
kind: 30018,
|
||||
tags: [
|
||||
['t', 'product'],
|
||||
['t', 'nostrmarket'],
|
||||
['t', 'stall', product.stall_id],
|
||||
...(product.categories || []).map(cat => ['t', cat])
|
||||
],
|
||||
content: JSON.stringify(productData),
|
||||
created_at: Math.floor(Date.now() / 1000)
|
||||
}
|
||||
|
||||
const event = finalizeEvent(eventTemplate, prvkey)
|
||||
const eventId = await relayHub.publishEvent(event)
|
||||
|
||||
console.log('Product published to nostrmarket:', {
|
||||
productId: product.id,
|
||||
eventId: eventId,
|
||||
content: productData
|
||||
})
|
||||
|
||||
return eventId
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish an order event (kind 4 encrypted DM) to nostrmarket
|
||||
*/
|
||||
async publishOrder(order: Order, merchantPubkey: string): Promise<string> {
|
||||
const { pubkey, prvkey } = this.getAuth()
|
||||
|
||||
// Convert order to nostrmarket format - exactly matching the specification
|
||||
const orderData = {
|
||||
type: 0, // DirectMessageType.CUSTOMER_ORDER
|
||||
id: order.id,
|
||||
items: order.items.map(item => ({
|
||||
product_id: item.productId,
|
||||
quantity: item.quantity
|
||||
})),
|
||||
contact: {
|
||||
name: order.contactInfo?.message || order.contactInfo?.email || 'Unknown',
|
||||
email: order.contactInfo?.email || ''
|
||||
// Remove phone field - not in nostrmarket specification
|
||||
},
|
||||
// Only include address if it's a physical good and address is provided
|
||||
...(order.shippingZone?.requiresPhysicalShipping && order.contactInfo?.address ? {
|
||||
address: order.contactInfo.address
|
||||
} : {}),
|
||||
shipping_id: order.shippingZone?.id || 'online'
|
||||
}
|
||||
|
||||
// Encrypt the message using NIP-04
|
||||
const encryptedContent = await nip04.encrypt(prvkey, merchantPubkey, JSON.stringify(orderData))
|
||||
|
||||
const eventTemplate: EventTemplate = {
|
||||
kind: 4, // Encrypted DM
|
||||
tags: [['p', merchantPubkey]], // Recipient (merchant)
|
||||
content: encryptedContent, // Use encrypted content
|
||||
created_at: Math.floor(Date.now() / 1000)
|
||||
}
|
||||
|
||||
const event = finalizeEvent(eventTemplate, prvkey)
|
||||
const eventId = await relayHub.publishEvent(event)
|
||||
|
||||
console.log('Order published to nostrmarket:', {
|
||||
orderId: order.id,
|
||||
eventId: eventId,
|
||||
merchantPubkey,
|
||||
content: orderData,
|
||||
encryptedContent: encryptedContent.substring(0, 50) + '...'
|
||||
})
|
||||
|
||||
return eventId
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming payment request from merchant (type 1)
|
||||
*/
|
||||
async handlePaymentRequest(paymentRequest: NostrmarketPaymentRequest): Promise<void> {
|
||||
console.log('Received payment request from merchant:', {
|
||||
orderId: paymentRequest.id,
|
||||
message: paymentRequest.message,
|
||||
paymentOptions: paymentRequest.payment_options
|
||||
})
|
||||
|
||||
// Find the Lightning payment option
|
||||
const lightningOption = paymentRequest.payment_options.find(option => option.type === 'ln')
|
||||
if (!lightningOption) {
|
||||
console.error('No Lightning payment option found in payment request')
|
||||
return
|
||||
}
|
||||
|
||||
// Update the order in the store with payment request
|
||||
const { useMarketStore } = await import('@/stores/market')
|
||||
const marketStore = useMarketStore()
|
||||
|
||||
const order = Object.values(marketStore.orders).find(o =>
|
||||
o.id === paymentRequest.id || o.originalOrderId === paymentRequest.id
|
||||
)
|
||||
|
||||
if (order) {
|
||||
// Update order with payment request details
|
||||
const updatedOrder = {
|
||||
...order,
|
||||
paymentRequest: lightningOption.link,
|
||||
paymentStatus: 'pending' as const,
|
||||
status: 'pending' as const, // Ensure status is pending for payment
|
||||
updatedAt: Math.floor(Date.now() / 1000),
|
||||
items: [...order.items] // Convert readonly to mutable
|
||||
}
|
||||
|
||||
// Generate QR code for the payment request
|
||||
try {
|
||||
const QRCode = await import('qrcode')
|
||||
const qrCodeDataUrl = await QRCode.toDataURL(lightningOption.link, {
|
||||
width: 256,
|
||||
margin: 2,
|
||||
color: {
|
||||
dark: '#000000',
|
||||
light: '#FFFFFF'
|
||||
}
|
||||
})
|
||||
updatedOrder.qrCodeDataUrl = qrCodeDataUrl
|
||||
updatedOrder.qrCodeLoading = false
|
||||
updatedOrder.qrCodeError = null
|
||||
} catch (error) {
|
||||
console.error('Failed to generate QR code:', error)
|
||||
updatedOrder.qrCodeError = 'Failed to generate QR code'
|
||||
updatedOrder.qrCodeLoading = false
|
||||
}
|
||||
|
||||
marketStore.updateOrder(order.id, updatedOrder)
|
||||
|
||||
console.log('Order updated with payment request:', {
|
||||
orderId: paymentRequest.id,
|
||||
paymentRequest: lightningOption.link.substring(0, 50) + '...',
|
||||
status: updatedOrder.status,
|
||||
paymentStatus: updatedOrder.paymentStatus,
|
||||
hasQRCode: !!updatedOrder.qrCodeDataUrl
|
||||
})
|
||||
} else {
|
||||
console.warn('Payment request received for unknown order:', paymentRequest.id)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming order status update from merchant (type 2)
|
||||
*/
|
||||
async handleOrderStatusUpdate(statusUpdate: NostrmarketOrderStatus): Promise<void> {
|
||||
console.log('Received order status update from merchant:', {
|
||||
orderId: statusUpdate.id,
|
||||
message: statusUpdate.message,
|
||||
paid: statusUpdate.paid,
|
||||
shipped: statusUpdate.shipped
|
||||
})
|
||||
|
||||
const { useMarketStore } = await import('@/stores/market')
|
||||
const marketStore = useMarketStore()
|
||||
|
||||
const order = Object.values(marketStore.orders).find(o =>
|
||||
o.id === statusUpdate.id || o.originalOrderId === statusUpdate.id
|
||||
)
|
||||
|
||||
if (order) {
|
||||
// Update order status
|
||||
if (statusUpdate.paid !== undefined) {
|
||||
const newStatus = statusUpdate.paid ? 'paid' : 'pending'
|
||||
marketStore.updateOrderStatus(order.id, newStatus)
|
||||
|
||||
// Also update payment status
|
||||
const updatedOrder = {
|
||||
...order,
|
||||
paymentStatus: (statusUpdate.paid ? 'paid' : 'pending') as 'paid' | 'pending' | 'expired',
|
||||
paidAt: statusUpdate.paid ? Math.floor(Date.now() / 1000) : undefined,
|
||||
updatedAt: Math.floor(Date.now() / 1000),
|
||||
items: [...order.items] // Convert readonly to mutable
|
||||
}
|
||||
marketStore.updateOrder(order.id, updatedOrder)
|
||||
}
|
||||
|
||||
if (statusUpdate.shipped !== undefined) {
|
||||
// Update shipping status if you have that field
|
||||
const updatedOrder = {
|
||||
...order,
|
||||
shipped: statusUpdate.shipped,
|
||||
status: statusUpdate.shipped ? 'shipped' : order.status,
|
||||
updatedAt: Math.floor(Date.now() / 1000),
|
||||
items: [...order.items] // Convert readonly to mutable
|
||||
}
|
||||
marketStore.updateOrder(order.id, updatedOrder)
|
||||
}
|
||||
|
||||
console.log('Order status updated:', {
|
||||
orderId: statusUpdate.id,
|
||||
paid: statusUpdate.paid,
|
||||
shipped: statusUpdate.shipped,
|
||||
newStatus: statusUpdate.paid ? 'paid' : 'pending'
|
||||
})
|
||||
} else {
|
||||
console.warn('Status update received for unknown order:', statusUpdate.id)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish all stalls and products for a merchant
|
||||
*/
|
||||
async publishMerchantCatalog(stalls: Stall[], products: Product[]): Promise<{
|
||||
stalls: Record<string, string>, // stallId -> eventId
|
||||
products: Record<string, string> // productId -> eventId
|
||||
}> {
|
||||
const results = {
|
||||
stalls: {} as Record<string, string>,
|
||||
products: {} as Record<string, string>
|
||||
}
|
||||
|
||||
// Publish stalls first
|
||||
for (const stall of stalls) {
|
||||
try {
|
||||
const eventId = await this.publishStall(stall)
|
||||
results.stalls[stall.id] = eventId
|
||||
} catch (error) {
|
||||
console.error(`Failed to publish stall ${stall.id}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
// Publish products
|
||||
for (const product of products) {
|
||||
try {
|
||||
const eventId = await this.publishProduct(product)
|
||||
results.products[product.id] = eventId
|
||||
} catch (error) {
|
||||
console.error(`Failed to publish product ${product.id}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const nostrmarketService = new NostrmarketService()
|
||||
275
src/lib/services/paymentMonitor.ts
Normal file
275
src/lib/services/paymentMonitor.ts
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
import { ref } from 'vue'
|
||||
import type { PaymentStatus, LightningInvoice } from './invoiceService'
|
||||
import type { Order } from '@/stores/market'
|
||||
|
||||
export interface PaymentMonitorState {
|
||||
isMonitoring: boolean
|
||||
activeInvoices: Map<string, LightningInvoice>
|
||||
paymentStatuses: Map<string, PaymentStatus>
|
||||
lastUpdate: number
|
||||
error: string | null
|
||||
}
|
||||
|
||||
export interface PaymentUpdate {
|
||||
orderId: string
|
||||
paymentHash: string
|
||||
status: 'pending' | 'paid' | 'expired'
|
||||
amount: number
|
||||
paidAt?: number
|
||||
}
|
||||
|
||||
class PaymentMonitorService {
|
||||
private state = ref<PaymentMonitorState>({
|
||||
isMonitoring: false,
|
||||
activeInvoices: new Map(),
|
||||
paymentStatuses: new Map(),
|
||||
lastUpdate: 0,
|
||||
error: null
|
||||
})
|
||||
|
||||
private monitoringInterval: NodeJS.Timeout | null = null
|
||||
private updateCallbacks: Map<string, (update: PaymentUpdate) => void> = new Map()
|
||||
|
||||
// Computed properties
|
||||
get isMonitoring() { return this.state.value.isMonitoring }
|
||||
get activeInvoices() { return this.state.value.activeInvoices }
|
||||
get paymentStatuses() { return this.state.value.paymentStatuses }
|
||||
get lastUpdate() { return this.state.value.lastUpdate }
|
||||
get error() { return this.state.value.error }
|
||||
|
||||
/**
|
||||
* Start monitoring payments for a specific order
|
||||
*/
|
||||
async startMonitoring(order: Order, invoice: LightningInvoice): Promise<void> {
|
||||
try {
|
||||
// Add invoice to active monitoring
|
||||
this.state.value.activeInvoices.set(order.id, invoice)
|
||||
this.state.value.paymentStatuses.set(invoice.payment_hash, {
|
||||
paid: false,
|
||||
amount_paid: 0,
|
||||
payment_hash: invoice.payment_hash
|
||||
})
|
||||
|
||||
// Start monitoring if not already running
|
||||
if (!this.state.value.isMonitoring) {
|
||||
await this.startMonitoringLoop()
|
||||
}
|
||||
|
||||
console.log('Started monitoring payment for order:', {
|
||||
orderId: order.id,
|
||||
paymentHash: invoice.payment_hash,
|
||||
amount: invoice.amount
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to start payment monitoring:', error)
|
||||
this.state.value.error = 'Failed to start payment monitoring'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop monitoring a specific order
|
||||
*/
|
||||
stopMonitoring(orderId: string): void {
|
||||
const invoice = this.state.value.activeInvoices.get(orderId)
|
||||
if (invoice) {
|
||||
this.state.value.activeInvoices.delete(orderId)
|
||||
this.state.value.paymentStatuses.delete(invoice.payment_hash)
|
||||
console.log('Stopped monitoring payment for order:', orderId)
|
||||
}
|
||||
|
||||
// Stop monitoring loop if no more active invoices
|
||||
if (this.state.value.activeInvoices.size === 0) {
|
||||
this.stopMonitoringLoop()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the monitoring loop
|
||||
*/
|
||||
private async startMonitoringLoop(): Promise<void> {
|
||||
if (this.state.value.isMonitoring) return
|
||||
|
||||
this.state.value.isMonitoring = true
|
||||
console.log('Starting payment monitoring loop')
|
||||
|
||||
// Check immediately
|
||||
await this.checkAllPayments()
|
||||
|
||||
// Set up interval for periodic checks
|
||||
this.monitoringInterval = setInterval(async () => {
|
||||
await this.checkAllPayments()
|
||||
}, 30000) // Check every 30 seconds
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the monitoring loop
|
||||
*/
|
||||
private stopMonitoringLoop(): void {
|
||||
if (this.monitoringInterval) {
|
||||
clearInterval(this.monitoringInterval)
|
||||
this.monitoringInterval = null
|
||||
}
|
||||
this.state.value.isMonitoring = false
|
||||
console.log('Stopped payment monitoring loop')
|
||||
}
|
||||
|
||||
/**
|
||||
* Check payment status for all active invoices
|
||||
*/
|
||||
private async checkAllPayments(): Promise<void> {
|
||||
try {
|
||||
this.state.value.error = null
|
||||
this.state.value.lastUpdate = Date.now()
|
||||
|
||||
const promises = Array.from(this.state.value.activeInvoices.entries()).map(
|
||||
async ([orderId, invoice]) => {
|
||||
try {
|
||||
// Get payment status from LNBits
|
||||
const status = await this.getPaymentStatus(invoice.payment_hash)
|
||||
|
||||
// Update local status
|
||||
this.state.value.paymentStatuses.set(invoice.payment_hash, status)
|
||||
|
||||
// Check if status changed
|
||||
const previousStatus = this.state.value.paymentStatuses.get(invoice.payment_hash)
|
||||
if (previousStatus && previousStatus.paid !== status.paid) {
|
||||
await this.handlePaymentStatusChange(orderId, invoice, status)
|
||||
}
|
||||
|
||||
return { orderId, status }
|
||||
} catch (error) {
|
||||
console.error(`Failed to check payment for order ${orderId}:`, error)
|
||||
return { orderId, error }
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
await Promise.allSettled(promises)
|
||||
} catch (error) {
|
||||
console.error('Payment monitoring error:', error)
|
||||
this.state.value.error = 'Payment monitoring failed'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get payment status from LNBits
|
||||
*/
|
||||
private async getPaymentStatus(paymentHash: string): Promise<PaymentStatus> {
|
||||
try {
|
||||
// For now, we'll simulate payment status checking since we don't have wallet context here
|
||||
// In production, this would integrate with LNBits webhooks or polling
|
||||
// TODO: Pass wallet information from the order context
|
||||
console.log('Payment status check requested for:', paymentHash)
|
||||
|
||||
// Return default pending status for now
|
||||
return {
|
||||
paid: false,
|
||||
amount_paid: 0,
|
||||
payment_hash: paymentHash
|
||||
}
|
||||
|
||||
// Uncomment when wallet context is available:
|
||||
// const status = await invoiceService.checkPaymentStatus(paymentHash, walletId, adminKey)
|
||||
// return status
|
||||
} catch (error) {
|
||||
console.error('Failed to get payment status:', error)
|
||||
// Return default pending status
|
||||
return {
|
||||
paid: false,
|
||||
amount_paid: 0,
|
||||
payment_hash: paymentHash
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle payment status changes
|
||||
*/
|
||||
private async handlePaymentStatusChange(
|
||||
orderId: string,
|
||||
invoice: LightningInvoice,
|
||||
status: PaymentStatus
|
||||
): Promise<void> {
|
||||
const update: PaymentUpdate = {
|
||||
orderId,
|
||||
paymentHash: invoice.payment_hash,
|
||||
status: status.paid ? 'paid' : 'pending',
|
||||
amount: invoice.amount,
|
||||
paidAt: status.paid_at
|
||||
}
|
||||
|
||||
console.log('Payment status changed:', update)
|
||||
|
||||
// Notify callbacks
|
||||
const callback = this.updateCallbacks.get(orderId)
|
||||
if (callback) {
|
||||
try {
|
||||
callback(update)
|
||||
} catch (error) {
|
||||
console.error('Payment update callback error:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// If payment is complete, stop monitoring this order
|
||||
if (status.paid) {
|
||||
this.stopMonitoring(orderId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a callback for payment updates
|
||||
*/
|
||||
onPaymentUpdate(orderId: string, callback: (update: PaymentUpdate) => void): void {
|
||||
this.updateCallbacks.set(orderId, callback)
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister a payment update callback
|
||||
*/
|
||||
offPaymentUpdate(orderId: string): void {
|
||||
this.updateCallbacks.delete(orderId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current payment status for an order
|
||||
*/
|
||||
getOrderPaymentStatus(orderId: string): PaymentStatus | null {
|
||||
const invoice = this.state.value.activeInvoices.get(orderId)
|
||||
if (!invoice) return null
|
||||
|
||||
return this.state.value.paymentStatuses.get(invoice.payment_hash) || null
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an order payment is complete
|
||||
*/
|
||||
isOrderPaid(orderId: string): boolean {
|
||||
const status = this.getOrderPaymentStatus(orderId)
|
||||
return status?.paid || false
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all pending payments
|
||||
*/
|
||||
getPendingPayments(): Array<{ orderId: string; invoice: LightningInvoice }> {
|
||||
return Array.from(this.state.value.activeInvoices.entries())
|
||||
.filter(([orderId]) => !this.isOrderPaid(orderId))
|
||||
.map(([orderId, invoice]) => ({ orderId, invoice }))
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up resources
|
||||
*/
|
||||
cleanup(): void {
|
||||
this.stopMonitoringLoop()
|
||||
this.state.value.activeInvoices.clear()
|
||||
this.state.value.paymentStatuses.clear()
|
||||
this.updateCallbacks.clear()
|
||||
this.state.value.error = null
|
||||
console.log('Payment monitor cleaned up')
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const paymentMonitor = new PaymentMonitorService()
|
||||
|
||||
11
src/lib/utils/bech32.ts
Normal file
11
src/lib/utils/bech32.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { nip19 } from 'nostr-tools'
|
||||
|
||||
// Helper function to convert bech32 to hex using nostr-tools
|
||||
export function bech32ToHex(bech32Key: string): string {
|
||||
if (bech32Key.startsWith('npub1') || bech32Key.startsWith('nsec1')) {
|
||||
const { type, data } = nip19.decode(bech32Key)
|
||||
return data as string
|
||||
}
|
||||
// Already hex format
|
||||
return bech32Key
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue