Merge branch 'main' into feature/extension-info-card-159

This commit is contained in:
Arc 2025-12-24 03:28:36 +00:00 committed by GitHub
commit 1f9f037fb9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 144 additions and 28 deletions

5
.gitignore vendored
View file

@ -22,4 +22,7 @@ node_modules
*.swp *.swp
*.pyo *.pyo
*.pyc *.pyc
*.env *.env
# Claude Code config
CLAUDE.md

104
CLAUDE.md Normal file
View file

@ -0,0 +1,104 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Nostr Market is an LNbits extension implementing NIP-15 (decentralized marketplace protocol) on Nostr. It enables merchants to create webshops (stalls) and sell products with Lightning Network payments, featuring encrypted customer-merchant communication via NIP-04.
**Prerequisites:** Requires the LNbits [nostrclient](https://github.com/lnbits/nostrclient) extension to be installed and configured.
## Common Commands
All commands are in the Makefile:
```bash
make format # Run prettier, black, and ruff formatters
make check # Run mypy, pyright, black check, ruff check, prettier check
make test # Run pytest with debug mode
make all # Run format and check
```
Individual tools:
```bash
make black # Format Python files
make ruff # Check and fix Python linting
make mypy # Static type checking
make pyright # Python static type checker
make prettier # Format JS/HTML/CSS files
```
## Local Development Setup
To run checks locally, install dependencies:
```bash
# Install Python autotools dependencies (needed for secp256k1)
sudo apt-get install -y automake autoconf libtool
# Install Python dependencies
uv sync
# Install Node dependencies (for prettier)
npm install
# Run all checks
make check
```
## Architecture
### Core Layers
1. **API Layer** (`views_api.py`) - REST endpoints for merchants, stalls, products, zones, orders, direct messages
2. **Business Logic** (`services.py`) - Order processing, Nostr event signing/publishing, message routing, invoice handling
3. **Data Layer** (`crud.py`) - Async SQLite operations via LNbits db module
4. **Models** (`models.py`) - Pydantic models for all entities
### Nostr Integration (`nostr/`)
- `nostr_client.py` - WebSocket client connecting to nostrclient extension for relay communication
- `event.py` - Nostr event model, serialization, ID computation (SHA256), Schnorr signatures
### Background Tasks (`__init__.py`, `tasks.py`)
Three permanent async tasks:
- `wait_for_paid_invoices()` - Lightning payment listener
- `wait_for_nostr_events()` - Incoming Nostr message processor
- `_subscribe_to_nostr_client()` - WebSocket connection manager
### Frontend (`static/`, `templates/`)
- Merchant dashboard: `templates/nostrmarket/index.html`
- Customer marketplace: `templates/nostrmarket/market.html` with Vue.js/Quasar in `static/market/`
- Use Quasar UI components when possible: https://quasar.dev/components
### Key Data Models
- **Merchant** - Shop owner with Nostr keypair, handles event signing and DM encryption
- **Stall** - Individual shop with products and shipping zones (kind 30017)
- **Product** - Items for sale with categories, images, quantity (kind 30018)
- **Zone** - Shipping configuration by region
- **Order** - Customer purchases with Lightning invoice tracking
- **DirectMessage** - Encrypted chat (NIP-04)
- **Customer** - Buyer profile with Nostr pubkey
### Key Patterns
- **Nostrable Interface** - Base class for models convertible to Nostr events (`to_nostr_event()`, `to_nostr_delete_event()`)
- **Parameterized Replaceable Events** - Stalls (kind 30017) and Products (kind 30018) per NIP-33
- **AES-256 Encryption** - Customer-merchant DMs use shared secret from ECDH
- **JSON Meta Fields** - Complex data (zones, items, config) stored as JSON in database
### Cryptography (`helpers.py`)
- Schnorr signatures for Nostr events
- NIP-04 encryption/decryption
- Key derivation and bech32 encoding (npub/nsec)
## Workflow
- Always check GitHub Actions after pushing to verify CI passes
- Run `make check` locally before pushing to catch issues early

View file

@ -19,7 +19,6 @@ window.app.component('shipping-zones', {
currencies: [], currencies: [],
shippingZoneOptions: [ shippingZoneOptions: [
'Free (digital)', 'Free (digital)',
'Flat rate',
'Worldwide', 'Worldwide',
'Europe', 'Europe',
'Australia', 'Australia',
@ -27,6 +26,7 @@ window.app.component('shipping-zones', {
'Belgium', 'Belgium',
'Brazil', 'Brazil',
'Canada', 'Canada',
'China',
'Denmark', 'Denmark',
'Finland', 'Finland',
'France', 'France',
@ -34,8 +34,8 @@ window.app.component('shipping-zones', {
'Greece', 'Greece',
'Hong Kong', 'Hong Kong',
'Hungary', 'Hungary',
'Ireland',
'Indonesia', 'Indonesia',
'Ireland',
'Israel', 'Israel',
'Italy', 'Italy',
'Japan', 'Japan',
@ -59,10 +59,9 @@ window.app.component('shipping-zones', {
'Thailand', 'Thailand',
'Turkey', 'Turkey',
'Ukraine', 'Ukraine',
'United Kingdom**', 'United Kingdom',
'United States***', 'United States',
'Vietnam', 'Vietnam'
'China'
] ]
} }
}, },

View file

@ -48,26 +48,36 @@
label="Countries" label="Countries"
v-model="zoneDialog.data.countries" v-model="zoneDialog.data.countries"
></q-select> ></q-select>
<q-select <div class="row items-start">
:disabled="!!zoneDialog.data.id" <div class="col q-mr-sm">
:readonly="!!zoneDialog.data.id" <q-input
filled filled
dense dense
v-model="zoneDialog.data.currency" label="Default shipping cost"
type="text" fill-mask="0"
label="Unit" reverse-fill-mask
:options="currencies" :step="zoneDialog.data.currency != 'sat' ? '0.01' : '1'"
></q-select> type="number"
<q-input v-model.trim="zoneDialog.data.cost"
filled :error="(zoneDialog.data.currency === 'sat' && zoneDialog.data.cost % 1 !== 0) || (zoneDialog.data.currency !== 'sat' && (zoneDialog.data.cost * 100) % 1 !== 0)"
dense :error-message="zoneDialog.data.currency === 'sat' ? 'Satoshis must be whole numbers' : 'Maximum 2 decimal places allowed'"
:label="'Cost of Shipping (' + zoneDialog.data.currency + ') *'" hint="Additional costs can be set per product"
fill-mask="0" ></q-input>
reverse-fill-mask </div>
:step="zoneDialog.data.currency != 'sat' ? '0.01' : '1'" <div class="col-auto">
type="number" <q-select
v-model.trim="zoneDialog.data.cost" :disabled="!!zoneDialog.data.id"
></q-input> :readonly="!!zoneDialog.data.id"
filled
dense
v-model="zoneDialog.data.currency"
type="text"
label="Currency"
:options="currencies"
style="min-width: 100px"
></q-select>
</div>
</div>
<div class="row q-mt-lg"> <div class="row q-mt-lg">
<div v-if="zoneDialog.data.id"> <div v-if="zoneDialog.data.id">
<q-btn unelevated color="primary" type="submit">Update</q-btn> <q-btn unelevated color="primary" type="submit">Update</q-btn>
@ -83,7 +93,7 @@
<q-btn <q-btn
unelevated unelevated
color="primary" color="primary"
:disable="!zoneDialog.data.countries || !zoneDialog.data.countries.length" :disable="!zoneDialog.data.name || !zoneDialog.data.countries || !zoneDialog.data.countries.length || (zoneDialog.data.currency === 'sat' && zoneDialog.data.cost % 1 !== 0) || (zoneDialog.data.currency !== 'sat' && (zoneDialog.data.cost * 100) % 1 !== 0)"
type="submit" type="submit"
>Create Shipping Zone</q-btn >Create Shipping Zone</q-btn
> >