Compare commits

..

21 commits

Author SHA1 Message Date
05ebf042ac Extract provision_merchant() service for shared use
Some checks failed
ci.yml / Extract provision_merchant() service for shared use (pull_request) Failing after 0s
ci.yml / Extract provision_merchant() service for shared use (push) Failing after 0s
Both _auto_create_merchant (lazy GET fallback in views_api) and
LNbits' _create_default_merchant (eager signup hook) used to
reimplement merchant + zone + stall creation independently. Moves the
canonical implementation to services.provision_merchant() so both
call sites stay in lockstep — future changes (NIP-17 kind 10050 relay
list, additional default zones, etc.) only happen in one place.

- services.provision_merchant(user_id, wallet_id, public_key,
  private_key, display_name, config): creates merchant if absent,
  default 'Online' zone, default '<username>'s Store' stall, and
  publishes the kind 30017 stall event. Idempotent on the merchant
  pubkey: returns the existing merchant unchanged if one exists.
- views_api._auto_create_merchant: now a 10-line wrapper that loads
  the account, generates fallback keys if missing, then delegates.

The LNbits-side hook (lnbits/core/services/users.py:_create_default_merchant)
will be updated in a companion commit to also call this service.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-03 16:59:03 +02:00
e481c9179d Auto-create + publish default stall, republish stall on product publish
Two complementary fixes for the "Unknown Stall" bug, where a customer
sees a product on the relay but the parent stall is missing.

1. _auto_create_merchant() now creates a default "<username>'s Store"
   stall and publishes its kind 30017 event before returning. New users
   land with a fully-published merchant identity, so the very first
   product they create has a known parent stall on relays.

2. POST /api/v1/product (api_create_product) now republishes the parent
   stall before publishing the product. NIP-33 parameterized replaceable
   events make this idempotent, but it self-heals every existing case
   where the stall publish failed or never happened (transient relay
   issues, accounts that pre-date the auto-publish flow, manual stall
   creation that didn't reach all relays).

This complements the LNbits-side fix in core/services/users.py
(_create_default_merchant publishes the stall on signup) and the
webapp self-heal in useMarketStallSelfHeal.ts. With all three layers,
"Unknown Stall" should disappear from the customer view.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-03 16:59:03 +02:00
3cc798aab2 Remove orphaned key import/generate UI
Phase 3 (auto-provision merchant from account keypair) removed the
generateKeys / importKeys methods and the dialog data fields, but
left the dialog templates and dropdown menu items behind. They
referenced importKeyDialog.show and generateKeyDialog.show, which
were now undefined — breaking the merchant dashboard with
"Cannot read properties of undefined (reading 'show')".

Removes:
- The Import Key and Generate New Key dialogs from index.html
- The corresponding dropdown items from merchant-tab.html
- The 'import-key' and 'generate-key' emits from merchant-tab.js
- The dangling @import-key / @generate-key listeners in index.html

Merchants are auto-provisioned from the account keypair on first
GET; key rotation is handled by the migrate-keys feature instead.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-03 16:59:03 +02:00
25023df8bd Add keypair rotation detection and migration feature
When a user rotates their Nostr keypair in account settings, the
merchant still holds the old key. This adds:

- key_mismatch flag on MerchantConfig (runtime, not persisted) -
  detected on each GET /api/v1/merchant by comparing account vs
  merchant pubkey
- POST /api/v1/merchant/{id}/migrate-keys endpoint that updates
  the merchant keys, republishes all stalls/products under the new
  identity, and resubscribes
- Warning banner in the UI with a "Migrate Keys" button and
  confirmation dialog
- update_merchant_keys() crud function

Orders and DM history are preserved since they reference customer
pubkeys. Old stall/product events on relays become orphaned.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-03 16:59:03 +02:00
5c38947fc6 Auto-provision merchant from account keypair on first access
The LNbits user account IS the merchant identity. GET /api/v1/merchant
now auto-creates the merchant record using the account's existing Nostr
keypair if one doesn't exist yet, so the extension is immediately
usable without any setup screen.

- Extract _auto_create_merchant() helper used by both GET and POST
- Remove welcome/key-generation screen (replaced with loading spinner)
- Remove dead frontend code (generateKeys, importKeys dialogs)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-03 16:59:03 +02:00
725944ae9c Replace NIP-04 messaging with NIP-17 (NIP-44 + NIP-59 gift wrapping)
Modernize the entire customer-merchant communication layer from deprecated
NIP-04 (kind 4, AES-256-CBC) to NIP-17 private direct messages using
NIP-44 v2 encryption (ChaCha20 + HMAC-SHA256) and NIP-59 gift wrapping
(rumor/seal/gift-wrap protocol). No backwards compatibility retained.

New modules:
- nostr/nip44.py: NIP-44 v2 encryption verified against official spec vectors
- nostr/nip59.py: NIP-59 gift wrap with wrap/unwrap convenience functions
- tests/: 44 unit tests for NIP-44 and NIP-59

Key changes:
- Subscription filters: kind 4 → kind 1059 gift wraps
- Message handler: _handle_nip04_message → _handle_gift_wrap (unwrap + route)
- send_dm/reply_to_structured_dm: NIP-59 gift wrap to recipient + self-archive
- Merchant model: removed NIP-04 crypto methods (decrypt/encrypt/build_dm_event)
- helpers.py: removed NIP-04 functions, kept Schnorr signing + key normalization
- views_api.py: consolidated DM sending through send_dm() service function

Reliability improvements:
- Event deduplication via bounded LRU set in NostrClient
- Subscription health monitor (resubscribes after 120s of silence)
- Preserved 5-minute lenient time window from prior work

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-03 16:59:03 +02:00
319d5eeb04 Refactors type hints for clarity
Some checks failed
CI / lint (push) Has been cancelled
Updates type hints in `crud.py`, `helpers.py`, and `models.py` for improved readability and maintainability.

Replaces `Merchant | None` with `Optional[Merchant]` and `list[X]` with `List[X]` for consistency with standard Python typing practices.
2026-04-24 01:59:27 -04:00
d3229cd094 Improves Nostr message handling and error logging
Enhances the processing of Nostr messages by adding more robust error handling and logging, providing better insights into potential issues.

Specifically:
- Improves the checks on the websocket connection to log errors and debug information.
- Implements more comprehensive error logging for failed product quantity checks.
- Enhances logging and validation of EVENT messages to prevent potential errors.
- Implements a more robust merchant lookup logic to avoid double processing of events.
- Implements a more lenient time window for direct message subscriptions.
2026-04-24 01:59:27 -04:00
039a26d1df Adds order discovery analysis document
Adds a document analyzing the order discovery mechanism in Nostrmarket.

The document identifies the reasons merchants need to manually refresh to see new orders, instead of receiving them automatically. It analyzes timing window issues, connection stability, subscription state management, and event processing delays. It proposes solutions such as enhanced persistent subscriptions, periodic auto-refresh, WebSocket health monitoring, and event gap detection.
2026-04-24 01:59:27 -04:00
0b7523fbeb Enhances websocket connection robustness
Improves websocket connection reliability by predefining the websocket URL and handling potential queueing errors.

This change also updates the websocket close message for clarity.
2026-04-24 01:59:27 -04:00
c7f6b209dd TEMP: set merchant active to True by default 2026-04-24 01:59:27 -04:00
a8eeace36d Improve merchant creation with automatic keypair generation
Enhance the merchant creation process by automatically generating Nostr keypairs
for users who don't have them, and streamline the API interface.

Changes:
- Add CreateMerchantRequest model to simplify merchant creation API
- Auto-generate Nostr keypairs for users without existing keys
- Update merchant creation endpoint to use user account keypairs
- Improve error handling and validation in merchant creation flow
- Clean up frontend JavaScript for merchant creation

This ensures all merchants have proper Nostr keypairs for marketplace
functionality without requiring manual key management from users.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-04-24 01:59:27 -04:00
d4c1bc04ec interactive rebase commit, clean logs 2026-04-24 01:59:27 -04:00
0ebd5f642c add DEBUG logs 2026-04-24 01:59:27 -04:00
Arc
125481bda8
Merge pull request #171 from lnbits/more_pages
Changes to more pages
2026-02-16 14:22:14 +00:00
Arc
77a7ab8153
Merge pull request #170 from lnbits/ui_tweaks
fix: frontend page
2026-02-16 14:21:31 +00:00
DoktorShift
ac879e29f2 Changes to more pages 2026-01-25 14:21:19 +01:00
arcbtc
754003eb52 make 2025-12-30 14:49:33 +00:00
arcbtc
75c6d388a5 frontend page fix 2025-12-30 14:46:31 +00:00
arcbtc
94af43c3f5 add open button 2025-12-30 12:48:43 +00:00
arcbtc
216b53cb31 tweaks 2025-12-30 12:07:11 +00:00
6 changed files with 133 additions and 38 deletions

View file

@ -1,3 +1,13 @@
<a href="https://lnbits.com" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://i.imgur.com/QE6SIrs.png">
<img src="https://i.imgur.com/fyKPgVT.png" alt="LNbits" style="width:280px">
</picture>
</a>
[![License: MIT](https://img.shields.io/badge/License-MIT-success?logo=open-source-initiative&logoColor=white)](./LICENSE)
[![Built for LNbits](https://img.shields.io/badge/Built%20for-LNbits-4D4DFF?logo=lightning&logoColor=white)](https://github.com/lnbits/lnbits)
# Nostr Market ([NIP-15](https://github.com/nostr-protocol/nips/blob/master/15.md)) - <small>[LNbits](https://github.com/lnbits/lnbits) extension</small>
<small>For more about LNBits extension check [this tutorial](https://github.com/lnbits/lnbits/wiki/LNbits-Extensions).</small>
@ -147,3 +157,10 @@ Stall and product are _Parameterized Replaceable Events_ according to [NIP-33](h
Order placing, invoicing, payment details and order statuses are handled over Nostr using [NIP-04](https://github.com/nostr-protocol/nips/blob/master/04.md).
Customer support is handled over whatever communication method was specified. If communicationg via nostr, [NIP-04](https://github.com/nostr-protocol/nips/blob/master/04.md) is used.
## Powered by LNbits
[LNbits](https://lnbits.com) is a free and open-source lightning accounts system.
[![Visit LNbits Shop](https://img.shields.io/badge/Visit-LNbits%20Shop-7C3AED?logo=shopping-cart&logoColor=white&labelColor=5B21B6)](https://shop.lnbits.com/)
[![Try myLNbits SaaS](https://img.shields.io/badge/Try-myLNbits%20SaaS-2563EB?logo=lightning&logoColor=white&labelColor=1E40AF)](https://my.lnbits.com/login)

View file

@ -1,12 +1,15 @@
{
"name": "Nostr Market",
"id": "nostrmarket",
"version": "1.1.0",
"name": "Nostr Market",
"repo": "https://github.com/lnbits/nostrmarket",
"short_description": "Nostr Webshop/market on LNbits",
"tile": "/nostrmarket/static/images/nostr-market.png",
"description": "",
"tile": "/nostrmarket/static/images/bitcoin-shop.png",
"min_lnbits_version": "1.4.0",
"contributors": [
{
"name": "motorina0",
"name": "Vlad Stan",
"uri": "https://github.com/motorina0",
"role": "Contributor"
},
@ -19,6 +22,11 @@
"name": "talvasconcelos",
"uri": "https://github.com/talvasconcelos",
"role": "Developer"
},
{
"name": "BenGWeeks",
"uri": "https://github.com/BenGWeeks",
"role": "Developer"
}
],
"images": [
@ -44,5 +52,9 @@
],
"description_md": "https://raw.githubusercontent.com/lnbits/nostrmarket/main/description.md",
"terms_and_conditions_md": "https://raw.githubusercontent.com/lnbits/nostrmarket/main/toc.md",
"license": "MIT"
"license": "MIT",
"paid_features": "",
"tags": ["Nostr", "Marketplace"],
"donate": "",
"hidden": false
}

View file

@ -1,12 +1,10 @@
> IMPORTANT: Nostr market needs the nostr-client extension installed.
Buy and sell products over Nostr using the NIP-15 marketplace protocol.
Buy and sell things over Nostr, using NIP15 https://github.com/nostr-protocol/nips/blob/master/15.md
Its functions include:
Nostr was partly based on the the previous version of this extension "Diagon Alley", so lends itself very well to buying and sellinng over Nostr.
- Managing products, sales, and customer communication as a merchant
- Browsing and ordering products as a customer
- Tracking order status and delivery
- Communicating via NIP-17 private direct messages (NIP-44 encryption + NIP-59 gift wrapping)
The Nostr Market extension includes:
- A merchant client to manage products, sales and communication with customers.
- A customer client to find and order products from merchants, communicate with merchants and track status of ordered products.
All communication happens over NIP-17 private direct messages (NIP-44 encryption + NIP-59 gift wrapping).
A decentralized commerce solution for merchants and buyers who want to trade goods and services over Nostr with end-to-end encrypted communication.

View file

@ -29,8 +29,14 @@ window.app.component('merchant-tab', {
},
computed: {
marketClientUrl: function () {
if (!this.publicKey) {
return '/nostrmarket/market'
}
const url = new URL('/nostrmarket/market', window.location.origin)
url.searchParams.set('merchant', this.publicKey)
return url.pathname + url.search
}
},
methods: {
publishProfile: async function () {

View file

@ -1,5 +1,43 @@
var NostrTools = window.NostrTools
;(function ensureRandomUUID() {
if (!globalThis.crypto) {
globalThis.crypto = {}
}
if (!globalThis.crypto.randomUUID) {
globalThis.crypto.randomUUID = function () {
const getRandomValues = globalThis.crypto.getRandomValues
if (getRandomValues) {
const bytes = new Uint8Array(16)
getRandomValues.call(globalThis.crypto, bytes)
bytes[6] = (bytes[6] & 0x0f) | 0x40
bytes[8] = (bytes[8] & 0x3f) | 0x80
const hex = Array.from(bytes, b =>
b.toString(16).padStart(2, '0')
).join('')
return (
hex.slice(0, 8) +
'-' +
hex.slice(8, 12) +
'-' +
hex.slice(12, 16) +
'-' +
hex.slice(16, 20) +
'-' +
hex.slice(20)
)
}
let d = Date.now()
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
const r = (d + Math.random() * 16) % 16 | 0
d = Math.floor(d / 16)
return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16)
})
}
}
})()
var defaultRelays = [
'wss://relay.damus.io',
'wss://relay.snort.social',
@ -44,13 +82,24 @@ function confirm(message) {
async function hash(string) {
const subtle = globalThis.crypto && globalThis.crypto.subtle
if (subtle && subtle.digest) {
const utf8 = new TextEncoder().encode(string)
const hashBuffer = await crypto.subtle.digest('SHA-256', utf8)
const hashBuffer = await subtle.digest('SHA-256', utf8)
const hashArray = Array.from(new Uint8Array(hashBuffer))
const hashHex = hashArray
.map(bytes => bytes.toString(16).padStart(2, '0'))
.join('')
return hashHex
return hashArray.map(bytes => bytes.toString(16).padStart(2, '0')).join('')
}
// Fallback for non-secure contexts where crypto.subtle is unavailable.
return fallbackHash(string)
}
function fallbackHash(string) {
let hash = 5381
for (let i = 0; i < string.length; i++) {
hash = ((hash << 5) + hash) + string.charCodeAt(i)
}
return (hash >>> 0).toString(16).padStart(8, '0')
}
function isJson(str) {

View file

@ -9,29 +9,26 @@
</div>
</div>
<div class="row q-mb-md q-gutter-sm">
<q-btn
outline
color="primary"
@click="showEditProfileDialog = true"
icon="edit"
>Edit</q-btn
>
<q-btn
outline
color="primary"
icon="qr_code"
@click="showKeysDialog = true"
>
<q-tooltip>Show Keys</q-tooltip>
</q-btn>
<q-btn-dropdown
split
outline
color="primary"
icon="swap_horiz"
label="Switch"
icon="vpn_key"
label="Keys"
>
<q-list>
<q-item clickable v-close-popup @click="showKeysDialog = true">
<q-item-section avatar>
<q-icon name="vpn_key" color="primary"></q-icon>
</q-item-section>
<q-item-section>
<q-item-label>View Keys</q-item-label>
<q-item-label caption
>Show public/private keys</q-item-label
>
</q-item-section>
</q-item>
<q-separator></q-separator>
<q-item-label header>Saved Profiles</q-item-label>
<q-item>
<q-item-section avatar>
@ -69,6 +66,13 @@
</q-item>
</q-list>
</q-btn-dropdown>
<q-btn
outline
color="primary"
@click="showEditProfileDialog = true"
icon="edit"
label="Edit Profile"
></q-btn>
<q-btn-dropdown
split
outline
@ -121,6 +125,15 @@
</q-item>
</q-list>
</q-btn-dropdown>
<q-btn
outline
color="primary"
icon="storefront"
label="Marketplace"
:href="marketClientUrl"
target="_blank"
rel="noopener"
></q-btn>
</div>
</q-card-section>