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:
padreug 2025-08-13 15:31:18 +02:00
parent 93ffb8bf32
commit ea5a2380f1
43 changed files with 8983 additions and 146 deletions

View file

@ -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 || ''
}

View 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'
})

View file

@ -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
View 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
}

View file

@ -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)
}
}

View 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()

View 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()

View 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
View 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
}