diff --git a/.gitignore b/.gitignore index f3a8853..056489e 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,7 @@ node_modules *.swp *.pyo *.pyc -*.env \ No newline at end of file +*.env + +# Claude Code config +CLAUDE.md \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..db2ef06 --- /dev/null +++ b/CLAUDE.md @@ -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 diff --git a/config.json b/config.json index a02b0b0..1aa34a8 100644 --- a/config.json +++ b/config.json @@ -2,7 +2,7 @@ "name": "Nostr Market", "version": "1.1.0", "short_description": "Nostr Webshop/market on LNbits", - "tile": "/nostrmarket/static/images/bitcoin-shop.png", + "tile": "/nostrmarket/static/images/nostr-market.png", "min_lnbits_version": "1.4.0", "contributors": [ { diff --git a/static/components/shipping-zones.js b/static/components/shipping-zones.js index 742021a..9332d1e 100644 --- a/static/components/shipping-zones.js +++ b/static/components/shipping-zones.js @@ -19,7 +19,6 @@ window.app.component('shipping-zones', { currencies: [], shippingZoneOptions: [ 'Free (digital)', - 'Flat rate', 'Worldwide', 'Europe', 'Australia', @@ -27,6 +26,7 @@ window.app.component('shipping-zones', { 'Belgium', 'Brazil', 'Canada', + 'China', 'Denmark', 'Finland', 'France', @@ -34,8 +34,8 @@ window.app.component('shipping-zones', { 'Greece', 'Hong Kong', 'Hungary', - 'Ireland', 'Indonesia', + 'Ireland', 'Israel', 'Italy', 'Japan', @@ -59,10 +59,9 @@ window.app.component('shipping-zones', { 'Thailand', 'Turkey', 'Ukraine', - 'United Kingdom**', - 'United States***', - 'Vietnam', - 'China' + 'United Kingdom', + 'United States', + 'Vietnam' ] } }, @@ -162,22 +161,13 @@ window.app.component('shipping-zones', { LNbits.utils.notifyApiError(error) } }, - async getCurrencies() { - try { - const {data} = await LNbits.api.request( - 'GET', - '/nostrmarket/api/v1/currencies', - this.inkey - ) - - this.currencies = ['sat', ...data] - } catch (error) { - LNbits.utils.notifyApiError(error) - } + getCurrencies() { + const currencies = window.g.allowedCurrencies || [] + this.currencies = ['sat', ...currencies] } }, created: async function () { await this.getZones() - await this.getCurrencies() + this.getCurrencies() } }) diff --git a/static/images/generate_logo.py b/static/images/generate_logo.py new file mode 100644 index 0000000..cb66c59 --- /dev/null +++ b/static/images/generate_logo.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +""" +Generate the Nostr Market logo. +Requires: pip install Pillow +""" + +from PIL import Image, ImageDraw # type: ignore[import-not-found] + +# Render at 4x size for antialiasing +scale = 4 +size = 128 * scale +final_size = 128 + +# Consistent color scheme with Nostr Proxy +dark_purple = (80, 40, 120) +light_purple = (140, 100, 180) +white = (255, 255, 255) + +margin = 4 * scale + +swoosh_center = ((128 + 100) * scale, -90 * scale) +swoosh_radius = 220 * scale + +# Create rounded rectangle mask +mask = Image.new("L", (size, size), 0) +mask_draw = ImageDraw.Draw(mask) +corner_radius = 20 * scale +mask_draw.rounded_rectangle( + [margin, margin, size - margin, size - margin], + radius=corner_radius, + fill=255, +) + +# Create background with swoosh +bg = Image.new("RGBA", (size, size), (0, 0, 0, 0)) +bg_draw = ImageDraw.Draw(bg) +bg_draw.rounded_rectangle( + [margin, margin, size - margin, size - margin], + radius=corner_radius, + fill=dark_purple, +) +bg_draw.ellipse( + [ + swoosh_center[0] - swoosh_radius, + swoosh_center[1] - swoosh_radius, + swoosh_center[0] + swoosh_radius, + swoosh_center[1] + swoosh_radius, + ], + fill=light_purple, +) + +# Apply rounded rectangle mask +final = Image.new("RGBA", (size, size), (0, 0, 0, 0)) +final.paste(bg, mask=mask) +draw = ImageDraw.Draw(final) + +center_x, center_y = size // 2, size // 2 + +# Shop/storefront - wider and shorter for shop look +shop_width = 80 * scale +awning_height = 18 * scale +body_height = 45 * scale +total_height = awning_height + body_height + +shop_left = center_x - shop_width // 2 +shop_right = center_x + shop_width // 2 + +# Center vertically +awning_top = center_y - total_height // 2 +awning_bottom = awning_top + awning_height +shop_bottom = awning_bottom + body_height +awning_extend = 5 * scale + +# Draw awning background (white base) +draw.rectangle( + [shop_left - awning_extend, awning_top, shop_right + awning_extend, awning_bottom], + fill=white, +) + +# Vertical stripes on awning (alternating dark purple) +stripe_count = 8 +stripe_width = (shop_width + 2 * awning_extend) // stripe_count +for i in range(1, stripe_count, 2): + x_left = shop_left - awning_extend + i * stripe_width + draw.rectangle( + [x_left, awning_top, x_left + stripe_width, awning_bottom], + fill=dark_purple, + ) + +# Shop body (below awning) +draw.rectangle( + [shop_left, awning_bottom, shop_right, shop_bottom], + fill=white, +) + +# Large display window (shop style) +window_margin = 8 * scale +window_top = awning_bottom + 6 * scale +window_bottom = shop_bottom - 6 * scale +# Left display window +draw.rectangle( + [shop_left + window_margin, window_top, center_x - 10 * scale, window_bottom], + fill=dark_purple, +) +# Right display window +draw.rectangle( + [center_x + 10 * scale, window_top, shop_right - window_margin, window_bottom], + fill=dark_purple, +) + +# Door (center, dark purple cutout) +door_width = 14 * scale +door_left = center_x - door_width // 2 +draw.rectangle( + [door_left, window_top, door_left + door_width, shop_bottom], + fill=dark_purple, +) + +# Downscale with LANCZOS for antialiasing +final = final.resize((final_size, final_size), Image.LANCZOS) + +final.save("nostr-market.png") +print("Logo saved to nostr-market.png") diff --git a/static/images/nostr-market.png b/static/images/nostr-market.png new file mode 100644 index 0000000..3e924b5 Binary files /dev/null and b/static/images/nostr-market.png differ diff --git a/templates/nostrmarket/_api_docs.html b/templates/nostrmarket/_api_docs.html index 6bce480..664d6ba 100644 --- a/templates/nostrmarket/_api_docs.html +++ b/templates/nostrmarket/_api_docs.html @@ -1,44 +1,213 @@ - - -

- Nostr Market
- - Created by, + + + +

+ Nostr (Notes and Other Stuff Transmitted by Relays) is + a decentralized protocol for censorship-resistant communication. Unlike + traditional platforms, your identity and data aren't controlled by any + single company. +

+

+ Your Nostr identity is a cryptographic key pair - a public key (npub) + that others use to find you, and a private key (nsec) that proves you + are you. Keep your nsec safe and never share it! +

+
+
+ + + + + +

1. Generate or Import Keys

+

+ Create a new Nostr identity or import an existing one using your nsec. + Your keys are used to sign all marketplace events. +

+

2. Create a Stall

+

+ A stall is your shop. Give it a name, description, and configure + shipping zones for delivery. +

+

3. Add Products

+

+ List items for sale with images, descriptions, and prices in your + preferred currency. +

+

4. Publish to Nostr

+

+ Your stall and products are published to Nostr relays where customers + can discover them using any compatible marketplace client. +

+
+
+
+ + + + +

+ Decentralized Commerce - Your shop exists on Nostr + relays, not a single server. No platform fees, no deplatforming risk. +

+

+ Lightning Payments - Accept instant, low-fee Bitcoin + payments via the Lightning Network. +

+

+ Encrypted Messages - Communicate privately with + customers using NIP-04 encrypted direct messages. +

+

+ Portable Identity - Your merchant reputation travels + with your Nostr keys across any compatible marketplace. +

+

+ Global Reach - Your stalls and products are + automatically visible on any Nostr marketplace client that supports + NIP-15, including Amethyst, Plebeian Market, and others. +

+
+
+
+ + + + +

+ Browse the Market - Use the Market Client to discover + stalls and products from merchants around the world. +

+

+ Pay with Lightning - Fast, private payments with + minimal fees using Bitcoin's Lightning Network. +

+

+ Direct Communication - Message merchants directly via + encrypted Nostr DMs for questions, custom orders, or support. +

+
+
+
+ + + + +

This extension was created by:

+
Tal Vasconcelos - Ben Arc + Tal Vasconcelos + + Ben Arc + + motorina0 -

- Swagger REST API Documentation - - - Visit the market clientMarket client - - + target="_blank" + class="text-decoration-none" + > + motorina0 + + + Ben Weeks + +
+ + + + + + + + + + + + Market Client + Browse and shop from stalls + + + + + + + + + + + + API Documentation + Swagger REST API reference + + + + + + + + + + + + NIP-15 Specification + Nostr Marketplace protocol + + + + + + + + + + + + Report Issues / Feedback + GitHub Issues + + + + + diff --git a/templates/nostrmarket/components/shipping-zones.html b/templates/nostrmarket/components/shipping-zones.html index 3f0fd08..b0dbc34 100644 --- a/templates/nostrmarket/components/shipping-zones.html +++ b/templates/nostrmarket/components/shipping-zones.html @@ -48,26 +48,36 @@ label="Countries" v-model="zoneDialog.data.countries" > - - +
+
+ +
+
+ +
+
Update @@ -83,7 +93,7 @@ Create Shipping Zone diff --git a/templates/nostrmarket/index.html b/templates/nostrmarket/index.html index 5c94d4f..ed9a39f 100644 --- a/templates/nostrmarket/index.html +++ b/templates/nostrmarket/index.html @@ -274,6 +274,58 @@
+
+
+ + + + + Restart the connection to the nostrclient extension + + + + +
+
+ + + +
Nostr Market
+
+ A decentralized marketplace extension for LNbits implementing the + NIP-15 protocol. Create stalls, list products, and accept Lightning + payments while communicating with customers via encrypted Nostr + direct messages. +
+
+ + {% include "nostrmarket/_api_docs.html" %} + +
+
+
+ + +
+
+