misc docs/helpers
This commit is contained in:
parent
dffc54c0d2
commit
5983774e22
5 changed files with 792 additions and 0 deletions
88
misc-aio/AUTO_CREDIT_CHANGES.md
Normal file
88
misc-aio/AUTO_CREDIT_CHANGES.md
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
# LNBits Auto-Credit Changes
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Modified LNBits server to automatically credit new accounts with 1 million satoshis (1,000,000 sats) when they are created.
|
||||||
|
|
||||||
|
## Changes Made
|
||||||
|
|
||||||
|
### 1. Modified `lnbits/core/services/users.py`
|
||||||
|
|
||||||
|
**Added imports:**
|
||||||
|
- `get_wallet` from `..crud`
|
||||||
|
- `update_wallet_balance` from `.payments`
|
||||||
|
|
||||||
|
**Modified `create_user_account_no_ckeck` function:**
|
||||||
|
- Changed `create_wallet` call to capture the returned wallet object
|
||||||
|
- Added automatic credit of 1,000,000 sats after wallet creation
|
||||||
|
- Added error handling and logging for the credit operation
|
||||||
|
|
||||||
|
**Code changes:**
|
||||||
|
```python
|
||||||
|
# Before:
|
||||||
|
await create_wallet(
|
||||||
|
user_id=account.id,
|
||||||
|
wallet_name=wallet_name or settings.lnbits_default_wallet_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
# After:
|
||||||
|
wallet = await create_wallet(
|
||||||
|
user_id=account.id,
|
||||||
|
wallet_name=wallet_name or settings.lnbits_default_wallet_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Credit new account with 1 million satoshis
|
||||||
|
try:
|
||||||
|
await update_wallet_balance(wallet, 1_000_000)
|
||||||
|
logger.info(f"Credited new account {account.id} with 1,000,000 sats")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to credit new account {account.id} with 1,000,000 sats: {e}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Updated Tests in `tests/api/test_auth.py`
|
||||||
|
|
||||||
|
**Modified test functions:**
|
||||||
|
- `test_register_ok`: Added balance verification for regular user registration
|
||||||
|
- `test_register_nostr_ok`: Added balance verification for Nostr authentication
|
||||||
|
|
||||||
|
**Added assertions:**
|
||||||
|
```python
|
||||||
|
# Check that the wallet has 1 million satoshis
|
||||||
|
wallet = user.wallets[0]
|
||||||
|
assert wallet.balance == 1_000_000, f"Expected 1,000,000 sats balance, got {wallet.balance} sats"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Affected Account Creation Paths
|
||||||
|
|
||||||
|
The automatic credit will be applied to all new accounts created through:
|
||||||
|
|
||||||
|
1. **Regular user registration** (`/api/v1/auth/register`)
|
||||||
|
2. **Nostr authentication** (`/api/v1/auth/nostr`)
|
||||||
|
3. **SSO login** (when new account is created)
|
||||||
|
4. **API account creation** (`/api/v1/account`)
|
||||||
|
5. **Admin user creation** (via admin interface)
|
||||||
|
|
||||||
|
## Excluded Paths
|
||||||
|
|
||||||
|
- **Superuser/Admin account creation** (`init_admin_settings`): This function creates the admin account directly and bypasses the user creation flow, so it won't receive the automatic credit.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
To test the changes:
|
||||||
|
|
||||||
|
1. Install dependencies: `poetry install`
|
||||||
|
2. Run the modified tests: `poetry run pytest tests/api/test_auth.py::test_register_ok -v`
|
||||||
|
3. Run Nostr test: `poetry run pytest tests/api/test_auth.py::test_register_nostr_ok -v`
|
||||||
|
|
||||||
|
## Logging
|
||||||
|
|
||||||
|
The system will log:
|
||||||
|
- Success: `"Credited new account {account.id} with 1,000,000 sats"`
|
||||||
|
- Failure: `"Failed to credit new account {account.id} with 1,000,000 sats: {error}"`
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The credit uses the existing `update_wallet_balance` function which creates an internal payment record
|
||||||
|
- The credit is applied after wallet creation but before user extensions are set up
|
||||||
|
- Error handling ensures that account creation continues even if the credit fails
|
||||||
|
- The credit amount is hardcoded to 1,000,000 sats (1MM sats)
|
||||||
|
|
||||||
460
misc-aio/lnbits-websocket-guide.md
Normal file
460
misc-aio/lnbits-websocket-guide.md
Normal file
|
|
@ -0,0 +1,460 @@
|
||||||
|
# LNbits WebSocket Implementation Guide
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
LNbits provides real-time WebSocket connections for monitoring wallet status, payment confirmations, and transaction updates. This guide covers how to implement and use these WebSocket connections in your applications.
|
||||||
|
|
||||||
|
## WebSocket Endpoints
|
||||||
|
|
||||||
|
### 1. Payment Monitoring WebSocket
|
||||||
|
- **URL**: `ws://localhost:5006/api/v1/ws/{wallet_inkey}`
|
||||||
|
- **HTTPS**: `wss://your-domain.com/api/v1/ws/{wallet_inkey}`
|
||||||
|
- **Purpose**: Real-time payment notifications and wallet updates
|
||||||
|
|
||||||
|
### 2. Generic WebSocket Communication
|
||||||
|
- **URL**: `ws://localhost:5006/api/v1/ws/{item_id}`
|
||||||
|
- **Purpose**: Custom real-time communication channels
|
||||||
|
|
||||||
|
## Client-Side Implementation
|
||||||
|
|
||||||
|
### JavaScript/Browser Implementation
|
||||||
|
|
||||||
|
#### Basic WebSocket Connection
|
||||||
|
```javascript
|
||||||
|
// Construct WebSocket URL
|
||||||
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||||
|
const websocketUrl = `${protocol}//${window.location.host}/api/v1/ws`
|
||||||
|
|
||||||
|
// Connect to payment monitoring
|
||||||
|
const ws = new WebSocket(`${websocketUrl}/${wallet.inkey}`)
|
||||||
|
|
||||||
|
// Handle incoming messages
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
const data = JSON.parse(event.data)
|
||||||
|
console.log('Received:', data)
|
||||||
|
|
||||||
|
if (data.payment) {
|
||||||
|
handlePaymentReceived(data.payment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle connection events
|
||||||
|
ws.onopen = () => {
|
||||||
|
console.log('WebSocket connected')
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onclose = () => {
|
||||||
|
console.log('WebSocket disconnected')
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onerror = (error) => {
|
||||||
|
console.error('WebSocket error:', error)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Using LNbits Built-in Event System
|
||||||
|
```javascript
|
||||||
|
// Using the built-in LNbits event system
|
||||||
|
LNbits.events.onInvoicePaid(wallet, (data) => {
|
||||||
|
if (data.payment) {
|
||||||
|
console.log('Payment confirmed:', data.payment)
|
||||||
|
|
||||||
|
// Update UI
|
||||||
|
updateWalletBalance(data.payment.amount)
|
||||||
|
showPaymentNotification(data.payment)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Vue.js Implementation Example
|
||||||
|
```javascript
|
||||||
|
// Vue component method
|
||||||
|
initWebSocket() {
|
||||||
|
const protocol = location.protocol === 'http:' ? 'ws://' : 'wss://'
|
||||||
|
const wsUrl = `${protocol}${document.domain}:${location.port}/api/v1/ws/${this.wallet.inkey}`
|
||||||
|
|
||||||
|
this.ws = new WebSocket(wsUrl)
|
||||||
|
|
||||||
|
this.ws.addEventListener('message', async ({ data }) => {
|
||||||
|
const response = JSON.parse(data.toString())
|
||||||
|
|
||||||
|
if (response.payment) {
|
||||||
|
// Handle payment update
|
||||||
|
await this.handlePaymentUpdate(response.payment)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.ws.addEventListener('open', () => {
|
||||||
|
this.connectionStatus = 'connected'
|
||||||
|
})
|
||||||
|
|
||||||
|
this.ws.addEventListener('close', () => {
|
||||||
|
this.connectionStatus = 'disconnected'
|
||||||
|
// Implement reconnection logic
|
||||||
|
setTimeout(() => this.initWebSocket(), 5000)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Python Client Implementation
|
||||||
|
|
||||||
|
```python
|
||||||
|
import asyncio
|
||||||
|
import websockets
|
||||||
|
import json
|
||||||
|
|
||||||
|
async def listen_to_wallet(wallet_inkey, base_url="ws://localhost:5006"):
|
||||||
|
uri = f"{base_url}/api/v1/ws/{wallet_inkey}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with websockets.connect(uri) as websocket:
|
||||||
|
print(f"Connected to WebSocket: {uri}")
|
||||||
|
|
||||||
|
async for message in websocket:
|
||||||
|
data = json.loads(message)
|
||||||
|
|
||||||
|
if 'payment' in data:
|
||||||
|
payment = data['payment']
|
||||||
|
print(f"Payment received: {payment['amount']} sat")
|
||||||
|
print(f"Payment hash: {payment['payment_hash']}")
|
||||||
|
|
||||||
|
# Process payment
|
||||||
|
await handle_payment_received(payment)
|
||||||
|
|
||||||
|
except websockets.exceptions.ConnectionClosed:
|
||||||
|
print("WebSocket connection closed")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"WebSocket error: {e}")
|
||||||
|
|
||||||
|
async def handle_payment_received(payment):
|
||||||
|
"""Process incoming payment"""
|
||||||
|
# Update database
|
||||||
|
# Send notifications
|
||||||
|
# Update application state
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Run the WebSocket listener
|
||||||
|
if __name__ == "__main__":
|
||||||
|
wallet_inkey = "your_wallet_inkey_here"
|
||||||
|
asyncio.run(listen_to_wallet(wallet_inkey))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Node.js Client Implementation
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const WebSocket = require('ws')
|
||||||
|
|
||||||
|
class LNbitsWebSocketClient {
|
||||||
|
constructor(walletInkey, baseUrl = 'ws://localhost:5006') {
|
||||||
|
this.walletInkey = walletInkey
|
||||||
|
this.baseUrl = baseUrl
|
||||||
|
this.ws = null
|
||||||
|
this.reconnectInterval = 5000
|
||||||
|
}
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
const url = `${this.baseUrl}/api/v1/ws/${this.walletInkey}`
|
||||||
|
this.ws = new WebSocket(url)
|
||||||
|
|
||||||
|
this.ws.on('open', () => {
|
||||||
|
console.log(`Connected to LNbits WebSocket: ${url}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.ws.on('message', (data) => {
|
||||||
|
try {
|
||||||
|
const message = JSON.parse(data.toString())
|
||||||
|
this.handleMessage(message)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error parsing WebSocket message:', error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.ws.on('close', () => {
|
||||||
|
console.log('WebSocket connection closed. Reconnecting...')
|
||||||
|
setTimeout(() => this.connect(), this.reconnectInterval)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.ws.on('error', (error) => {
|
||||||
|
console.error('WebSocket error:', error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMessage(message) {
|
||||||
|
if (message.payment) {
|
||||||
|
console.log('Payment received:', message.payment)
|
||||||
|
this.onPaymentReceived(message.payment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onPaymentReceived(payment) {
|
||||||
|
// Override this method to handle payments
|
||||||
|
console.log(`Received ${payment.amount} sat`)
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
if (this.ws) {
|
||||||
|
this.ws.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
const client = new LNbitsWebSocketClient('your_wallet_inkey_here')
|
||||||
|
client.onPaymentReceived = (payment) => {
|
||||||
|
// Custom payment handling
|
||||||
|
console.log(`Processing payment: ${payment.payment_hash}`)
|
||||||
|
}
|
||||||
|
client.connect()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Server-Side Implementation (LNbits Extensions)
|
||||||
|
|
||||||
|
### Sending WebSocket Updates
|
||||||
|
|
||||||
|
```python
|
||||||
|
from lnbits.core.services import websocket_manager
|
||||||
|
|
||||||
|
async def notify_wallet_update(wallet_inkey: str, payment_data: dict):
|
||||||
|
"""Send payment update to connected WebSocket clients"""
|
||||||
|
message = {
|
||||||
|
"payment": payment_data,
|
||||||
|
"timestamp": int(time.time())
|
||||||
|
}
|
||||||
|
|
||||||
|
await websocket_manager.send(wallet_inkey, json.dumps(message))
|
||||||
|
|
||||||
|
# Example usage in payment processing
|
||||||
|
async def process_payment_confirmation(payment_hash: str):
|
||||||
|
payment = await get_payment(payment_hash)
|
||||||
|
|
||||||
|
if payment.wallet:
|
||||||
|
await notify_wallet_update(payment.wallet, {
|
||||||
|
"payment_hash": payment.payment_hash,
|
||||||
|
"amount": payment.amount,
|
||||||
|
"memo": payment.memo,
|
||||||
|
"status": "confirmed"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### HTTP Endpoints for WebSocket Updates
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Send data via GET request
|
||||||
|
@router.get("/notify/{wallet_inkey}/{message}")
|
||||||
|
async def notify_wallet_get(wallet_inkey: str, message: str):
|
||||||
|
await websocket_manager.send(wallet_inkey, message)
|
||||||
|
return {"sent": True, "message": message}
|
||||||
|
|
||||||
|
# Send data via POST request
|
||||||
|
@router.post("/notify/{wallet_inkey}")
|
||||||
|
async def notify_wallet_post(wallet_inkey: str, data: str):
|
||||||
|
await websocket_manager.send(wallet_inkey, data)
|
||||||
|
return {"sent": True, "data": data}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Message Format
|
||||||
|
|
||||||
|
### Payment Notification Message
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"payment": {
|
||||||
|
"payment_hash": "abc123...",
|
||||||
|
"amount": 1000,
|
||||||
|
"memo": "Test payment",
|
||||||
|
"status": "confirmed",
|
||||||
|
"timestamp": 1640995200,
|
||||||
|
"fee": 1,
|
||||||
|
"wallet_id": "wallet_uuid"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Message Format
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "balance_update",
|
||||||
|
"wallet_id": "wallet_uuid",
|
||||||
|
"balance": 50000,
|
||||||
|
"timestamp": 1640995200
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### 1. Connection Management
|
||||||
|
- Implement automatic reconnection logic
|
||||||
|
- Handle connection timeouts gracefully
|
||||||
|
- Use exponential backoff for reconnection attempts
|
||||||
|
|
||||||
|
### 2. Error Handling
|
||||||
|
```javascript
|
||||||
|
class WebSocketManager {
|
||||||
|
constructor(walletInkey) {
|
||||||
|
this.walletInkey = walletInkey
|
||||||
|
this.maxReconnectAttempts = 10
|
||||||
|
this.reconnectAttempts = 0
|
||||||
|
this.reconnectDelay = 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
try {
|
||||||
|
this.ws = new WebSocket(this.getWebSocketUrl())
|
||||||
|
this.setupEventHandlers()
|
||||||
|
} catch (error) {
|
||||||
|
this.handleConnectionError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleConnectionError(error) {
|
||||||
|
console.error('WebSocket connection error:', error)
|
||||||
|
|
||||||
|
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
||||||
|
this.reconnectAttempts++
|
||||||
|
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1)
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log(`Reconnection attempt ${this.reconnectAttempts}`)
|
||||||
|
this.connect()
|
||||||
|
}, delay)
|
||||||
|
} else {
|
||||||
|
console.error('Max reconnection attempts reached')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Message Validation
|
||||||
|
```javascript
|
||||||
|
function validatePaymentMessage(data) {
|
||||||
|
if (!data.payment) return false
|
||||||
|
|
||||||
|
const payment = data.payment
|
||||||
|
return (
|
||||||
|
typeof payment.payment_hash === 'string' &&
|
||||||
|
typeof payment.amount === 'number' &&
|
||||||
|
payment.amount > 0 &&
|
||||||
|
['pending', 'confirmed', 'failed'].includes(payment.status)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Security Considerations
|
||||||
|
- Use HTTPS/WSS in production
|
||||||
|
- Validate wallet permissions before connecting
|
||||||
|
- Implement rate limiting for WebSocket connections
|
||||||
|
- Never expose admin keys through WebSocket messages
|
||||||
|
|
||||||
|
## Testing WebSocket Connections
|
||||||
|
|
||||||
|
### Using wscat (Command Line Tool)
|
||||||
|
```bash
|
||||||
|
# Install wscat
|
||||||
|
npm install -g wscat
|
||||||
|
|
||||||
|
# Connect to WebSocket
|
||||||
|
wscat -c ws://localhost:5006/api/v1/ws/your_wallet_inkey
|
||||||
|
|
||||||
|
# Test with SSL
|
||||||
|
wscat -c wss://your-domain.com/api/v1/ws/your_wallet_inkey
|
||||||
|
```
|
||||||
|
|
||||||
|
### Browser Console Testing
|
||||||
|
```javascript
|
||||||
|
// Open browser console and run:
|
||||||
|
const ws = new WebSocket('ws://localhost:5006/api/v1/ws/your_wallet_inkey')
|
||||||
|
ws.onmessage = (e) => console.log('Received:', JSON.parse(e.data))
|
||||||
|
ws.onopen = () => console.log('Connected')
|
||||||
|
ws.onclose = () => console.log('Disconnected')
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sending Test Messages
|
||||||
|
```bash
|
||||||
|
# Using curl to trigger WebSocket message
|
||||||
|
curl "http://localhost:5006/api/v1/ws/your_wallet_inkey/test_message"
|
||||||
|
|
||||||
|
# Using POST
|
||||||
|
curl -X POST "http://localhost:5006/api/v1/ws/your_wallet_inkey" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '"test message data"'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
1. **Connection Refused**
|
||||||
|
- Verify LNbits server is running on correct port
|
||||||
|
- Check firewall settings
|
||||||
|
- Ensure WebSocket endpoint is enabled
|
||||||
|
|
||||||
|
2. **Authentication Errors**
|
||||||
|
- Verify wallet inkey is correct
|
||||||
|
- Check wallet permissions
|
||||||
|
- Ensure wallet exists and is active
|
||||||
|
|
||||||
|
3. **Message Not Received**
|
||||||
|
- Check WebSocket connection status
|
||||||
|
- Verify message format
|
||||||
|
- Test with browser dev tools
|
||||||
|
|
||||||
|
4. **Frequent Disconnections**
|
||||||
|
- Implement proper reconnection logic
|
||||||
|
- Check network stability
|
||||||
|
- Monitor server logs for errors
|
||||||
|
|
||||||
|
### Debug Logging
|
||||||
|
```javascript
|
||||||
|
// Enable verbose WebSocket logging
|
||||||
|
const ws = new WebSocket(wsUrl)
|
||||||
|
ws.addEventListener('open', (event) => {
|
||||||
|
console.log('WebSocket opened:', event)
|
||||||
|
})
|
||||||
|
ws.addEventListener('close', (event) => {
|
||||||
|
console.log('WebSocket closed:', event.code, event.reason)
|
||||||
|
})
|
||||||
|
ws.addEventListener('error', (event) => {
|
||||||
|
console.error('WebSocket error:', event)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Production Deployment
|
||||||
|
|
||||||
|
### Nginx Configuration
|
||||||
|
```nginx
|
||||||
|
location /api/v1/ws/ {
|
||||||
|
proxy_pass http://localhost:5006;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_read_timeout 86400;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### SSL/TLS Configuration
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 443 ssl http2;
|
||||||
|
server_name your-domain.com;
|
||||||
|
|
||||||
|
ssl_certificate /path/to/cert.pem;
|
||||||
|
ssl_certificate_key /path/to/key.pem;
|
||||||
|
|
||||||
|
location /api/v1/ws/ {
|
||||||
|
proxy_pass http://localhost:5006;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
# ... other headers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
LNbits WebSocket implementation provides a robust foundation for real-time wallet monitoring and payment processing. By following this guide, you can implement reliable WebSocket connections that enhance user experience with instant payment notifications and live wallet updates.
|
||||||
|
|
||||||
|
Remember to implement proper error handling, reconnection logic, and security measures when deploying to production environments.
|
||||||
BIN
misc-aio/lnbits-websocket-guide.pdf
Normal file
BIN
misc-aio/lnbits-websocket-guide.pdf
Normal file
Binary file not shown.
139
misc-aio/publish_profiles_from_csv.py
Normal file
139
misc-aio/publish_profiles_from_csv.py
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Bulk publish Nostr profiles from CSV file
|
||||||
|
Usage: python publish_profiles_from_csv.py <csv_file> <relay_url>
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import csv
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import hashlib
|
||||||
|
import secp256k1
|
||||||
|
from websocket import create_connection
|
||||||
|
|
||||||
|
def sign_event(event, public_key_hex, private_key):
|
||||||
|
"""Sign a Nostr event"""
|
||||||
|
# Create the signature data
|
||||||
|
signature_data = json.dumps([
|
||||||
|
0,
|
||||||
|
public_key_hex,
|
||||||
|
event["created_at"],
|
||||||
|
event["kind"],
|
||||||
|
event["tags"],
|
||||||
|
event["content"]
|
||||||
|
], separators=(',', ':'), ensure_ascii=False)
|
||||||
|
|
||||||
|
# Calculate event ID
|
||||||
|
event_id = hashlib.sha256(signature_data.encode()).hexdigest()
|
||||||
|
event["id"] = event_id
|
||||||
|
event["pubkey"] = public_key_hex
|
||||||
|
|
||||||
|
# Sign the event
|
||||||
|
signature = private_key.schnorr_sign(bytes.fromhex(event_id), None, raw=True).hex()
|
||||||
|
event["sig"] = signature
|
||||||
|
|
||||||
|
return event
|
||||||
|
|
||||||
|
def publish_profile_metadata(private_key_hex, profile_name, relay_url):
|
||||||
|
"""Publish a Nostr kind 0 metadata event"""
|
||||||
|
try:
|
||||||
|
# Convert hex private key to secp256k1 PrivateKey and get public key
|
||||||
|
private_key = secp256k1.PrivateKey(bytes.fromhex(private_key_hex))
|
||||||
|
public_key_hex = private_key.pubkey.serialize()[1:].hex() # Remove the 0x02 prefix
|
||||||
|
|
||||||
|
print(f"Publishing profile for: {profile_name}")
|
||||||
|
print(f" Public key: {public_key_hex}")
|
||||||
|
|
||||||
|
# Create Nostr kind 0 metadata event
|
||||||
|
metadata = {
|
||||||
|
"name": profile_name,
|
||||||
|
"display_name": profile_name,
|
||||||
|
"about": f"Profile for {profile_name}"
|
||||||
|
}
|
||||||
|
|
||||||
|
event = {
|
||||||
|
"kind": 0,
|
||||||
|
"created_at": int(time.time()),
|
||||||
|
"tags": [],
|
||||||
|
"content": json.dumps(metadata, separators=(',', ':'))
|
||||||
|
}
|
||||||
|
|
||||||
|
# Sign the event
|
||||||
|
signed_event = sign_event(event, public_key_hex, private_key)
|
||||||
|
|
||||||
|
# Connect to relay and publish
|
||||||
|
ws = create_connection(relay_url, timeout=15)
|
||||||
|
|
||||||
|
# Send the event
|
||||||
|
event_message = f'["EVENT",{json.dumps(signed_event)}]'
|
||||||
|
ws.send(event_message)
|
||||||
|
|
||||||
|
# Wait for response
|
||||||
|
try:
|
||||||
|
response = ws.recv()
|
||||||
|
print(f" ✅ Published successfully: {response}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ⚠️ No immediate response: {e}")
|
||||||
|
|
||||||
|
# Close connection
|
||||||
|
ws.close()
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ❌ Failed to publish: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) != 3:
|
||||||
|
print("Usage: python publish_profiles_from_csv.py <csv_file> <relay_url>")
|
||||||
|
print("Example: python publish_profiles_from_csv.py publish-these.csv wss://relay.example.com")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
csv_file = sys.argv[1]
|
||||||
|
relay_url = sys.argv[2]
|
||||||
|
|
||||||
|
print(f"Publishing profiles from {csv_file} to {relay_url}")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
published_count = 0
|
||||||
|
failed_count = 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(csv_file, 'r') as file:
|
||||||
|
csv_reader = csv.DictReader(file)
|
||||||
|
|
||||||
|
for row_num, row in enumerate(csv_reader, start=2): # start=2 because header is line 1
|
||||||
|
username = row['username'].strip()
|
||||||
|
private_key_hex = row['prvkey'].strip()
|
||||||
|
|
||||||
|
if not username or not private_key_hex:
|
||||||
|
print(f"Row {row_num}: Skipping empty row")
|
||||||
|
continue
|
||||||
|
|
||||||
|
print(f"\nRow {row_num}: Processing {username}")
|
||||||
|
|
||||||
|
success = publish_profile_metadata(private_key_hex, username, relay_url)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
published_count += 1
|
||||||
|
else:
|
||||||
|
failed_count += 1
|
||||||
|
|
||||||
|
# Small delay between publishes to be nice to the relay
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
except FileNotFoundError:
|
||||||
|
print(f"❌ File not found: {csv_file}")
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error processing CSV: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print(f"Publishing complete!")
|
||||||
|
print(f"✅ Successfully published: {published_count}")
|
||||||
|
print(f"❌ Failed: {failed_count}")
|
||||||
|
print(f"📊 Total processed: {published_count + failed_count}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
105
misc-aio/test_nostr_connection.py
Normal file
105
misc-aio/test_nostr_connection.py
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Manual Nostr profile metadata publisher
|
||||||
|
Usage: python test_nostr_connection.py <private_key_hex> <profile_name> <relay_url>
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import hashlib
|
||||||
|
import secp256k1
|
||||||
|
from websocket import create_connection
|
||||||
|
|
||||||
|
def sign_event(event, public_key_hex, private_key):
|
||||||
|
"""Sign a Nostr event"""
|
||||||
|
# Create the signature data
|
||||||
|
signature_data = json.dumps([
|
||||||
|
0,
|
||||||
|
public_key_hex,
|
||||||
|
event["created_at"],
|
||||||
|
event["kind"],
|
||||||
|
event["tags"],
|
||||||
|
event["content"]
|
||||||
|
], separators=(',', ':'), ensure_ascii=False)
|
||||||
|
|
||||||
|
# Calculate event ID
|
||||||
|
event_id = hashlib.sha256(signature_data.encode()).hexdigest()
|
||||||
|
event["id"] = event_id
|
||||||
|
event["pubkey"] = public_key_hex
|
||||||
|
|
||||||
|
# Sign the event
|
||||||
|
signature = private_key.schnorr_sign(bytes.fromhex(event_id), None, raw=True).hex()
|
||||||
|
event["sig"] = signature
|
||||||
|
|
||||||
|
return event
|
||||||
|
|
||||||
|
def publish_profile_metadata(private_key_hex, profile_name, relay_url):
|
||||||
|
"""Publish a Nostr kind 0 metadata event"""
|
||||||
|
try:
|
||||||
|
# Convert hex private key to secp256k1 PrivateKey and get public key
|
||||||
|
private_key = secp256k1.PrivateKey(bytes.fromhex(private_key_hex))
|
||||||
|
public_key_hex = private_key.pubkey.serialize()[1:].hex() # Remove the 0x02 prefix
|
||||||
|
|
||||||
|
print(f"Private key: {private_key_hex}")
|
||||||
|
print(f"Public key: {public_key_hex}")
|
||||||
|
print(f"Profile name: {profile_name}")
|
||||||
|
print(f"Relay URL: {relay_url}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Create Nostr kind 0 metadata event
|
||||||
|
metadata = {
|
||||||
|
"name": profile_name,
|
||||||
|
"display_name": profile_name,
|
||||||
|
"about": f"Manual profile update for {profile_name}"
|
||||||
|
}
|
||||||
|
|
||||||
|
event = {
|
||||||
|
"kind": 0,
|
||||||
|
"created_at": int(time.time()),
|
||||||
|
"tags": [],
|
||||||
|
"content": json.dumps(metadata, separators=(',', ':'))
|
||||||
|
}
|
||||||
|
|
||||||
|
# Sign the event
|
||||||
|
signed_event = sign_event(event, public_key_hex, private_key)
|
||||||
|
|
||||||
|
print(f"Signed event: {json.dumps(signed_event, indent=2)}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Connect to relay and publish
|
||||||
|
print(f"Connecting to relay: {relay_url}")
|
||||||
|
ws = create_connection(relay_url, timeout=15)
|
||||||
|
print("✅ Connected successfully!")
|
||||||
|
|
||||||
|
# Send the event
|
||||||
|
event_message = f'["EVENT",{json.dumps(signed_event)}]'
|
||||||
|
print(f"Sending EVENT: {event_message}")
|
||||||
|
ws.send(event_message)
|
||||||
|
|
||||||
|
# Wait for response
|
||||||
|
try:
|
||||||
|
response = ws.recv()
|
||||||
|
print(f"✅ Relay response: {response}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ No immediate response: {e}")
|
||||||
|
|
||||||
|
# Close connection
|
||||||
|
ws.close()
|
||||||
|
print("✅ Connection closed successfully")
|
||||||
|
print(f"Profile metadata for '{profile_name}' published successfully!")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Failed to publish profile: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) != 4:
|
||||||
|
print("Usage: python test_nostr_connection.py <private_key_hex> <profile_name> <relay_url>")
|
||||||
|
print("Example: python test_nostr_connection.py abc123... 'My Name' wss://relay.example.com")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
private_key_hex = sys.argv[1]
|
||||||
|
profile_name = sys.argv[2]
|
||||||
|
relay_url = sys.argv[3]
|
||||||
|
|
||||||
|
publish_profile_metadata(private_key_hex, profile_name, relay_url)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue