Compare commits
9 commits
911abc40ed
...
204b559f78
| Author | SHA1 | Date | |
|---|---|---|---|
| 204b559f78 | |||
| 0ba1719046 | |||
| db025145aa | |||
| a45d7ad11d | |||
| b5806533e6 | |||
| 3bae338b30 | |||
| e8d629c2ba | |||
| adddcecf87 | |||
| c7609fedf5 |
15 changed files with 1429 additions and 23 deletions
|
|
@ -298,6 +298,7 @@ async def create_user(username: str, password: str):
|
|||
account.hash_password(password)
|
||||
user = await create_user_account_no_ckeck(account)
|
||||
click.echo(f"User '{user.username}' created. Id: '{user.id}'")
|
||||
click.echo(f"Nostr public key: {account.pubkey}")
|
||||
|
||||
|
||||
@users.command("cleanup-accounts")
|
||||
|
|
|
|||
|
|
@ -23,9 +23,32 @@ async def create_account(
|
|||
) -> Account:
|
||||
if account:
|
||||
account.validate_fields()
|
||||
# If account doesn't have Nostr keys, generate them
|
||||
# Exception: Nostr login users who already have a public key but no private key
|
||||
# should not get a new private key generated - they use their existing Nostr identity
|
||||
if not account.pubkey and not account.prvkey:
|
||||
from lnbits.utils.nostr import generate_keypair
|
||||
nostr_private_key, nostr_public_key = generate_keypair()
|
||||
account.pubkey = nostr_public_key
|
||||
account.prvkey = nostr_private_key
|
||||
elif account.pubkey and not account.prvkey:
|
||||
# This is a Nostr login user - they already have a public key from their existing identity
|
||||
# We don't generate a private key for them as they use their own Nostr client
|
||||
# The chat system will need to handle this case by requesting the private key from the user
|
||||
pass
|
||||
else:
|
||||
# Generate Nostr keypair for new account
|
||||
from lnbits.utils.nostr import generate_keypair
|
||||
nostr_private_key, nostr_public_key = generate_keypair()
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
account = Account(id=uuid4().hex, created_at=now, updated_at=now)
|
||||
account = Account(
|
||||
id=uuid4().hex,
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
pubkey=nostr_public_key, # Use Nostr public key as the pubkey
|
||||
prvkey=nostr_private_key,
|
||||
)
|
||||
await (conn or db).insert("accounts", account)
|
||||
return account
|
||||
|
||||
|
|
@ -68,6 +91,7 @@ async def get_accounts(
|
|||
accounts.username,
|
||||
accounts.email,
|
||||
accounts.pubkey,
|
||||
accounts.prvkey,
|
||||
accounts.external_id,
|
||||
SUM(COALESCE((
|
||||
SELECT balance FROM balances WHERE wallet_id = wallets.id
|
||||
|
|
@ -182,11 +206,12 @@ async def get_user_from_account(
|
|||
) -> Optional[User]:
|
||||
extensions = await get_user_active_extensions_ids(account.id, conn)
|
||||
wallets = await get_wallets(account.id, False, conn=conn)
|
||||
|
||||
return User(
|
||||
id=account.id,
|
||||
email=account.email,
|
||||
username=account.username,
|
||||
pubkey=account.pubkey,
|
||||
pubkey=account.pubkey, # This is now the Nostr public key
|
||||
external_id=account.external_id,
|
||||
extra=account.extra,
|
||||
created_at=account.created_at,
|
||||
|
|
|
|||
|
|
@ -238,12 +238,12 @@ async def m007_set_invoice_expiries(db: Connection):
|
|||
|
||||
expiration_date = invoice.date + invoice.expiry
|
||||
logger.info(
|
||||
f"Migration: {i+1}/{len(rows)} setting expiry of invoice"
|
||||
f"Migration: {i + 1}/{len(rows)} setting expiry of invoice"
|
||||
f" {invoice.payment_hash} to {expiration_date}"
|
||||
)
|
||||
await db.execute(
|
||||
f"""
|
||||
UPDATE apipayments SET expiry = {db.timestamp_placeholder('expiry')}
|
||||
UPDATE apipayments SET expiry = {db.timestamp_placeholder("expiry")}
|
||||
WHERE checking_id = :checking_id AND amount > 0
|
||||
""",
|
||||
{"expiry": expiration_date, "checking_id": checking_id},
|
||||
|
|
@ -455,14 +455,14 @@ async def m017_add_timestamp_columns_to_accounts_and_wallets(db: Connection):
|
|||
now = int(time())
|
||||
await db.execute(
|
||||
f"""
|
||||
UPDATE wallets SET created_at = {db.timestamp_placeholder('now')}
|
||||
UPDATE wallets SET created_at = {db.timestamp_placeholder("now")}
|
||||
WHERE created_at IS NULL
|
||||
""",
|
||||
{"now": now},
|
||||
)
|
||||
await db.execute(
|
||||
f"""
|
||||
UPDATE accounts SET created_at = {db.timestamp_placeholder('now')}
|
||||
UPDATE accounts SET created_at = {db.timestamp_placeholder("now")}
|
||||
WHERE created_at IS NULL
|
||||
""",
|
||||
{"now": now},
|
||||
|
|
@ -613,7 +613,7 @@ async def m027_update_apipayments_data(db: Connection):
|
|||
payments: list[dict[Any, Any]] = []
|
||||
logger.info("Updating payments")
|
||||
while len(payments) > 0 or offset == 0:
|
||||
logger.info(f"Updating {offset} to {offset+limit}")
|
||||
logger.info(f"Updating {offset} to {offset + limit}")
|
||||
|
||||
result = await db.execute(
|
||||
f"SELECT * FROM apipayments ORDER BY time LIMIT {limit} OFFSET {offset}"
|
||||
|
|
@ -645,7 +645,6 @@ async def m027_update_apipayments_data(db: Connection):
|
|||
|
||||
|
||||
async def m028_update_settings(db: Connection):
|
||||
|
||||
await db.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS system_settings (
|
||||
|
|
@ -723,3 +722,13 @@ async def m032_add_external_id_to_accounts(db: Connection):
|
|||
|
||||
async def m033_update_payment_table(db: Connection):
|
||||
await db.execute("ALTER TABLE apipayments ADD COLUMN fiat_provider TEXT")
|
||||
|
||||
async def m9001_add_nostr_private_key_to_accounts(db: Connection):
|
||||
"""
|
||||
Adds prvkey column to accounts for storing Nostr private keys.
|
||||
FORK MIGRATION - DO NOT MERGE UPSTREAM
|
||||
"""
|
||||
try:
|
||||
await db.execute("ALTER TABLE accounts ADD COLUMN prvkey TEXT")
|
||||
except OperationalError:
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@ class Account(BaseModel):
|
|||
username: str | None = None
|
||||
password_hash: str | None = None
|
||||
pubkey: str | None = None
|
||||
prvkey: str | None = None # Nostr private key for user
|
||||
email: str | None = None
|
||||
extra: UserExtra = UserExtra()
|
||||
|
||||
|
|
@ -114,6 +115,19 @@ class Account(BaseModel):
|
|||
|
||||
def __init__(self, **data):
|
||||
super().__init__(**data)
|
||||
# NOTE: I tried this in the past and it resulted in unexpected behavior
|
||||
# all accounts were suddenly showing up in the peers list, however, if
|
||||
# they did not have a key-pair, they were being assigned one on the fly.
|
||||
# Something about fetching the users was causing this code to trigger.
|
||||
#
|
||||
#
|
||||
# # Generate Nostr keypair if not already provided
|
||||
# if not self.pubkey or not self.prvkey:
|
||||
# from lnbits.utils.nostr import generate_keypair
|
||||
# nostr_public_key, nostr_private_key = generate_keypair()
|
||||
# self.pubkey = nostr_public_key
|
||||
# self.prvkey = nostr_private_key
|
||||
#
|
||||
self.is_super_user = settings.is_super_user(self.id)
|
||||
self.is_admin = settings.is_admin_user(self.id)
|
||||
self.fiat_providers = settings.get_fiat_providers_for_user(self.id)
|
||||
|
|
@ -187,7 +201,7 @@ class User(BaseModel):
|
|||
updated_at: datetime
|
||||
email: str | None = None
|
||||
username: str | None = None
|
||||
pubkey: str | None = None
|
||||
pubkey: str | None = None # This is now the Nostr public key
|
||||
external_id: str | None = None # for external account linking
|
||||
extensions: list[str] = []
|
||||
wallets: list[Wallet] = []
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import json
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from uuid import uuid4
|
||||
|
|
@ -85,6 +87,12 @@ async def create_user_account_no_ckeck(
|
|||
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
|
||||
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)
|
||||
assert user, "Cannot find user for account."
|
||||
|
|
@ -199,7 +207,7 @@ async def _create_default_pay_link(account: Account, wallet) -> None:
|
|||
# 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")
|
||||
|
|
@ -212,16 +220,16 @@ async def _create_default_pay_link(account: Account, wallet) -> None:
|
|||
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,
|
||||
|
|
@ -236,7 +244,108 @@ async def _create_default_pay_link(account: Account, wallet) -> None:
|
|||
|
||||
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"""
|
||||
try:
|
||||
import importlib
|
||||
import sys
|
||||
import secp256k1
|
||||
|
||||
# Note: We publish directly to nostrrelay database, no need to get relay URLs
|
||||
|
||||
# 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)
|
||||
}
|
||||
|
||||
# Sign the event using LNbits utilities
|
||||
from lnbits.utils.nostr import sign_event
|
||||
|
||||
# Convert hex private key to secp256k1 PrivateKey
|
||||
private_key = secp256k1.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)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to publish Nostr metadata event: {e}")
|
||||
# Don't raise - we don't want user creation to fail if Nostr publishing fails
|
||||
|
||||
|
||||
|
||||
|
||||
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)}")
|
||||
|
||||
|
|
|
|||
|
|
@ -70,6 +70,49 @@ async def get_auth_user(user: User = Depends(check_user_exists)) -> User:
|
|||
return user
|
||||
|
||||
|
||||
@auth_router.get("/nostr/me", description="Get current user with Nostr keys")
|
||||
async def get_auth_user_with_nostr(user: User = Depends(check_user_exists)) -> dict:
|
||||
"""Get current user information including Nostr private key for chat"""
|
||||
from lnbits.core.crud.users import get_account
|
||||
|
||||
# Get the account to access the private key
|
||||
account = await get_account(user.id)
|
||||
if not account:
|
||||
raise HTTPException(HTTPStatus.NOT_FOUND, "User not found.")
|
||||
|
||||
return {
|
||||
"id": user.id,
|
||||
"username": user.username,
|
||||
"email": user.email,
|
||||
"pubkey": user.pubkey,
|
||||
"prvkey": account.prvkey, # Include private key for Nostr chat
|
||||
"created_at": user.created_at,
|
||||
"updated_at": user.updated_at
|
||||
}
|
||||
|
||||
|
||||
@auth_router.get("/nostr/pubkeys", description="Get all user Nostr public keys")
|
||||
async def get_nostr_pubkeys(user: User = Depends(check_user_exists)) -> list[dict[str, str]]:
|
||||
"""Get all user Nostr public keys for chat"""
|
||||
from lnbits.core.crud.users import get_accounts
|
||||
from lnbits.db import Filters
|
||||
|
||||
# Get all accounts
|
||||
filters = Filters()
|
||||
accounts_page = await get_accounts(filters=filters)
|
||||
|
||||
pubkeys = []
|
||||
for account in accounts_page.data:
|
||||
if account.pubkey: # pubkey is now the Nostr public key
|
||||
pubkeys.append({
|
||||
"user_id": account.id,
|
||||
"username": account.username,
|
||||
"pubkey": account.pubkey
|
||||
})
|
||||
|
||||
return pubkeys
|
||||
|
||||
|
||||
@auth_router.post("", description="Login via the username and password")
|
||||
async def login(data: LoginUsernamePassword) -> JSONResponse:
|
||||
if not settings.is_auth_method_allowed(AuthMethods.username_and_password):
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ from lnbits.core.crud import (
|
|||
delete_account,
|
||||
delete_wallet,
|
||||
force_delete_wallet,
|
||||
get_account,
|
||||
get_accounts,
|
||||
get_user,
|
||||
get_wallet,
|
||||
|
|
@ -41,7 +42,7 @@ from lnbits.core.services import (
|
|||
update_wallet_balance,
|
||||
)
|
||||
from lnbits.db import Filters, Page
|
||||
from lnbits.decorators import check_admin, check_super_user, parse_filters
|
||||
from lnbits.decorators import check_admin, check_super_user, check_user_exists, parse_filters
|
||||
from lnbits.helpers import (
|
||||
encrypt_internal_message,
|
||||
generate_filter_params_openapi,
|
||||
|
|
@ -99,7 +100,6 @@ async def api_create_user(data: CreateUser) -> CreateUser:
|
|||
id=uuid4().hex,
|
||||
username=data.username,
|
||||
email=data.email,
|
||||
pubkey=data.pubkey,
|
||||
external_id=data.external_id,
|
||||
extra=data.extra,
|
||||
)
|
||||
|
|
@ -331,3 +331,54 @@ async def api_update_balance(data: UpdateBalance) -> SimpleStatus:
|
|||
)
|
||||
|
||||
return SimpleStatus(success=True, message="Balance updated.")
|
||||
|
||||
|
||||
@users_router.get(
|
||||
"/nostr/pubkeys",
|
||||
name="Get all user Nostr public keys",
|
||||
summary="Get a list of all user Nostr public keys",
|
||||
dependencies=[], # Override global admin requirement
|
||||
)
|
||||
async def api_get_nostr_pubkeys() -> list[dict[str, str]]:
|
||||
"""Get all user Nostr public keys"""
|
||||
from lnbits.core.crud.users import get_accounts
|
||||
from lnbits.db import Filters
|
||||
|
||||
# Get all accounts
|
||||
filters = Filters()
|
||||
accounts_page = await get_accounts(filters=filters)
|
||||
|
||||
pubkeys = []
|
||||
for account in accounts_page.data:
|
||||
if account.pubkey: # pubkey is now the Nostr public key
|
||||
pubkeys.append({
|
||||
"user_id": account.id,
|
||||
"username": account.username,
|
||||
"pubkey": account.pubkey # Use consistent naming
|
||||
})
|
||||
|
||||
return pubkeys
|
||||
|
||||
|
||||
@users_router.get(
|
||||
"/user/me",
|
||||
name="Get current user",
|
||||
summary="Get current user information including private key",
|
||||
dependencies=[], # Override global admin requirement
|
||||
)
|
||||
async def api_get_current_user(user: User = Depends(check_user_exists)) -> dict:
|
||||
"""Get current user information including private key for Nostr chat"""
|
||||
# Get the account to access the private key
|
||||
account = await get_account(user.id)
|
||||
if not account:
|
||||
raise HTTPException(HTTPStatus.NOT_FOUND, "User not found.")
|
||||
|
||||
return {
|
||||
"id": user.id,
|
||||
"username": user.username,
|
||||
"email": user.email,
|
||||
"pubkey": user.pubkey,
|
||||
"prvkey": account.prvkey, # Include private key for Nostr chat
|
||||
"created_at": user.created_at,
|
||||
"updated_at": user.updated_at
|
||||
}
|
||||
|
|
|
|||
140
misc-aio/NOSTR_INTEGRATION.md
Normal file
140
misc-aio/NOSTR_INTEGRATION.md
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
# Nostr Integration for LNBits Users
|
||||
|
||||
This document describes the changes made to integrate Nostr keypairs with LNBits user accounts.
|
||||
|
||||
## Overview
|
||||
|
||||
The integration adds Nostr keypair generation to user accounts, allowing each user to have a unique Nostr identity. The private key is stored securely in the database, while the public key is derived and made available through the API.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### 1. Database Migration
|
||||
|
||||
**File**: `lnbits/core/migrations.py`
|
||||
- Added `m034_add_nostr_private_key_to_accounts()` migration
|
||||
- Adds `prvkey` column to the `accounts` table for Nostr private keys
|
||||
|
||||
### 2. Model Updates
|
||||
|
||||
**File**: `lnbits/core/models/users.py`
|
||||
- Added `prvkey` field to `Account` model for Nostr private key
|
||||
- The existing `pubkey` field is now used to store the Nostr public key
|
||||
|
||||
### 3. CRUD Operations
|
||||
|
||||
**File**: `lnbits/core/crud/users.py`
|
||||
- Updated `get_user_from_account()` to use the existing pubkey field (now contains Nostr public key)
|
||||
- Updated `get_accounts()` SQL query to include `prvkey` field
|
||||
|
||||
### 4. API Endpoints
|
||||
|
||||
**File**: `lnbits/core/views/user_api.py`
|
||||
- Modified user creation to automatically generate Nostr keypair and set the pubkey field
|
||||
- Added new endpoint `/users/api/v1/nostr/pubkeys` to get all user public keys
|
||||
- Endpoint requires admin privileges and returns:
|
||||
```json
|
||||
[
|
||||
{
|
||||
"user_id": "user_id",
|
||||
"username": "username",
|
||||
"nostr_public_key": "public_key_hex"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### 5. Command Line Interface
|
||||
|
||||
**File**: `lnbits/commands.py`
|
||||
- Updated `create_user` command to generate Nostr keypair
|
||||
- Displays the generated public key when creating users via CLI
|
||||
|
||||
## API Usage
|
||||
|
||||
### Creating a User (Automatically generates Nostr keypair)
|
||||
|
||||
```bash
|
||||
POST /users/api/v1/user
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "testuser",
|
||||
"email": "test@example.com",
|
||||
"password": "password123",
|
||||
"password_repeat": "password123"
|
||||
}
|
||||
```
|
||||
|
||||
### Getting All User Public Keys
|
||||
|
||||
```bash
|
||||
GET /users/api/v1/nostr/pubkeys
|
||||
Authorization: Bearer <admin_token>
|
||||
|
||||
Response:
|
||||
[
|
||||
{
|
||||
"user_id": "abc123",
|
||||
"username": "user1",
|
||||
"pubkey": "02a1b2c3d4e5f6..."
|
||||
},
|
||||
{
|
||||
"user_id": "def456",
|
||||
"username": "user2",
|
||||
"pubkey": "03b2c3d4e5f6a1..."
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Getting Individual User (includes Nostr public key)
|
||||
|
||||
```bash
|
||||
GET /users/api/v1/user/{user_id}
|
||||
|
||||
Response:
|
||||
{
|
||||
"id": "abc123",
|
||||
"username": "user1",
|
||||
"email": "user1@example.com",
|
||||
"pubkey": "02a1b2c3d4e5f6...", # This is the Nostr public key
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
"updated_at": "2024-01-01T00:00:00Z",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Private Key Storage**: Nostr private keys are stored in the `prvkey` field but are never exposed through the API
|
||||
2. **Public Key Storage**: Nostr public keys are stored directly in the `pubkey` field for efficiency
|
||||
3. **Admin Access**: The public key listing endpoint requires admin privileges
|
||||
4. **Consistent Naming**: `pubkey` and `prvkey` provide clear, consistent field names
|
||||
|
||||
## Migration
|
||||
|
||||
To apply the database changes:
|
||||
|
||||
1. Run the migration: `python -m lnbits db migrate`
|
||||
2. The migration will add the `prvkey` column to existing accounts for Nostr private keys
|
||||
3. New users will automatically get Nostr keypairs generated
|
||||
|
||||
## Testing
|
||||
|
||||
Use the provided test script to verify the integration:
|
||||
|
||||
```bash
|
||||
python test_nostr_integration.py
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
The integration uses the existing `lnbits.utils.nostr` module which provides:
|
||||
- `generate_keypair()`: Generates new Nostr keypairs
|
||||
- `PrivateKey` class: For key manipulation and public key derivation
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential improvements could include:
|
||||
1. NIP-19 encoding support (npub/nsec format)
|
||||
2. Nostr event signing capabilities
|
||||
3. Integration with Nostr relays for user discovery
|
||||
4. User profile metadata storage
|
||||
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)
|
||||
96
misc-aio/test_nostr_integration.py
Normal file
96
misc-aio/test_nostr_integration.py
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script to verify Nostr keypair integration with LNBits User model
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add the lnbits directory to the path
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'lnbits'))
|
||||
|
||||
def test_nostr_keypair_generation():
|
||||
"""Test that we can generate Nostr keypairs"""
|
||||
try:
|
||||
from lnbits.utils.nostr import generate_keypair
|
||||
private_key, public_key = generate_keypair()
|
||||
print(f"✓ Nostr keypair generation works")
|
||||
print(f" Private key: {private_key[:16]}...")
|
||||
print(f" Public key: {public_key}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"✗ Nostr keypair generation failed: {e}")
|
||||
return False
|
||||
|
||||
def test_account_model():
|
||||
"""Test that the Account model includes prvkey field"""
|
||||
try:
|
||||
from lnbits.core.models.users import Account
|
||||
account = Account(
|
||||
id="test123",
|
||||
username="testuser",
|
||||
)
|
||||
print(f"✓ Account model includes prvkey field")
|
||||
print(f" prvkey: {account.prvkey}")
|
||||
print(f" pubkey: {account.pubkey}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"✗ Account model test failed: {e}")
|
||||
return False
|
||||
|
||||
def test_user_model():
|
||||
"""Test that the User model includes pubkey field"""
|
||||
try:
|
||||
from lnbits.core.models.users import User
|
||||
user = User(
|
||||
id="test123",
|
||||
created_at=None,
|
||||
updated_at=None,
|
||||
pubkey="test_public_key"
|
||||
)
|
||||
print(f"✓ User model includes pubkey field")
|
||||
print(f" pubkey: {user.pubkey}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"✗ User model test failed: {e}")
|
||||
return False
|
||||
|
||||
def test_migration():
|
||||
"""Test that the migration function exists"""
|
||||
try:
|
||||
from lnbits.core.migrations import m034_add_nostr_private_key_to_accounts
|
||||
print(f"✓ Migration function exists")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"✗ Migration test failed: {e}")
|
||||
return False
|
||||
|
||||
def main():
|
||||
print("Testing Nostr integration with LNBits...")
|
||||
print("=" * 50)
|
||||
|
||||
tests = [
|
||||
test_nostr_keypair_generation,
|
||||
test_account_model,
|
||||
test_user_model,
|
||||
test_migration,
|
||||
]
|
||||
|
||||
passed = 0
|
||||
total = len(tests)
|
||||
|
||||
for test in tests:
|
||||
if test():
|
||||
passed += 1
|
||||
print()
|
||||
|
||||
print("=" * 50)
|
||||
print(f"Tests passed: {passed}/{total}")
|
||||
|
||||
if passed == total:
|
||||
print("✓ All tests passed! Nostr integration is ready.")
|
||||
else:
|
||||
print("✗ Some tests failed. Please check the implementation.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
213
nix/modules/README.md
Normal file
213
nix/modules/README.md
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
# LNBits NixOS Installation Guide
|
||||
|
||||
This guide shows how to install LNBits on a fresh NixOS system.
|
||||
|
||||
## Quick Start (Recommended)
|
||||
|
||||
Add this to your NixOS configuration (`/etc/nixos/configuration.nix`):
|
||||
|
||||
```nix
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
let
|
||||
lnbitsFlake = builtins.getFlake "github:lnbits/lnbits";
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
# Import LNBits service module directly from GitHub
|
||||
"${lnbitsFlake}/nix/modules/lnbits-service.nix"
|
||||
];
|
||||
|
||||
# Enable flakes (required)
|
||||
nix.settings.experimental-features = [ "nix-command" "flakes" ];
|
||||
|
||||
# Configure LNBits service
|
||||
services.lnbits = {
|
||||
enable = true;
|
||||
host = "0.0.0.0"; # Listen on all interfaces
|
||||
port = 5000; # Default port
|
||||
openFirewall = true; # Open firewall port automatically
|
||||
|
||||
# Use package from the same flake (adjust system architecture as needed)
|
||||
package = lnbitsFlake.packages.x86_64-linux.lnbits;
|
||||
|
||||
env = {
|
||||
LNBITS_ADMIN_UI = "true";
|
||||
# Configure your Lightning backend:
|
||||
# LNBITS_BACKEND_WALLET_CLASS = "LndRestWallet";
|
||||
# LND_REST_ENDPOINT = "https://localhost:8080";
|
||||
# LND_REST_CERT = "/path/to/tls.cert";
|
||||
# LND_REST_MACAROON = "/path/to/admin.macaroon";
|
||||
};
|
||||
};
|
||||
|
||||
# Rebuild and switch
|
||||
# sudo nixos-rebuild switch
|
||||
}
|
||||
```
|
||||
|
||||
> **⚠️ System Architecture Note**: The examples above use `x86_64-linux`. Replace this with your system architecture:
|
||||
>
|
||||
> - `x86_64-linux` - Intel/AMD 64-bit Linux
|
||||
> - `aarch64-linux` - ARM 64-bit Linux (e.g., Raspberry Pi 4, Apple Silicon under Linux)
|
||||
> - `x86_64-darwin` - Intel Mac
|
||||
> - `aarch64-darwin` - Apple Silicon Mac
|
||||
>
|
||||
> You can check your system with: `nix eval --impure --raw --expr 'builtins.currentSystem'`
|
||||
|
||||
## Alternative: Using Your Own Flake
|
||||
|
||||
Create a `flake.nix` for your system configuration:
|
||||
|
||||
```nix
|
||||
{
|
||||
description = "My NixOS configuration with LNBits";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
|
||||
lnbits.url = "github:lnbits/lnbits";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, lnbits }: {
|
||||
nixosConfigurations.myserver = nixpkgs.lib.nixosSystem {
|
||||
system = "x86_64-linux"; # Adjust architecture as needed
|
||||
modules = [
|
||||
./hardware-configuration.nix
|
||||
{
|
||||
services.lnbits = {
|
||||
enable = true;
|
||||
host = "0.0.0.0";
|
||||
port = 5000;
|
||||
openFirewall = true;
|
||||
package = lnbits.packages.x86_64-linux.lnbits; # Adjust architecture as needed
|
||||
env = {
|
||||
LNBITS_ADMIN_UI = "true";
|
||||
# Add your Lightning backend configuration
|
||||
};
|
||||
};
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Then deploy with:
|
||||
|
||||
```bash
|
||||
sudo nixos-rebuild switch --flake .#myserver
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### Basic Options
|
||||
|
||||
- `enable`: Enable the LNBits service (default: `false`)
|
||||
- `host`: Host to bind to (default: `"127.0.0.1"`)
|
||||
- `port`: Port to run on (default: `8231`)
|
||||
- `openFirewall`: Automatically open firewall port (default: `false`)
|
||||
- `user`: User to run as (default: `"lnbits"`)
|
||||
- `group`: Group to run as (default: `"lnbits"`)
|
||||
- `stateDir`: State directory (default: `"/var/lib/lnbits"`)
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Configure LNBits through the `env` option. Common variables:
|
||||
|
||||
```nix
|
||||
services.lnbits.env = {
|
||||
# Admin UI
|
||||
LNBITS_ADMIN_UI = "true";
|
||||
|
||||
# LND Backend Example:
|
||||
|
||||
# LND
|
||||
LNBITS_BACKEND_WALLET_CLASS = "LndRestWallet";
|
||||
LND_REST_ENDPOINT = "https://localhost:8080";
|
||||
LND_REST_CERT = "/path/to/tls.cert";
|
||||
LND_REST_MACAROON = "/path/to/admin.macaroon";
|
||||
};
|
||||
```
|
||||
|
||||
See the [LNBits documentation](https://docs.lnbits.org/guide/wallets.html) for all supported backends.
|
||||
|
||||
## State Directory Structure
|
||||
|
||||
LNBits data is stored in `/var/lib/lnbits` (default) with this structure:
|
||||
|
||||
```
|
||||
/var/lib/lnbits/
|
||||
├── data/ # Application data
|
||||
│ ├── database.sqlite3 # Main database
|
||||
│ ├── ext_<extension>.sqlite3 # Extension database
|
||||
│ ├── images/ # Uploaded images
|
||||
│ ├── logs/ # Log files
|
||||
│ └── upgrades/ # Migration files
|
||||
└── extensions/ # Installed extensions
|
||||
```
|
||||
|
||||
## First Time Setup
|
||||
|
||||
1. **Deploy the configuration:**
|
||||
|
||||
```bash
|
||||
sudo nixos-rebuild switch
|
||||
```
|
||||
|
||||
2. **Check service status:**
|
||||
|
||||
```bash
|
||||
systemctl status lnbits
|
||||
```
|
||||
|
||||
3. **Access the web interface:**
|
||||
|
||||
```
|
||||
http://your-server-ip:5000
|
||||
```
|
||||
|
||||
4. **Follow the first-time setup wizard** to configure your Lightning backend and create your first wallet.
|
||||
|
||||
5. **Bonus** Add Reverse Proxy with generated SSL Cert
|
||||
|
||||
```nix
|
||||
{
|
||||
# Enable nginx
|
||||
services.nginx = {
|
||||
enable = true;
|
||||
|
||||
virtualHosts."lnbits.mydomain.com" = {
|
||||
forceSSL = true;
|
||||
enableACME = true;
|
||||
locations."/" = {
|
||||
proxyPass = "http://127.0.0.1:5000";
|
||||
proxyWebsockets = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Service won't start
|
||||
|
||||
```bash
|
||||
# Check service logs
|
||||
journalctl -u lnbits -f
|
||||
|
||||
# Check if port is available
|
||||
ss -tlnp | grep 5000
|
||||
```
|
||||
|
||||
### Can't access web interface
|
||||
|
||||
- Ensure `openFirewall = true` is set
|
||||
- Check if the port is correct: `services.lnbits.port`
|
||||
- Verify host binding: `services.lnbits.host = "0.0.0.0"`
|
||||
|
||||
## Further Reading
|
||||
|
||||
- [LNBits Documentation](https://docs.lnbits.org)
|
||||
- [Lightning Wallet Configuration](https://docs.lnbits.org/guide/wallets.html)
|
||||
- [LNBits Extensions](https://docs.lnbits.org/devs/extensions.html)
|
||||
|
|
@ -35,7 +35,7 @@ in
|
|||
type = types.path;
|
||||
default = "/var/lib/lnbits";
|
||||
description = ''
|
||||
The lnbits state directory which LNBITS_DATA_FOLDER will be set to
|
||||
The lnbits state directory
|
||||
'';
|
||||
};
|
||||
host = mkOption {
|
||||
|
|
@ -90,6 +90,7 @@ in
|
|||
|
||||
systemd.tmpfiles.rules = [
|
||||
"d ${cfg.stateDir} 0700 ${cfg.user} ${cfg.group} - -"
|
||||
"d ${cfg.stateDir}/data 0700 ${cfg.user} ${cfg.group} - -"
|
||||
];
|
||||
|
||||
systemd.services.lnbits = {
|
||||
|
|
@ -99,18 +100,18 @@ in
|
|||
after = [ "network-online.target" ];
|
||||
environment = lib.mkMerge [
|
||||
{
|
||||
LNBITS_DATA_FOLDER = "${cfg.stateDir}";
|
||||
LNBITS_EXTENSIONS_PATH = "${cfg.stateDir}/extensions";
|
||||
LNBITS_PATH = "${cfg.package.src}";
|
||||
LNBITS_DATA_FOLDER = "${cfg.stateDir}/data";
|
||||
# LNBits automatically appends '/extensions' to this path
|
||||
LNBITS_EXTENSIONS_PATH = "${cfg.stateDir}";
|
||||
}
|
||||
cfg.env
|
||||
];
|
||||
serviceConfig = {
|
||||
User = cfg.user;
|
||||
Group = cfg.group;
|
||||
WorkingDirectory = "${cfg.package.src}";
|
||||
StateDirectory = "${cfg.stateDir}";
|
||||
ExecStart = "${lib.getExe cfg.package} --port ${toString cfg.port} --host ${cfg.host}";
|
||||
WorkingDirectory = "${cfg.package}/lib/python3.12/site-packages";
|
||||
StateDirectory = "lnbits";
|
||||
ExecStart = "${cfg.package}/bin/lnbits --port ${toString cfg.port} --host ${cfg.host}";
|
||||
Restart = "always";
|
||||
PrivateTmp = true;
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue