mirror of
https://github.com/Such-Software/wowcheckout.git
synced 2026-10-17 05:51:45 +02:00
feat: port upstream system status, health monitoring, connectivity caching
Ports xmrcheckout upstream features (7 commits since 655d037): - SystemStatus model for reconciler heartbeat tracking - Monero connectivity caching in system status - Health status endpoints in routes - Hardened reconciler status updates with SQLAlchemyError handling - Status schema additions Adapted for Wownero: 3-decimal precision (0.001 WOW) in amount rounding.
This commit is contained in:
+78
-3
@@ -11,7 +11,7 @@ from .admin_routes import router as admin_router
|
||||
from .btcpay_routes import router as btcpay_router
|
||||
from .routes import router
|
||||
|
||||
app = FastAPI(title="wowcheckout.com API", version="0.1.0")
|
||||
app = FastAPI(title="xmrcheckout.com API", version="0.1.0")
|
||||
|
||||
logging.basicConfig(level=os.getenv("LOG_LEVEL", "INFO").upper())
|
||||
|
||||
@@ -196,7 +196,7 @@ def startup():
|
||||
connection.execute(
|
||||
text(
|
||||
"ALTER TABLE users "
|
||||
"ADD COLUMN IF NOT EXISTS default_qr_logo VARCHAR DEFAULT 'wownero'"
|
||||
"ADD COLUMN IF NOT EXISTS default_qr_logo VARCHAR DEFAULT 'monero'"
|
||||
)
|
||||
)
|
||||
connection.execute(
|
||||
@@ -248,13 +248,88 @@ def startup():
|
||||
)
|
||||
connection.execute(
|
||||
text(
|
||||
"UPDATE users SET default_qr_logo = 'wownero' "
|
||||
"UPDATE users SET default_qr_logo = 'monero' "
|
||||
"WHERE default_qr_logo IS NULL"
|
||||
)
|
||||
)
|
||||
connection.execute(
|
||||
text("CREATE INDEX IF NOT EXISTS ix_users_payment_address ON users (payment_address)")
|
||||
)
|
||||
connection.execute(
|
||||
text(
|
||||
"CREATE TABLE IF NOT EXISTS system_status ("
|
||||
"name VARCHAR PRIMARY KEY, "
|
||||
"last_reconcile_started_at TIMESTAMP WITH TIME ZONE, "
|
||||
"last_reconcile_completed_at TIMESTAMP WITH TIME ZONE, "
|
||||
"last_reconcile_error VARCHAR, "
|
||||
"wallet_rpc VARCHAR, "
|
||||
"daemon VARCHAR, "
|
||||
"daemon_height INTEGER, "
|
||||
"checked_at TIMESTAMP WITH TIME ZONE, "
|
||||
"updated_at TIMESTAMP WITH TIME ZONE DEFAULT now()"
|
||||
")"
|
||||
)
|
||||
)
|
||||
connection.execute(
|
||||
text(
|
||||
"ALTER TABLE system_status "
|
||||
"ADD COLUMN IF NOT EXISTS last_reconcile_started_at TIMESTAMP WITH TIME ZONE"
|
||||
)
|
||||
)
|
||||
connection.execute(
|
||||
text(
|
||||
"ALTER TABLE system_status "
|
||||
"ADD COLUMN IF NOT EXISTS last_reconcile_completed_at TIMESTAMP WITH TIME ZONE"
|
||||
)
|
||||
)
|
||||
connection.execute(
|
||||
text(
|
||||
"ALTER TABLE system_status "
|
||||
"ADD COLUMN IF NOT EXISTS last_reconcile_error VARCHAR"
|
||||
)
|
||||
)
|
||||
connection.execute(
|
||||
text(
|
||||
"ALTER TABLE system_status "
|
||||
"ADD COLUMN IF NOT EXISTS wallet_rpc VARCHAR"
|
||||
)
|
||||
)
|
||||
connection.execute(
|
||||
text(
|
||||
"ALTER TABLE system_status "
|
||||
"ADD COLUMN IF NOT EXISTS daemon VARCHAR"
|
||||
)
|
||||
)
|
||||
connection.execute(
|
||||
text(
|
||||
"ALTER TABLE system_status "
|
||||
"ADD COLUMN IF NOT EXISTS daemon_height INTEGER"
|
||||
)
|
||||
)
|
||||
connection.execute(
|
||||
text(
|
||||
"ALTER TABLE system_status "
|
||||
"ADD COLUMN IF NOT EXISTS checked_at TIMESTAMP WITH TIME ZONE"
|
||||
)
|
||||
)
|
||||
connection.execute(
|
||||
text(
|
||||
"ALTER TABLE system_status "
|
||||
"ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP WITH TIME ZONE DEFAULT now()"
|
||||
)
|
||||
)
|
||||
connection.execute(
|
||||
text(
|
||||
"INSERT INTO system_status (name) VALUES ('reconciler') "
|
||||
"ON CONFLICT (name) DO NOTHING"
|
||||
)
|
||||
)
|
||||
connection.execute(
|
||||
text(
|
||||
"INSERT INTO system_status (name) VALUES ('monero_connectivity') "
|
||||
"ON CONFLICT (name) DO NOTHING"
|
||||
)
|
||||
)
|
||||
finally:
|
||||
if lock_acquired:
|
||||
connection.execute(
|
||||
|
||||
@@ -133,6 +133,20 @@ class User(Base):
|
||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
|
||||
|
||||
class SystemStatus(Base):
|
||||
__tablename__ = "system_status"
|
||||
|
||||
name = Column(String, primary_key=True)
|
||||
last_reconcile_started_at = Column(DateTime(timezone=True), nullable=True)
|
||||
last_reconcile_completed_at = Column(DateTime(timezone=True), nullable=True)
|
||||
last_reconcile_error = Column(String, nullable=True)
|
||||
wallet_rpc = Column(String, nullable=True)
|
||||
daemon = Column(String, nullable=True)
|
||||
daemon_height = Column(Integer, nullable=True)
|
||||
checked_at = Column(DateTime(timezone=True), nullable=True)
|
||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
|
||||
|
||||
class ProfileHistory(Base):
|
||||
__tablename__ = "profile_history"
|
||||
|
||||
|
||||
@@ -98,12 +98,17 @@ class MoneroWalletService:
|
||||
self._daemon_address = _normalize_daemon_address(MONERO_DAEMON_URL)
|
||||
self._wallet_dir = MONERO_WALLET_RPC_WALLET_DIR.strip()
|
||||
|
||||
def get_status(self) -> dict[str, str]:
|
||||
def get_status(self) -> dict[str, str | int | None]:
|
||||
daemon_height = self._daemon_height()
|
||||
for backend in self._backends:
|
||||
try:
|
||||
backend.client.raw_request("get_version")
|
||||
except Exception:
|
||||
return {"wallet_rpc": "unreachable"}
|
||||
return {
|
||||
"wallet_rpc": "unreachable",
|
||||
"daemon": "ok" if daemon_height is not None else "unreachable",
|
||||
"daemon_height": daemon_height,
|
||||
}
|
||||
|
||||
if self._daemon_address:
|
||||
try:
|
||||
@@ -114,8 +119,16 @@ class MoneroWalletService:
|
||||
except RPCError as exc:
|
||||
message = str(exc)
|
||||
if "no connection to daemon" in message or "no_connection_to_daemon" in message:
|
||||
return {"wallet_rpc": "ok", "daemon": "unreachable"}
|
||||
return {"wallet_rpc": "ok", "daemon": "ok" if self._daemon_address else "unknown"}
|
||||
return {
|
||||
"wallet_rpc": "ok",
|
||||
"daemon": "unreachable",
|
||||
"daemon_height": daemon_height,
|
||||
}
|
||||
return {
|
||||
"wallet_rpc": "ok",
|
||||
"daemon": "ok" if self._daemon_address else "unknown",
|
||||
"daemon_height": daemon_height,
|
||||
}
|
||||
|
||||
def create_subaddress(self, user: User, label: str) -> SubaddressResult:
|
||||
start_total = time.monotonic()
|
||||
|
||||
+122
-2
@@ -6,16 +6,18 @@ from datetime import datetime, timedelta, timezone
|
||||
from decimal import Decimal, ROUND_DOWN
|
||||
|
||||
from sqlalchemy import and_, or_, text
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from .btcpay_webhooks import dispatch_btcpay_webhooks
|
||||
from .config import INVOICE_RECONCILE_INTERVAL_SECONDS, LATE_PAYMENT_LOOKBACK_HOURS
|
||||
from .db import SessionLocal
|
||||
from .models import Invoice, InvoiceTransfer, User
|
||||
from .models import Invoice, InvoiceTransfer, SystemStatus, User
|
||||
from .monero_service import MoneroWalletService, TransferDetail
|
||||
from .webhooks import dispatch_webhooks
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
MONERO_CONNECTIVITY_STATUS_NAME = "monero_connectivity"
|
||||
|
||||
|
||||
def main() -> None:
|
||||
@@ -26,12 +28,36 @@ def main() -> None:
|
||||
level_name = "INFO"
|
||||
level = getattr(logging, level_name.upper(), logging.INFO)
|
||||
logging.basicConfig(level=level)
|
||||
service = MoneroWalletService()
|
||||
while True:
|
||||
status_db: Session | None = None
|
||||
try:
|
||||
status_db = SessionLocal()
|
||||
service = MoneroWalletService()
|
||||
_safe_update_monero_connectivity_status(status_db, service)
|
||||
_safe_update_reconciler_status(
|
||||
status_db,
|
||||
started_at=datetime.now(timezone.utc),
|
||||
completed_at=None,
|
||||
error_message=None,
|
||||
)
|
||||
_reconcile_invoices(service)
|
||||
_safe_update_reconciler_status(
|
||||
status_db,
|
||||
completed_at=datetime.now(timezone.utc),
|
||||
error_message=None,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.exception("Invoice reconcile failed: %s", exc)
|
||||
if status_db is None:
|
||||
status_db = SessionLocal()
|
||||
_safe_update_monero_connectivity_error(status_db)
|
||||
_safe_update_reconciler_status(
|
||||
status_db,
|
||||
error_message=str(exc),
|
||||
)
|
||||
finally:
|
||||
if status_db is not None:
|
||||
status_db.close()
|
||||
time.sleep(INVOICE_RECONCILE_INTERVAL_SECONDS)
|
||||
|
||||
|
||||
@@ -286,6 +312,100 @@ def _sync_invoice_transfers(
|
||||
return changed
|
||||
|
||||
|
||||
def _update_reconciler_status(
|
||||
db: Session,
|
||||
*,
|
||||
started_at: datetime | None = None,
|
||||
completed_at: datetime | None = None,
|
||||
error_message: str | None = None,
|
||||
) -> None:
|
||||
status_row = db.query(SystemStatus).filter(SystemStatus.name == "reconciler").first()
|
||||
if status_row is None:
|
||||
status_row = SystemStatus(name="reconciler")
|
||||
if started_at is not None:
|
||||
status_row.last_reconcile_started_at = started_at
|
||||
if completed_at is not None:
|
||||
status_row.last_reconcile_completed_at = completed_at
|
||||
status_row.last_reconcile_error = error_message
|
||||
db.add(status_row)
|
||||
db.commit()
|
||||
|
||||
|
||||
def _update_monero_connectivity_status(
|
||||
db: Session,
|
||||
*,
|
||||
wallet_rpc: str,
|
||||
daemon: str,
|
||||
daemon_height: int | None,
|
||||
) -> None:
|
||||
status_row = (
|
||||
db.query(SystemStatus)
|
||||
.filter(SystemStatus.name == MONERO_CONNECTIVITY_STATUS_NAME)
|
||||
.first()
|
||||
)
|
||||
if status_row is None:
|
||||
status_row = SystemStatus(name=MONERO_CONNECTIVITY_STATUS_NAME)
|
||||
status_row.wallet_rpc = wallet_rpc
|
||||
status_row.daemon = daemon
|
||||
status_row.daemon_height = daemon_height
|
||||
status_row.checked_at = datetime.now(timezone.utc)
|
||||
db.add(status_row)
|
||||
db.commit()
|
||||
|
||||
|
||||
def _safe_update_monero_connectivity_status(
|
||||
db: Session,
|
||||
service: MoneroWalletService,
|
||||
) -> None:
|
||||
try:
|
||||
status_payload = service.get_status()
|
||||
_update_monero_connectivity_status(
|
||||
db,
|
||||
wallet_rpc=str(status_payload.get("wallet_rpc", "unreachable")),
|
||||
daemon=str(status_payload.get("daemon", "unknown")),
|
||||
daemon_height=(
|
||||
int(status_payload["daemon_height"])
|
||||
if isinstance(status_payload.get("daemon_height"), int)
|
||||
else None
|
||||
),
|
||||
)
|
||||
except Exception:
|
||||
db.rollback()
|
||||
logger.warning("Unable to persist Monero connectivity status", exc_info=True)
|
||||
|
||||
|
||||
def _safe_update_monero_connectivity_error(db: Session) -> None:
|
||||
try:
|
||||
_update_monero_connectivity_status(
|
||||
db,
|
||||
wallet_rpc="unreachable",
|
||||
daemon="unknown",
|
||||
daemon_height=None,
|
||||
)
|
||||
except SQLAlchemyError:
|
||||
db.rollback()
|
||||
logger.warning("Unable to persist Monero connectivity fallback", exc_info=True)
|
||||
|
||||
|
||||
def _safe_update_reconciler_status(
|
||||
db: Session,
|
||||
*,
|
||||
started_at: datetime | None = None,
|
||||
completed_at: datetime | None = None,
|
||||
error_message: str | None = None,
|
||||
) -> None:
|
||||
try:
|
||||
_update_reconciler_status(
|
||||
db,
|
||||
started_at=started_at,
|
||||
completed_at=completed_at,
|
||||
error_message=error_message,
|
||||
)
|
||||
except SQLAlchemyError:
|
||||
db.rollback()
|
||||
logger.warning("Unable to persist reconciler status heartbeat", exc_info=True)
|
||||
|
||||
|
||||
|
||||
|
||||
def _xmr_to_atomic(amount: Decimal) -> int:
|
||||
|
||||
+74
-58
@@ -22,13 +22,15 @@ from .config import (
|
||||
FOUNDER_PAYMENT_ADDRESS,
|
||||
FOUNDER_VIEW_KEY,
|
||||
INVOICE_DEFAULT_EXPIRY_HOURS,
|
||||
INVOICE_RECONCILE_INTERVAL_SECONDS,
|
||||
OPEN_REGISTRATION,
|
||||
QR_STORAGE_DIR,
|
||||
)
|
||||
from .db import get_db
|
||||
from .formatting import format_xmr_amount
|
||||
from .models import Invoice, ProfileHistory, User, Webhook, WebhookDelivery
|
||||
from .rates import get_wow_rate
|
||||
from .models import Invoice, ProfileHistory, SystemStatus, User, Webhook, WebhookDelivery
|
||||
from monero.address import Address, IntegratedAddress, SubAddress
|
||||
from .rates import get_xmr_rate
|
||||
from .subaddress_allocator import MAX_SUBADDRESS_INDEX, create_subaddress_for_user
|
||||
from .schemas import (
|
||||
ApiCredentialsResetRequest,
|
||||
@@ -42,6 +44,7 @@ from .schemas import (
|
||||
LoginResponse,
|
||||
ProfileResponse,
|
||||
ProfileUpdate,
|
||||
SystemStatusResponse,
|
||||
WebhookCreate,
|
||||
WebhookDeliveryResponse,
|
||||
WebhookResponse,
|
||||
@@ -58,7 +61,6 @@ from .security import (
|
||||
)
|
||||
from .webhooks import build_webhook_payload, dispatch_webhooks
|
||||
from .qr_codes import ensure_invoice_qr_png, invoice_qr_url, resolve_qr_settings
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
WEBHOOK_EVENTS = (
|
||||
@@ -77,12 +79,57 @@ def _require_donations_enabled() -> None:
|
||||
)
|
||||
|
||||
MAX_QR_LOGO_DATA_URL_LENGTH = 120_000
|
||||
MONERO_CONNECTIVITY_STATUS_NAME = "monero_connectivity"
|
||||
|
||||
|
||||
def _get_user_for_api_key(db: Session, api_key: str) -> User | None:
|
||||
return db.query(User).filter(User.api_key_hash == hash_api_key(api_key)).first()
|
||||
|
||||
|
||||
def _status_response_from_rows(
|
||||
monero_status: SystemStatus | None,
|
||||
reconciler_status: SystemStatus | None,
|
||||
) -> SystemStatusResponse:
|
||||
wallet_rpc = (
|
||||
str(monero_status.wallet_rpc)
|
||||
if monero_status and monero_status.wallet_rpc
|
||||
else "unreachable"
|
||||
)
|
||||
daemon = (
|
||||
str(monero_status.daemon)
|
||||
if monero_status and monero_status.daemon
|
||||
else "unknown"
|
||||
)
|
||||
daemon_height = monero_status.daemon_height if monero_status else None
|
||||
return SystemStatusResponse(
|
||||
wallet_rpc=wallet_rpc,
|
||||
daemon=daemon,
|
||||
daemon_height=daemon_height,
|
||||
invoice_reconcile_interval_seconds=INVOICE_RECONCILE_INTERVAL_SECONDS,
|
||||
last_reconcile_started_at=(
|
||||
reconciler_status.last_reconcile_started_at if reconciler_status else None
|
||||
),
|
||||
last_reconcile_completed_at=(
|
||||
reconciler_status.last_reconcile_completed_at if reconciler_status else None
|
||||
),
|
||||
last_reconcile_error=(
|
||||
reconciler_status.last_reconcile_error if reconciler_status else None
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _load_public_system_status(db: Session) -> SystemStatusResponse:
|
||||
reconciler_status = (
|
||||
db.query(SystemStatus).filter(SystemStatus.name == "reconciler").first()
|
||||
)
|
||||
monero_status = (
|
||||
db.query(SystemStatus)
|
||||
.filter(SystemStatus.name == MONERO_CONNECTIVITY_STATUS_NAME)
|
||||
.first()
|
||||
)
|
||||
return _status_response_from_rows(monero_status, reconciler_status)
|
||||
|
||||
|
||||
def _get_founder_user(db: Session) -> User:
|
||||
if not FOUNDER_PAYMENT_ADDRESS or not FOUNDER_VIEW_KEY:
|
||||
raise HTTPException(
|
||||
@@ -331,7 +378,7 @@ def _resolve_invoice_amount(
|
||||
detail="currency is required when amount_fiat is provided",
|
||||
)
|
||||
try:
|
||||
quote = get_wow_rate(requested_currency)
|
||||
quote = get_xmr_rate(requested_currency)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
@@ -345,7 +392,6 @@ def _resolve_invoice_amount(
|
||||
# Round UP to 3 decimal places so we never undercharge.
|
||||
# 3 decimals = 0.001 WOW — more than enough precision for Wownero.
|
||||
# See also: btcpay_routes.py for the same rounding.
|
||||
# To change precision: adjust the Decimal quantize string here and in btcpay_routes.py.
|
||||
amount_xmr = (Decimal(requested_amount_fiat) / quote.rate).quantize(
|
||||
Decimal("0.001"), rounding=ROUND_UP
|
||||
)
|
||||
@@ -361,63 +407,28 @@ def _resolve_invoice_amount(
|
||||
|
||||
|
||||
def _validate_payment_address_and_view_key(payment_address: str, view_key: str) -> None:
|
||||
"""Validate a Wownero primary address and its secret view key.
|
||||
|
||||
Wownero uses 2-byte varint prefixes (unlike Monero's 1-byte), so the
|
||||
standard monero Python library cannot parse them correctly. We do
|
||||
base58-decode + keccak checksum + ed25519 view-key verification here.
|
||||
"""
|
||||
from binascii import hexlify, unhexlify
|
||||
from monero import base58, ed25519
|
||||
from monero.keccak import keccak_256
|
||||
|
||||
# Wownero base58 prefix varints (first decoded byte)
|
||||
_WOW_PRIMARY_BYTE0 = 178 # base58 prefix 4146 -> "Wo"
|
||||
_WOW_SUBADDR_BYTE0 = 176 # base58 prefix 12208 -> "Ww"
|
||||
_WOW_INTEGRATED_BYTE0 = 179 # base58 prefix 4147
|
||||
|
||||
try:
|
||||
decoded = bytearray(unhexlify(base58.decode(payment_address)))
|
||||
except Exception:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Primary address is invalid",
|
||||
)
|
||||
|
||||
# Verify keccak-256 checksum (last 4 bytes)
|
||||
if decoded[-4:] != keccak_256(decoded[:-4]).digest()[:4]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Primary address is invalid",
|
||||
)
|
||||
|
||||
first_byte = decoded[0]
|
||||
if first_byte == _WOW_SUBADDR_BYTE0:
|
||||
address = Address(payment_address)
|
||||
except ValueError as exc:
|
||||
try:
|
||||
SubAddress(payment_address)
|
||||
except ValueError:
|
||||
try:
|
||||
IntegratedAddress(payment_address)
|
||||
except ValueError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Primary address is invalid",
|
||||
) from exc
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Integrated addresses are not supported. Use the primary address.",
|
||||
) from exc
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Subaddresses are not supported. Use the primary address.",
|
||||
)
|
||||
if first_byte == _WOW_INTEGRATED_BYTE0:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Integrated addresses are not supported. Use the primary address.",
|
||||
)
|
||||
if first_byte != _WOW_PRIMARY_BYTE0:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Primary address is invalid",
|
||||
)
|
||||
|
||||
# Wownero has 2-byte varint prefix -> keys start at offset 2
|
||||
pub_spend = decoded[2:34]
|
||||
pub_view = decoded[34:66]
|
||||
|
||||
# Derive public view key from the secret view key and compare
|
||||
try:
|
||||
derived_pub_view = ed25519.public_from_secret_hex(view_key)
|
||||
if unhexlify(derived_pub_view) != bytes(pub_view):
|
||||
raise ValueError("mismatch")
|
||||
except Exception:
|
||||
) from exc
|
||||
if not address.check_private_view_key(view_key):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Secret view key does not match the primary address",
|
||||
@@ -575,6 +586,11 @@ def get_donation_status(
|
||||
return _public_invoice_status_response(db, invoice, request)
|
||||
|
||||
|
||||
@router.get("/api/core/public/system/status", response_model=SystemStatusResponse)
|
||||
def get_public_system_status(db: Session = Depends(get_db)):
|
||||
return _load_public_system_status(db)
|
||||
|
||||
|
||||
@router.post("/api/core/webhooks", response_model=WebhookResponse)
|
||||
def register_webhook(
|
||||
payload: WebhookCreate,
|
||||
|
||||
+12
-2
@@ -18,7 +18,7 @@ from .formatting import format_xmr_amount
|
||||
|
||||
InvoiceStatus = Literal["pending", "payment_detected", "confirmed", "expired", "invalid"]
|
||||
BtcpayCheckoutStyle = Literal["standard", "btcpay_classic"]
|
||||
QrLogoMode = Literal["wownero", "none", "custom"]
|
||||
QrLogoMode = Literal["monero", "none", "custom"]
|
||||
|
||||
|
||||
class InvoiceCreate(BaseModel):
|
||||
@@ -184,13 +184,23 @@ class InvoiceStatusResponse(BaseModel):
|
||||
return format_xmr_amount(value)
|
||||
|
||||
|
||||
class SystemStatusResponse(BaseModel):
|
||||
wallet_rpc: Literal["ok", "unreachable"]
|
||||
daemon: Literal["ok", "unreachable", "unknown"]
|
||||
daemon_height: int | None = None
|
||||
invoice_reconcile_interval_seconds: int
|
||||
last_reconcile_started_at: datetime | None = None
|
||||
last_reconcile_completed_at: datetime | None = None
|
||||
last_reconcile_error: str | None = None
|
||||
|
||||
|
||||
class ProfileResponse(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: UUID
|
||||
payment_address: str
|
||||
default_confirmation_target: conint(ge=0, le=10) = 1
|
||||
default_qr_logo: QrLogoMode = "wownero"
|
||||
default_qr_logo: QrLogoMode = "monero"
|
||||
default_qr_logo_data_url: str | None = None
|
||||
btcpay_checkout_style: BtcpayCheckoutStyle = "btcpay_classic"
|
||||
created_at: datetime
|
||||
|
||||
Reference in New Issue
Block a user