diff --git a/misc-aio/AUTO_CREDIT_CHANGES.md b/misc-aio/AUTO_CREDIT_CHANGES.md new file mode 100644 index 00000000..408f6987 --- /dev/null +++ b/misc-aio/AUTO_CREDIT_CHANGES.md @@ -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) + diff --git a/misc-aio/lnbits-websocket-guide.md b/misc-aio/lnbits-websocket-guide.md new file mode 100644 index 00000000..d6662b1d --- /dev/null +++ b/misc-aio/lnbits-websocket-guide.md @@ -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. \ No newline at end of file diff --git a/misc-aio/lnbits-websocket-guide.pdf b/misc-aio/lnbits-websocket-guide.pdf new file mode 100644 index 00000000..38fb0666 Binary files /dev/null and b/misc-aio/lnbits-websocket-guide.pdf differ diff --git a/misc-aio/publish_profiles_from_csv.py b/misc-aio/publish_profiles_from_csv.py new file mode 100644 index 00000000..b33ab666 --- /dev/null +++ b/misc-aio/publish_profiles_from_csv.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 +""" +Bulk publish Nostr profiles from CSV file +Usage: python publish_profiles_from_csv.py +""" +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 ") + 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() \ No newline at end of file diff --git a/misc-aio/test_nostr_connection.py b/misc-aio/test_nostr_connection.py new file mode 100644 index 00000000..d00b75b7 --- /dev/null +++ b/misc-aio/test_nostr_connection.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +""" +Manual Nostr profile metadata publisher +Usage: python test_nostr_connection.py +""" +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 ") + 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) \ No newline at end of file