Compare commits
No commits in common. "3c29099f3ad475143cc0f3cd2225649215ade5d1" and "e5c39cdbd06f4ccb923681388453a135f09b6048" have entirely different histories.
3c29099f3a
...
e5c39cdbd0
13 changed files with 8 additions and 1354 deletions
|
|
@ -1,213 +0,0 @@
|
|||
# Custom Frontend URL - Simple Redirect Approach
|
||||
|
||||
## Overview
|
||||
|
||||
This is the **simplest** approach to integrate LNbits with a custom frontend. LNbits handles all authentication (login, register, password reset) and then redirects users to your custom frontend URL instead of `/wallet`.
|
||||
|
||||
## How It Works
|
||||
|
||||
### Password Reset Flow
|
||||
|
||||
1. **Admin generates reset link** → User receives: `https://lnbits.com/?reset_key=...`
|
||||
2. **User clicks link** → LNbits shows password reset form
|
||||
3. **User submits new password** → LNbits sets auth cookies
|
||||
4. **LNbits redirects** → `https://myapp.com/` (your custom frontend)
|
||||
5. **Web-app loads** → `checkAuth()` sees valid LNbits cookies → ✅ User is logged in!
|
||||
|
||||
### Login Flow
|
||||
|
||||
1. **User visits** → `https://lnbits.com/`
|
||||
2. **User logs in** → LNbits validates credentials and sets cookies
|
||||
3. **LNbits redirects** → `https://myapp.com/`
|
||||
4. **Web-app loads** → ✅ User is logged in!
|
||||
|
||||
### Register Flow
|
||||
|
||||
1. **User visits** → `https://lnbits.com/`
|
||||
2. **User registers** → LNbits creates account and sets cookies
|
||||
3. **LNbits redirects** → `https://myapp.com/`
|
||||
4. **Web-app loads** → ✅ User is logged in!
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variable
|
||||
|
||||
```bash
|
||||
# In .env or environment
|
||||
export LNBITS_CUSTOM_FRONTEND_URL=https://myapp.com
|
||||
```
|
||||
|
||||
Or configure through LNbits admin UI: **Settings → Operations → Custom Frontend URL**
|
||||
|
||||
### Default Behavior
|
||||
|
||||
- **If not set**: Redirects to `/wallet` (default LNbits behavior)
|
||||
- **If set**: Redirects to your custom frontend URL
|
||||
|
||||
## Implementation
|
||||
|
||||
### Changes Made
|
||||
|
||||
1. **Added setting** (`lnbits/settings.py:282-285`):
|
||||
```python
|
||||
class OpsSettings(LNbitsSettings):
|
||||
lnbits_custom_frontend_url: str | None = Field(
|
||||
default=None,
|
||||
description="Custom frontend URL for post-auth redirects"
|
||||
)
|
||||
```
|
||||
|
||||
2. **Exposed to frontend** (`lnbits/helpers.py:88`):
|
||||
```python
|
||||
window_settings = {
|
||||
# ...
|
||||
"LNBITS_CUSTOM_FRONTEND_URL": settings.lnbits_custom_frontend_url,
|
||||
# ...
|
||||
}
|
||||
```
|
||||
|
||||
3. **Updated redirects** (`lnbits/static/js/index.js`):
|
||||
- `login()` - line 78
|
||||
- `register()` - line 56
|
||||
- `reset()` - line 68
|
||||
- `loginUsr()` - line 88
|
||||
|
||||
All now use:
|
||||
```javascript
|
||||
window.location.href = this.LNBITS_CUSTOM_FRONTEND_URL || '/wallet'
|
||||
```
|
||||
|
||||
## Advantages
|
||||
|
||||
### ✅ Zero Changes to Web-App
|
||||
Your custom frontend doesn't need to:
|
||||
- Parse `?reset_key=` from URLs
|
||||
- Build password reset UI
|
||||
- Handle password reset API calls
|
||||
- Manage error states
|
||||
- Implement validation
|
||||
|
||||
### ✅ Auth Cookies Work Automatically
|
||||
LNbits sets httponly cookies that your web-app automatically sees:
|
||||
- `cookie_access_token`
|
||||
- `is_lnbits_user_authorized`
|
||||
|
||||
Your existing `auth.checkAuth()` will detect these and log the user in.
|
||||
|
||||
### ✅ Simple & Elegant
|
||||
Only 3 files changed in LNbits, zero changes in web-app.
|
||||
|
||||
### ✅ Backwards Compatible
|
||||
Existing LNbits installations continue to work. Setting is optional.
|
||||
|
||||
## Disadvantages
|
||||
|
||||
### ⚠️ Brief Branding Inconsistency
|
||||
Users see LNbits UI briefly during:
|
||||
- Login form
|
||||
- Registration form
|
||||
- Password reset form
|
||||
|
||||
Then get redirected to your branded web-app.
|
||||
|
||||
**This is usually acceptable** for most use cases, especially for password reset which is infrequent.
|
||||
|
||||
## Testing
|
||||
|
||||
1. **Set the environment variable**:
|
||||
```bash
|
||||
export LNBITS_CUSTOM_FRONTEND_URL=http://localhost:5173
|
||||
```
|
||||
|
||||
2. **Restart LNbits**:
|
||||
```bash
|
||||
poetry run lnbits
|
||||
```
|
||||
|
||||
3. **Test Login**:
|
||||
- Visit `http://localhost:5000/`
|
||||
- Log in with credentials
|
||||
- Verify redirect to `http://localhost:5173`
|
||||
- Verify web-app shows you as logged in
|
||||
|
||||
4. **Test Password Reset**:
|
||||
- Admin generates reset link in users panel
|
||||
- User clicks link with `?reset_key=...`
|
||||
- User enters new password
|
||||
- Verify redirect to custom frontend
|
||||
- Verify web-app shows you as logged in
|
||||
|
||||
## Security
|
||||
|
||||
### ✅ Secure
|
||||
- Auth cookies are httponly (can't be accessed by JavaScript)
|
||||
- LNbits handles all auth logic
|
||||
- No sensitive data in URLs except one-time reset keys
|
||||
- Reset keys expire based on `auth_token_expire_minutes`
|
||||
|
||||
### 🔒 HTTPS Required
|
||||
Always use HTTPS for custom frontend URLs in production:
|
||||
```bash
|
||||
export LNBITS_CUSTOM_FRONTEND_URL=https://myapp.com
|
||||
```
|
||||
|
||||
## Migration
|
||||
|
||||
**No database migration required!**
|
||||
|
||||
Settings are stored as JSON in `system_settings` table. New fields are automatically included.
|
||||
|
||||
## Alternative: Full Custom Frontend Approach
|
||||
|
||||
If you need **complete branding consistency** (no LNbits UI shown), you would need to:
|
||||
|
||||
1. Build password reset form in web-app
|
||||
2. Parse `?reset_key=` from URL
|
||||
3. Add API method to call `/api/v1/auth/reset`
|
||||
4. Handle validation, errors, loading states
|
||||
5. Update admin UI to generate links pointing to web-app
|
||||
|
||||
This is **significantly more work** for marginal benefit (users see LNbits UI for ~5 seconds during password reset).
|
||||
|
||||
## Recommendation
|
||||
|
||||
**Use this simple approach** unless you have specific requirements for complete UI consistency. The brief LNbits UI is a small trade-off for the simplicity gained.
|
||||
|
||||
## Related Files
|
||||
|
||||
- `lnbits/settings.py` - Setting definition
|
||||
- `lnbits/helpers.py` - Expose to frontend
|
||||
- `lnbits/static/js/index.js` - Redirect logic
|
||||
|
||||
## Example `.env`
|
||||
|
||||
```bash
|
||||
# LNbits Configuration
|
||||
LNBITS_DATA_FOLDER=./data
|
||||
LNBITS_DATABASE_URL=sqlite:///./data/database.sqlite3
|
||||
|
||||
# Custom Frontend Integration
|
||||
LNBITS_CUSTOM_FRONTEND_URL=https://myapp.com
|
||||
|
||||
# Other settings...
|
||||
```
|
||||
|
||||
## How Web-App Benefits
|
||||
|
||||
Your web-app at `https://myapp.com` can now:
|
||||
|
||||
1. **Receive logged-in users** from LNbits without any code changes
|
||||
2. **Use existing `auth.checkAuth()`** - it just works
|
||||
3. **Focus on your features** - don't rebuild auth UI
|
||||
4. **Trust LNbits security** - it's battle-tested
|
||||
|
||||
The auth cookies LNbits sets are valid for your domain if LNbits is on a subdomain (e.g., `api.myapp.com`) or you're using proper CORS configuration.
|
||||
|
||||
## Future Enhancement
|
||||
|
||||
If you later need the full custom UI approach, all the groundwork is there:
|
||||
- Setting exists and is configurable
|
||||
- Just add the web-app UI components
|
||||
- Update admin panel to generate web-app links
|
||||
|
||||
But start with this simple approach first! 🚀
|
||||
113
extensions.json
113
extensions.json
|
|
@ -1,113 +0,0 @@
|
|||
{
|
||||
"featured": [
|
||||
"satmachineclient",
|
||||
"castle"
|
||||
],
|
||||
"extensions": [
|
||||
{
|
||||
"id": "satmachineadmin",
|
||||
"repo": "https://git.aiolabs.dev/lnbits-exts/satmachineadmin",
|
||||
"name": "Satoshi Machine Admin",
|
||||
"version": "0.0.2",
|
||||
"short_description": "Admin Dashboard for Satoshi Machine",
|
||||
"icon": "https://git.aiolabs.dev/lnbits-exts/satmachineadmin/raw/branch/main/static/image/aio.png",
|
||||
"archive": "https://git.aiolabs.dev/lnbits-exts/satmachineadmin/archive/v0.0.2.zip",
|
||||
"hash": "e46863d3c1133897d9fe9cc4afe149804df25b2aeee1716de4da4332ae9789b1"
|
||||
},
|
||||
{
|
||||
"id": "satmachineclient",
|
||||
"repo": "https://git.aiolabs.dev/lnbits-exts/satmachineclient",
|
||||
"name": "Satoshi Machine Client",
|
||||
"version": "0.0.1",
|
||||
"short_description": "Client Dashboard for Satoshi Machine",
|
||||
"icon": "https://git.aiolabs.dev/lnbits-exts/satmachineclient/raw/branch/main/static/image/aio.png",
|
||||
"archive": "https://git.aiolabs.dev/lnbits-exts/satmachineclient/archive/v0.0.1.zip",
|
||||
"hash": "6d93e1a537ca3565fcf4dc4811497de10660a14639046854c1006b06d9a045e5"
|
||||
},
|
||||
{
|
||||
"id": "events",
|
||||
"repo": "https://git.aiolabs.dev/lnbits-exts/events",
|
||||
"name": "AIO Events",
|
||||
"version": "0.0.1",
|
||||
"short_description": "AIO fork of lnbits-exts/events",
|
||||
"icon": "https://git.aiolabs.dev/lnbits-exts/events/raw/branch/main/static/image/aio.png",
|
||||
"archive": "https://git.aiolabs.dev/lnbits-exts/events/archive/v0.0.1.zip",
|
||||
"hash": "681408f3f5f5b0773f0aa5fd7fbdefcd47ed9190b44409b80d2b119cc5516335"
|
||||
},
|
||||
{
|
||||
"id": "nostrrelay",
|
||||
"repo": "https://git.aiolabs.dev/lnbits-exts/nostrrelay",
|
||||
"name": "AIO Nostrrelay",
|
||||
"version": "0.0.2",
|
||||
"short_description": "AIO fork of lnbits-exts/nostrrelay",
|
||||
"icon": "https://git.aiolabs.dev/lnbits-exts/nostrrelay/raw/branch/main/static/image/aio.png",
|
||||
"archive": "https://git.aiolabs.dev/lnbits-exts/nostrrelay/archive/v0.0.2.zip",
|
||||
"hash": "a46475f076557f5c5a426a9e3cbb680af855b833a9f66ba32b6f36778336302f"
|
||||
},
|
||||
{
|
||||
"id": "nostrclient",
|
||||
"repo": "https://git.aiolabs.dev/lnbits-exts/nostrclient",
|
||||
"name": "AIO nostrclient",
|
||||
"version": "0.0.1",
|
||||
"short_description": "AIO fork of lnbits-exts/nostrclient",
|
||||
"icon": "https://git.aiolabs.dev/lnbits-exts/nostrclient/raw/branch/main/static/image/aio.png",
|
||||
"archive": "https://git.aiolabs.dev/lnbits-exts/nostrclient/archive/v0.0.1.zip",
|
||||
"hash": "c71c6bd2105e299a2ff342fb9d94d619570f28f231a3025f52f29757f9b31d04"
|
||||
},
|
||||
{
|
||||
"id": "nostrmarket",
|
||||
"repo": "https://git.aiolabs.dev/lnbits-exts/nostrmarket",
|
||||
"name": "AIO nostrmarket",
|
||||
"version": "0.0.1",
|
||||
"short_description": "AIO fork of lnbits-exts/nostrmarket",
|
||||
"icon": "https://git.aiolabs.dev/lnbits-exts/nostrmarket/raw/branch/main/static/image/aio.png",
|
||||
"archive": "https://git.aiolabs.dev/lnbits-exts/nostrmarket/archive/v0.0.1.zip",
|
||||
"hash": "5177dfdae62554c75ba8244d5a26e371de6e512a8b6dc7bb6f90e8b05916ed35"
|
||||
},
|
||||
{
|
||||
"id": "castle",
|
||||
"repo": "https://git.aiolabs.dev/lnbits-exts/castle",
|
||||
"name": "Castle",
|
||||
"version": "0.0.1",
|
||||
"short_description": "Castle Accounting",
|
||||
"icon": "https://git.aiolabs.dev/lnbits-exts/castle/raw/branch/main/static/image/castle.png",
|
||||
"archive": "https://git.aiolabs.dev/lnbits-exts/castle/archive/v0.0.1.zip",
|
||||
"hash": "0b152190902e7377bfba9feb4c82998a9400a69413ee5482343f15da8308b89b"
|
||||
},
|
||||
{
|
||||
"id": "castle",
|
||||
"repo": "https://git.aiolabs.dev/lnbits-exts/castle",
|
||||
"name": "Castle",
|
||||
"version": "0.0.2",
|
||||
"short_description": "Castle Accounting",
|
||||
"icon": "https://git.aiolabs.dev/lnbits-exts/castle/raw/branch/main/static/image/castle.png",
|
||||
"archive": "https://git.aiolabs.dev/lnbits-exts/castle/archive/v0.0.2.zip",
|
||||
"hash": "0f742deb4777480a32ad239f6eae42c57583bf7a8ca9bd131903b6bb7282f069"
|
||||
},
|
||||
{
|
||||
"id": "lnurlp",
|
||||
"repo": "https://github.com/lnbits/lnurlp",
|
||||
"name": "Pay Links",
|
||||
"version": "1.1.3",
|
||||
"min_lnbits_version": "1.3.0",
|
||||
"short_description": "Make reusable LNURL pay links",
|
||||
"icon": "https://github.com/lnbits/lnurlp/raw/main/static/image/lnurl-pay.png",
|
||||
"details_link": "https://raw.githubusercontent.com/lnbits/lnurlp/main/config.json",
|
||||
"archive": "https://github.com/lnbits/lnurlp/archive/refs/tags/v1.1.3.zip",
|
||||
"hash": "913b6880fd824e801f05ae50ed6f57817c9a580f1ed1b02b435823d5754e1b98",
|
||||
"max_lnbits_version": "1.4.0"
|
||||
},
|
||||
{
|
||||
"id": "lnurlp",
|
||||
"repo": "https://github.com/lnbits/lnurlp",
|
||||
"name": "Pay Links",
|
||||
"version": "1.2.0",
|
||||
"min_lnbits_version": "1.4.0",
|
||||
"short_description": "Make reusable LNURL pay links",
|
||||
"icon": "https://github.com/lnbits/lnurlp/raw/main/static/image/lnurl-pay.png",
|
||||
"details_link": "https://raw.githubusercontent.com/lnbits/lnurlp/main/config.json",
|
||||
"archive": "https://github.com/lnbits/lnurlp/archive/refs/tags/v1.2.0.zip",
|
||||
"hash": "d3872eb5f65b9e962fc201d6784f94f3fd576284582b980805142a2b3f62d8e8"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,5 +1,3 @@
|
|||
import json
|
||||
import time
|
||||
from pathlib import Path
|
||||
from uuid import uuid4
|
||||
|
||||
|
|
@ -74,7 +72,7 @@ async def create_user_account_no_ckeck(
|
|||
account.id = uuid4().hex
|
||||
|
||||
account = await create_account(account, conn=conn)
|
||||
wallet = await create_wallet(
|
||||
await create_wallet(
|
||||
user_id=account.id,
|
||||
wallet_name=wallet_name or settings.lnbits_default_wallet_name,
|
||||
conn=conn,
|
||||
|
|
@ -88,22 +86,6 @@ async def create_user_account_no_ckeck(
|
|||
except Exception as e:
|
||||
logger.error(f"Error enabeling default extension {ext_id}: {e}")
|
||||
|
||||
# Create default pay link for users with username
|
||||
if account.username and "lnurlp" in user_extensions:
|
||||
try:
|
||||
await _create_default_pay_link(account, wallet)
|
||||
logger.info(f"Created default pay link for user {account.username}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create default pay link for user {account.username}: {e}")
|
||||
|
||||
# Publish Nostr kind 0 metadata event if user has username and Nostr keys
|
||||
if account.username and account.pubkey and account.prvkey:
|
||||
try:
|
||||
await _publish_nostr_metadata_event(account)
|
||||
logger.info(f"Published Nostr metadata event for user {account.username}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to publish Nostr metadata for user {account.username}: {e}")
|
||||
|
||||
user = await get_user_from_account(account, conn=conn)
|
||||
if not user:
|
||||
raise ValueError("Cannot find user for account.")
|
||||
|
|
@ -210,160 +192,3 @@ async def init_admin_settings(super_user: str | None = None) -> SuperSettings:
|
|||
|
||||
editable_settings = EditableSettings.from_dict(settings.dict())
|
||||
return await create_admin_settings(account.id, editable_settings.dict())
|
||||
|
||||
|
||||
async def _create_default_pay_link(account: Account, wallet) -> None:
|
||||
"""Create a default pay link for new users with username (Bitcoinmat receiving address)"""
|
||||
try:
|
||||
# Try dynamic import that works with extensions in different locations
|
||||
import importlib
|
||||
import sys
|
||||
|
||||
# First try the standard import path
|
||||
try:
|
||||
lnurlp_crud = importlib.import_module("lnbits.extensions.lnurlp.crud")
|
||||
lnurlp_models = importlib.import_module("lnbits.extensions.lnurlp.models")
|
||||
except ImportError:
|
||||
# If that fails, try importing from external extensions path
|
||||
# This handles cases where extensions are in /var/lib/lnbits/extensions
|
||||
try:
|
||||
# Add extensions path to sys.path if not already there
|
||||
extensions_path = (
|
||||
settings.lnbits_extensions_path or "/var/lib/lnbits/extensions"
|
||||
)
|
||||
if extensions_path not in sys.path:
|
||||
sys.path.insert(0, extensions_path)
|
||||
|
||||
lnurlp_crud = importlib.import_module("lnurlp.crud")
|
||||
lnurlp_models = importlib.import_module("lnurlp.models")
|
||||
except ImportError as e:
|
||||
logger.warning(f"lnurlp extension not found in any location: {e}")
|
||||
return
|
||||
|
||||
create_pay_link = lnurlp_crud.create_pay_link
|
||||
CreatePayLinkData = lnurlp_models.CreatePayLinkData
|
||||
|
||||
pay_link_data = CreatePayLinkData(
|
||||
description="Bitcoinmat Receiving Address",
|
||||
wallet=wallet.id,
|
||||
# Note default `currency` is satoshis when set as NULL in db
|
||||
min=1, # minimum 1 sat
|
||||
max=500000, # maximum 500,000 sats
|
||||
comment_chars=140,
|
||||
username=account.username, # use the username as lightning address
|
||||
zaps=True,
|
||||
disposable=True,
|
||||
)
|
||||
|
||||
await create_pay_link(pay_link_data)
|
||||
|
||||
logger.info(
|
||||
f"Successfully created default pay link for user {account.username}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create default pay link: {e}")
|
||||
# Don't raise - we don't want user creation to fail if pay link creation fails
|
||||
|
||||
|
||||
async def _publish_nostr_metadata_event(account: Account) -> None:
|
||||
"""Publish a Nostr kind 0 metadata event for a new user"""
|
||||
import coincurve
|
||||
|
||||
from lnbits.utils.nostr import sign_event
|
||||
|
||||
# Create Nostr kind 0 metadata event
|
||||
metadata = {
|
||||
"name": account.username,
|
||||
"display_name": account.username,
|
||||
"about": f"LNbits user: {account.username}",
|
||||
}
|
||||
|
||||
event = {
|
||||
"kind": 0,
|
||||
"created_at": int(time.time()),
|
||||
"tags": [],
|
||||
"content": json.dumps(metadata),
|
||||
}
|
||||
|
||||
# Convert hex private key to coincurve PrivateKey
|
||||
private_key = coincurve.PrivateKey(bytes.fromhex(account.prvkey))
|
||||
|
||||
# Sign the event
|
||||
signed_event = sign_event(event, account.pubkey, private_key)
|
||||
|
||||
# Publish directly to nostrrelay database (hacky but works around WebSocket issues)
|
||||
await _insert_event_into_nostrrelay(signed_event, account.username)
|
||||
|
||||
|
||||
async def _insert_event_into_nostrrelay(event: dict, username: str) -> None:
|
||||
"""Directly insert Nostr event into nostrrelay database (hacky workaround)"""
|
||||
try:
|
||||
import importlib
|
||||
import sys
|
||||
|
||||
# Try to import nostrrelay from various possible locations
|
||||
nostrrelay_crud = None
|
||||
NostrEvent = None
|
||||
try:
|
||||
nostrrelay_crud = importlib.import_module(
|
||||
"lnbits.extensions.nostrrelay.crud"
|
||||
)
|
||||
NostrEvent = importlib.import_module(
|
||||
"lnbits.extensions.nostrrelay.relay.event"
|
||||
).NostrEvent
|
||||
except ImportError:
|
||||
try:
|
||||
# Check if nostrrelay is in external extensions path
|
||||
extensions_path = (
|
||||
settings.lnbits_extensions_path or "/var/lib/lnbits/extensions"
|
||||
)
|
||||
if extensions_path not in sys.path:
|
||||
sys.path.insert(0, extensions_path)
|
||||
nostrrelay_crud = importlib.import_module("nostrrelay.crud")
|
||||
NostrEvent = importlib.import_module(
|
||||
"nostrrelay.relay.event"
|
||||
).NostrEvent
|
||||
except ImportError:
|
||||
# Try from the lnbits-nostrmarket project path
|
||||
nostrmarket_path = "/home/padreug/Projects/lnbits-nostrmarket"
|
||||
if nostrmarket_path not in sys.path:
|
||||
sys.path.insert(0, nostrmarket_path)
|
||||
nostrrelay_crud = importlib.import_module("nostrrelay.crud")
|
||||
NostrEvent = importlib.import_module(
|
||||
"nostrrelay.relay.event"
|
||||
).NostrEvent
|
||||
|
||||
if not nostrrelay_crud or not NostrEvent:
|
||||
logger.warning(
|
||||
"Could not import nostrrelay - skipping direct database insert"
|
||||
)
|
||||
return
|
||||
|
||||
# Use a default relay_id for the proof of concept
|
||||
relay_id = "test1"
|
||||
logger.debug(f"Using relay_id: {relay_id} for nostrrelay event")
|
||||
|
||||
# Create NostrEvent object for nostrrelay
|
||||
nostr_event = NostrEvent(
|
||||
id=event["id"],
|
||||
relay_id=relay_id,
|
||||
publisher=event["pubkey"],
|
||||
pubkey=event["pubkey"],
|
||||
created_at=event["created_at"],
|
||||
kind=event["kind"],
|
||||
tags=event.get("tags", []),
|
||||
content=event["content"],
|
||||
sig=event["sig"],
|
||||
)
|
||||
|
||||
# Insert directly into nostrrelay database
|
||||
await nostrrelay_crud.create_event(nostr_event)
|
||||
|
||||
logger.info(
|
||||
f"Successfully inserted Nostr metadata event for {username} into nostrrelay database"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to insert event into nostrrelay database: {e}")
|
||||
logger.debug(f"Exception details: {type(e).__name__}: {str(e)}")
|
||||
|
|
|
|||
|
|
@ -100,16 +100,6 @@ async def api_list_currencies_available() -> list[str]:
|
|||
return allowed_currencies()
|
||||
|
||||
|
||||
@api_router.get("/api/v1/default-currency")
|
||||
async def api_get_default_currency() -> dict[str, str | None]:
|
||||
"""
|
||||
Get the default accounting currency for this LNbits instance.
|
||||
Returns the configured default, or None if not set.
|
||||
"""
|
||||
default_currency = settings.lnbits_default_accounting_currency
|
||||
return {"default_currency": default_currency}
|
||||
|
||||
|
||||
@api_router.post("/api/v1/conversion")
|
||||
async def api_fiat_as_sats(data: ConversionData):
|
||||
output = {}
|
||||
|
|
|
|||
|
|
@ -96,9 +96,6 @@ async def api_create_user(data: CreateUser) -> CreateUser:
|
|||
data.extra = data.extra or UserExtra()
|
||||
data.extra.provider = data.extra.provider or "lnbits"
|
||||
|
||||
if data.pubkey:
|
||||
data.pubkey = normalize_public_key(data.pubkey)
|
||||
|
||||
account = Account(
|
||||
id=uuid4().hex,
|
||||
username=data.username,
|
||||
|
|
|
|||
|
|
@ -87,7 +87,6 @@ def template_renderer(additional_folders: list | None = None) -> Jinja2Templates
|
|||
"LNBITS_CUSTOM_IMAGE": settings.lnbits_custom_image,
|
||||
"LNBITS_CUSTOM_BADGE": settings.lnbits_custom_badge,
|
||||
"LNBITS_CUSTOM_BADGE_COLOR": settings.lnbits_custom_badge_color,
|
||||
"LNBITS_CUSTOM_FRONTEND_URL": settings.lnbits_custom_frontend_url,
|
||||
"LNBITS_EXTENSIONS_DEACTIVATE_ALL": settings.lnbits_extensions_deactivate_all,
|
||||
"LNBITS_NEW_ACCOUNTS_ALLOWED": settings.new_accounts_allowed,
|
||||
"LNBITS_NODE_UI": settings.lnbits_node_ui and settings.has_nodemanager,
|
||||
|
|
|
|||
|
|
@ -960,10 +960,6 @@ class EnvSettings(LNbitsSettings):
|
|||
lnbits_title: str = Field(default="LNbits API")
|
||||
lnbits_path: str = Field(default=".")
|
||||
lnbits_extensions_path: str = Field(default="lnbits")
|
||||
lnbits_custom_frontend_url: str | None = Field(
|
||||
default=None,
|
||||
description="Custom frontend URL for redirects after auth (e.g., https://myapp.com). If not set, redirects to /wallet. This is read-only and must be set via environment variable."
|
||||
)
|
||||
super_user: str = Field(default="")
|
||||
auth_secret_key: str = Field(default="")
|
||||
version: str = Field(default="0.0.0")
|
||||
|
|
|
|||
|
|
@ -79,12 +79,7 @@ window.PageHome = {
|
|||
this.password,
|
||||
this.passwordRepeat
|
||||
)
|
||||
// Redirect to custom frontend URL if configured, otherwise use router
|
||||
if (this.LNBITS_CUSTOM_FRONTEND_URL) {
|
||||
window.location.href = this.LNBITS_CUSTOM_FRONTEND_URL
|
||||
} else {
|
||||
this.refreshAuthUser()
|
||||
}
|
||||
this.refreshAuthUser()
|
||||
} catch (e) {
|
||||
LNbits.utils.notifyApiError(e)
|
||||
}
|
||||
|
|
@ -96,12 +91,7 @@ window.PageHome = {
|
|||
this.password,
|
||||
this.passwordRepeat
|
||||
)
|
||||
// Redirect to custom frontend URL if configured, otherwise use router
|
||||
if (this.LNBITS_CUSTOM_FRONTEND_URL) {
|
||||
window.location.href = this.LNBITS_CUSTOM_FRONTEND_URL
|
||||
} else {
|
||||
this.refreshAuthUser()
|
||||
}
|
||||
this.refreshAuthUser()
|
||||
} catch (e) {
|
||||
LNbits.utils.notifyApiError(e)
|
||||
}
|
||||
|
|
@ -109,12 +99,7 @@ window.PageHome = {
|
|||
async login() {
|
||||
try {
|
||||
await LNbits.api.login(this.username, this.password)
|
||||
// Redirect to custom frontend URL if configured, otherwise use router
|
||||
if (this.LNBITS_CUSTOM_FRONTEND_URL) {
|
||||
window.location.href = this.LNBITS_CUSTOM_FRONTEND_URL
|
||||
} else {
|
||||
this.refreshAuthUser()
|
||||
}
|
||||
this.refreshAuthUser()
|
||||
} catch (e) {
|
||||
LNbits.utils.notifyApiError(e)
|
||||
}
|
||||
|
|
@ -122,13 +107,7 @@ window.PageHome = {
|
|||
async loginUsr() {
|
||||
try {
|
||||
await LNbits.api.loginUsr(this.usr)
|
||||
this.usr = ''
|
||||
// Redirect to custom frontend URL if configured, otherwise use router
|
||||
if (this.LNBITS_CUSTOM_FRONTEND_URL) {
|
||||
window.location.href = this.LNBITS_CUSTOM_FRONTEND_URL
|
||||
} else {
|
||||
this.refreshAuthUser()
|
||||
}
|
||||
this.refreshAuthUser()
|
||||
} catch (e) {
|
||||
console.warn(e)
|
||||
LNbits.utils.notifyApiError(e)
|
||||
|
|
@ -159,28 +138,14 @@ window.PageHome = {
|
|||
}
|
||||
},
|
||||
created() {
|
||||
if (this.g.isUserAuthorized) {
|
||||
return this.refreshAuthUser()
|
||||
}
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
|
||||
// Check for reset_key FIRST - password reset should work even if user is logged in
|
||||
this.reset_key = urlParams.get('reset_key')
|
||||
if (this.reset_key) {
|
||||
this.authAction = 'reset'
|
||||
// Clear existing auth cookies to allow password reset to work
|
||||
this.$q.cookies.remove('is_lnbits_user_authorized')
|
||||
this.$q.cookies.remove('cookie_access_token')
|
||||
return // Don't redirect to /wallet
|
||||
}
|
||||
|
||||
// Redirect authorized users
|
||||
if (this.g.isUserAuthorized) {
|
||||
// Redirect to custom frontend URL if configured, otherwise use router
|
||||
if (this.LNBITS_CUSTOM_FRONTEND_URL) {
|
||||
window.location.href = this.LNBITS_CUSTOM_FRONTEND_URL
|
||||
} else {
|
||||
return this.refreshAuthUser()
|
||||
}
|
||||
}
|
||||
|
||||
// check if lightning parameters are present in the URL
|
||||
if (urlParams.has('lightning')) {
|
||||
this.lnurl = urlParams.get('lightning')
|
||||
|
|
|
|||
|
|
@ -1,88 +0,0 @@
|
|||
# LNBits Auto-Credit Changes
|
||||
|
||||
## Overview
|
||||
Modified LNBits server to automatically credit new accounts with 1 million satoshis (1,000,000 sats) when they are created.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### 1. Modified `lnbits/core/services/users.py`
|
||||
|
||||
**Added imports:**
|
||||
- `get_wallet` from `..crud`
|
||||
- `update_wallet_balance` from `.payments`
|
||||
|
||||
**Modified `create_user_account_no_ckeck` function:**
|
||||
- Changed `create_wallet` call to capture the returned wallet object
|
||||
- Added automatic credit of 1,000,000 sats after wallet creation
|
||||
- Added error handling and logging for the credit operation
|
||||
|
||||
**Code changes:**
|
||||
```python
|
||||
# Before:
|
||||
await create_wallet(
|
||||
user_id=account.id,
|
||||
wallet_name=wallet_name or settings.lnbits_default_wallet_name,
|
||||
)
|
||||
|
||||
# After:
|
||||
wallet = await create_wallet(
|
||||
user_id=account.id,
|
||||
wallet_name=wallet_name or settings.lnbits_default_wallet_name,
|
||||
)
|
||||
|
||||
# Credit new account with 1 million satoshis
|
||||
try:
|
||||
await update_wallet_balance(wallet, 1_000_000)
|
||||
logger.info(f"Credited new account {account.id} with 1,000,000 sats")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to credit new account {account.id} with 1,000,000 sats: {e}")
|
||||
```
|
||||
|
||||
### 2. Updated Tests in `tests/api/test_auth.py`
|
||||
|
||||
**Modified test functions:**
|
||||
- `test_register_ok`: Added balance verification for regular user registration
|
||||
- `test_register_nostr_ok`: Added balance verification for Nostr authentication
|
||||
|
||||
**Added assertions:**
|
||||
```python
|
||||
# Check that the wallet has 1 million satoshis
|
||||
wallet = user.wallets[0]
|
||||
assert wallet.balance == 1_000_000, f"Expected 1,000,000 sats balance, got {wallet.balance} sats"
|
||||
```
|
||||
|
||||
## Affected Account Creation Paths
|
||||
|
||||
The automatic credit will be applied to all new accounts created through:
|
||||
|
||||
1. **Regular user registration** (`/api/v1/auth/register`)
|
||||
2. **Nostr authentication** (`/api/v1/auth/nostr`)
|
||||
3. **SSO login** (when new account is created)
|
||||
4. **API account creation** (`/api/v1/account`)
|
||||
5. **Admin user creation** (via admin interface)
|
||||
|
||||
## Excluded Paths
|
||||
|
||||
- **Superuser/Admin account creation** (`init_admin_settings`): This function creates the admin account directly and bypasses the user creation flow, so it won't receive the automatic credit.
|
||||
|
||||
## Testing
|
||||
|
||||
To test the changes:
|
||||
|
||||
1. Install dependencies: `poetry install`
|
||||
2. Run the modified tests: `poetry run pytest tests/api/test_auth.py::test_register_ok -v`
|
||||
3. Run Nostr test: `poetry run pytest tests/api/test_auth.py::test_register_nostr_ok -v`
|
||||
|
||||
## Logging
|
||||
|
||||
The system will log:
|
||||
- Success: `"Credited new account {account.id} with 1,000,000 sats"`
|
||||
- Failure: `"Failed to credit new account {account.id} with 1,000,000 sats: {error}"`
|
||||
|
||||
## Notes
|
||||
|
||||
- The credit uses the existing `update_wallet_balance` function which creates an internal payment record
|
||||
- The credit is applied after wallet creation but before user extensions are set up
|
||||
- Error handling ensures that account creation continues even if the credit fails
|
||||
- The credit amount is hardcoded to 1,000,000 sats (1MM sats)
|
||||
|
||||
|
|
@ -1,460 +0,0 @@
|
|||
# LNbits WebSocket Implementation Guide
|
||||
|
||||
## Overview
|
||||
|
||||
LNbits provides real-time WebSocket connections for monitoring wallet status, payment confirmations, and transaction updates. This guide covers how to implement and use these WebSocket connections in your applications.
|
||||
|
||||
## WebSocket Endpoints
|
||||
|
||||
### 1. Payment Monitoring WebSocket
|
||||
- **URL**: `ws://localhost:5006/api/v1/ws/{wallet_inkey}`
|
||||
- **HTTPS**: `wss://your-domain.com/api/v1/ws/{wallet_inkey}`
|
||||
- **Purpose**: Real-time payment notifications and wallet updates
|
||||
|
||||
### 2. Generic WebSocket Communication
|
||||
- **URL**: `ws://localhost:5006/api/v1/ws/{item_id}`
|
||||
- **Purpose**: Custom real-time communication channels
|
||||
|
||||
## Client-Side Implementation
|
||||
|
||||
### JavaScript/Browser Implementation
|
||||
|
||||
#### Basic WebSocket Connection
|
||||
```javascript
|
||||
// Construct WebSocket URL
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||
const websocketUrl = `${protocol}//${window.location.host}/api/v1/ws`
|
||||
|
||||
// Connect to payment monitoring
|
||||
const ws = new WebSocket(`${websocketUrl}/${wallet.inkey}`)
|
||||
|
||||
// Handle incoming messages
|
||||
ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data)
|
||||
console.log('Received:', data)
|
||||
|
||||
if (data.payment) {
|
||||
handlePaymentReceived(data.payment)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle connection events
|
||||
ws.onopen = () => {
|
||||
console.log('WebSocket connected')
|
||||
}
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log('WebSocket disconnected')
|
||||
}
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('WebSocket error:', error)
|
||||
}
|
||||
```
|
||||
|
||||
#### Using LNbits Built-in Event System
|
||||
```javascript
|
||||
// Using the built-in LNbits event system
|
||||
LNbits.events.onInvoicePaid(wallet, (data) => {
|
||||
if (data.payment) {
|
||||
console.log('Payment confirmed:', data.payment)
|
||||
|
||||
// Update UI
|
||||
updateWalletBalance(data.payment.amount)
|
||||
showPaymentNotification(data.payment)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
#### Vue.js Implementation Example
|
||||
```javascript
|
||||
// Vue component method
|
||||
initWebSocket() {
|
||||
const protocol = location.protocol === 'http:' ? 'ws://' : 'wss://'
|
||||
const wsUrl = `${protocol}${document.domain}:${location.port}/api/v1/ws/${this.wallet.inkey}`
|
||||
|
||||
this.ws = new WebSocket(wsUrl)
|
||||
|
||||
this.ws.addEventListener('message', async ({ data }) => {
|
||||
const response = JSON.parse(data.toString())
|
||||
|
||||
if (response.payment) {
|
||||
// Handle payment update
|
||||
await this.handlePaymentUpdate(response.payment)
|
||||
}
|
||||
})
|
||||
|
||||
this.ws.addEventListener('open', () => {
|
||||
this.connectionStatus = 'connected'
|
||||
})
|
||||
|
||||
this.ws.addEventListener('close', () => {
|
||||
this.connectionStatus = 'disconnected'
|
||||
// Implement reconnection logic
|
||||
setTimeout(() => this.initWebSocket(), 5000)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Python Client Implementation
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
import websockets
|
||||
import json
|
||||
|
||||
async def listen_to_wallet(wallet_inkey, base_url="ws://localhost:5006"):
|
||||
uri = f"{base_url}/api/v1/ws/{wallet_inkey}"
|
||||
|
||||
try:
|
||||
async with websockets.connect(uri) as websocket:
|
||||
print(f"Connected to WebSocket: {uri}")
|
||||
|
||||
async for message in websocket:
|
||||
data = json.loads(message)
|
||||
|
||||
if 'payment' in data:
|
||||
payment = data['payment']
|
||||
print(f"Payment received: {payment['amount']} sat")
|
||||
print(f"Payment hash: {payment['payment_hash']}")
|
||||
|
||||
# Process payment
|
||||
await handle_payment_received(payment)
|
||||
|
||||
except websockets.exceptions.ConnectionClosed:
|
||||
print("WebSocket connection closed")
|
||||
except Exception as e:
|
||||
print(f"WebSocket error: {e}")
|
||||
|
||||
async def handle_payment_received(payment):
|
||||
"""Process incoming payment"""
|
||||
# Update database
|
||||
# Send notifications
|
||||
# Update application state
|
||||
pass
|
||||
|
||||
# Run the WebSocket listener
|
||||
if __name__ == "__main__":
|
||||
wallet_inkey = "your_wallet_inkey_here"
|
||||
asyncio.run(listen_to_wallet(wallet_inkey))
|
||||
```
|
||||
|
||||
### Node.js Client Implementation
|
||||
|
||||
```javascript
|
||||
const WebSocket = require('ws')
|
||||
|
||||
class LNbitsWebSocketClient {
|
||||
constructor(walletInkey, baseUrl = 'ws://localhost:5006') {
|
||||
this.walletInkey = walletInkey
|
||||
this.baseUrl = baseUrl
|
||||
this.ws = null
|
||||
this.reconnectInterval = 5000
|
||||
}
|
||||
|
||||
connect() {
|
||||
const url = `${this.baseUrl}/api/v1/ws/${this.walletInkey}`
|
||||
this.ws = new WebSocket(url)
|
||||
|
||||
this.ws.on('open', () => {
|
||||
console.log(`Connected to LNbits WebSocket: ${url}`)
|
||||
})
|
||||
|
||||
this.ws.on('message', (data) => {
|
||||
try {
|
||||
const message = JSON.parse(data.toString())
|
||||
this.handleMessage(message)
|
||||
} catch (error) {
|
||||
console.error('Error parsing WebSocket message:', error)
|
||||
}
|
||||
})
|
||||
|
||||
this.ws.on('close', () => {
|
||||
console.log('WebSocket connection closed. Reconnecting...')
|
||||
setTimeout(() => this.connect(), this.reconnectInterval)
|
||||
})
|
||||
|
||||
this.ws.on('error', (error) => {
|
||||
console.error('WebSocket error:', error)
|
||||
})
|
||||
}
|
||||
|
||||
handleMessage(message) {
|
||||
if (message.payment) {
|
||||
console.log('Payment received:', message.payment)
|
||||
this.onPaymentReceived(message.payment)
|
||||
}
|
||||
}
|
||||
|
||||
onPaymentReceived(payment) {
|
||||
// Override this method to handle payments
|
||||
console.log(`Received ${payment.amount} sat`)
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
if (this.ws) {
|
||||
this.ws.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
const client = new LNbitsWebSocketClient('your_wallet_inkey_here')
|
||||
client.onPaymentReceived = (payment) => {
|
||||
// Custom payment handling
|
||||
console.log(`Processing payment: ${payment.payment_hash}`)
|
||||
}
|
||||
client.connect()
|
||||
```
|
||||
|
||||
## Server-Side Implementation (LNbits Extensions)
|
||||
|
||||
### Sending WebSocket Updates
|
||||
|
||||
```python
|
||||
from lnbits.core.services import websocket_manager
|
||||
|
||||
async def notify_wallet_update(wallet_inkey: str, payment_data: dict):
|
||||
"""Send payment update to connected WebSocket clients"""
|
||||
message = {
|
||||
"payment": payment_data,
|
||||
"timestamp": int(time.time())
|
||||
}
|
||||
|
||||
await websocket_manager.send(wallet_inkey, json.dumps(message))
|
||||
|
||||
# Example usage in payment processing
|
||||
async def process_payment_confirmation(payment_hash: str):
|
||||
payment = await get_payment(payment_hash)
|
||||
|
||||
if payment.wallet:
|
||||
await notify_wallet_update(payment.wallet, {
|
||||
"payment_hash": payment.payment_hash,
|
||||
"amount": payment.amount,
|
||||
"memo": payment.memo,
|
||||
"status": "confirmed"
|
||||
})
|
||||
```
|
||||
|
||||
### HTTP Endpoints for WebSocket Updates
|
||||
|
||||
```python
|
||||
# Send data via GET request
|
||||
@router.get("/notify/{wallet_inkey}/{message}")
|
||||
async def notify_wallet_get(wallet_inkey: str, message: str):
|
||||
await websocket_manager.send(wallet_inkey, message)
|
||||
return {"sent": True, "message": message}
|
||||
|
||||
# Send data via POST request
|
||||
@router.post("/notify/{wallet_inkey}")
|
||||
async def notify_wallet_post(wallet_inkey: str, data: str):
|
||||
await websocket_manager.send(wallet_inkey, data)
|
||||
return {"sent": True, "data": data}
|
||||
```
|
||||
|
||||
## Message Format
|
||||
|
||||
### Payment Notification Message
|
||||
```json
|
||||
{
|
||||
"payment": {
|
||||
"payment_hash": "abc123...",
|
||||
"amount": 1000,
|
||||
"memo": "Test payment",
|
||||
"status": "confirmed",
|
||||
"timestamp": 1640995200,
|
||||
"fee": 1,
|
||||
"wallet_id": "wallet_uuid"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Message Format
|
||||
```json
|
||||
{
|
||||
"type": "balance_update",
|
||||
"wallet_id": "wallet_uuid",
|
||||
"balance": 50000,
|
||||
"timestamp": 1640995200
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Connection Management
|
||||
- Implement automatic reconnection logic
|
||||
- Handle connection timeouts gracefully
|
||||
- Use exponential backoff for reconnection attempts
|
||||
|
||||
### 2. Error Handling
|
||||
```javascript
|
||||
class WebSocketManager {
|
||||
constructor(walletInkey) {
|
||||
this.walletInkey = walletInkey
|
||||
this.maxReconnectAttempts = 10
|
||||
this.reconnectAttempts = 0
|
||||
this.reconnectDelay = 1000
|
||||
}
|
||||
|
||||
connect() {
|
||||
try {
|
||||
this.ws = new WebSocket(this.getWebSocketUrl())
|
||||
this.setupEventHandlers()
|
||||
} catch (error) {
|
||||
this.handleConnectionError(error)
|
||||
}
|
||||
}
|
||||
|
||||
handleConnectionError(error) {
|
||||
console.error('WebSocket connection error:', error)
|
||||
|
||||
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
||||
this.reconnectAttempts++
|
||||
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1)
|
||||
|
||||
setTimeout(() => {
|
||||
console.log(`Reconnection attempt ${this.reconnectAttempts}`)
|
||||
this.connect()
|
||||
}, delay)
|
||||
} else {
|
||||
console.error('Max reconnection attempts reached')
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Message Validation
|
||||
```javascript
|
||||
function validatePaymentMessage(data) {
|
||||
if (!data.payment) return false
|
||||
|
||||
const payment = data.payment
|
||||
return (
|
||||
typeof payment.payment_hash === 'string' &&
|
||||
typeof payment.amount === 'number' &&
|
||||
payment.amount > 0 &&
|
||||
['pending', 'confirmed', 'failed'].includes(payment.status)
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Security Considerations
|
||||
- Use HTTPS/WSS in production
|
||||
- Validate wallet permissions before connecting
|
||||
- Implement rate limiting for WebSocket connections
|
||||
- Never expose admin keys through WebSocket messages
|
||||
|
||||
## Testing WebSocket Connections
|
||||
|
||||
### Using wscat (Command Line Tool)
|
||||
```bash
|
||||
# Install wscat
|
||||
npm install -g wscat
|
||||
|
||||
# Connect to WebSocket
|
||||
wscat -c ws://localhost:5006/api/v1/ws/your_wallet_inkey
|
||||
|
||||
# Test with SSL
|
||||
wscat -c wss://your-domain.com/api/v1/ws/your_wallet_inkey
|
||||
```
|
||||
|
||||
### Browser Console Testing
|
||||
```javascript
|
||||
// Open browser console and run:
|
||||
const ws = new WebSocket('ws://localhost:5006/api/v1/ws/your_wallet_inkey')
|
||||
ws.onmessage = (e) => console.log('Received:', JSON.parse(e.data))
|
||||
ws.onopen = () => console.log('Connected')
|
||||
ws.onclose = () => console.log('Disconnected')
|
||||
```
|
||||
|
||||
### Sending Test Messages
|
||||
```bash
|
||||
# Using curl to trigger WebSocket message
|
||||
curl "http://localhost:5006/api/v1/ws/your_wallet_inkey/test_message"
|
||||
|
||||
# Using POST
|
||||
curl -X POST "http://localhost:5006/api/v1/ws/your_wallet_inkey" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '"test message data"'
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Connection Refused**
|
||||
- Verify LNbits server is running on correct port
|
||||
- Check firewall settings
|
||||
- Ensure WebSocket endpoint is enabled
|
||||
|
||||
2. **Authentication Errors**
|
||||
- Verify wallet inkey is correct
|
||||
- Check wallet permissions
|
||||
- Ensure wallet exists and is active
|
||||
|
||||
3. **Message Not Received**
|
||||
- Check WebSocket connection status
|
||||
- Verify message format
|
||||
- Test with browser dev tools
|
||||
|
||||
4. **Frequent Disconnections**
|
||||
- Implement proper reconnection logic
|
||||
- Check network stability
|
||||
- Monitor server logs for errors
|
||||
|
||||
### Debug Logging
|
||||
```javascript
|
||||
// Enable verbose WebSocket logging
|
||||
const ws = new WebSocket(wsUrl)
|
||||
ws.addEventListener('open', (event) => {
|
||||
console.log('WebSocket opened:', event)
|
||||
})
|
||||
ws.addEventListener('close', (event) => {
|
||||
console.log('WebSocket closed:', event.code, event.reason)
|
||||
})
|
||||
ws.addEventListener('error', (event) => {
|
||||
console.error('WebSocket error:', event)
|
||||
})
|
||||
```
|
||||
|
||||
## Production Deployment
|
||||
|
||||
### Nginx Configuration
|
||||
```nginx
|
||||
location /api/v1/ws/ {
|
||||
proxy_pass http://localhost:5006;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 86400;
|
||||
}
|
||||
```
|
||||
|
||||
### SSL/TLS Configuration
|
||||
```nginx
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name your-domain.com;
|
||||
|
||||
ssl_certificate /path/to/cert.pem;
|
||||
ssl_certificate_key /path/to/key.pem;
|
||||
|
||||
location /api/v1/ws/ {
|
||||
proxy_pass http://localhost:5006;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
# ... other headers
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Conclusion
|
||||
|
||||
LNbits WebSocket implementation provides a robust foundation for real-time wallet monitoring and payment processing. By following this guide, you can implement reliable WebSocket connections that enhance user experience with instant payment notifications and live wallet updates.
|
||||
|
||||
Remember to implement proper error handling, reconnection logic, and security measures when deploying to production environments.
|
||||
Binary file not shown.
|
|
@ -1,139 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Bulk publish Nostr profiles from CSV file
|
||||
Usage: python publish_profiles_from_csv.py <csv_file> <relay_url>
|
||||
"""
|
||||
import sys
|
||||
import csv
|
||||
import json
|
||||
import time
|
||||
import hashlib
|
||||
import secp256k1
|
||||
from websocket import create_connection
|
||||
|
||||
def sign_event(event, public_key_hex, private_key):
|
||||
"""Sign a Nostr event"""
|
||||
# Create the signature data
|
||||
signature_data = json.dumps([
|
||||
0,
|
||||
public_key_hex,
|
||||
event["created_at"],
|
||||
event["kind"],
|
||||
event["tags"],
|
||||
event["content"]
|
||||
], separators=(',', ':'), ensure_ascii=False)
|
||||
|
||||
# Calculate event ID
|
||||
event_id = hashlib.sha256(signature_data.encode()).hexdigest()
|
||||
event["id"] = event_id
|
||||
event["pubkey"] = public_key_hex
|
||||
|
||||
# Sign the event
|
||||
signature = private_key.schnorr_sign(bytes.fromhex(event_id), None, raw=True).hex()
|
||||
event["sig"] = signature
|
||||
|
||||
return event
|
||||
|
||||
def publish_profile_metadata(private_key_hex, profile_name, relay_url):
|
||||
"""Publish a Nostr kind 0 metadata event"""
|
||||
try:
|
||||
# Convert hex private key to secp256k1 PrivateKey and get public key
|
||||
private_key = secp256k1.PrivateKey(bytes.fromhex(private_key_hex))
|
||||
public_key_hex = private_key.pubkey.serialize()[1:].hex() # Remove the 0x02 prefix
|
||||
|
||||
print(f"Publishing profile for: {profile_name}")
|
||||
print(f" Public key: {public_key_hex}")
|
||||
|
||||
# Create Nostr kind 0 metadata event
|
||||
metadata = {
|
||||
"name": profile_name,
|
||||
"display_name": profile_name,
|
||||
"about": f"Profile for {profile_name}"
|
||||
}
|
||||
|
||||
event = {
|
||||
"kind": 0,
|
||||
"created_at": int(time.time()),
|
||||
"tags": [],
|
||||
"content": json.dumps(metadata, separators=(',', ':'))
|
||||
}
|
||||
|
||||
# Sign the event
|
||||
signed_event = sign_event(event, public_key_hex, private_key)
|
||||
|
||||
# Connect to relay and publish
|
||||
ws = create_connection(relay_url, timeout=15)
|
||||
|
||||
# Send the event
|
||||
event_message = f'["EVENT",{json.dumps(signed_event)}]'
|
||||
ws.send(event_message)
|
||||
|
||||
# Wait for response
|
||||
try:
|
||||
response = ws.recv()
|
||||
print(f" ✅ Published successfully: {response}")
|
||||
except Exception as e:
|
||||
print(f" ⚠️ No immediate response: {e}")
|
||||
|
||||
# Close connection
|
||||
ws.close()
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f" ❌ Failed to publish: {e}")
|
||||
return False
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 3:
|
||||
print("Usage: python publish_profiles_from_csv.py <csv_file> <relay_url>")
|
||||
print("Example: python publish_profiles_from_csv.py publish-these.csv wss://relay.example.com")
|
||||
sys.exit(1)
|
||||
|
||||
csv_file = sys.argv[1]
|
||||
relay_url = sys.argv[2]
|
||||
|
||||
print(f"Publishing profiles from {csv_file} to {relay_url}")
|
||||
print("=" * 60)
|
||||
|
||||
published_count = 0
|
||||
failed_count = 0
|
||||
|
||||
try:
|
||||
with open(csv_file, 'r') as file:
|
||||
csv_reader = csv.DictReader(file)
|
||||
|
||||
for row_num, row in enumerate(csv_reader, start=2): # start=2 because header is line 1
|
||||
username = row['username'].strip()
|
||||
private_key_hex = row['prvkey'].strip()
|
||||
|
||||
if not username or not private_key_hex:
|
||||
print(f"Row {row_num}: Skipping empty row")
|
||||
continue
|
||||
|
||||
print(f"\nRow {row_num}: Processing {username}")
|
||||
|
||||
success = publish_profile_metadata(private_key_hex, username, relay_url)
|
||||
|
||||
if success:
|
||||
published_count += 1
|
||||
else:
|
||||
failed_count += 1
|
||||
|
||||
# Small delay between publishes to be nice to the relay
|
||||
time.sleep(1)
|
||||
|
||||
except FileNotFoundError:
|
||||
print(f"❌ File not found: {csv_file}")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"❌ Error processing CSV: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print(f"Publishing complete!")
|
||||
print(f"✅ Successfully published: {published_count}")
|
||||
print(f"❌ Failed: {failed_count}")
|
||||
print(f"📊 Total processed: {published_count + failed_count}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Manual Nostr profile metadata publisher
|
||||
Usage: python test_nostr_connection.py <private_key_hex> <profile_name> <relay_url>
|
||||
"""
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
import hashlib
|
||||
import secp256k1
|
||||
from websocket import create_connection
|
||||
|
||||
def sign_event(event, public_key_hex, private_key):
|
||||
"""Sign a Nostr event"""
|
||||
# Create the signature data
|
||||
signature_data = json.dumps([
|
||||
0,
|
||||
public_key_hex,
|
||||
event["created_at"],
|
||||
event["kind"],
|
||||
event["tags"],
|
||||
event["content"]
|
||||
], separators=(',', ':'), ensure_ascii=False)
|
||||
|
||||
# Calculate event ID
|
||||
event_id = hashlib.sha256(signature_data.encode()).hexdigest()
|
||||
event["id"] = event_id
|
||||
event["pubkey"] = public_key_hex
|
||||
|
||||
# Sign the event
|
||||
signature = private_key.schnorr_sign(bytes.fromhex(event_id), None, raw=True).hex()
|
||||
event["sig"] = signature
|
||||
|
||||
return event
|
||||
|
||||
def publish_profile_metadata(private_key_hex, profile_name, relay_url):
|
||||
"""Publish a Nostr kind 0 metadata event"""
|
||||
try:
|
||||
# Convert hex private key to secp256k1 PrivateKey and get public key
|
||||
private_key = secp256k1.PrivateKey(bytes.fromhex(private_key_hex))
|
||||
public_key_hex = private_key.pubkey.serialize()[1:].hex() # Remove the 0x02 prefix
|
||||
|
||||
print(f"Private key: {private_key_hex}")
|
||||
print(f"Public key: {public_key_hex}")
|
||||
print(f"Profile name: {profile_name}")
|
||||
print(f"Relay URL: {relay_url}")
|
||||
print()
|
||||
|
||||
# Create Nostr kind 0 metadata event
|
||||
metadata = {
|
||||
"name": profile_name,
|
||||
"display_name": profile_name,
|
||||
"about": f"Manual profile update for {profile_name}"
|
||||
}
|
||||
|
||||
event = {
|
||||
"kind": 0,
|
||||
"created_at": int(time.time()),
|
||||
"tags": [],
|
||||
"content": json.dumps(metadata, separators=(',', ':'))
|
||||
}
|
||||
|
||||
# Sign the event
|
||||
signed_event = sign_event(event, public_key_hex, private_key)
|
||||
|
||||
print(f"Signed event: {json.dumps(signed_event, indent=2)}")
|
||||
print()
|
||||
|
||||
# Connect to relay and publish
|
||||
print(f"Connecting to relay: {relay_url}")
|
||||
ws = create_connection(relay_url, timeout=15)
|
||||
print("✅ Connected successfully!")
|
||||
|
||||
# Send the event
|
||||
event_message = f'["EVENT",{json.dumps(signed_event)}]'
|
||||
print(f"Sending EVENT: {event_message}")
|
||||
ws.send(event_message)
|
||||
|
||||
# Wait for response
|
||||
try:
|
||||
response = ws.recv()
|
||||
print(f"✅ Relay response: {response}")
|
||||
except Exception as e:
|
||||
print(f"⚠️ No immediate response: {e}")
|
||||
|
||||
# Close connection
|
||||
ws.close()
|
||||
print("✅ Connection closed successfully")
|
||||
print(f"Profile metadata for '{profile_name}' published successfully!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to publish profile: {e}")
|
||||
raise
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 4:
|
||||
print("Usage: python test_nostr_connection.py <private_key_hex> <profile_name> <relay_url>")
|
||||
print("Example: python test_nostr_connection.py abc123... 'My Name' wss://relay.example.com")
|
||||
sys.exit(1)
|
||||
|
||||
private_key_hex = sys.argv[1]
|
||||
profile_name = sys.argv[2]
|
||||
relay_url = sys.argv[3]
|
||||
|
||||
publish_profile_metadata(private_key_hex, profile_name, relay_url)
|
||||
Loading…
Add table
Add a link
Reference in a new issue