Compare commits
10 commits
e5c39cdbd0
...
3c29099f3a
| Author | SHA1 | Date | |
|---|---|---|---|
| 3c29099f3a | |||
| bdf71d3ea9 | |||
| 0d579892a8 | |||
| 44ba4e4086 | |||
| 2795ad9ac3 | |||
| 7f66c4b092 | |||
| 7075abdbbd | |||
| 26fcf50462 | |||
| 5983774e22 | |||
| dffc54c0d2 |
13 changed files with 1354 additions and 8 deletions
213
CUSTOM_FRONTEND_URL_SIMPLE.md
Normal file
213
CUSTOM_FRONTEND_URL_SIMPLE.md
Normal file
|
|
@ -0,0 +1,213 @@
|
||||||
|
# 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
Normal file
113
extensions.json
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
{
|
||||||
|
"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,3 +1,5 @@
|
||||||
|
import json
|
||||||
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
|
|
@ -72,7 +74,7 @@ async def create_user_account_no_ckeck(
|
||||||
account.id = uuid4().hex
|
account.id = uuid4().hex
|
||||||
|
|
||||||
account = await create_account(account, conn=conn)
|
account = await create_account(account, conn=conn)
|
||||||
await create_wallet(
|
wallet = await create_wallet(
|
||||||
user_id=account.id,
|
user_id=account.id,
|
||||||
wallet_name=wallet_name or settings.lnbits_default_wallet_name,
|
wallet_name=wallet_name or settings.lnbits_default_wallet_name,
|
||||||
conn=conn,
|
conn=conn,
|
||||||
|
|
@ -86,6 +88,22 @@ async def create_user_account_no_ckeck(
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error enabeling default extension {ext_id}: {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)
|
user = await get_user_from_account(account, conn=conn)
|
||||||
if not user:
|
if not user:
|
||||||
raise ValueError("Cannot find user for account.")
|
raise ValueError("Cannot find user for account.")
|
||||||
|
|
@ -192,3 +210,160 @@ async def init_admin_settings(super_user: str | None = None) -> SuperSettings:
|
||||||
|
|
||||||
editable_settings = EditableSettings.from_dict(settings.dict())
|
editable_settings = EditableSettings.from_dict(settings.dict())
|
||||||
return await create_admin_settings(account.id, editable_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,6 +100,16 @@ async def api_list_currencies_available() -> list[str]:
|
||||||
return allowed_currencies()
|
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")
|
@api_router.post("/api/v1/conversion")
|
||||||
async def api_fiat_as_sats(data: ConversionData):
|
async def api_fiat_as_sats(data: ConversionData):
|
||||||
output = {}
|
output = {}
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,9 @@ async def api_create_user(data: CreateUser) -> CreateUser:
|
||||||
data.extra = data.extra or UserExtra()
|
data.extra = data.extra or UserExtra()
|
||||||
data.extra.provider = data.extra.provider or "lnbits"
|
data.extra.provider = data.extra.provider or "lnbits"
|
||||||
|
|
||||||
|
if data.pubkey:
|
||||||
|
data.pubkey = normalize_public_key(data.pubkey)
|
||||||
|
|
||||||
account = Account(
|
account = Account(
|
||||||
id=uuid4().hex,
|
id=uuid4().hex,
|
||||||
username=data.username,
|
username=data.username,
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,7 @@ def template_renderer(additional_folders: list | None = None) -> Jinja2Templates
|
||||||
"LNBITS_CUSTOM_IMAGE": settings.lnbits_custom_image,
|
"LNBITS_CUSTOM_IMAGE": settings.lnbits_custom_image,
|
||||||
"LNBITS_CUSTOM_BADGE": settings.lnbits_custom_badge,
|
"LNBITS_CUSTOM_BADGE": settings.lnbits_custom_badge,
|
||||||
"LNBITS_CUSTOM_BADGE_COLOR": settings.lnbits_custom_badge_color,
|
"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_EXTENSIONS_DEACTIVATE_ALL": settings.lnbits_extensions_deactivate_all,
|
||||||
"LNBITS_NEW_ACCOUNTS_ALLOWED": settings.new_accounts_allowed,
|
"LNBITS_NEW_ACCOUNTS_ALLOWED": settings.new_accounts_allowed,
|
||||||
"LNBITS_NODE_UI": settings.lnbits_node_ui and settings.has_nodemanager,
|
"LNBITS_NODE_UI": settings.lnbits_node_ui and settings.has_nodemanager,
|
||||||
|
|
|
||||||
|
|
@ -960,6 +960,10 @@ class EnvSettings(LNbitsSettings):
|
||||||
lnbits_title: str = Field(default="LNbits API")
|
lnbits_title: str = Field(default="LNbits API")
|
||||||
lnbits_path: str = Field(default=".")
|
lnbits_path: str = Field(default=".")
|
||||||
lnbits_extensions_path: str = Field(default="lnbits")
|
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="")
|
super_user: str = Field(default="")
|
||||||
auth_secret_key: str = Field(default="")
|
auth_secret_key: str = Field(default="")
|
||||||
version: str = Field(default="0.0.0")
|
version: str = Field(default="0.0.0")
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,12 @@ window.PageHome = {
|
||||||
this.password,
|
this.password,
|
||||||
this.passwordRepeat
|
this.passwordRepeat
|
||||||
)
|
)
|
||||||
this.refreshAuthUser()
|
// 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()
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
LNbits.utils.notifyApiError(e)
|
LNbits.utils.notifyApiError(e)
|
||||||
}
|
}
|
||||||
|
|
@ -91,7 +96,12 @@ window.PageHome = {
|
||||||
this.password,
|
this.password,
|
||||||
this.passwordRepeat
|
this.passwordRepeat
|
||||||
)
|
)
|
||||||
this.refreshAuthUser()
|
// 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()
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
LNbits.utils.notifyApiError(e)
|
LNbits.utils.notifyApiError(e)
|
||||||
}
|
}
|
||||||
|
|
@ -99,7 +109,12 @@ window.PageHome = {
|
||||||
async login() {
|
async login() {
|
||||||
try {
|
try {
|
||||||
await LNbits.api.login(this.username, this.password)
|
await LNbits.api.login(this.username, this.password)
|
||||||
this.refreshAuthUser()
|
// 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()
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
LNbits.utils.notifyApiError(e)
|
LNbits.utils.notifyApiError(e)
|
||||||
}
|
}
|
||||||
|
|
@ -107,7 +122,13 @@ window.PageHome = {
|
||||||
async loginUsr() {
|
async loginUsr() {
|
||||||
try {
|
try {
|
||||||
await LNbits.api.loginUsr(this.usr)
|
await LNbits.api.loginUsr(this.usr)
|
||||||
this.refreshAuthUser()
|
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()
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(e)
|
console.warn(e)
|
||||||
LNbits.utils.notifyApiError(e)
|
LNbits.utils.notifyApiError(e)
|
||||||
|
|
@ -138,14 +159,28 @@ window.PageHome = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
if (this.g.isUserAuthorized) {
|
|
||||||
return this.refreshAuthUser()
|
|
||||||
}
|
|
||||||
const urlParams = new URLSearchParams(window.location.search)
|
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')
|
this.reset_key = urlParams.get('reset_key')
|
||||||
if (this.reset_key) {
|
if (this.reset_key) {
|
||||||
this.authAction = 'reset'
|
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
|
// check if lightning parameters are present in the URL
|
||||||
if (urlParams.has('lightning')) {
|
if (urlParams.has('lightning')) {
|
||||||
this.lnurl = urlParams.get('lightning')
|
this.lnurl = urlParams.get('lightning')
|
||||||
|
|
|
||||||
88
misc-aio/AUTO_CREDIT_CHANGES.md
Normal file
88
misc-aio/AUTO_CREDIT_CHANGES.md
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
# 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)
|
||||||
|
|
||||||
460
misc-aio/lnbits-websocket-guide.md
Normal file
460
misc-aio/lnbits-websocket-guide.md
Normal file
|
|
@ -0,0 +1,460 @@
|
||||||
|
# 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.
|
||||||
BIN
misc-aio/lnbits-websocket-guide.pdf
Normal file
BIN
misc-aio/lnbits-websocket-guide.pdf
Normal file
Binary file not shown.
139
misc-aio/publish_profiles_from_csv.py
Normal file
139
misc-aio/publish_profiles_from_csv.py
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
#!/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()
|
||||||
105
misc-aio/test_nostr_connection.py
Normal file
105
misc-aio/test_nostr_connection.py
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
#!/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