Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
name: CI
on:
push:
branches:
- main
pull_request:

jobs:
lint:
uses: lnbits/lnbits/.github/workflows/lint.yml@dev
59 changes: 59 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
on:
push:
tags:
- "v[0-9]+.[0-9]+.[0-9]+"

jobs:

release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Create github release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.ref_name }}
run: |
gh release create "$tag" --generate-notes

pullrequest:
needs: [release]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
token: ${{ secrets.EXT_GITHUB }}
repository: lnbits/lnbits-extensions
path: './lnbits-extensions'

- name: setup git user
run: |
git config --global user.name "alan"
git config --global user.email "alan@lnbits.com"

- name: Create pull request in extensions repo
env:
GH_TOKEN: ${{ secrets.EXT_GITHUB }}
repo_name: "${{ github.event.repository.name }}"
tag: "${{ github.ref_name }}"
branch: "update-${{ github.event.repository.name }}-${{ github.ref_name }}"
title: "[UPDATE] ${{ github.event.repository.name }} to ${{ github.ref_name }}"
body: "https://github.com/lnbits/${{ github.event.repository.name }}/releases/${{ github.ref_name }}"
archive: "https://github.com/lnbits/${{ github.event.repository.name }}/archive/refs/tags/${{ github.ref_name }}.zip"
run: |
cd lnbits-extensions
git checkout -b $branch

# if there is another open PR
git pull origin $branch || echo "branch does not exist"

sh util.sh update_extension $repo_name $tag

git add -A
git commit -am "$title"
git push origin $branch

# check if pr exists before creating it
gh config set pager cat
check=$(gh pr list -H $branch | wc -l)
test $check -ne 0 || gh pr create --title "$title" --body "$body" --repo lnbits/lnbits-extensions
32 changes: 32 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
.DS_Store
._*

__pycache__
*.py[cod]
*$py.class
.mypy_cache
.vscode
*-lock.json

*.egg
*.egg-info
.coverage
.pytest_cache
.webassets-cache
htmlcov
test-reports
tests/data/*.sqlite3
node_modules

*.swo
*.swp
*.pyo
*.pyc
*.env
env.dev
.venv/
.ruff_cache/

# Claude Code config
CLAUDE.md
.claude/
14 changes: 14 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
**/.git
**/.svn
**/.hg
**/node_modules

*.yml

**/static/market/*
**/static/js/nostr.bundle.js*


flake.lock

.venv
12 changes: 12 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"semi": false,
"arrowParens": "avoid",
"insertPragma": false,
"printWidth": 80,
"proseWrap": "preserve",
"singleQuote": true,
"trailingComma": "none",
"useTabs": false,
"bracketSameLine": false,
"bracketSpacing": false
}
48 changes: 48 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
all: format check

format: prettier black ruff

# Note: mypy/pyright skipped due to hyphen in directory name "diagon-alley"
check: checkblack checkruff checkprettier

prettier:
uv run ./node_modules/.bin/prettier --write .
pyright:
uv run ./node_modules/.bin/pyright

mypy:
uv run mypy .

black:
uv run black .

ruff:
uv run ruff check . --fix

checkruff:
uv run ruff check .

checkprettier:
uv run ./node_modules/.bin/prettier --check .

checkblack:
uv run black --check .

checkeditorconfig:
editorconfig-checker

test:
PYTHONUNBUFFERED=1 \
DEBUG=true \
uv run pytest
install-pre-commit-hook:
@echo "Installing pre-commit hook to git"
@echo "Uninstall the hook with uv run pre-commit uninstall"
uv run pre-commit install

pre-commit:
uv run pre-commit run --all-files


checkbundle:
@echo "skipping checkbundle"
153 changes: 90 additions & 63 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,87 +1,114 @@
<br/>
# Diagon Alley

<p align="center">
<img src="static/images/diagon-alley.png" width="128" alt="Diagon Alley Logo">
</p>

<p align="center">
<strong>Public Nostr Marketplace Aggregator</strong>
</p>

<p align="center">
<img src="https://i.imgur.com/SuoAxtp.png" width="60%">
An LNbits extension for browsing and discovering products from multiple Nostr merchants.
</p>

---

## Overview

Diagon Alley is a public-facing marketplace aggregator that allows users to browse products from multiple Nostr merchants in one place. It implements the buyer/browser side of the Nostr marketplace protocol, making it easy to discover and purchase products using Lightning Network payments.

![Diagon Alley Marketplace](static/images/screenshot-marketplace.png)

## Features

- **Browse Multiple Merchants** - Discover products from various Nostr vendors in a single interface
- **Import Merchants** - Add merchants by their public key or `naddr` identifier
- **Search & Filter** - Find products across all connected merchants
- **No Account Required** - Browse the marketplace without logging in
- **Lightning Payments** - Pay for products instantly via the Lightning Network

## Nostr Protocol Support

### NIP-15: Nostr Marketplace

Diagon Alley implements [NIP-15](https://github.com/nostr-protocol/nips/blob/master/15.md) for marketplace functionality:

- Product listings (kind 30018)
- Stall/merchant information (kind 30017)
- Direct messages for orders

### NIP-99: Classified Listings (Coming Soon)

Support for [NIP-99](https://github.com/nostr-protocol/nips/blob/master/99.md) classified listings is planned for a future release.

### Gamma Market Spec

Compatible with the [Gamma Market](https://github.com/ArcadeLabsInc/gamma) specification for enhanced marketplace features.

## Installation

### As an LNbits Extension

1. Clone this repository into your LNbits extensions folder:

# Diagon Alley: Decentralised Market-Stall Protocol
Diagon Alley is a decentralised market-stall protocol, that shifts emphasis from the frontend market to the merchants stall. If a frontend market (indexer) gets taken down, merchants just point their stalls elsewhere. Game-theoretically the winner of Diagon Alley is the most forthright, although suggestions on limiting bad behaviour are very welcome.
```bash
cd /path/to/lnbits/lnbits/extensions
ln -s /path/to/diagon-alley diagonalley
```

## Indexers
An indexer is a simple frontend server and GUI that routes product, payment and shipping information between merchant and buyer. Each merchant has products in a *stall*. The stall chooses what products to list with the indexer. An indexer has one endpoint.
2. Restart LNbits

* `/register/<stall_ID>` **POST** The `<stall_ID>` is generated by the stall. the endpoint is for stalls to fetch rating data (0-100%), register products and check the indexer is online.
3. Enable the extension in the LNbits admin panel under **Extensions > Installed**

Body (application/json)<br/>
```{"stall_url": <string>}```
### Routes

Returns 200 OK (application/json)<br/>
```{"shopstatus": <boolean>, "rating": <int>}```
| Route | Description | Auth Required |
| --------------------- | ---------------------- | ----------------- |
| `/diagonalley/` | Extension landing page | Yes (LNbits user) |
| `/diagonalley/market` | Public marketplace | No |

The indexer uses the `<stall_url>` and `<indexer_ID>` for the stall endpoints.
The public marketplace URL can be shared with anyone - no login required.

The indexer may present information from the stalls it has registered in any way it want, either as a web shopping experience or an API or something else. It must show the stall_ID along with each product listing. When the customer clicks "buy" or equivalent it must fetch the invoice from the stall and present it to the customer.
## Development

## Stalls
A stall has a keypair it uses to register itself to indexes and sign invoices. That keypair isn't related to any Lightning Network keypair, it's independent.
### Prerequisites

A stall can choose to list some/all products with an *indexer*. A stall is a small server that has three endpoints.
- Python 3.10+
- LNbits 1.4.0+
- Node.js (for frontend development)

* `/products/<indexer_ID>` **GET** for fetching all products associated with an indexer ID
### Local Development

Returns 200 OK (application/json)<br/>
```
[
{
"product_id": <string>,
"product_name": <string>,
"categories": <list>,
"description": <string>,
"image": <string>,
"price": <int>,
"quantity": <int>
},
...
]
```
```bash
# Clone the repository
git clone https://github.com/BenGWeeks/diagon-alley.git

# Create symbolic link in LNbits
ln -s /path/to/diagon-alley /path/to/lnbits/lnbits/extensions/diagonalley

* `/order/<indexer_ID>` **POST** for placing an order and sending shipping data. Returns a Lightning invoice, `metadata` and `checking_id`.
# Run LNbits
cd /path/to/lnbits
uv run lnbits --port 5000
```

`metadata` is a JSON array encoded as string containing at least two items:
### Code Formatting

```
[
[
"text/plain",
<string> // detailed description of the item that will be shipped and its destination>
],
[
"application/vnd.diagonalley.signature",
<string> // DER-encoded ECDSA secp256k1 signature of the <string> tagged as "text/plain" the with the stall private key
]
]
```
```bash
uvx black .
uvx ruff check --fix .
```

Invoices must contain the [`h` tag (`description_hash`)](https://github.com/lightningnetwork/lightning-rfc/blob/master/11-payment-encoding.md#tagged-fields) set to `sha256(metadata)`.
## Related Projects

This is such that customers can be sure they are paying an invoice originating from the stall and not from a malicious indexer. The information on `metadata` may be displayed in a nice way by the indexer interface, but its validity should be checked independently by the customer, perhaps at an independent website that allows him to copy-and-paste `payment_request`, `metadata` and stall_ID.
- [Nostr Market](https://github.com/lnbits/nostrmarket) - Merchant stall management extension
- [LNbits](https://github.com/lnbits/lnbits) - Lightning Network wallet/accounts system
- [Nostr Protocol](https://github.com/nostr-protocol/nostr) - The Nostr protocol specification

Body (application/json)<br/>
```
{
"product_id": <string>,
"address": <string>,
"shippingzone": <integer>,
"email": <string>,
"quantity": <integer>
}
```
## Contributing

Returns 201 CREATED (application/json)<br/>
```{"metadata": <string>, "payment_request": <string>, "checking_id": <string>}```
Contributions are welcome! Please feel free to submit a Pull Request.

* `/status/<checking_id>` **GET** for checking an order status.
## License

Returns 200 OK (application/json)<br/>
```{"status": "SHIPPED" | "PAID" | "UNKNOWN"}```
MIT License - see [LICENSE](LICENSE) for details.
21 changes: 21 additions & 0 deletions __init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from fastapi import APIRouter
from lnbits.db import Database
from lnbits.helpers import template_renderer

db = Database("ext_diagonalley")

diagonalley_ext: APIRouter = APIRouter(prefix="/diagonalley", tags=["diagonalley"])

diagonalley_static_files = [
{
"path": "/diagonalley/static",
"name": "diagonalley_static",
}
]


def diagonalley_renderer():
return template_renderer(["diagonalley/templates"])


from .views import * # noqa
Loading