mirror of
https://github.com/Routstr/routstr-core.git
synced 2026-07-22 12:22:20 +00:00
Compare commits
4 Commits
custom-mod
...
fix-withdr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aced8737f5 | ||
|
|
27de0921ae | ||
|
|
a48d8f102c | ||
|
|
7cb6dfa774 |
134
WITHDRAWAL_FIX.md
Normal file
134
WITHDRAWAL_FIX.md
Normal file
@@ -0,0 +1,134 @@
|
||||
# Withdrawal Token Persistence Fix
|
||||
|
||||
This document describes the fix for the admin panel withdrawal feature that was causing coin loss due to non-persistent cashu tokens.
|
||||
|
||||
## Problem
|
||||
|
||||
The original admin panel withdrawal feature had a critical issue:
|
||||
- Generated cashu tokens were only displayed momentarily in the UI
|
||||
- If the modal was closed accidentally, the token was lost forever
|
||||
- No record of pending withdrawals was maintained
|
||||
- This resulted in actual coin loss for administrators
|
||||
|
||||
## Solution
|
||||
|
||||
### 1. Token Persistence
|
||||
- Added `PendingWithdrawal` database model to store all withdrawal tokens
|
||||
- Every withdrawal is now automatically saved to the database
|
||||
- Tokens are never lost, even if the UI is closed accidentally
|
||||
|
||||
### 2. Pending Withdrawals Display
|
||||
- New "Pending Withdrawals" section in the admin panel
|
||||
- Shows all unclaimed withdrawal tokens with details:
|
||||
- Amount and currency unit
|
||||
- Mint URL
|
||||
- Creation timestamp
|
||||
- Auto-send status
|
||||
- Lightning address (if auto-sent)
|
||||
- Easy copy-to-clipboard functionality for tokens
|
||||
- Ability to mark withdrawals as claimed when used
|
||||
|
||||
### 3. Automatic Lightning Address Sending
|
||||
- Optional auto-send feature for withdrawals
|
||||
- If `RECEIVE_LN_ADDRESS` is configured and auto-send is enabled:
|
||||
- Tokens are automatically sent to the Lightning address
|
||||
- No manual token handling required
|
||||
- Withdrawal is marked as completed automatically
|
||||
- Fallback to manual token storage if auto-send fails
|
||||
|
||||
### 4. Enhanced UI/UX
|
||||
- Checkbox option to enable auto-send during withdrawal
|
||||
- Better feedback messages for successful operations
|
||||
- Real-time updates of pending withdrawals list
|
||||
- Improved error handling and user notifications
|
||||
|
||||
## Database Changes
|
||||
|
||||
### New Table: `pending_withdrawals`
|
||||
```sql
|
||||
CREATE TABLE pending_withdrawals (
|
||||
id INTEGER PRIMARY KEY,
|
||||
token TEXT NOT NULL,
|
||||
amount INTEGER NOT NULL,
|
||||
unit TEXT NOT NULL,
|
||||
mint_url TEXT NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
claimed BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
auto_sent BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
ln_address TEXT,
|
||||
notes TEXT
|
||||
);
|
||||
```
|
||||
|
||||
## API Changes
|
||||
|
||||
### Enhanced `/admin/withdraw` endpoint
|
||||
- New optional parameter: `auto_send_to_ln: bool`
|
||||
- Returns additional fields:
|
||||
- `withdrawal_id`: Database ID of the stored withdrawal
|
||||
- `auto_sent`: Whether the token was automatically sent
|
||||
- `ln_address`: Lightning address used (if auto-sent)
|
||||
- `amount_received`: Amount received by Lightning address
|
||||
|
||||
### New endpoint: `/admin/api/mark-withdrawal-claimed/{withdrawal_id}`
|
||||
- POST endpoint to mark a withdrawal as claimed
|
||||
- Prevents accidental reuse of tokens
|
||||
- Helps track withdrawal status
|
||||
|
||||
### New partial: `/admin/partials/pending-withdrawals`
|
||||
- HTMX-powered component for displaying pending withdrawals
|
||||
- Auto-refreshes when new withdrawals are created
|
||||
- Provides interactive token management
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
The fix uses existing configuration:
|
||||
- `RECEIVE_LN_ADDRESS`: Lightning address for auto-send functionality
|
||||
- `ADMIN_PASSWORD`: Required for admin panel access
|
||||
|
||||
### Settings
|
||||
Auto-send functionality respects the existing `receive_ln_address` setting that can be configured through the admin panel settings interface.
|
||||
|
||||
## Usage
|
||||
|
||||
### Manual Withdrawal (Default)
|
||||
1. Click "💸 Withdraw Balance" in admin panel
|
||||
2. Select mint and currency
|
||||
3. Enter withdrawal amount
|
||||
4. Click "Withdraw"
|
||||
5. Token is generated and stored in pending withdrawals
|
||||
6. Copy token from the result or from pending withdrawals list
|
||||
7. Use token in your cashu wallet
|
||||
8. Mark as claimed when used (optional)
|
||||
|
||||
### Auto-Send Withdrawal
|
||||
1. Ensure `RECEIVE_LN_ADDRESS` is configured in settings
|
||||
2. Click "💸 Withdraw Balance" in admin panel
|
||||
3. Select mint and currency
|
||||
4. Enter withdrawal amount
|
||||
5. Check "Auto-send to Lightning address" checkbox
|
||||
6. Click "Withdraw"
|
||||
7. Funds are automatically sent to your Lightning address
|
||||
8. Withdrawal is marked as completed automatically
|
||||
|
||||
## Migration
|
||||
|
||||
The database migration `123abc456def_add_pending_withdrawals_table.py` will be automatically applied when the application starts, creating the new `pending_withdrawals` table.
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **No More Coin Loss**: All withdrawal tokens are permanently stored
|
||||
2. **Better UX**: Clear visibility of all pending withdrawals
|
||||
3. **Automation**: Optional auto-send reduces manual token handling
|
||||
4. **Audit Trail**: Complete history of all withdrawals
|
||||
5. **Recovery**: Ability to retrieve tokens even after UI accidents
|
||||
6. **Flexibility**: Choice between manual tokens and automatic Lightning sends
|
||||
|
||||
## Backward Compatibility
|
||||
|
||||
This fix is fully backward compatible:
|
||||
- Existing withdrawal functionality continues to work
|
||||
- No breaking changes to existing APIs
|
||||
- New features are opt-in
|
||||
- Database migration is automatic and safe
|
||||
@@ -0,0 +1,40 @@
|
||||
"""Add pending withdrawals table
|
||||
|
||||
Revision ID: 123abc456def
|
||||
Revises: 898f00ea481e
|
||||
Create Date: 2025-10-17 04:00:00.000000
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import sqlite
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '123abc456def'
|
||||
down_revision = '898f00ea481e'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('pending_withdrawals',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('token', sa.String(), nullable=False),
|
||||
sa.Column('amount', sa.Integer(), nullable=False),
|
||||
sa.Column('unit', sa.String(), nullable=False),
|
||||
sa.Column('mint_url', sa.String(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('claimed', sa.Boolean(), nullable=False),
|
||||
sa.Column('auto_sent', sa.Boolean(), nullable=False),
|
||||
sa.Column('ln_address', sa.String(), nullable=True),
|
||||
sa.Column('notes', sa.String(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('pending_withdrawals')
|
||||
# ### end Alembic commands ###
|
||||
@@ -6,7 +6,7 @@ from pathlib import Path
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
from pydantic import BaseModel
|
||||
from sqlmodel import select
|
||||
from sqlmodel import select, desc
|
||||
|
||||
from ..wallet import (
|
||||
fetch_all_balances,
|
||||
@@ -14,8 +14,9 @@ from ..wallet import (
|
||||
get_wallet,
|
||||
send_token,
|
||||
slow_filter_spend_proofs,
|
||||
send_to_lnurl,
|
||||
)
|
||||
from .db import ApiKey, create_session
|
||||
from .db import ApiKey, PendingWithdrawal, create_session
|
||||
from .logging import get_logger
|
||||
from .settings import SettingsService, settings
|
||||
|
||||
@@ -127,6 +128,64 @@ async def partial_apikeys(request: Request) -> str:
|
||||
"""
|
||||
|
||||
|
||||
@admin_router.get(
|
||||
"/partials/pending-withdrawals",
|
||||
dependencies=[Depends(require_admin_api)],
|
||||
response_class=HTMLResponse,
|
||||
)
|
||||
async def partial_pending_withdrawals(request: Request) -> str:
|
||||
async with create_session() as session:
|
||||
result = await session.exec(
|
||||
select(PendingWithdrawal)
|
||||
.where(PendingWithdrawal.claimed == False)
|
||||
.order_by(desc(PendingWithdrawal.created_at))
|
||||
.limit(20)
|
||||
)
|
||||
pending_withdrawals = result.all()
|
||||
|
||||
if not pending_withdrawals:
|
||||
return """
|
||||
<h2>Pending Withdrawals</h2>
|
||||
<p style="color: #718096; font-style: italic;">No pending withdrawals</p>
|
||||
"""
|
||||
|
||||
rows = "".join(
|
||||
[
|
||||
f"""<tr>
|
||||
<td>{withdrawal.amount} {withdrawal.unit.upper()}</td>
|
||||
<td>{withdrawal.mint_url.replace("https://", "").replace("http://", "")}</td>
|
||||
<td>{withdrawal.created_at.strftime('%Y-%m-%d %H:%M:%S')} UTC</td>
|
||||
<td>{"✅ Auto-sent" if withdrawal.auto_sent else "⏳ Manual"}</td>
|
||||
<td>{withdrawal.ln_address or "-"}</td>
|
||||
<td>
|
||||
<button class="copy-token-btn" onclick="copyWithdrawalToken('{withdrawal.token}')">📋 Copy Token</button>
|
||||
<button class="claim-btn" onclick="markWithdrawalClaimed({withdrawal.id})">✅ Mark Claimed</button>
|
||||
</td>
|
||||
</tr>"""
|
||||
for withdrawal in pending_withdrawals
|
||||
]
|
||||
)
|
||||
|
||||
return f"""
|
||||
<h2>Pending Withdrawals</h2>
|
||||
<p style="margin-bottom: 1rem; font-size: 0.9rem; color: #718096;">
|
||||
These are cashu tokens that have been generated but not yet claimed.
|
||||
Copy the token to use it, or mark as claimed when used.
|
||||
</p>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Amount</th>
|
||||
<th>Mint</th>
|
||||
<th>Created</th>
|
||||
<th>Status</th>
|
||||
<th>LN Address</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
{rows}
|
||||
</table>
|
||||
"""
|
||||
|
||||
|
||||
@admin_router.get("/api/balances", dependencies=[Depends(require_admin_api)])
|
||||
async def get_balances_api(request: Request) -> list[dict[str, object]]:
|
||||
balance_details, _tw, _tu, _ow = await fetch_all_balances()
|
||||
@@ -167,6 +226,30 @@ class WithdrawRequest(BaseModel):
|
||||
amount: int
|
||||
mint_url: str | None = None
|
||||
unit: str = "sat"
|
||||
auto_send_to_ln: bool = False
|
||||
|
||||
|
||||
@admin_router.post("/api/mark-withdrawal-claimed/{withdrawal_id}", dependencies=[Depends(require_admin_api)])
|
||||
async def mark_withdrawal_claimed(withdrawal_id: int) -> dict[str, str]:
|
||||
async with create_session() as session:
|
||||
result = await session.exec(
|
||||
select(PendingWithdrawal).where(PendingWithdrawal.id == withdrawal_id)
|
||||
)
|
||||
withdrawal = result.first()
|
||||
|
||||
if not withdrawal:
|
||||
raise HTTPException(status_code=404, detail="Withdrawal not found")
|
||||
|
||||
withdrawal.claimed = True
|
||||
session.add(withdrawal)
|
||||
await session.commit()
|
||||
|
||||
logger.info(
|
||||
"Withdrawal marked as claimed",
|
||||
extra={"withdrawal_id": withdrawal_id, "amount": withdrawal.amount, "unit": withdrawal.unit}
|
||||
)
|
||||
|
||||
return {"status": "success", "message": "Withdrawal marked as claimed"}
|
||||
|
||||
|
||||
def login_form() -> str:
|
||||
@@ -325,6 +408,7 @@ async def dashboard(request: Request) -> str:
|
||||
const amount = parseInt(document.getElementById('withdraw-amount').value);
|
||||
const select = document.getElementById('mint-unit-select');
|
||||
const selectedValue = select.value;
|
||||
const autoSendCheckbox = document.getElementById('auto-send-ln');
|
||||
const button = document.getElementById('confirm-withdraw-btn');
|
||||
const tokenResult = document.getElementById('token-result');
|
||||
|
||||
@@ -359,15 +443,23 @@ async def dashboard(request: Request) -> str:
|
||||
body: JSON.stringify({
|
||||
amount: amount,
|
||||
mint_url: mint,
|
||||
unit: unit
|
||||
unit: unit,
|
||||
auto_send_to_ln: autoSendCheckbox.checked
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
document.getElementById('token-text').textContent = data.token;
|
||||
tokenResult.style.display = 'block';
|
||||
closeWithdrawModal();
|
||||
if (data.auto_sent) {
|
||||
alert(`Successfully sent ${amount} ${unit} to ${data.ln_address}!`);
|
||||
closeWithdrawModal();
|
||||
refreshPendingWithdrawals();
|
||||
} else {
|
||||
document.getElementById('token-text').textContent = data.token;
|
||||
tokenResult.style.display = 'block';
|
||||
closeWithdrawModal();
|
||||
refreshPendingWithdrawals();
|
||||
}
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
alert('Failed to withdraw balance: ' + (errorData.detail || 'Unknown error'));
|
||||
@@ -394,6 +486,44 @@ async def dashboard(request: Request) -> str:
|
||||
});
|
||||
}
|
||||
|
||||
function copyWithdrawalToken(token) {
|
||||
navigator.clipboard.writeText(token).then(() => {
|
||||
alert('Token copied to clipboard!');
|
||||
}).catch(err => {
|
||||
alert('Failed to copy token');
|
||||
});
|
||||
}
|
||||
|
||||
async function markWithdrawalClaimed(withdrawalId) {
|
||||
if (!confirm('Mark this withdrawal as claimed? This action cannot be undone.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/admin/api/mark-withdrawal-claimed/${withdrawalId}`, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert('Withdrawal marked as claimed');
|
||||
refreshPendingWithdrawals();
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
alert('Failed to mark withdrawal as claimed: ' + (errorData.detail || 'Unknown error'));
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Error: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
function refreshPendingWithdrawals() {
|
||||
const container = document.getElementById('pending-withdrawals-container');
|
||||
if (container) {
|
||||
htmx.trigger(container, 'refresh');
|
||||
}
|
||||
}
|
||||
|
||||
function refreshPage() {
|
||||
window.location.reload();
|
||||
}
|
||||
@@ -546,6 +676,12 @@ async def dashboard(request: Request) -> str:
|
||||
<div id="withdraw-warning" class="warning" style="display: none;">
|
||||
⚠️ Warning: Withdrawing more than your balance will use user funds!
|
||||
</div>
|
||||
<div style="margin: 10px 0;">
|
||||
<label>
|
||||
<input type="checkbox" id="auto-send-ln">
|
||||
Auto-send to Lightning address (if configured)
|
||||
</label>
|
||||
</div>
|
||||
<button id="confirm-withdraw-btn" onclick="performWithdraw()">💸 Withdraw</button>
|
||||
<button onclick="closeWithdrawModal()" style="background-color: #718096;">Cancel</button>
|
||||
</div>
|
||||
@@ -580,7 +716,15 @@ async def dashboard(request: Request) -> str:
|
||||
<strong>Withdrawal Token:</strong>
|
||||
<div id="token-text"></div>
|
||||
<button id="copy-btn" class="copy-btn" onclick="copyToken()">Copy Token</button>
|
||||
<p><em>Save this token! It represents your withdrawn balance.</em></p>
|
||||
<p><em>Save this token! It represents your withdrawn balance and is now stored in pending withdrawals below.</em></p>
|
||||
</div>
|
||||
|
||||
<div id="pending-withdrawals-container"
|
||||
hx-get="/admin/partials/pending-withdrawals"
|
||||
hx-trigger="load, refresh"
|
||||
hx-swap="innerHTML">
|
||||
<h2>Pending Withdrawals</h2>
|
||||
<div style="color:#718096;">Loading pending withdrawals…</div>
|
||||
</div>
|
||||
|
||||
<div id="apikeys-table"
|
||||
@@ -720,7 +864,7 @@ async def view_logs(request: Request, request_id: str) -> str:
|
||||
@admin_router.post("/withdraw", dependencies=[Depends(require_admin_api)])
|
||||
async def withdraw(
|
||||
request: Request, withdraw_request: WithdrawRequest
|
||||
) -> dict[str, str]:
|
||||
) -> dict[str, object]:
|
||||
# Get wallet and check balance
|
||||
from .settings import settings as global_settings
|
||||
|
||||
@@ -744,10 +888,91 @@ async def withdraw(
|
||||
if withdraw_request.amount > current_balance:
|
||||
raise HTTPException(status_code=400, detail="Insufficient wallet balance")
|
||||
|
||||
# Generate the token
|
||||
token = await send_token(
|
||||
withdraw_request.amount, withdraw_request.unit, withdraw_request.mint_url
|
||||
)
|
||||
return {"token": token}
|
||||
|
||||
# Store the withdrawal in the database
|
||||
async with create_session() as session:
|
||||
pending_withdrawal = PendingWithdrawal(
|
||||
token=token,
|
||||
amount=withdraw_request.amount,
|
||||
unit=withdraw_request.unit,
|
||||
mint_url=withdraw_request.mint_url or global_settings.primary_mint,
|
||||
claimed=False,
|
||||
auto_sent=False,
|
||||
ln_address=None,
|
||||
notes=None
|
||||
)
|
||||
|
||||
# If auto-send is requested and LN address is configured, try to send
|
||||
if withdraw_request.auto_send_to_ln and global_settings.receive_ln_address:
|
||||
try:
|
||||
amount_received = await send_to_lnurl(
|
||||
withdraw_request.amount,
|
||||
withdraw_request.unit,
|
||||
withdraw_request.mint_url or global_settings.primary_mint,
|
||||
global_settings.receive_ln_address
|
||||
)
|
||||
|
||||
pending_withdrawal.auto_sent = True
|
||||
pending_withdrawal.ln_address = global_settings.receive_ln_address
|
||||
pending_withdrawal.claimed = True
|
||||
pending_withdrawal.notes = f"Auto-sent {amount_received} to {global_settings.receive_ln_address}"
|
||||
|
||||
session.add(pending_withdrawal)
|
||||
await session.commit()
|
||||
|
||||
logger.info(
|
||||
"Withdrawal auto-sent to Lightning address",
|
||||
extra={
|
||||
"amount": withdraw_request.amount,
|
||||
"unit": withdraw_request.unit,
|
||||
"ln_address": global_settings.receive_ln_address,
|
||||
"amount_received": amount_received
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"auto_sent": True,
|
||||
"ln_address": global_settings.receive_ln_address,
|
||||
"amount_received": amount_received,
|
||||
"withdrawal_id": pending_withdrawal.id
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Failed to auto-send withdrawal to Lightning address",
|
||||
extra={
|
||||
"error": str(e),
|
||||
"ln_address": global_settings.receive_ln_address,
|
||||
"amount": withdraw_request.amount,
|
||||
"unit": withdraw_request.unit
|
||||
}
|
||||
)
|
||||
# Continue with manual token storage if auto-send fails
|
||||
pending_withdrawal.notes = f"Auto-send failed: {str(e)}"
|
||||
|
||||
session.add(pending_withdrawal)
|
||||
await session.commit()
|
||||
|
||||
logger.info(
|
||||
"Withdrawal token generated and stored",
|
||||
extra={
|
||||
"withdrawal_id": pending_withdrawal.id,
|
||||
"amount": withdraw_request.amount,
|
||||
"unit": withdraw_request.unit,
|
||||
"mint_url": withdraw_request.mint_url or global_settings.primary_mint,
|
||||
"auto_send_requested": withdraw_request.auto_send_to_ln
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"token": token,
|
||||
"withdrawal_id": pending_withdrawal.id,
|
||||
"auto_sent": False
|
||||
}
|
||||
|
||||
|
||||
DASHBOARD_CSS: str = """
|
||||
@@ -767,6 +992,10 @@ button:disabled { background: #a0aec0; cursor: not-allowed; transform: none; }
|
||||
.refresh-btn { background: #48bb78; }
|
||||
.refresh-btn:hover { background: #38a169; }
|
||||
.investigate-btn { background: #4299e1; }
|
||||
.copy-token-btn { background: #38a169; padding: 6px 12px; font-size: 12px; margin-right: 5px; }
|
||||
.copy-token-btn:hover { background: #2f855a; }
|
||||
.claim-btn { background: #e53e3e; padding: 6px 12px; font-size: 12px; }
|
||||
.claim-btn:hover { background: #c53030; }
|
||||
.balance-card { background: white; padding: 2rem; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 2rem; }
|
||||
.balance-item { display: flex; justify-content: space-between; margin-bottom: 1rem; }
|
||||
.balance-label { color: #718096; }
|
||||
@@ -789,7 +1018,8 @@ button:disabled { background: #a0aec0; cursor: not-allowed; transform: none; }
|
||||
@keyframes slideIn { from { transform: translateY(-20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
|
||||
.close { color: #a0aec0; float: right; font-size: 28px; font-weight: bold; cursor: pointer; margin: -10px -10px 0 0; }
|
||||
.close:hover { color: #2d3748; }
|
||||
input[type="number"], input[type="text"], select { width: 100%; padding: 10px; margin: 10px 0; border: 2px solid #e2e8f0; border-radius: 6px; font-size: 16px; transition: border 0.2s; }
|
||||
input[type="number"], input[type="text"], input[type="checkbox"], select { width: 100%; padding: 10px; margin: 10px 0; border: 2px solid #e2e8f0; border-radius: 6px; font-size: 16px; transition: border 0.2s; }
|
||||
input[type="checkbox"] { width: auto; margin-right: 8px; }
|
||||
input[type="number"]:focus, input[type="text"]:focus, select:focus { outline: none; border-color: #4299e1; }
|
||||
.warning { color: #e53e3e; font-weight: 600; margin: 10px 0; padding: 10px; background: #fff5f5; border-radius: 6px; }
|
||||
"""
|
||||
@@ -813,4 +1043,4 @@ h1 { color: #333; }
|
||||
.log-field { margin: 2px 0; color: #666; word-break: break-all; }
|
||||
.no-logs { text-align: center; color: #666; padding: 40px; }
|
||||
.request-id-display { background-color: #e9ecef; padding: 10px; border-radius: 4px; margin-bottom: 20px; font-family: monospace; }
|
||||
"""
|
||||
"""
|
||||
@@ -1,6 +1,7 @@
|
||||
import os
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import AsyncGenerator
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from alembic import command
|
||||
from alembic.config import Config
|
||||
@@ -66,6 +67,21 @@ class ModelRow(SQLModel, table=True): # type: ignore
|
||||
top_provider: str | None = Field(default=None)
|
||||
|
||||
|
||||
class PendingWithdrawal(SQLModel, table=True): # type: ignore
|
||||
__tablename__ = "pending_withdrawals"
|
||||
|
||||
id: int = Field(primary_key=True)
|
||||
token: str = Field(description="The cashu token string")
|
||||
amount: int = Field(description="Amount in the token's unit")
|
||||
unit: str = Field(description="Currency unit (sat, msat)")
|
||||
mint_url: str = Field(description="Mint URL where the token was created")
|
||||
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
claimed: bool = Field(default=False, description="Whether the token has been claimed/used")
|
||||
auto_sent: bool = Field(default=False, description="Whether the token was automatically sent to LN address")
|
||||
ln_address: str | None = Field(default=None, description="Lightning address where token was sent")
|
||||
notes: str | None = Field(default=None, description="Optional notes about the withdrawal")
|
||||
|
||||
|
||||
async def balances_for_mint_and_unit(
|
||||
db_session: AsyncSession, mint_url: str, unit: str
|
||||
) -> int:
|
||||
@@ -126,4 +142,4 @@ def run_migrations() -> None:
|
||||
"Database migration failed",
|
||||
extra={"error": str(e), "error_type": type(e).__name__},
|
||||
)
|
||||
raise
|
||||
raise
|
||||
Reference in New Issue
Block a user