Add Lamassu database configuration: implement CRUD operations, polling tasks, and UI components for managing database settings. Introduce hourly transaction polling and manual poll functionality.

This commit is contained in:
padreug 2025-06-18 10:56:05 +02:00
parent c9f7140d95
commit 1f7999a556
9 changed files with 870 additions and 5 deletions

View file

@ -5,7 +5,7 @@ from lnbits.tasks import create_permanent_unique_task
from loguru import logger
from .crud import db
from .tasks import wait_for_paid_invoices
from .tasks import wait_for_paid_invoices, hourly_transaction_polling
from .views import myextension_generic_router
from .views_api import myextension_api_router
from .views_lnurl import myextension_lnurl_router
@ -40,8 +40,13 @@ def myextension_stop():
def myextension_start():
task = create_permanent_unique_task("ext_myextension", wait_for_paid_invoices)
scheduled_tasks.append(task)
# Start invoice listener task
invoice_task = create_permanent_unique_task("ext_myextension", wait_for_paid_invoices)
scheduled_tasks.append(invoice_task)
# Start hourly transaction polling task
polling_task = create_permanent_unique_task("ext_myextension_polling", hourly_transaction_polling)
scheduled_tasks.append(polling_task)
__all__ = [

95
crud.py
View file

@ -11,7 +11,8 @@ from .models import (
CreateDcaClientData, DcaClient, UpdateDcaClientData,
CreateDepositData, DcaDeposit, UpdateDepositStatusData,
CreateDcaPaymentData, DcaPayment,
ClientBalanceSummary
ClientBalanceSummary,
CreateLamassuConfigData, LamassuConfig, UpdateLamassuConfigData
)
db = Database("ext_myextension")
@ -292,3 +293,95 @@ async def get_fixed_mode_clients() -> List[DcaClient]:
"SELECT * FROM myextension.dca_clients WHERE dca_mode = 'fixed' AND status = 'active'",
model=DcaClient,
)
# Lamassu Configuration CRUD Operations
async def create_lamassu_config(data: CreateLamassuConfigData) -> LamassuConfig:
config_id = urlsafe_short_hash()
# Deactivate any existing configs first (only one active config allowed)
await db.execute(
"UPDATE myextension.lamassu_config SET is_active = false, updated_at = :updated_at",
{"updated_at": datetime.now()}
)
await db.execute(
"""
INSERT INTO myextension.lamassu_config
(id, host, port, database_name, username, password, is_active, created_at, updated_at)
VALUES (:id, :host, :port, :database_name, :username, :password, :is_active, :created_at, :updated_at)
""",
{
"id": config_id,
"host": data.host,
"port": data.port,
"database_name": data.database_name,
"username": data.username,
"password": data.password,
"is_active": True,
"created_at": datetime.now(),
"updated_at": datetime.now()
}
)
return await get_lamassu_config(config_id)
async def get_lamassu_config(config_id: str) -> Optional[LamassuConfig]:
return await db.fetchone(
"SELECT * FROM myextension.lamassu_config WHERE id = :id",
{"id": config_id},
LamassuConfig,
)
async def get_active_lamassu_config() -> Optional[LamassuConfig]:
return await db.fetchone(
"SELECT * FROM myextension.lamassu_config WHERE is_active = true ORDER BY created_at DESC LIMIT 1",
model=LamassuConfig,
)
async def get_all_lamassu_configs() -> List[LamassuConfig]:
return await db.fetchall(
"SELECT * FROM myextension.lamassu_config ORDER BY created_at DESC",
model=LamassuConfig,
)
async def update_lamassu_config(config_id: str, data: UpdateLamassuConfigData) -> Optional[LamassuConfig]:
update_data = {k: v for k, v in data.dict().items() if v is not None}
if not update_data:
return await get_lamassu_config(config_id)
update_data["updated_at"] = datetime.now()
set_clause = ", ".join([f"{k} = :{k}" for k in update_data.keys()])
update_data["id"] = config_id
await db.execute(
f"UPDATE myextension.lamassu_config SET {set_clause} WHERE id = :id",
update_data
)
return await get_lamassu_config(config_id)
async def update_config_test_result(config_id: str, success: bool) -> None:
await db.execute(
"""
UPDATE myextension.lamassu_config
SET test_connection_last = :test_time, test_connection_success = :success, updated_at = :updated_at
WHERE id = :id
""",
{
"id": config_id,
"test_time": datetime.now(),
"success": success,
"updated_at": datetime.now()
}
)
async def delete_lamassu_config(config_id: str) -> None:
await db.execute(
"DELETE FROM myextension.lamassu_config WHERE id = :id",
{"id": config_id}
)

View file

@ -93,3 +93,26 @@ async def m005_create_dca_payments(db):
);
"""
)
async def m006_create_lamassu_config(db):
"""
Create Lamassu database configuration table.
"""
await db.execute(
f"""
CREATE TABLE myextension.lamassu_config (
id TEXT PRIMARY KEY NOT NULL,
host TEXT NOT NULL,
port INTEGER NOT NULL DEFAULT 5432,
database_name TEXT NOT NULL,
username TEXT NOT NULL,
password TEXT NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT true,
test_connection_last TIMESTAMP,
test_connection_success BOOLEAN,
created_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
updated_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
);
"""
)

View file

@ -99,6 +99,38 @@ class LamassuTransaction(BaseModel):
timestamp: datetime
# Lamassu Configuration Models
class CreateLamassuConfigData(BaseModel):
host: str
port: int = 5432
database_name: str
username: str
password: str
class LamassuConfig(BaseModel):
id: str
host: str
port: int
database_name: str
username: str
password: str
is_active: bool
test_connection_last: Optional[datetime]
test_connection_success: Optional[bool]
created_at: datetime
updated_at: datetime
class UpdateLamassuConfigData(BaseModel):
host: Optional[str] = None
port: Optional[int] = None
database_name: Optional[str] = None
username: Optional[str] = None
password: Optional[str] = None
is_active: Optional[bool] = None
# Legacy models (keep for backward compatibility during transition)
class CreateMyExtensionData(BaseModel):
id: Optional[str] = ""

View file

@ -54,6 +54,24 @@ window.app = Vue.createApp({
amount: null,
notes: ''
},
// Polling status
lastPollTime: null,
testingConnection: false,
runningManualPoll: false,
lamassuConfig: null,
// Config dialog
configDialog: {
show: false,
data: {
host: '',
port: 5432,
database_name: '',
username: '',
password: ''
}
},
// Options
currencyOptions: [
@ -108,6 +126,62 @@ window.app = Vue.createApp({
return new Date(dateString).toLocaleDateString()
},
// Configuration Methods
async getLamassuConfig() {
try {
const {data} = await LNbits.api.request(
'GET',
'/myextension/api/v1/dca/config',
this.g.user.wallets[0].inkey
)
this.lamassuConfig = data
} catch (error) {
// It's OK if no config exists yet
this.lamassuConfig = null
}
},
async saveConfiguration() {
try {
const data = {
host: this.configDialog.data.host,
port: this.configDialog.data.port,
database_name: this.configDialog.data.database_name,
username: this.configDialog.data.username,
password: this.configDialog.data.password
}
const {data: config} = await LNbits.api.request(
'POST',
'/myextension/api/v1/dca/config',
this.g.user.wallets[0].adminkey,
data
)
this.lamassuConfig = config
this.closeConfigDialog()
this.$q.notify({
type: 'positive',
message: 'Database configuration saved successfully',
timeout: 5000
})
} catch (error) {
LNbits.utils.notifyApiError(error)
}
},
closeConfigDialog() {
this.configDialog.show = false
this.configDialog.data = {
host: '',
port: 5432,
database_name: '',
username: '',
password: ''
}
},
// DCA Client Methods
async getDcaClients() {
try {
@ -313,6 +387,53 @@ window.app = Vue.createApp({
async exportDepositsCSV() {
await LNbits.utils.exportCSV(this.depositsTable.columns, this.deposits)
},
// Polling Methods
async testDatabaseConnection() {
this.testingConnection = true
try {
const {data} = await LNbits.api.request(
'POST',
'/myextension/api/v1/dca/test-connection',
this.g.user.wallets[0].adminkey
)
this.$q.notify({
type: data.success ? 'positive' : 'negative',
message: data.message,
timeout: 5000
})
} catch (error) {
LNbits.utils.notifyApiError(error)
} finally {
this.testingConnection = false
}
},
async manualPoll() {
this.runningManualPoll = true
try {
const {data} = await LNbits.api.request(
'POST',
'/myextension/api/v1/dca/manual-poll',
this.g.user.wallets[0].adminkey
)
this.lastPollTime = new Date().toLocaleString()
this.$q.notify({
type: 'positive',
message: `Manual poll completed. Found ${data.transactions_processed} new transactions.`,
timeout: 5000
})
// Refresh data
await this.getDeposits()
} catch (error) {
LNbits.utils.notifyApiError(error)
} finally {
this.runningManualPoll = false
}
},
// Legacy Methods (keep for backward compatibility)
async closeFormDialog() {
@ -509,6 +630,7 @@ window.app = Vue.createApp({
async created() {
// Load DCA admin data
await Promise.all([
this.getLamassuConfig(),
this.getDcaClients(),
this.getDeposits()
])

View file

@ -1,11 +1,14 @@
import asyncio
from datetime import datetime
from lnbits.core.models import Payment
from lnbits.core.services import websocket_updater
from lnbits.tasks import register_invoice_listener
from loguru import logger
from .crud import get_myextension, update_myextension
from .models import CreateMyExtensionData
from .transaction_processor import poll_lamassu_transactions
#######################################
########## RUN YOUR TASKS HERE ########
@ -22,6 +25,25 @@ async def wait_for_paid_invoices():
await on_invoice_paid(payment)
async def hourly_transaction_polling():
"""Background task that polls Lamassu database every hour for new transactions"""
logger.info("Starting hourly Lamassu transaction polling task")
while True:
try:
logger.info(f"Running Lamassu transaction poll at {datetime.now()}")
await poll_lamassu_transactions()
logger.info("Completed Lamassu transaction poll, sleeping for 1 hour")
# Sleep for 1 hour (3600 seconds)
await asyncio.sleep(3600)
except Exception as e:
logger.error(f"Error in hourly polling task: {e}")
# Sleep for 5 minutes before retrying on error
await asyncio.sleep(300)
# Do somethhing when an invoice related top this extension is paid

View file

@ -332,6 +332,59 @@
</q-card-section>
</q-expansion-item>
<q-separator></q-separator>
<q-expansion-item
group="api"
icon="settings"
label="Lamassu Database Config"
:content-inset-level="0.5"
>
<q-card-section class="text-caption">
<div v-if="lamassuConfig">
<p><strong>Database:</strong> ${ lamassuConfig.host }:${ lamassuConfig.port }/${ lamassuConfig.database_name }</p>
<p><strong>Status:</strong>
<q-badge v-if="lamassuConfig.test_connection_success === true" color="green">Connected</q-badge>
<q-badge v-else-if="lamassuConfig.test_connection_success === false" color="red">Failed</q-badge>
<q-badge v-else color="grey">Not tested</q-badge>
</p>
<p><strong>Last Poll:</strong> ${ lastPollTime || 'Not yet run' }</p>
</div>
<div v-else>
<p><strong>Status:</strong> <q-badge color="orange">Not configured</q-badge></p>
</div>
<div class="q-mt-md">
<q-btn
size="sm"
color="primary"
@click="configDialog.show = true"
icon="settings"
>
Configure Database
</q-btn>
<q-btn
v-if="lamassuConfig"
size="sm"
color="accent"
@click="testDatabaseConnection"
:loading="testingConnection"
class="q-ml-sm"
>
Test Connection
</q-btn>
<q-btn
v-if="lamassuConfig"
size="sm"
color="secondary"
@click="manualPoll"
:loading="runningManualPoll"
class="q-ml-sm"
>
Manual Poll
</q-btn>
</div>
</q-card-section>
</q-expansion-item>
<q-separator></q-separator>
{% include "myextension/_api_docs.html" %}
</q-list>
</q-card-section>
@ -450,6 +503,85 @@
</q-card>
</q-dialog>
<!--/////////////////////////////////////////////////-->
<!--//////////////LAMASSU CONFIG DIALOG//////////////-->
<!--/////////////////////////////////////////////////-->
<q-dialog v-model="configDialog.show" position="top" @hide="closeConfigDialog">
<q-card class="q-pa-lg q-pt-xl" style="width: 600px; max-width: 90vw">
<div class="text-h6 q-mb-md">Lamassu Database Configuration</div>
<q-form @submit="saveConfiguration" class="q-gutter-md">
<q-input
filled
dense
v-model.trim="configDialog.data.host"
label="Database Host *"
placeholder="e.g., localhost or 192.168.1.100"
hint="Hostname or IP address of the Lamassu Postgres server"
></q-input>
<q-input
filled
dense
type="number"
v-model.number="configDialog.data.port"
label="Database Port *"
placeholder="5432"
hint="Postgres port (usually 5432)"
></q-input>
<q-input
filled
dense
v-model.trim="configDialog.data.database_name"
label="Database Name *"
placeholder="lamassu"
hint="Name of the Lamassu database"
></q-input>
<q-input
filled
dense
v-model.trim="configDialog.data.username"
label="Username *"
placeholder="postgres"
hint="Database username with read access"
></q-input>
<q-input
filled
dense
type="password"
v-model.trim="configDialog.data.password"
label="Password *"
placeholder="Enter database password"
hint="Database password"
></q-input>
<q-banner v-if="!configDialog.data.id" class="bg-blue-1 text-blue-9">
<template v-slot:avatar>
<q-icon name="info" color="blue" />
</template>
This configuration will be securely stored and used for hourly polling.
Only read access to the Lamassu database is required.
</q-banner>
<div class="row q-mt-lg">
<q-btn
unelevated
color="primary"
type="submit"
:disable="!configDialog.data.host || !configDialog.data.database_name || !configDialog.data.username"
>Save Configuration</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-form>
</q-card>
</q-dialog>
<!--/////////////////////////////////////////////////-->
<!--//////////////QR Code DIALOG/////////////////////-->
<!--/////////////////////////////////////////////////-->

300
transaction_processor.py Normal file
View file

@ -0,0 +1,300 @@
# Transaction processing and polling service for Lamassu ATM integration
import asyncio
import asyncpg
from datetime import datetime, timedelta
from typing import List, Optional, Dict, Any
from loguru import logger
from lnbits.core.services import create_invoice, pay_invoice
from lnbits.settings import settings
from .crud import (
get_flow_mode_clients,
get_payments_by_lamassu_transaction,
create_dca_payment,
get_client_balance_summary,
get_active_lamassu_config,
update_config_test_result
)
from .models import CreateDcaPaymentData, LamassuTransaction
class LamassuTransactionProcessor:
"""Handles polling Lamassu database and processing transactions for DCA distribution"""
def __init__(self):
self.last_check_time = None
self.processed_transaction_ids = set()
async def get_db_config(self) -> Optional[Dict[str, Any]]:
"""Get database configuration from the database"""
try:
config = await get_active_lamassu_config()
if not config:
logger.error("No active Lamassu database configuration found")
return None
return {
"host": config.host,
"port": config.port,
"database": config.database_name,
"user": config.username,
"password": config.password,
"config_id": config.id
}
except Exception as e:
logger.error(f"Error getting database configuration: {e}")
return None
async def connect_to_lamassu_db(self) -> Optional[asyncpg.Connection]:
"""Establish connection to Lamassu Postgres database"""
try:
db_config = await self.get_db_config()
if not db_config:
return None
connection = await asyncpg.connect(
host=db_config["host"],
port=db_config["port"],
database=db_config["database"],
user=db_config["user"],
password=db_config["password"],
timeout=30
)
logger.info("Successfully connected to Lamassu database")
# Update test result on successful connection
try:
await update_config_test_result(db_config["config_id"], True)
except Exception as e:
logger.warning(f"Could not update config test result: {e}")
return connection
except Exception as e:
logger.error(f"Failed to connect to Lamassu database: {e}")
# Update test result on failed connection
try:
db_config = await self.get_db_config()
if db_config:
await update_config_test_result(db_config["config_id"], False)
except Exception as update_error:
logger.warning(f"Could not update config test result: {update_error}")
return None
async def fetch_new_transactions(self, connection: asyncpg.Connection) -> List[Dict[str, Any]]:
"""Fetch new successful transactions from Lamassu database"""
try:
# Set the time window - check for transactions in the last hour + 5 minutes buffer
time_threshold = datetime.now() - timedelta(hours=1, minutes=5)
# Query for successful cash-out transactions (people selling BTC for fiat)
# These are the transactions that trigger DCA distributions
query = """
SELECT
co.id as transaction_id,
co.fiat as fiat_amount,
co.crypto as crypto_amount,
co.created as transaction_time,
co.session_id,
co.machine_id,
co.status,
co.commission_percentage,
co.tx_hash
FROM cash_out_txs co
WHERE co.created >= $1
AND co.status = 'confirmed'
AND co.id NOT IN (
-- Exclude already processed transactions
SELECT DISTINCT lamassu_transaction_id
FROM myextension.dca_payments
WHERE lamassu_transaction_id IS NOT NULL
)
ORDER BY co.created DESC
"""
rows = await connection.fetch(query, time_threshold)
transactions = []
for row in rows:
# Convert asyncpg.Record to dict
transaction = {
"transaction_id": str(row["transaction_id"]),
"fiat_amount": int(row["fiat_amount"]), # Amount in smallest currency unit
"crypto_amount": int(row["crypto_amount"]), # Amount in satoshis
"transaction_time": row["transaction_time"],
"session_id": row["session_id"],
"machine_id": row["machine_id"],
"status": row["status"],
"commission_percentage": float(row["commission_percentage"]) if row["commission_percentage"] else 0.0,
"tx_hash": row["tx_hash"]
}
transactions.append(transaction)
logger.info(f"Found {len(transactions)} new transactions to process")
return transactions
except Exception as e:
logger.error(f"Error fetching transactions from Lamassu database: {e}")
return []
async def calculate_distribution_amounts(self, transaction: Dict[str, Any]) -> Dict[str, int]:
"""Calculate how much each Flow Mode client should receive"""
try:
# Get all active Flow Mode clients
flow_clients = await get_flow_mode_clients()
if not flow_clients:
logger.info("No Flow Mode clients found - skipping distribution")
return {}
# Calculate principal amount (total - commission)
fiat_amount = transaction["fiat_amount"]
commission_percentage = transaction["commission_percentage"]
commission_amount = int(fiat_amount * commission_percentage / 100)
principal_amount = fiat_amount - commission_amount
logger.info(f"Transaction: {fiat_amount}, Commission: {commission_amount}, Principal: {principal_amount}")
# Get balance summaries for all clients to calculate proportions
client_balances = {}
total_confirmed_deposits = 0
for client in flow_clients:
balance = await get_client_balance_summary(client.id)
if balance.remaining_balance > 0: # Only include clients with remaining balance
client_balances[client.id] = balance.remaining_balance
total_confirmed_deposits += balance.remaining_balance
if total_confirmed_deposits == 0:
logger.info("No clients with remaining DCA balance - skipping distribution")
return {}
# Calculate proportional distribution
distributions = {}
exchange_rate = transaction["crypto_amount"] / transaction["fiat_amount"] # sats per fiat unit
for client_id, client_balance in client_balances.items():
# Calculate this client's proportion of the principal
proportion = client_balance / total_confirmed_deposits
client_fiat_amount = int(principal_amount * proportion)
client_sats_amount = int(client_fiat_amount * exchange_rate)
distributions[client_id] = {
"fiat_amount": client_fiat_amount,
"sats_amount": client_sats_amount,
"exchange_rate": exchange_rate
}
logger.info(f"Client {client_id[:8]}... gets {client_fiat_amount} fiat units = {client_sats_amount} sats")
return distributions
except Exception as e:
logger.error(f"Error calculating distribution amounts: {e}")
return {}
async def distribute_to_clients(self, transaction: Dict[str, Any], distributions: Dict[str, Dict[str, int]]) -> None:
"""Send Bitcoin payments to DCA clients"""
try:
transaction_id = transaction["transaction_id"]
for client_id, distribution in distributions.items():
try:
# Get client info
flow_clients = await get_flow_mode_clients()
client = next((c for c in flow_clients if c.id == client_id), None)
if not client:
logger.error(f"Client {client_id} not found")
continue
# Create DCA payment record
payment_data = CreateDcaPaymentData(
client_id=client_id,
amount_sats=distribution["sats_amount"],
amount_fiat=distribution["fiat_amount"],
exchange_rate=distribution["exchange_rate"],
transaction_type="flow",
lamassu_transaction_id=transaction_id
)
# Record the payment in our database
dca_payment = await create_dca_payment(payment_data)
# TODO: Actually send Bitcoin to client's wallet
# This will be implemented when we integrate with LNBits payment system
logger.info(f"DCA payment recorded for client {client_id[:8]}...: {distribution['sats_amount']} sats")
except Exception as e:
logger.error(f"Error processing distribution for client {client_id}: {e}")
continue
except Exception as e:
logger.error(f"Error distributing to clients: {e}")
async def process_transaction(self, transaction: Dict[str, Any]) -> None:
"""Process a single transaction - calculate and distribute DCA payments"""
try:
transaction_id = transaction["transaction_id"]
# Check if transaction already processed
existing_payments = await get_payments_by_lamassu_transaction(transaction_id)
if existing_payments:
logger.info(f"Transaction {transaction_id} already processed - skipping")
return
logger.info(f"Processing new transaction: {transaction_id}")
# Calculate distribution amounts
distributions = await self.calculate_distribution_amounts(transaction)
if not distributions:
logger.info(f"No distributions calculated for transaction {transaction_id}")
return
# Distribute to clients
await self.distribute_to_clients(transaction, distributions)
logger.info(f"Successfully processed transaction {transaction_id}")
except Exception as e:
logger.error(f"Error processing transaction {transaction.get('transaction_id', 'unknown')}: {e}")
async def poll_and_process(self) -> None:
"""Main polling function - checks for new transactions and processes them"""
try:
logger.info("Starting Lamassu transaction polling...")
# Connect to Lamassu database
connection = await self.connect_to_lamassu_db()
if not connection:
logger.error("Could not connect to Lamassu database - skipping this poll")
return
try:
# Fetch new transactions
new_transactions = await self.fetch_new_transactions(connection)
# Process each transaction
for transaction in new_transactions:
await self.process_transaction(transaction)
logger.info(f"Completed processing {len(new_transactions)} transactions")
finally:
await connection.close()
except Exception as e:
logger.error(f"Error in polling cycle: {e}")
# Global processor instance
transaction_processor = LamassuTransactionProcessor()
async def poll_lamassu_transactions() -> None:
"""Entry point for the polling task"""
await transaction_processor.poll_and_process()

View file

@ -1,6 +1,7 @@
# Description: This file contains the extensions API endpoints.
from http import HTTPStatus
from typing import Optional
from fastapi import APIRouter, Depends, Request
from lnbits.core.crud import get_user
@ -26,6 +27,14 @@ from .crud import (
get_deposit,
update_deposit_status,
get_client_balance_summary,
# Lamassu config CRUD operations
create_lamassu_config,
get_lamassu_config,
get_active_lamassu_config,
get_all_lamassu_configs,
update_lamassu_config,
update_config_test_result,
delete_lamassu_config,
)
from .helpers import lnurler
from .models import (
@ -33,7 +42,8 @@ from .models import (
# DCA models
CreateDcaClientData, DcaClient, UpdateDcaClientData,
CreateDepositData, DcaDeposit, UpdateDepositStatusData,
ClientBalanceSummary
ClientBalanceSummary,
CreateLamassuConfigData, LamassuConfig, UpdateLamassuConfigData
)
myextension_api_router = APIRouter()
@ -307,3 +317,129 @@ async def api_update_deposit_status(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Failed to update deposit."
)
return updated_deposit
# Transaction Polling Endpoints
@myextension_api_router.post("/api/v1/dca/test-connection")
async def api_test_database_connection(
wallet: WalletTypeInfo = Depends(require_admin_key),
):
"""Test connection to Lamassu database"""
try:
from .transaction_processor import transaction_processor
connection = await transaction_processor.connect_to_lamassu_db()
if connection:
await connection.close()
return {
"success": True,
"message": "Successfully connected to Lamassu database"
}
else:
return {
"success": False,
"message": "Failed to connect to Lamassu database. Check configuration."
}
except Exception as e:
return {
"success": False,
"message": f"Database connection error: {str(e)}"
}
@myextension_api_router.post("/api/v1/dca/manual-poll")
async def api_manual_poll(
wallet: WalletTypeInfo = Depends(require_admin_key),
):
"""Manually trigger a poll of the Lamassu database"""
try:
from .transaction_processor import transaction_processor
# Connect to database
connection = await transaction_processor.connect_to_lamassu_db()
if not connection:
raise HTTPException(
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
detail="Could not connect to Lamassu database"
)
try:
# Fetch and process transactions
new_transactions = await transaction_processor.fetch_new_transactions(connection)
transactions_processed = 0
for transaction in new_transactions:
await transaction_processor.process_transaction(transaction)
transactions_processed += 1
return {
"success": True,
"transactions_processed": transactions_processed,
"message": f"Processed {transactions_processed} new transactions"
}
finally:
await connection.close()
except Exception as e:
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail=f"Error during manual poll: {str(e)}"
)
# Lamassu Configuration Endpoints
@myextension_api_router.get("/api/v1/dca/config")
async def api_get_lamassu_config(
wallet: WalletTypeInfo = Depends(require_invoice_key),
) -> Optional[LamassuConfig]:
"""Get active Lamassu database configuration"""
return await get_active_lamassu_config()
@myextension_api_router.post("/api/v1/dca/config", status_code=HTTPStatus.CREATED)
async def api_create_lamassu_config(
data: CreateLamassuConfigData,
wallet: WalletTypeInfo = Depends(require_admin_key),
) -> LamassuConfig:
"""Create/update Lamassu database configuration"""
return await create_lamassu_config(data)
@myextension_api_router.put("/api/v1/dca/config/{config_id}")
async def api_update_lamassu_config(
config_id: str,
data: UpdateLamassuConfigData,
wallet: WalletTypeInfo = Depends(require_admin_key),
) -> LamassuConfig:
"""Update Lamassu database configuration"""
config = await get_lamassu_config(config_id)
if not config:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Configuration not found."
)
updated_config = await update_lamassu_config(config_id, data)
if not updated_config:
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Failed to update configuration."
)
return updated_config
@myextension_api_router.delete("/api/v1/dca/config/{config_id}")
async def api_delete_lamassu_config(
config_id: str,
wallet: WalletTypeInfo = Depends(require_admin_key),
):
"""Delete Lamassu database configuration"""
config = await get_lamassu_config(config_id)
if not config:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Configuration not found."
)
await delete_lamassu_config(config_id)
return {"message": "Configuration deleted successfully"}