diff --git a/crud.py b/crud.py
index adc0836..3bec1f2 100644
--- a/crud.py
+++ b/crud.py
@@ -1,5 +1,4 @@
import json
-from typing import List, Optional, Tuple
from lnbits.helpers import urlsafe_short_hash
@@ -44,7 +43,7 @@ async def create_merchant(user_id: str, m: PartialMerchant) -> Merchant:
async def update_merchant(
user_id: str, merchant_id: str, config: MerchantConfig
-) -> Optional[Merchant]:
+) -> Merchant | None:
await db.execute(
f"""
UPDATE nostrmarket.merchants SET meta = :meta, time = {db.timestamp_now}
@@ -55,7 +54,7 @@ async def update_merchant(
return await get_merchant(user_id, merchant_id)
-async def touch_merchant(user_id: str, merchant_id: str) -> Optional[Merchant]:
+async def touch_merchant(user_id: str, merchant_id: str) -> Merchant | None:
await db.execute(
f"""
UPDATE nostrmarket.merchants SET time = {db.timestamp_now}
@@ -66,7 +65,7 @@ async def touch_merchant(user_id: str, merchant_id: str) -> Optional[Merchant]:
return await get_merchant(user_id, merchant_id)
-async def get_merchant(user_id: str, merchant_id: str) -> Optional[Merchant]:
+async def get_merchant(user_id: str, merchant_id: str) -> Merchant | None:
row: dict = await db.fetchone(
"""SELECT * FROM nostrmarket.merchants WHERE user_id = :user_id AND id = :id""",
{
@@ -78,7 +77,7 @@ async def get_merchant(user_id: str, merchant_id: str) -> Optional[Merchant]:
return Merchant.from_row(row) if row else None
-async def get_merchant_by_pubkey(public_key: str) -> Optional[Merchant]:
+async def get_merchant_by_pubkey(public_key: str) -> Merchant | None:
row: dict = await db.fetchone(
"""SELECT * FROM nostrmarket.merchants WHERE public_key = :public_key""",
{"public_key": public_key},
@@ -87,7 +86,7 @@ async def get_merchant_by_pubkey(public_key: str) -> Optional[Merchant]:
return Merchant.from_row(row) if row else None
-async def get_merchants_ids_with_pubkeys() -> List[Tuple[str, str]]:
+async def get_merchants_ids_with_pubkeys() -> list[tuple[str, str]]:
rows: list[dict] = await db.fetchall(
"""SELECT id, public_key FROM nostrmarket.merchants""",
)
@@ -95,7 +94,7 @@ async def get_merchants_ids_with_pubkeys() -> List[Tuple[str, str]]:
return [(row["id"], row["public_key"]) for row in rows]
-async def get_merchant_for_user(user_id: str) -> Optional[Merchant]:
+async def get_merchant_for_user(user_id: str) -> Merchant | None:
row: dict = await db.fetchone(
"""SELECT * FROM nostrmarket.merchants WHERE user_id = :user_id """,
{"user_id": user_id},
@@ -138,7 +137,7 @@ async def create_zone(merchant_id: str, data: Zone) -> Zone:
return zone
-async def update_zone(merchant_id: str, z: Zone) -> Optional[Zone]:
+async def update_zone(merchant_id: str, z: Zone) -> Zone | None:
await db.execute(
"""
UPDATE nostrmarket.zones
@@ -157,7 +156,7 @@ async def update_zone(merchant_id: str, z: Zone) -> Optional[Zone]:
return await get_zone(merchant_id, z.id)
-async def get_zone(merchant_id: str, zone_id: str) -> Optional[Zone]:
+async def get_zone(merchant_id: str, zone_id: str) -> Zone | None:
row: dict = await db.fetchone(
"SELECT * FROM nostrmarket.zones WHERE merchant_id = :merchant_id AND id = :id",
{
@@ -168,7 +167,7 @@ async def get_zone(merchant_id: str, zone_id: str) -> Optional[Zone]:
return Zone.from_row(row) if row else None
-async def get_zones(merchant_id: str) -> List[Zone]:
+async def get_zones(merchant_id: str) -> list[Zone]:
rows: list[dict] = await db.fetchall(
"SELECT * FROM nostrmarket.zones WHERE merchant_id = :merchant_id",
{"merchant_id": merchant_id},
@@ -235,7 +234,7 @@ async def create_stall(merchant_id: str, data: Stall) -> Stall:
return stall
-async def get_stall(merchant_id: str, stall_id: str) -> Optional[Stall]:
+async def get_stall(merchant_id: str, stall_id: str) -> Stall | None:
row: dict = await db.fetchone(
"""
SELECT * FROM nostrmarket.stalls
@@ -249,7 +248,7 @@ async def get_stall(merchant_id: str, stall_id: str) -> Optional[Stall]:
return Stall.from_row(row) if row else None
-async def get_stalls(merchant_id: str, pending: Optional[bool] = False) -> List[Stall]:
+async def get_stalls(merchant_id: str, pending: bool | None = False) -> list[Stall]:
rows: list[dict] = await db.fetchall(
"""
SELECT * FROM nostrmarket.stalls
@@ -274,7 +273,7 @@ async def get_last_stall_update_time() -> int:
return row["event_created_at"] or 0 if row else 0
-async def update_stall(merchant_id: str, stall: Stall) -> Optional[Stall]:
+async def update_stall(merchant_id: str, stall: Stall) -> Stall | None:
await db.execute(
"""
UPDATE nostrmarket.stalls
@@ -398,9 +397,7 @@ async def update_product(merchant_id: str, product: Product) -> Product:
return updated_product
-async def update_product_quantity(
- product_id: str, new_quantity: int
-) -> Optional[Product]:
+async def update_product_quantity(product_id: str, new_quantity: int) -> Product | None:
await db.execute(
"""
UPDATE nostrmarket.products SET quantity = :quantity
@@ -415,7 +412,7 @@ async def update_product_quantity(
return Product.from_row(row) if row else None
-async def get_product(merchant_id: str, product_id: str) -> Optional[Product]:
+async def get_product(merchant_id: str, product_id: str) -> Product | None:
row: dict = await db.fetchone(
"""
SELECT * FROM nostrmarket.products
@@ -431,8 +428,8 @@ async def get_product(merchant_id: str, product_id: str) -> Optional[Product]:
async def get_products(
- merchant_id: str, stall_id: str, pending: Optional[bool] = False
-) -> List[Product]:
+ merchant_id: str, stall_id: str, pending: bool | None = False
+) -> list[Product]:
rows: list[dict] = await db.fetchall(
"""
SELECT * FROM nostrmarket.products
@@ -445,8 +442,8 @@ async def get_products(
async def get_products_by_ids(
- merchant_id: str, product_ids: List[str]
-) -> List[Product]:
+ merchant_id: str, product_ids: list[str]
+) -> list[Product]:
# todo: revisit
keys = []
@@ -467,7 +464,7 @@ async def get_products_by_ids(
return [Product.from_row(row) for row in rows]
-async def get_wallet_for_product(product_id: str) -> Optional[str]:
+async def get_wallet_for_product(product_id: str) -> str | None:
row: dict = await db.fetchone(
"""
SELECT s.wallet as wallet FROM nostrmarket.products p
@@ -574,7 +571,7 @@ async def create_order(merchant_id: str, o: Order) -> Order:
return order
-async def get_order(merchant_id: str, order_id: str) -> Optional[Order]:
+async def get_order(merchant_id: str, order_id: str) -> Order | None:
row: dict = await db.fetchone(
"""
SELECT * FROM nostrmarket.orders
@@ -588,7 +585,7 @@ async def get_order(merchant_id: str, order_id: str) -> Optional[Order]:
return Order.from_row(row) if row else None
-async def get_order_by_event_id(merchant_id: str, event_id: str) -> Optional[Order]:
+async def get_order_by_event_id(merchant_id: str, event_id: str) -> Order | None:
row: dict = await db.fetchone(
"""
SELECT * FROM nostrmarket.orders
@@ -602,7 +599,7 @@ async def get_order_by_event_id(merchant_id: str, event_id: str) -> Optional[Ord
return Order.from_row(row) if row else None
-async def get_orders(merchant_id: str, **kwargs) -> List[Order]:
+async def get_orders(merchant_id: str, **kwargs) -> list[Order]:
q = " AND ".join(
[
f"{field[0]} = :{field[0]}"
@@ -629,7 +626,7 @@ async def get_orders(merchant_id: str, **kwargs) -> List[Order]:
async def get_orders_for_stall(
merchant_id: str, stall_id: str, **kwargs
-) -> List[Order]:
+) -> list[Order]:
q = " AND ".join(
[
f"{field[0]} = :{field[0]}"
@@ -655,7 +652,7 @@ async def get_orders_for_stall(
return [Order.from_row(row) for row in rows]
-async def update_order(merchant_id: str, order_id: str, **kwargs) -> Optional[Order]:
+async def update_order(merchant_id: str, order_id: str, **kwargs) -> Order | None:
q = ", ".join(
[
f"{field[0]} = :{field[0]}"
@@ -679,7 +676,7 @@ async def update_order(merchant_id: str, order_id: str, **kwargs) -> Optional[Or
return await get_order(merchant_id, order_id)
-async def update_order_paid_status(order_id: str, paid: bool) -> Optional[Order]:
+async def update_order_paid_status(order_id: str, paid: bool) -> Order | None:
await db.execute(
"UPDATE nostrmarket.orders SET paid = :paid WHERE id = :id",
{"paid": paid, "id": order_id},
@@ -693,7 +690,7 @@ async def update_order_paid_status(order_id: str, paid: bool) -> Optional[Order]
async def update_order_shipped_status(
merchant_id: str, order_id: str, shipped: bool
-) -> Optional[Order]:
+) -> Order | None:
await db.execute(
"""
UPDATE nostrmarket.orders
@@ -757,7 +754,7 @@ async def create_direct_message(
return msg
-async def get_direct_message(merchant_id: str, dm_id: str) -> Optional[DirectMessage]:
+async def get_direct_message(merchant_id: str, dm_id: str) -> DirectMessage | None:
row: dict = await db.fetchone(
"""
SELECT * FROM nostrmarket.direct_messages
@@ -773,7 +770,7 @@ async def get_direct_message(merchant_id: str, dm_id: str) -> Optional[DirectMes
async def get_direct_message_by_event_id(
merchant_id: str, event_id: str
-) -> Optional[DirectMessage]:
+) -> DirectMessage | None:
row: dict = await db.fetchone(
"""
SELECT * FROM nostrmarket.direct_messages
@@ -787,7 +784,7 @@ async def get_direct_message_by_event_id(
return DirectMessage.from_row(row) if row else None
-async def get_direct_messages(merchant_id: str, public_key: str) -> List[DirectMessage]:
+async def get_direct_messages(merchant_id: str, public_key: str) -> list[DirectMessage]:
rows: list[dict] = await db.fetchall(
"""
SELECT * FROM nostrmarket.direct_messages
@@ -799,7 +796,7 @@ async def get_direct_messages(merchant_id: str, public_key: str) -> List[DirectM
return [DirectMessage.from_row(row) for row in rows]
-async def get_orders_from_direct_messages(merchant_id: str) -> List[DirectMessage]:
+async def get_orders_from_direct_messages(merchant_id: str) -> list[DirectMessage]:
rows: list[dict] = await db.fetchall(
"""
SELECT * FROM nostrmarket.direct_messages
@@ -860,7 +857,7 @@ async def create_customer(merchant_id: str, data: Customer) -> Customer:
return customer
-async def get_customer(merchant_id: str, public_key: str) -> Optional[Customer]:
+async def get_customer(merchant_id: str, public_key: str) -> Customer | None:
row: dict = await db.fetchone(
"""
SELECT * FROM nostrmarket.customers
@@ -874,7 +871,7 @@ async def get_customer(merchant_id: str, public_key: str) -> Optional[Customer]:
return Customer.from_row(row) if row else None
-async def get_customers(merchant_id: str) -> List[Customer]:
+async def get_customers(merchant_id: str) -> list[Customer]:
rows: list[dict] = await db.fetchall(
"SELECT * FROM nostrmarket.customers WHERE merchant_id = :merchant_id",
{"merchant_id": merchant_id},
@@ -882,7 +879,7 @@ async def get_customers(merchant_id: str) -> List[Customer]:
return [Customer.from_row(row) for row in rows]
-async def get_all_unique_customers() -> List[Customer]:
+async def get_all_unique_customers() -> list[Customer]:
q = """
SELECT public_key, MAX(merchant_id) as merchant_id, MAX(event_created_at)
FROM nostrmarket.customers
diff --git a/helpers.py b/helpers.py
index dd26116..dcc0f06 100644
--- a/helpers.py
+++ b/helpers.py
@@ -1,6 +1,5 @@
import base64
import secrets
-from typing import Optional
import coincurve
from bech32 import bech32_decode, convertbits
@@ -37,7 +36,7 @@ def decrypt_message(encoded_message: str, encryption_key) -> str:
return unpadded_data.decode()
-def encrypt_message(message: str, encryption_key, iv: Optional[bytes] = None) -> str:
+def encrypt_message(message: str, encryption_key, iv: bytes | None = None) -> str:
padder = padding.PKCS7(128).padder()
padded_data = padder.update(message.encode()) + padder.finalize()
diff --git a/misc-docs/ORDER-DISCOVERY-ANALYSIS.md b/misc-docs/ORDER-DISCOVERY-ANALYSIS.md
deleted file mode 100644
index de393e2..0000000
--- a/misc-docs/ORDER-DISCOVERY-ANALYSIS.md
+++ /dev/null
@@ -1,320 +0,0 @@
-# Nostrmarket Order Discovery Analysis
-
-## Executive Summary
-
-This document analyzes the order discovery mechanism in the Nostrmarket extension and identifies why merchants must manually refresh to see new orders instead of receiving them automatically through persistent subscriptions.
-
----
-
-## Current Architecture
-
-### Two Subscription Systems
-
-The Nostrmarket extension implements two distinct subscription mechanisms for receiving Nostr events:
-
-#### 1. **Persistent Subscriptions (Background Task)**
-
-**Purpose**: Continuous monitoring for new orders, products, and merchant events
-
-**Implementation**:
-
-- Runs via `wait_for_nostr_events()` background task
-- Initiated on extension startup (15-second delay)
-- Creates subscription ID: `nostrmarket-{hash}`
-- Monitors all merchant public keys continuously
-
-**Code Location**: `/nostrmarket/tasks.py:37-49`
-
-```python
-async def wait_for_nostr_events(nostr_client: NostrClient):
- while True:
- try:
- await subscribe_to_all_merchants()
- while True:
- message = await nostr_client.get_event()
- await process_nostr_message(message)
-```
-
-**Subscription Filters**:
-
-- Direct messages (kind 4) - for orders
-- Stall events (kind 30017)
-- Product events (kind 30018)
-- Profile updates (kind 0)
-
-#### 2. **Temporary Subscriptions (Manual Refresh)**
-
-**Purpose**: Catch up on missed events when merchant clicks "Refresh from Nostr"
-
-**Implementation**:
-
-- Duration: 10 seconds only
-- Triggered by user action
-- Creates subscription ID: `merchant-{hash}`
-- Fetches ALL events from time=0
-
-**Code Location**: `/nostrmarket/nostr/nostr_client.py:100-120`
-
-```python
-async def merchant_temp_subscription(self, pk, duration=10):
- dm_filters = self._filters_for_direct_messages([pk], 0)
- # ... creates filters with time=0 (all history)
- await self.send_req_queue.put(["REQ", subscription_id] + merchant_filters)
- asyncio.create_task(unsubscribe_with_delay(subscription_id, duration))
-```
-
----
-
-## Problem Identification
-
-### Why Manual Refresh is Required
-
-#### **Issue 1: Timing Window Problem**
-
-The persistent subscription uses timestamps from the last database update:
-
-```python
-async def subscribe_to_all_merchants():
- last_dm_time = await get_last_direct_messages_created_at()
- last_stall_time = await get_last_stall_update_time()
- last_prod_time = await get_last_product_update_time()
-
- await nostr_client.subscribe_merchants(
- public_keys, last_dm_time, last_stall_time, last_prod_time, 0
- )
-```
-
-**Problem**: Events that occur between:
-
-- The last database update time
-- When the subscription becomes active
- ...are potentially missed
-
-#### **Issue 2: Connection Stability**
-
-The WebSocket connection between components may be unstable:
-
-```
-[Nostrmarket] <--WebSocket--> [Nostrclient] <--WebSocket--> [Nostr Relays]
- Extension Extension (Global)
-```
-
-**Potential failure points**:
-
-1. Connection drops between nostrmarket → nostrclient
-2. Connection drops between nostrclient → relays
-3. Reconnection doesn't re-establish subscriptions
-
-#### **Issue 3: Subscription State Management**
-
-**Current behavior**:
-
-- Single persistent subscription per merchant
-- No automatic resubscription on failure
-- No heartbeat/keepalive mechanism
-- No verification that subscription is active
-
-#### **Issue 4: Event Processing Delays**
-
-The startup sequence has intentional delays:
-
-```python
-async def _subscribe_to_nostr_client():
- await asyncio.sleep(10) # Wait for nostrclient
- await nostr_client.run_forever()
-
-async def _wait_for_nostr_events():
- await asyncio.sleep(15) # Wait for extension init
- await wait_for_nostr_events(nostr_client)
-```
-
-**Problem**: Orders arriving during initialization are missed
-
----
-
-## Why Manual Refresh Works
-
-The temporary subscription succeeds because:
-
-1. **Fetches from time=0**: Gets ALL historical events
-2. **Fresh connection**: Creates new subscription request
-3. **Immediate processing**: No startup delays
-4. **Direct feedback**: User sees results immediately
-
-```python
-# Temporary subscription uses time=0 (all events)
-dm_filters = self._filters_for_direct_messages([pk], 0) # ← 0 means all time
-
-# Persistent subscription uses last update time
-dm_filters = self._filters_for_direct_messages(public_keys, dm_time) # ← can miss events
-```
-
----
-
-## Impact Analysis
-
-### User Experience Issues
-
-1. **Merchants miss orders** without manual refresh
-2. **No real-time notifications** for new orders
-3. **Uncertainty** about order status
-4. **Extra manual steps** required
-5. **Delayed order fulfillment**
-
-### Technical Implications
-
-1. **Not truly decentralized** - requires active monitoring
-2. **Scalability concerns** - manual refresh doesn't scale
-3. **Reliability issues** - depends on user action
-4. **Performance overhead** - fetching all events repeatedly
-
----
-
-## Recommended Solutions
-
-### Solution A: Enhanced Persistent Subscriptions
-
-**Implement redundant subscription mechanisms:**
-
-```python
-class EnhancedSubscriptionManager:
- def __init__(self):
- self.last_heartbeat = time.time()
- self.subscription_active = False
-
- async def maintain_subscription(self):
- while True:
- if not self.subscription_active or \
- time.time() - self.last_heartbeat > 30:
- await self.resubscribe_with_overlap()
- await asyncio.sleep(10)
-
- async def resubscribe_with_overlap(self):
- # Use timestamp with 5-minute overlap
- overlap_time = int(time.time()) - 300
- await subscribe_to_all_merchants(since=overlap_time)
-```
-
-### Solution B: Periodic Auto-Refresh
-
-**Add automatic temporary subscriptions:**
-
-```python
-async def auto_refresh_loop():
- while True:
- await asyncio.sleep(60) # Every minute
- merchants = await get_all_active_merchants()
- for merchant in merchants:
- await merchant_temp_subscription(merchant.pubkey, duration=5)
-```
-
-### Solution C: WebSocket Health Monitoring
-
-**Implement connection health checks:**
-
-```python
-class WebSocketHealthMonitor:
- async def check_connection_health(self):
- try:
- # Send ping to nostrclient
- response = await nostr_client.ping(timeout=5)
- if not response:
- await self.reconnect_and_resubscribe()
- except Exception:
- await self.reconnect_and_resubscribe()
-```
-
-### Solution D: Event Gap Detection
-
-**Detect and fill gaps in event sequence:**
-
-```python
-async def detect_event_gaps():
- # Check for gaps in event timestamps
- last_known = await get_last_event_time()
- current_time = int(time.time())
-
- if current_time - last_known > 60: # 1 minute gap
- # Perform temporary subscription to fill gap
- await fetch_missing_events(since=last_known)
-```
-
----
-
-## Implementation Priority
-
-### Phase 1: Quick Fixes (1-2 days)
-
-1. [DONE] Increase temp subscription duration (10s → 30s)
-2. [DONE] Add connection health logging
-3. [DONE] Reduce startup delays
-
-### Phase 2: Reliability (3-5 days)
-
-1. [TODO] Implement subscription heartbeat
-2. [TODO] Add automatic resubscription on failure
-3. [TODO] Create event gap detection
-
-### Phase 3: Full Solution (1-2 weeks)
-
-1. [TODO] WebSocket connection monitoring
-2. [TODO] Redundant subscription system
-3. [TODO] Real-time order notifications
-4. [TODO] Event deduplication logic
-
----
-
-## Testing Recommendations
-
-### Test Scenarios
-
-1. **Order during startup**: Send order within 15 seconds of server start
-2. **Long-running test**: Keep server running for 24 hours, send periodic orders
-3. **Connection interruption**: Disconnect nostrclient, send order, reconnect
-4. **High volume**: Send 100 orders rapidly
-5. **Network latency**: Add artificial delay between components
-
-### Monitoring Metrics
-
-- Time between order sent → order discovered
-- Percentage of orders requiring manual refresh
-- WebSocket connection uptime
-- Subscription success rate
-- Event processing latency
-
----
-
-## Conclusion
-
-The current order discovery system relies on manual refresh due to:
-
-1. **Timing gaps** in persistent subscriptions
-2. **Connection stability** issues
-3. **Lack of redundancy** in subscription management
-4. **No automatic recovery** mechanisms
-
-While the temporary subscription (manual refresh) provides a workaround, a proper solution requires implementing connection monitoring, subscription health checks, and automatic gap-filling mechanisms to ensure reliable real-time order discovery.
-
----
-
-## Appendix: Code References
-
-### Key Files
-
-- `/nostrmarket/tasks.py` - Background task management
-- `/nostrmarket/nostr/nostr_client.py` - Nostr client implementation
-- `/nostrmarket/services.py` - Order processing logic
-- `/nostrmarket/views_api.py` - API endpoints for refresh
-
-### Relevant Functions
-
-- `wait_for_nostr_events()` - Main event loop
-- `subscribe_to_all_merchants()` - Persistent subscription
-- `merchant_temp_subscription()` - Manual refresh
-- `process_nostr_message()` - Event processing
-
----
-
-_Document prepared: January 2025_
-_Analysis based on: Nostrmarket v1.0_
-_Status: Active Investigation_
diff --git a/models.py b/models.py
index b12b775..f1af073 100644
--- a/models.py
+++ b/models.py
@@ -2,7 +2,7 @@ import json
import time
from abc import abstractmethod
from enum import Enum
-from typing import Any, List, Optional, Tuple
+from typing import Any
from lnbits.utils.exchange_rates import btc_price, fiat_amount_as_satoshis
from pydantic import BaseModel
@@ -32,26 +32,16 @@ class Nostrable:
class MerchantProfile(BaseModel):
- name: Optional[str] = None
- display_name: Optional[str] = None
- about: Optional[str] = None
- picture: Optional[str] = None
- banner: Optional[str] = None
- website: Optional[str] = None
- nip05: Optional[str] = None
- lud16: Optional[str] = None
+ name: str | None = None
+ about: str | None = None
+ picture: str | None = None
class MerchantConfig(MerchantProfile):
- event_id: Optional[str] = None
+ event_id: str | None = None
sync_from_nostr: bool = False
- # TODO: switched to True for AIO demo; determine if we leave this as True
- active: bool = True
- restore_in_progress: Optional[bool] = False
-
-
-class CreateMerchantRequest(BaseModel):
- config: MerchantConfig = MerchantConfig()
+ active: bool = False
+ restore_in_progress: bool | None = False
class PartialMerchant(BaseModel):
@@ -62,7 +52,7 @@ class PartialMerchant(BaseModel):
class Merchant(PartialMerchant, Nostrable):
id: str
- time: Optional[int] = 0
+ time: int | None = 0
def sign_hash(self, hash_: bytes) -> str:
return sign_message_hash(self.private_key, hash_)
@@ -96,23 +86,11 @@ class Merchant(PartialMerchant, Nostrable):
return merchant
def to_nostr_event(self, pubkey: str) -> NostrEvent:
- content: dict[str, str] = {}
- if self.config.name:
- content["name"] = self.config.name
- if self.config.display_name:
- content["display_name"] = self.config.display_name
- if self.config.about:
- content["about"] = self.config.about
- if self.config.picture:
- content["picture"] = self.config.picture
- if self.config.banner:
- content["banner"] = self.config.banner
- if self.config.website:
- content["website"] = self.config.website
- if self.config.nip05:
- content["nip05"] = self.config.nip05
- if self.config.lud16:
- content["lud16"] = self.config.lud16
+ content = {
+ "name": self.config.name,
+ "about": self.config.about,
+ "picture": self.config.picture,
+ }
event = NostrEvent(
pubkey=pubkey,
created_at=round(time.time()),
@@ -144,11 +122,11 @@ class Merchant(PartialMerchant, Nostrable):
######################################## ZONES ########################################
class Zone(BaseModel):
- id: Optional[str] = None
- name: Optional[str] = None
+ id: str | None = None
+ name: str | None = None
currency: str
cost: float
- countries: List[str] = []
+ countries: list[str] = []
@classmethod
def from_row(cls, row: dict) -> "Zone":
@@ -161,22 +139,22 @@ class Zone(BaseModel):
class StallConfig(BaseModel):
- image_url: Optional[str] = None
- description: Optional[str] = None
+ image_url: str | None = None
+ description: str | None = None
class Stall(BaseModel, Nostrable):
- id: Optional[str] = None
+ id: str | None = None
wallet: str
name: str
currency: str = "sat"
- shipping_zones: List[Zone] = []
+ shipping_zones: list[Zone] = []
config: StallConfig = StallConfig()
pending: bool = False
"""Last published nostr event for this Stall"""
- event_id: Optional[str] = None
- event_created_at: Optional[int] = None
+ event_id: str | None = None
+ event_created_at: int | None = None
def validate_stall(self):
for z in self.shipping_zones:
@@ -234,19 +212,19 @@ class ProductShippingCost(BaseModel):
class ProductConfig(BaseModel):
- description: Optional[str] = None
- currency: Optional[str] = None
- use_autoreply: Optional[bool] = False
- autoreply_message: Optional[str] = None
- shipping: List[ProductShippingCost] = []
+ description: str | None = None
+ currency: str | None = None
+ use_autoreply: bool | None = False
+ autoreply_message: str | None = None
+ shipping: list[ProductShippingCost] = []
class Product(BaseModel, Nostrable):
- id: Optional[str] = None
+ id: str | None = None
stall_id: str
name: str
- categories: List[str] = []
- images: List[str] = []
+ categories: list[str] = []
+ images: list[str] = []
price: float
quantity: int
active: bool = True
@@ -254,8 +232,8 @@ class Product(BaseModel, Nostrable):
config: ProductConfig = ProductConfig()
"""Last published nostr event for this Product"""
- event_id: Optional[str] = None
- event_created_at: Optional[int] = None
+ event_id: str | None = None
+ event_created_at: int | None = None
def to_nostr_event(self, pubkey: str) -> NostrEvent:
content = {
@@ -312,7 +290,7 @@ class ProductOverview(BaseModel):
id: str
name: str
price: float
- product_shipping_cost: Optional[float] = None
+ product_shipping_cost: float | None = None
@classmethod
def from_product(cls, p: Product) -> "ProductOverview":
@@ -329,21 +307,21 @@ class OrderItem(BaseModel):
class OrderContact(BaseModel):
- nostr: Optional[str] = None
- phone: Optional[str] = None
- email: Optional[str] = None
+ nostr: str | None = None
+ phone: str | None = None
+ email: str | None = None
class OrderExtra(BaseModel):
- products: List[ProductOverview]
+ products: list[ProductOverview]
currency: str
btc_price: str
shipping_cost: float = 0
shipping_cost_sat: float = 0
- fail_message: Optional[str] = None
+ fail_message: str | None = None
@classmethod
- async def from_products(cls, products: List[Product]):
+ async def from_products(cls, products: list[Product]):
currency = products[0].config.currency if len(products) else "sat"
exchange_rate = (
await btc_price(currency) if currency and currency != "sat" else 1
@@ -359,19 +337,19 @@ class OrderExtra(BaseModel):
class PartialOrder(BaseModel):
id: str
- event_id: Optional[str] = None
- event_created_at: Optional[int] = None
+ event_id: str | None = None
+ event_created_at: int | None = None
public_key: str
merchant_public_key: str
shipping_id: str
- items: List[OrderItem]
- contact: Optional[OrderContact] = None
- address: Optional[str] = None
+ items: list[OrderItem]
+ contact: OrderContact | None = None
+ address: str | None = None
def validate_order(self):
assert len(self.items) != 0, f"Order has no items. Order: '{self.id}'"
- def validate_order_items(self, product_list: List[Product]):
+ def validate_order_items(self, product_list: list[Product]):
assert len(self.items) != 0, f"Order has no items. Order: '{self.id}'"
assert (
len(product_list) != 0
@@ -392,8 +370,8 @@ class PartialOrder(BaseModel):
)
async def costs_in_sats(
- self, products: List[Product], shipping_id: str, stall_shipping_cost: float
- ) -> Tuple[float, float]:
+ self, products: list[Product], shipping_id: str, stall_shipping_cost: float
+ ) -> tuple[float, float]:
product_prices = {}
for p in products:
product_shipping_cost = next(
@@ -422,7 +400,7 @@ class PartialOrder(BaseModel):
return product_cost, stall_shipping_cost
def receipt(
- self, products: List[Product], shipping_id: str, stall_shipping_cost: float
+ self, products: list[Product], shipping_id: str, stall_shipping_cost: float
) -> str:
if len(products) == 0:
return "[No Products]"
@@ -471,7 +449,7 @@ class Order(PartialOrder):
total: float
paid: bool = False
shipped: bool = False
- time: Optional[int] = None
+ time: int | None = None
extra: OrderExtra
@classmethod
@@ -485,14 +463,14 @@ class Order(PartialOrder):
class OrderStatusUpdate(BaseModel):
id: str
- message: Optional[str] = None
- paid: Optional[bool] = False
- shipped: Optional[bool] = None
+ message: str | None = None
+ paid: bool | None = False
+ shipped: bool | None = None
class OrderReissue(BaseModel):
id: str
- shipping_id: Optional[str] = None
+ shipping_id: str | None = None
class PaymentOption(BaseModel):
@@ -502,8 +480,8 @@ class PaymentOption(BaseModel):
class PaymentRequest(BaseModel):
id: str
- message: Optional[str] = None
- payment_options: List[PaymentOption]
+ message: str | None = None
+ payment_options: list[PaymentOption]
######################################## MESSAGE #######################################
@@ -519,16 +497,16 @@ class DirectMessageType(Enum):
class PartialDirectMessage(BaseModel):
- event_id: Optional[str] = None
- event_created_at: Optional[int] = None
+ event_id: str | None = None
+ event_created_at: int | None = None
message: str
public_key: str
type: int = DirectMessageType.PLAIN_TEXT.value
incoming: bool = False
- time: Optional[int] = None
+ time: int | None = None
@classmethod
- def parse_message(cls, msg) -> Tuple[DirectMessageType, Optional[Any]]:
+ def parse_message(cls, msg) -> tuple[DirectMessageType, Any | None]:
try:
msg_json = json.loads(msg)
if "type" in msg_json:
@@ -551,15 +529,15 @@ class DirectMessage(PartialDirectMessage):
class CustomerProfile(BaseModel):
- name: Optional[str] = None
- about: Optional[str] = None
+ name: str | None = None
+ about: str | None = None
class Customer(BaseModel):
merchant_id: str
public_key: str
- event_created_at: Optional[int] = None
- profile: Optional[CustomerProfile] = None
+ event_created_at: int | None = None
+ profile: CustomerProfile | None = None
unread_messages: int = 0
@classmethod
diff --git a/nostr/nostr_client.py b/nostr/nostr_client.py
index 967bc1b..a611980 100644
--- a/nostr/nostr_client.py
+++ b/nostr/nostr_client.py
@@ -31,11 +31,9 @@ class NostrClient:
logger.debug(f"Connecting to websockets for 'nostrclient' extension...")
relay_endpoint = encrypt_internal_message("relay", urlsafe=True)
- ws_url = f"ws://localhost:{settings.port}/nostrclient/api/v1/{relay_endpoint}"
-
on_open, on_message, on_error, on_close = self._ws_handlers()
ws = WebSocketApp(
- ws_url,
+ f"ws://localhost:{settings.port}/nostrclient/api/v1/{relay_endpoint}",
on_message=on_message,
on_open=on_open,
on_close=on_close,
@@ -67,7 +65,6 @@ class NostrClient:
async def get_event(self):
value = await self.recieve_event_queue.get()
if isinstance(value, ValueError):
- logger.error(f"[NOSTRMARKET] ❌ Queue returned error: {value}")
raise value
return value
@@ -94,6 +91,10 @@ class NostrClient:
self.subscription_id = "nostrmarket-" + urlsafe_short_hash()[:32]
await self.send_req_queue.put(["REQ", self.subscription_id] + merchant_filters)
+ logger.debug(
+ f"Subscribing to events for: {len(public_keys)} keys. New subscription id: {self.subscription_id}"
+ )
+
async def merchant_temp_subscription(self, pk, duration=10):
dm_filters = self._filters_for_direct_messages([pk], 0)
stall_filters = self._filters_for_stall_events([pk], 0)
@@ -174,21 +175,16 @@ class NostrClient:
def _ws_handlers(self):
def on_open(_):
- logger.debug("[NOSTRMARKET DEBUG] ✅ Connected to 'nostrclient' websocket successfully")
+ logger.info("Connected to 'nostrclient' websocket")
def on_message(_, message):
- logger.debug(f"[NOSTRMARKET DEBUG] 📨 Received websocket message: {message[:200]}...")
- try:
- self.recieve_event_queue.put_nowait(message)
- logger.debug(f"[NOSTRMARKET DEBUG] 📤 Message queued successfully")
- except Exception as e:
- logger.error(f"[NOSTRMARKET] ❌ Failed to queue message: {e}")
+ self.recieve_event_queue.put_nowait(message)
def on_error(_, error):
- logger.warning(f"[NOSTRMARKET] ❌ Websocket error: {error}")
+ logger.warning(error)
def on_close(x, status_code, message):
- logger.warning(f"[NOSTRMARKET] 🔌 Websocket closed: {x}: '{status_code}' '{message}'")
+ logger.warning(f"Websocket closed: {x}: '{status_code}' '{message}'")
# force re-subscribe
self.recieve_event_queue.put_nowait(ValueError("Websocket close."))
diff --git a/services.py b/services.py
index 3057292..4039dbb 100644
--- a/services.py
+++ b/services.py
@@ -1,8 +1,7 @@
import asyncio
import json
-from typing import List, Optional, Tuple
-from lnbits.bolt11 import decode
+from bolt11 import decode
from lnbits.core.crud import get_wallet
from lnbits.core.services import create_invoice, websocket_updater
from loguru import logger
@@ -60,12 +59,11 @@ from .nostr.event import NostrEvent
async def create_new_order(
merchant_public_key: str, data: PartialOrder
-) -> Optional[PaymentRequest]:
+) -> PaymentRequest | None:
merchant = await get_merchant_by_pubkey(merchant_public_key)
assert merchant, "Cannot find merchant for order!"
- existing_order = await get_order(merchant.id, data.id)
- if existing_order:
+ if await get_order(merchant.id, data.id):
return None
if data.event_id and await get_order_by_event_id(merchant.id, data.event_id):
return None
@@ -75,24 +73,20 @@ async def create_new_order(
)
await create_order(merchant.id, order)
- payment_request = PaymentRequest(
+ return PaymentRequest(
id=data.id,
payment_options=[PaymentOption(type="ln", link=invoice)],
message=receipt,
)
- return payment_request
async def build_order_with_payment(
merchant_id: str, merchant_public_key: str, data: PartialOrder
):
-
products = await get_products_by_ids(
merchant_id, [p.product_id for p in data.items]
)
-
data.validate_order_items(products)
-
shipping_zone = await get_zone(merchant_id, data.shipping_id)
assert shipping_zone, f"Shipping zone not found for order '{data.id}'"
@@ -100,7 +94,6 @@ async def build_order_with_payment(
product_cost_sat, shipping_cost_sat = await data.costs_in_sats(
products, shipping_zone.id, shipping_zone.cost
)
-
receipt = data.receipt(products, shipping_zone.id, shipping_zone.cost)
wallet_id = await get_wallet_for_product(data.items[0].product_id)
@@ -111,13 +104,11 @@ async def build_order_with_payment(
merchant_id, product_ids, data.items
)
if not success:
- logger.error(f"[NOSTRMARKET] ❌ Product quantity check failed: {message}")
raise ValueError(message)
- total_amount_sat = round(product_cost_sat + shipping_cost_sat)
payment = await create_invoice(
wallet_id=wallet_id,
- amount=total_amount_sat,
+ amount=round(product_cost_sat + shipping_cost_sat),
memo=f"Order '{data.id}' for pubkey '{data.public_key}'",
extra={
"tag": "nostrmarket",
@@ -145,7 +136,7 @@ async def update_merchant_to_nostr(
merchant: Merchant, delete_merchant=False
) -> Merchant:
stalls = await get_stalls(merchant.id)
- event: Optional[NostrEvent] = None
+ event: NostrEvent | None = None
for stall in stalls:
assert stall.id
products = await get_products(merchant.id, stall.id)
@@ -158,8 +149,9 @@ async def update_merchant_to_nostr(
stall.event_id = event.id
stall.event_created_at = event.created_at
await update_stall(merchant.id, stall)
- # Always publish merchant profile (kind 0)
- event = await sign_and_send_to_nostr(merchant, merchant, delete_merchant)
+ if delete_merchant:
+ # merchant profile updates not supported yet
+ event = await sign_and_send_to_nostr(merchant, merchant, delete_merchant)
assert event
merchant.config.event_id = event.id
return merchant
@@ -229,7 +221,7 @@ async def notify_client_of_order_status(
async def update_products_for_order(
merchant: Merchant, order: Order
-) -> Tuple[bool, str]:
+) -> tuple[bool, str]:
product_ids = [i.product_id for i in order.items]
success, products, message = await compute_products_new_quantity(
merchant.id, product_ids, order.items
@@ -297,9 +289,9 @@ async def send_dm(
async def compute_products_new_quantity(
- merchant_id: str, product_ids: List[str], items: List[OrderItem]
-) -> Tuple[bool, List[Product], str]:
- products: List[Product] = await get_products_by_ids(merchant_id, product_ids)
+ merchant_id: str, product_ids: list[str], items: list[OrderItem]
+) -> tuple[bool, list[Product], str]:
+ products: list[Product] = await get_products_by_ids(merchant_id, product_ids)
for p in products:
required_quantity = next(
@@ -322,17 +314,11 @@ async def compute_products_new_quantity(
async def process_nostr_message(msg: str):
try:
- parsed_msg = json.loads(msg)
- type_, *rest = parsed_msg
-
+ type_, *rest = json.loads(msg)
if type_.upper() == "EVENT":
- if len(rest) < 2:
- logger.warning(f"[NOSTRMARKET] ⚠️ EVENT message missing data: {rest}")
- return
_, event = rest
event = NostrEvent(**event)
-
if event.kind == 0:
await _handle_customer_profile_update(event)
elif event.kind == 4:
@@ -341,15 +327,10 @@ async def process_nostr_message(msg: str):
await _handle_stall(event)
elif event.kind == 30018:
await _handle_product(event)
- else:
- logger.info(f"[NOSTRMARKET] ❓ Unhandled event kind: {event.kind} - event: {event.id}")
return
- else:
- logger.info(f"[NOSTRMARKET] 🔄 Non-EVENT message type: {type_}")
except Exception as ex:
- logger.error(f"[NOSTRMARKET] ❌ Error processing nostr message: {ex}")
- logger.error(f"[NOSTRMARKET] 📄 Raw message that failed: {msg}")
+ logger.debug(ex)
async def create_or_update_order_from_dm(
@@ -431,29 +412,28 @@ async def extract_customer_order_from_dm(
async def _handle_nip04_message(event: NostrEvent):
-
- p_tags = event.tag_values("p")
-
- # PRIORITY 1: Check if any recipient (p_tag) is a merchant → incoming message TO merchant
- for p_tag in p_tags:
- if p_tag:
- potential_merchant = await get_merchant_by_pubkey(p_tag)
- if potential_merchant:
- clear_text_msg = potential_merchant.decrypt_message(event.content, event.pubkey)
- await _handle_incoming_dms(event, potential_merchant, clear_text_msg)
- return # IMPORTANT: Return immediately to prevent double processing
-
- # PRIORITY 2: If no recipient merchant found, check if sender is a merchant → outgoing message FROM merchant
- sender_merchant = await get_merchant_by_pubkey(event.pubkey)
- if sender_merchant:
- assert len(event.tag_values("p")) != 0, "Outgoing message has no 'p' tag"
- clear_text_msg = sender_merchant.decrypt_message(
+ merchant_public_key = event.pubkey
+ merchant = await get_merchant_by_pubkey(merchant_public_key)
+
+ if not merchant:
+ p_tags = event.tag_values("p")
+ if len(p_tags) and p_tags[0]:
+ merchant_public_key = p_tags[0]
+ merchant = await get_merchant_by_pubkey(merchant_public_key)
+
+ assert merchant, f"Merchant not found for public key '{merchant_public_key}'"
+
+ if event.pubkey == merchant_public_key:
+ assert len(event.tag_values("p")) != 0, "Outgong message has no 'p' tag"
+ clear_text_msg = merchant.decrypt_message(
event.content, event.tag_values("p")[0]
)
- await _handle_outgoing_dms(event, sender_merchant, clear_text_msg)
- return # IMPORTANT: Return immediately
-
- # No merchant found in either direction
+ await _handle_outgoing_dms(event, merchant, clear_text_msg)
+ elif event.has_tag_value("p", merchant_public_key):
+ clear_text_msg = merchant.decrypt_message(event.content, event.pubkey)
+ await _handle_incoming_dms(event, merchant, clear_text_msg)
+ else:
+ logger.warning(f"Bad NIP04 event: '{event.id}'")
async def _handle_incoming_dms(
@@ -503,18 +483,17 @@ async def _handle_outgoing_dms(
async def _handle_incoming_structured_dm(
merchant: Merchant, dm: DirectMessage, json_data: dict
-) -> Tuple[DirectMessageType, Optional[str]]:
+) -> tuple[DirectMessageType, str | None]:
try:
if dm.type == DirectMessageType.CUSTOMER_ORDER.value and merchant.config.active:
json_resp = await _handle_new_order(
merchant.id, merchant.public_key, dm, json_data
)
+
return DirectMessageType.PAYMENT_REQUEST, json_resp
- else:
- logger.info(f"[NOSTRMARKET] Skipping order processing - type: {dm.type}, expected: {DirectMessageType.CUSTOMER_ORDER.value}, merchant_active: {merchant.config.active}")
except Exception as ex:
- logger.error(f"[NOSTRMARKET] Error in _handle_incoming_structured_dm: {ex}")
+ logger.warning(ex)
return DirectMessageType.PLAIN_TEXT, None
@@ -595,31 +574,9 @@ async def _handle_new_order(
wallet = await get_wallet(wallet_id)
assert wallet, f"Cannot find wallet for product id: {first_product_id}"
-
payment_req = await create_new_order(merchant_public_key, partial_order)
-
- if payment_req is None:
- # Return existing order data instead of creating a failed order
- existing_order = await get_order(merchant_id, partial_order.id)
- if existing_order and existing_order.invoice_id != "None":
- # Order exists with invoice, return existing payment request
- duplicate_response = json.dumps({
- "type": DirectMessageType.PAYMENT_REQUEST.value,
- "id": existing_order.id,
- "message": "Order already received and processed",
- "payment_options": []
- }, separators=(",", ":"), ensure_ascii=False)
- return duplicate_response
- else:
- # Order exists but no invoice, skip processing
- logger.info(f"[NOSTRMARKET] Order exists but no invoice, returning empty string")
- return ""
-
except Exception as e:
- logger.error(f"[NOSTRMARKET] Error creating order: {e}")
- logger.error(f"[NOSTRMARKET] Order data: {json_data}")
- logger.error(f"[NOSTRMARKET] Exception type: {type(e).__name__}")
- logger.error(f"[NOSTRMARKET] Exception details: {str(e)}")
+ logger.debug(e)
payment_req = await create_new_failed_order(
merchant_id,
merchant_public_key,
@@ -627,17 +584,12 @@ async def _handle_new_order(
json_data,
"Order received, but cannot be processed. Please contact merchant.",
)
-
- if not payment_req:
- logger.error(f"[NOSTRMARKET] No payment request returned for order: {partial_order.id}")
- return ""
-
+ assert payment_req
response = {
"type": DirectMessageType.PAYMENT_REQUEST.value,
**payment_req.dict(),
}
- response_json = json.dumps(response, separators=(",", ":"), ensure_ascii=False)
- return response_json
+ return json.dumps(response, separators=(",", ":"), ensure_ascii=False)
async def create_new_failed_order(
@@ -670,11 +622,8 @@ async def subscribe_to_all_merchants():
last_stall_time = await get_last_stall_update_time()
last_prod_time = await get_last_product_update_time()
- # Make dm_time more lenient by subtracting 5 minutes to avoid missing recent events
- lenient_dm_time = max(0, last_dm_time - 300) if last_dm_time > 0 else 0
-
await nostr_client.subscribe_merchants(
- public_keys, lenient_dm_time, last_stall_time, last_prod_time, 0
+ public_keys, last_dm_time, last_stall_time, last_prod_time, 0
)
diff --git a/static/components/edit-profile-dialog.js b/static/components/edit-profile-dialog.js
deleted file mode 100644
index 0736695..0000000
--- a/static/components/edit-profile-dialog.js
+++ /dev/null
@@ -1,91 +0,0 @@
-window.app.component('edit-profile-dialog', {
- name: 'edit-profile-dialog',
- template: '#edit-profile-dialog',
- delimiters: ['${', '}'],
- props: ['model-value', 'merchant-id', 'merchant-config', 'adminkey'],
- emits: ['update:model-value', 'profile-updated'],
- data: function () {
- return {
- saving: false,
- formData: {
- name: '',
- display_name: '',
- about: '',
- picture: '',
- banner: '',
- website: '',
- nip05: '',
- lud16: ''
- }
- }
- },
- computed: {
- show: {
- get() {
- return this.modelValue
- },
- set(value) {
- this.$emit('update:model-value', value)
- }
- }
- },
- methods: {
- saveProfile: async function () {
- this.saving = true
- try {
- const config = {
- ...this.merchantConfig,
- name: this.formData.name || null,
- display_name: this.formData.display_name || null,
- about: this.formData.about || null,
- picture: this.formData.picture || null,
- banner: this.formData.banner || null,
- website: this.formData.website || null,
- nip05: this.formData.nip05 || null,
- lud16: this.formData.lud16 || null
- }
- await LNbits.api.request(
- 'PATCH',
- `/nostrmarket/api/v1/merchant/${this.merchantId}`,
- this.adminkey,
- config
- )
- // Publish to Nostr
- await LNbits.api.request(
- 'PUT',
- `/nostrmarket/api/v1/merchant/${this.merchantId}/nostr`,
- this.adminkey
- )
- this.show = false
- this.$q.notify({
- type: 'positive',
- message: 'Profile saved and published to Nostr!'
- })
- this.$emit('profile-updated')
- } catch (error) {
- LNbits.utils.notifyApiError(error)
- } finally {
- this.saving = false
- }
- },
- loadFormData: function () {
- if (this.merchantConfig) {
- this.formData.name = this.merchantConfig.name || ''
- this.formData.display_name = this.merchantConfig.display_name || ''
- this.formData.about = this.merchantConfig.about || ''
- this.formData.picture = this.merchantConfig.picture || ''
- this.formData.banner = this.merchantConfig.banner || ''
- this.formData.website = this.merchantConfig.website || ''
- this.formData.nip05 = this.merchantConfig.nip05 || ''
- this.formData.lud16 = this.merchantConfig.lud16 || ''
- }
- }
- },
- watch: {
- modelValue(newVal) {
- if (newVal) {
- this.loadFormData()
- }
- }
- }
-})
diff --git a/static/components/key-pair.js b/static/components/key-pair.js
new file mode 100644
index 0000000..5bf9d23
--- /dev/null
+++ b/static/components/key-pair.js
@@ -0,0 +1,22 @@
+window.app.component('key-pair', {
+ name: 'key-pair',
+ template: '#key-pair',
+ delimiters: ['${', '}'],
+ props: ['public-key', 'private-key'],
+ data: function () {
+ return {
+ showPrivateKey: false
+ }
+ },
+ methods: {
+ copyText: function (text, message, position) {
+ var notify = this.$q.notify
+ Quasar.copyToClipboard(text).then(function () {
+ notify({
+ message: message || 'Copied to clipboard!',
+ position: position || 'bottom'
+ })
+ })
+ }
+ }
+})
diff --git a/static/components/merchant-tab.js b/static/components/merchant-tab.js
index 1194420..c82d853 100644
--- a/static/components/merchant-tab.js
+++ b/static/components/merchant-tab.js
@@ -10,46 +10,14 @@ window.app.component('merchant-tab', {
'merchant-active',
'public-key',
'private-key',
- 'is-admin',
- 'merchant-config'
+ 'is-admin'
],
- emits: [
- 'toggle-show-keys',
- 'hide-keys',
- 'merchant-deleted',
- 'toggle-merchant-state',
- 'restart-nostr-connection',
- 'profile-updated',
- 'import-key',
- 'generate-key'
- ],
- data: function () {
- return {
- showEditProfileDialog: false,
- showKeysDialog: false
- }
- },
computed: {
marketClientUrl: function () {
return '/nostrmarket/market'
}
},
methods: {
- publishProfile: async function () {
- try {
- await LNbits.api.request(
- 'PUT',
- `/nostrmarket/api/v1/merchant/${this.merchantId}/nostr`,
- this.adminkey
- )
- this.$q.notify({
- type: 'positive',
- message: 'Profile published to Nostr!'
- })
- } catch (error) {
- LNbits.utils.notifyApiError(error)
- }
- },
toggleShowKeys: function () {
this.$emit('toggle-show-keys')
},
@@ -59,40 +27,11 @@ window.app.component('merchant-tab', {
handleMerchantDeleted: function () {
this.$emit('merchant-deleted')
},
- removeMerchant: function () {
- const name =
- this.merchantConfig?.display_name ||
- this.merchantConfig?.name ||
- 'this merchant'
- LNbits.utils
- .confirmDialog(
- `Are you sure you want to remove "${name}"? This will delete all associated data (stalls, products, orders, messages).`
- )
- .onOk(async () => {
- try {
- await LNbits.api.request(
- 'DELETE',
- `/nostrmarket/api/v1/merchant/${this.merchantId}`,
- this.adminkey
- )
- this.$emit('merchant-deleted')
- this.$q.notify({
- type: 'positive',
- message: 'Merchant removed'
- })
- } catch (error) {
- LNbits.utils.notifyApiError(error)
- }
- })
- },
toggleMerchantState: function () {
this.$emit('toggle-merchant-state')
},
restartNostrConnection: function () {
this.$emit('restart-nostr-connection')
- },
- handleImageError: function (e) {
- e.target.style.display = 'none'
}
}
})
diff --git a/static/components/nostr-keys-dialog.js b/static/components/nostr-keys-dialog.js
deleted file mode 100644
index 81c451f..0000000
--- a/static/components/nostr-keys-dialog.js
+++ /dev/null
@@ -1,56 +0,0 @@
-window.app.component('nostr-keys-dialog', {
- name: 'nostr-keys-dialog',
- template: '#nostr-keys-dialog',
- delimiters: ['${', '}'],
- props: ['public-key', 'private-key', 'model-value'],
- emits: ['update:model-value'],
- data: function () {
- return {
- showNsec: false
- }
- },
- computed: {
- show: {
- get() {
- return this.modelValue
- },
- set(value) {
- this.$emit('update:model-value', value)
- }
- },
- npub: function () {
- if (!this.publicKey) return ''
- try {
- return window.NostrTools.nip19.npubEncode(this.publicKey)
- } catch (e) {
- return this.publicKey
- }
- },
- nsec: function () {
- if (!this.privateKey) return ''
- try {
- return window.NostrTools.nip19.nsecEncode(this.privateKey)
- } catch (e) {
- return this.privateKey
- }
- }
- },
- methods: {
- copyText: function (text, message) {
- var notify = this.$q.notify
- Quasar.copyToClipboard(text).then(function () {
- notify({
- message: message || 'Copied to clipboard!',
- position: 'bottom'
- })
- })
- }
- },
- watch: {
- modelValue(newVal) {
- if (!newVal) {
- this.showNsec = false
- }
- }
- }
-})
diff --git a/static/js/index.js b/static/js/index.js
index b10220c..00560df 100644
--- a/static/js/index.js
+++ b/static/js/index.js
@@ -5,7 +5,7 @@ window.app = Vue.createApp({
mixins: [window.windowMixin],
data: function () {
return {
- activeTab: 'orders',
+ activeTab: 'merchant',
selectedStallFilter: null,
merchant: {},
shippingZones: [],
@@ -19,13 +19,6 @@ window.app = Vue.createApp({
privateKey: null
}
},
- generateKeyDialog: {
- show: false,
- privateKey: null,
- nsec: null,
- npub: null,
- showNsec: false
- },
wsConnection: null,
nostrStatus: {
connected: false,
@@ -50,18 +43,26 @@ window.app = Vue.createApp({
},
methods: {
generateKeys: async function () {
- // No longer need to generate keys here - the backend will use user's existing keypairs
- await this.createMerchant()
+ const privateKey = nostr.generatePrivateKey()
+ await this.createMerchant(privateKey)
},
importKeys: async function () {
this.importKeyDialog.show = false
- // Import keys functionality removed since we use user's native keypairs
- // Show a message that this is no longer needed
- this.$q.notify({
- type: 'info',
- message: 'Merchants now use your account Nostr keys automatically. Key import is no longer needed.',
- timeout: 3000
- })
+ let privateKey = this.importKeyDialog.data.privateKey
+ if (!privateKey) {
+ return
+ }
+ try {
+ if (privateKey.toLowerCase().startsWith('nsec')) {
+ privateKey = nostr.nip19.decode(privateKey).data
+ }
+ } catch (error) {
+ this.$q.notify({
+ type: 'negative',
+ message: `${error}`
+ })
+ }
+ await this.createMerchant(privateKey)
},
showImportKeysDialog: async function () {
this.importKeyDialog.show = true
@@ -116,9 +117,12 @@ window.app = Vue.createApp({
this.showKeys = false
this.stallCount = 0
},
- createMerchant: async function () {
+ createMerchant: async function (privateKey) {
try {
+ const pubkey = nostr.getPublicKey(privateKey)
const payload = {
+ private_key: privateKey,
+ public_key: pubkey,
config: {}
}
const {data} = await LNbits.api.request(
diff --git a/tasks.py b/tasks.py
index 774951f..013a281 100644
--- a/tasks.py
+++ b/tasks.py
@@ -35,17 +35,13 @@ async def on_invoice_paid(payment: Payment) -> None:
async def wait_for_nostr_events(nostr_client: NostrClient):
- logger.info("[NOSTRMARKET DEBUG] Starting wait_for_nostr_events task")
while True:
try:
- logger.info("[NOSTRMARKET DEBUG] Subscribing to all merchants...")
await subscribe_to_all_merchants()
while True:
- logger.debug("[NOSTRMARKET DEBUG] Waiting for nostr event...")
message = await nostr_client.get_event()
- logger.info(f"[NOSTRMARKET DEBUG] Received event from nostr_client: {message[:100]}...")
await process_nostr_message(message)
except Exception as e:
- logger.warning(f"[NOSTRMARKET DEBUG] Subscription failed. Will retry in 10 seconds: {e}")
+ logger.warning(f"Subcription failed. Will retry in one minute: {e}")
await asyncio.sleep(10)
diff --git a/templates/nostrmarket/components/edit-profile-dialog.html b/templates/nostrmarket/components/edit-profile-dialog.html
deleted file mode 100644
index a447237..0000000
--- a/templates/nostrmarket/components/edit-profile-dialog.html
+++ /dev/null
@@ -1,68 +0,0 @@
-