Compare commits

...

10 commits

Author SHA1 Message Date
ee5ea3d876 FIX: correct nixos module state directories
Particularly, this places the allows the extensions folder to be
/var/lib/lnbits/extensions and the data folder to be
/var/lib/lnbits/data
2025-10-13 23:47:36 +02:00
8355a7b717 feat: publish Nostr metadata events for new user accounts
- Added functionality to publish Nostr kind 0 metadata events during
user account creation if the user has a username and Nostr keys.
- Implemented error handling and logging for the metadata publishing
process.
- Introduced helper functions to manage the creation and publishing of
Nostr events to multiple relays.

refactor: improve relay handling in Nostr metadata publishing

- Updated the relay extraction logic to ensure only valid relay URLs are used.
- Added logging for retrieved relays and the number of active relays found.
- Removed default relay fallback, opting to skip publishing if no relays are configured, with appropriate logging for this scenario.

fix: increase WebSocket connection timeout for relay publishing

- Updated the timeout for WebSocket connections in the event publishing function from 3 seconds to 9 seconds to improve reliability in relay communication.

refactor: streamline Nostr metadata publishing by inserting directly into nostrrelay database

- Removed the WebSocket relay publishing method in favor of a direct database insertion approach to simplify the process and avoid WebSocket issues.
- Updated the logic to retrieve relay information from nostrclient and handle potential import errors more gracefully.
- Enhanced logging for the new insertion method and added fallback mechanisms for relay identification.
2025-10-13 23:47:36 +02:00
3dbe7637d3 Merge branch 'auto-lnurlp-acct' into dev-pm 2025-10-13 23:46:47 +02:00
2f920d6cb2 Merge branch 'auto-lnurlp-acct' into dev-pm 2025-10-13 23:45:58 +02:00
7c8044775f Merge branch 'auto-lnurlp-acct' into dev-pm 2025-10-13 23:45:07 +02:00
14f0c20208 Merge branch 'auto-credit-1MM' into dev-pm 2025-10-13 23:45:07 +02:00
57394c15dd feat: integrate Nostr keypair generation with LNBits user accounts
- Added Nostr private key storage to the accounts table via a new
migration.
- Updated Account model to include Nostr private key field.
- Modified user creation process to automatically generate Nostr
keypairs.
- Introduced new API endpoints for retrieving Nostr public keys and user
information including private keys.
- Implemented tests to verify Nostr keypair generation and account model
updates.
2025-10-13 23:44:59 +02:00
2fcbb397f4 feat: add default pay link creation for users with username in user account setup 2025-10-12 13:40:03 +02:00
a7f1af3f19 feat: add default pay link creation for users with username in user account setup 2025-10-12 13:40:03 +02:00
068e68e185 feat: implement automatic crediting of new accounts with 1 million satoshis
- Modified the user account creation process to automatically credit new accounts with 1,000,000 satoshis upon creation.
- Updated the `create_user_account_no_ckeck` function to include error handling and logging for the credit operation.
- Enhanced tests to verify the balance for newly registered users, ensuring the correct credit amount is applied.
- Documented affected account creation paths and testing instructions in the new `AUTO_CREDIT_CHANGES.md` file.
2025-10-12 13:40:03 +02:00
13 changed files with 831 additions and 24 deletions

88
AUTO_CREDIT_CHANGES.md Normal file
View 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)

140
NOSTR_INTEGRATION.md Normal file
View 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

View file

@ -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")

View file

@ -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,

View file

@ -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

View file

@ -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] = []

View file

@ -1,3 +1,5 @@
import json
import time
from pathlib import Path
from typing import Optional
from uuid import uuid4
@ -23,6 +25,7 @@ from ..crud import (
get_super_settings,
get_user_extensions,
get_user_from_account,
get_wallet,
update_account,
update_super_user,
update_user_extension,
@ -34,6 +37,7 @@ from ..models import (
UserExtra,
)
from .settings import update_cached_settings
from .payments import update_wallet_balance
async def create_user_account(
@ -65,11 +69,18 @@ async def create_user_account_no_ckeck(
account.id = uuid4().hex
account = await create_account(account)
await create_wallet(
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}")
user_extensions = (default_exts or []) + settings.lnbits_user_default_extensions
for ext_id in user_extensions:
try:
@ -85,6 +96,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 +216,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 +229,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 +253,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)}")

View file

@ -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):

View file

@ -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
}

213
nix/modules/README.md Normal file
View 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)

View file

@ -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;
};

96
test_nostr_integration.py Normal file
View 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()

View file

@ -293,6 +293,10 @@ async def test_register_ok(http_client: AsyncClient):
assert (
len(user.wallets) == 1
), f"Expected 1 default wallet, not {len(user.wallets)}."
# 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"
@pytest.mark.anyio
@ -586,6 +590,10 @@ async def test_register_nostr_ok(http_client: AsyncClient, settings: Settings):
assert (
len(user.wallets) == 1
), f"Expected 1 default wallet, not {len(user.wallets)}."
# 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"
@pytest.mark.anyio