diff --git a/crud.py b/crud.py index 8ab9d16..1103794 100644 --- a/crud.py +++ b/crud.py @@ -6,42 +6,11 @@ from datetime import datetime, timezone from lnbits.db import Database from lnbits.helpers import urlsafe_short_hash -from .models import ( - CreateDcaClientData, DcaClient, UpdateDcaClientData, - CreateDepositData, DcaDeposit, UpdateDepositStatusData, - CreateDcaPaymentData, DcaPayment, - ClientBalanceSummary, - CreateLamassuConfigData, LamassuConfig, UpdateLamassuConfigData, - CreateLamassuTransactionData, StoredLamassuTransaction -) +from .models import () db = Database("ext_satmachineclient") -# DCA Client CRUD Operations -async def create_dca_client(data: CreateDcaClientData) -> DcaClient: - client_id = urlsafe_short_hash() - await db.execute( - """ - INSERT INTO satmachineclient.dca_clients - (id, user_id, wallet_id, username, dca_mode, fixed_mode_daily_limit, status, created_at, updated_at) - VALUES (:id, :user_id, :wallet_id, :username, :dca_mode, :fixed_mode_daily_limit, :status, :created_at, :updated_at) - """, - { - "id": client_id, - "user_id": data.user_id, - "wallet_id": data.wallet_id, - "username": data.username, - "dca_mode": data.dca_mode, - "fixed_mode_daily_limit": data.fixed_mode_daily_limit, - "status": "active", - "created_at": datetime.now(), - "updated_at": datetime.now() - } - ) - return await get_dca_client(client_id) - - async def get_dca_client(client_id: str) -> Optional[DcaClient]: return await db.fetchone( "SELECT * FROM satmachineclient.dca_clients WHERE id = :id", @@ -50,13 +19,6 @@ async def get_dca_client(client_id: str) -> Optional[DcaClient]: ) -async def get_dca_clients() -> List[DcaClient]: - return await db.fetchall( - "SELECT * FROM satmachineclient.dca_clients ORDER BY created_at DESC", - model=DcaClient, - ) - - async def get_dca_client_by_user(user_id: str) -> Optional[DcaClient]: return await db.fetchone( "SELECT * FROM satmachineclient.dca_clients WHERE user_id = :user_id", diff --git a/static/js/index.js b/static/js/index.js index 3a61f32..cf2dd8d 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -4,122 +4,6 @@ window.app = Vue.createApp({ delimiters: ['${', '}'], data: function () { return { - // DCA Client Data - dcaClients: [], - deposits: [], - lamassuTransactions: [], - - // Table configurations - clientsTable: { - columns: [ - { name: 'username', align: 'left', label: 'Username', field: 'username' }, - { name: 'user_id', align: 'left', label: 'User ID', field: 'user_id' }, - { name: 'wallet_id', align: 'left', label: 'Wallet ID', field: 'wallet_id' }, - { name: 'dca_mode', align: 'left', label: 'DCA Mode', field: 'dca_mode' }, - { name: 'remaining_balance', align: 'right', label: 'Remaining Balance', field: 'remaining_balance' }, - { name: 'fixed_mode_daily_limit', align: 'left', label: 'Daily Limit', field: 'fixed_mode_daily_limit' }, - { name: 'status', align: 'left', label: 'Status', field: 'status' } - ], - pagination: { - rowsPerPage: 10 - } - }, - depositsTable: { - columns: [ - { name: 'client_id', align: 'left', label: 'Client', field: 'client_id' }, - { name: 'amount', align: 'left', label: 'Amount', field: 'amount' }, - { name: 'currency', align: 'left', label: 'Currency', field: 'currency' }, - { name: 'status', align: 'left', label: 'Status', field: 'status' }, - { name: 'created_at', align: 'left', label: 'Created', field: 'created_at' }, - { name: 'notes', align: 'left', label: 'Notes', field: 'notes' } - ], - pagination: { - rowsPerPage: 10 - } - }, - lamassuTransactionsTable: { - columns: [ - { name: 'lamassu_transaction_id', align: 'left', label: 'Transaction ID', field: 'lamassu_transaction_id' }, - { name: 'transaction_time', align: 'left', label: 'Time', field: 'transaction_time' }, - { name: 'fiat_amount', align: 'right', label: 'Fiat Amount', field: 'fiat_amount' }, - { name: 'crypto_amount', align: 'right', label: 'Total Sats', field: 'crypto_amount' }, - { name: 'commission_amount_sats', align: 'right', label: 'Commission', field: 'commission_amount_sats' }, - { name: 'base_amount_sats', align: 'right', label: 'Base Amount', field: 'base_amount_sats' }, - { name: 'distributions_total_sats', align: 'right', label: 'Distributed', field: 'distributions_total_sats' }, - { name: 'clients_count', align: 'center', label: 'Clients', field: 'clients_count' } - ], - pagination: { - rowsPerPage: 10 - } - }, - distributionDetailsTable: { - columns: [ - { name: 'client_username', align: 'left', label: 'Client', field: 'client_username' }, - { name: 'amount_sats', align: 'right', label: 'Amount (sats)', field: 'amount_sats' }, - { name: 'amount_fiat', align: 'right', label: 'Amount (fiat)', field: 'amount_fiat' }, - { name: 'status', align: 'center', label: 'Status', field: 'status' }, - { name: 'created_at', align: 'left', label: 'Created', field: 'created_at' } - ] - }, - - // Dialog states - depositFormDialog: { - show: false, - data: { - currency: 'GTQ' - } - }, - clientDetailsDialog: { - show: false, - data: null, - balance: null - }, - distributionDialog: { - show: false, - transaction: null, - distributions: [] - }, - - // Quick deposit form - quickDepositForm: { - selectedClient: null, - amount: null, - notes: '' - }, - - // Polling status - lastPollTime: null, - testingConnection: false, - runningManualPoll: false, - runningTestTransaction: false, - lamassuConfig: null, - - // Config dialog - configDialog: { - show: false, - data: { - host: '', - port: 5432, - database_name: '', - username: '', - password: '', - selectedWallet: null, - selectedCommissionWallet: null, - // SSH Tunnel settings - use_ssh_tunnel: false, - ssh_host: '', - ssh_port: 22, - ssh_username: '', - ssh_password: '', - ssh_private_key: '' - } - }, - - // Options - currencyOptions: [ - { label: 'GTQ', value: 'GTQ' }, - { label: 'USD', value: 'USD' } - ] } }, @@ -154,535 +38,16 @@ window.app = Vue.createApp({ return new Intl.NumberFormat('en-US').format(amount) + ' sats' }, - getClientUsername(clientId) { - const client = this.dcaClients.find(c => c.id === clientId) - return client ? (client.username || client.user_id.substring(0, 8) + '...') : clientId - }, - - - // Configuration Methods - async getLamassuConfig() { - try { - const {data} = await LNbits.api.request( - 'GET', - '/satmachineclient/api/v1/dca/config', - this.g.user.wallets[0].inkey - ) - this.lamassuConfig = data - - // When opening config dialog, populate the selected wallets if they exist - if (data && data.source_wallet_id) { - const wallet = this.g.user.wallets.find(w => w.id === data.source_wallet_id) - if (wallet) { - this.configDialog.data.selectedWallet = wallet - } - } - if (data && data.commission_wallet_id) { - const commissionWallet = this.g.user.wallets.find(w => w.id === data.commission_wallet_id) - if (commissionWallet) { - this.configDialog.data.selectedCommissionWallet = commissionWallet - } - } - } 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, - source_wallet_id: this.configDialog.data.selectedWallet?.id, - commission_wallet_id: this.configDialog.data.selectedCommissionWallet?.id, - // SSH Tunnel settings - use_ssh_tunnel: this.configDialog.data.use_ssh_tunnel, - ssh_host: this.configDialog.data.ssh_host, - ssh_port: this.configDialog.data.ssh_port, - ssh_username: this.configDialog.data.ssh_username, - ssh_password: this.configDialog.data.ssh_password, - ssh_private_key: this.configDialog.data.ssh_private_key - } - - const {data: config} = await LNbits.api.request( - 'POST', - '/satmachineclient/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: '', - selectedWallet: null, - selectedCommissionWallet: null, - // SSH Tunnel settings - use_ssh_tunnel: false, - ssh_host: '', - ssh_port: 22, - ssh_username: '', - ssh_password: '', - ssh_private_key: '' - } - }, - - // DCA Client Methods - async getDcaClients() { - try { - const { data } = await LNbits.api.request( - 'GET', - '/satmachineclient/api/v1/dca/clients', - this.g.user.wallets[0].inkey - ) - - // Fetch balance data for each client - const clientsWithBalances = await Promise.all( - data.map(async (client) => { - try { - const { data: balance } = await LNbits.api.request( - 'GET', - `/satmachineclient/api/v1/dca/clients/${client.id}/balance`, - this.g.user.wallets[0].inkey - ) - return { - ...client, - remaining_balance: balance.remaining_balance - } - } catch (error) { - console.error(`Error fetching balance for client ${client.id}:`, error) - return { - ...client, - remaining_balance: 0 - } - } - }) - ) - - this.dcaClients = clientsWithBalances - } catch (error) { - LNbits.utils.notifyApiError(error) - } - }, - - // Test Client Creation (temporary for testing) - async createTestClient() { - try { - const testData = { - user_id: this.g.user.id, - wallet_id: this.g.user.wallets[0].id, - username: this.g.user.username || `user_${this.g.user.id.substring(0, 8)}`, - dca_mode: 'flow' - } - - const { data: newClient } = await LNbits.api.request( - 'POST', - '/satmachineclient/api/v1/dca/clients', - this.g.user.wallets[0].adminkey, - testData - ) - - this.dcaClients.push(newClient) - - this.$q.notify({ - type: 'positive', - message: 'Test client created successfully!', - timeout: 5000 - }) - } catch (error) { - LNbits.utils.notifyApiError(error) - } - }, - - // Quick Deposit Methods - async sendQuickDeposit() { - try { - const data = { - client_id: this.quickDepositForm.selectedClient?.value, - amount: this.quickDepositForm.amount, - currency: 'GTQ', - notes: this.quickDepositForm.notes - } - - const { data: newDeposit } = await LNbits.api.request( - 'POST', - '/satmachineclient/api/v1/dca/deposits', - this.g.user.wallets[0].adminkey, - data - ) - - this.deposits.unshift(newDeposit) - - // Reset form - this.quickDepositForm = { - selectedClient: null, - amount: null, - notes: '' - } - - this.$q.notify({ - type: 'positive', - message: 'Deposit created successfully', - timeout: 5000 - }) - } catch (error) { - LNbits.utils.notifyApiError(error) - } - }, - - async viewClientDetails(client) { - try { - const { data: balance } = await LNbits.api.request( - 'GET', - `/satmachineclient/api/v1/dca/clients/${client.id}/balance`, - this.g.user.wallets[0].inkey - ) - this.clientDetailsDialog.data = client - this.clientDetailsDialog.balance = balance - this.clientDetailsDialog.show = true - } catch (error) { - LNbits.utils.notifyApiError(error) - } - }, - - // Deposit Methods - async getDeposits() { - try { - const { data } = await LNbits.api.request( - 'GET', - '/satmachineclient/api/v1/dca/deposits', - this.g.user.wallets[0].inkey - ) - this.deposits = data - } catch (error) { - LNbits.utils.notifyApiError(error) - } - }, - - addDepositDialog(client) { - this.depositFormDialog.data = { - client_id: client.id, - client_name: client.username || `${client.user_id.substring(0, 8)}...`, - currency: 'GTQ' - } - this.depositFormDialog.show = true - }, - - async sendDepositData() { - try { - const data = { - client_id: this.depositFormDialog.data.client_id, - amount: this.depositFormDialog.data.amount, - currency: this.depositFormDialog.data.currency, - notes: this.depositFormDialog.data.notes - } - - if (this.depositFormDialog.data.id) { - // Update existing deposit (mainly for notes/status) - const { data: updatedDeposit } = await LNbits.api.request( - 'PUT', - `/satmachineclient/api/v1/dca/deposits/${this.depositFormDialog.data.id}`, - this.g.user.wallets[0].adminkey, - { status: this.depositFormDialog.data.status, notes: data.notes } - ) - const index = this.deposits.findIndex(d => d.id === updatedDeposit.id) - if (index !== -1) { - this.deposits.splice(index, 1, updatedDeposit) - } - } else { - // Create new deposit - const { data: newDeposit } = await LNbits.api.request( - 'POST', - '/satmachineclient/api/v1/dca/deposits', - this.g.user.wallets[0].adminkey, - data - ) - this.deposits.unshift(newDeposit) - } - - this.closeDepositFormDialog() - this.$q.notify({ - type: 'positive', - message: this.depositFormDialog.data.id ? 'Deposit updated successfully' : 'Deposit created successfully', - timeout: 5000 - }) - } catch (error) { - LNbits.utils.notifyApiError(error) - } - }, - - closeDepositFormDialog() { - this.depositFormDialog.show = false - this.depositFormDialog.data = { - currency: 'GTQ' - } - }, - - async confirmDeposit(deposit) { - try { - await LNbits.utils - .confirmDialog('Confirm that this deposit has been physically placed in the ATM machine?') - .onOk(async () => { - const { data: updatedDeposit } = await LNbits.api.request( - 'PUT', - `/satmachineclient/api/v1/dca/deposits/${deposit.id}/status`, - this.g.user.wallets[0].adminkey, - { status: 'confirmed', notes: 'Confirmed by admin - money placed in machine' } - ) - const index = this.deposits.findIndex(d => d.id === deposit.id) - if (index !== -1) { - this.deposits.splice(index, 1, updatedDeposit) - } - this.$q.notify({ - type: 'positive', - message: 'Deposit confirmed! DCA is now active for this client.', - timeout: 5000 - }) - }) - } catch (error) { - LNbits.utils.notifyApiError(error) - } - }, - - editDeposit(deposit) { - this.depositFormDialog.data = { ...deposit } - this.depositFormDialog.show = true - }, - - // Export Methods - async exportClientsCSV() { - await LNbits.utils.exportCSV(this.clientsTable.columns, this.dcaClients) - }, - - async exportDepositsCSV() { - await LNbits.utils.exportCSV(this.depositsTable.columns, this.deposits) - }, - - async exportLamassuTransactionsCSV() { - await LNbits.utils.exportCSV(this.lamassuTransactionsTable.columns, this.lamassuTransactions) - }, - - // Polling Methods - async testDatabaseConnection() { - this.testingConnection = true - try { - const {data} = await LNbits.api.request( - 'POST', - '/satmachineclient/api/v1/dca/test-connection', - this.g.user.wallets[0].adminkey - ) - - // Show detailed results in a dialog - const stepsList = data.steps ? data.steps.join('\n') : 'No detailed steps available' - - let dialogContent = `Connection Test Results

` - - if (data.ssh_tunnel_used) { - dialogContent += `SSH Tunnel: ${data.ssh_tunnel_success ? '✅ Success' : '❌ Failed'}
` - } - - dialogContent += `Database: ${data.database_connection_success ? '✅ Success' : '❌ Failed'}

` - dialogContent += `Detailed Steps:
` - dialogContent += stepsList.replace(/\n/g, '
') - - this.$q.dialog({ - title: data.success ? 'Connection Test Passed' : 'Connection Test Failed', - message: dialogContent, - html: true, - ok: { - color: data.success ? 'positive' : 'negative', - label: 'Close' - } - }) - - // Also show a brief notification - this.$q.notify({ - type: data.success ? 'positive' : 'negative', - message: data.message, - timeout: 3000 - }) - - } catch (error) { - LNbits.utils.notifyApiError(error) - } finally { - this.testingConnection = false - } - }, - - async manualPoll() { - this.runningManualPoll = true - try { - const {data} = await LNbits.api.request( - 'POST', - '/satmachineclient/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.getDcaClients() // Refresh to show updated balances - await this.getDeposits() - await this.getLamassuTransactions() - await this.getLamassuConfig() - } catch (error) { - LNbits.utils.notifyApiError(error) - } finally { - this.runningManualPoll = false - } - }, - - async testTransaction() { - this.runningTestTransaction = true - try { - const {data} = await LNbits.api.request( - 'POST', - '/satmachineclient/api/v1/dca/test-transaction', - this.g.user.wallets[0].adminkey - ) - - // Show detailed results in a dialog - const details = data.transaction_details - - let dialogContent = `Test Transaction Results

` - dialogContent += `Transaction ID: ${details.transaction_id}
` - dialogContent += `Total Amount: ${details.total_amount_sats} sats
` - dialogContent += `Base Amount: ${details.base_amount_sats} sats
` - dialogContent += `Commission: ${details.commission_amount_sats} sats (${details.commission_percentage}%)
` - if (details.discount > 0) { - dialogContent += `Discount: ${details.discount}%
` - dialogContent += `Effective Commission: ${details.effective_commission}%
` - } - dialogContent += `
Check your wallets to see the distributions!` - - this.$q.dialog({ - title: 'Test Transaction Completed', - message: dialogContent, - html: true, - ok: { - color: 'positive', - label: 'Great!' - } - }) - - // Also show a brief notification - this.$q.notify({ - type: 'positive', - message: `Test transaction processed: ${details.total_amount_sats} sats distributed`, - timeout: 5000 - }) - - // Refresh data - await this.getDcaClients() // Refresh to show updated balances - await this.getDeposits() - await this.getLamassuTransactions() - await this.getLamassuConfig() - - } catch (error) { - LNbits.utils.notifyApiError(error) - } finally { - this.runningTestTransaction = false - } - }, - - // Lamassu Transaction Methods - async getLamassuTransactions() { - try { - const { data } = await LNbits.api.request( - 'GET', - '/satmachineclient/api/v1/dca/transactions', - this.g.user.wallets[0].inkey - ) - this.lamassuTransactions = data - } catch (error) { - LNbits.utils.notifyApiError(error) - } - }, - - async viewTransactionDistributions(transaction) { - try { - const { data: distributions } = await LNbits.api.request( - 'GET', - `/satmachineclient/api/v1/dca/transactions/${transaction.id}/distributions`, - this.g.user.wallets[0].inkey - ) - - this.distributionDialog.transaction = transaction - this.distributionDialog.distributions = distributions - this.distributionDialog.show = true - } catch (error) { - LNbits.utils.notifyApiError(error) - } - }, - }, /////////////////////////////////////////////////// //////LIFECYCLE FUNCTIONS RUNNING ON PAGE LOAD///// /////////////////////////////////////////////////// async created() { - // Load DCA admin data + // Load DCA client data await Promise.all([ - this.getLamassuConfig(), - this.getDcaClients(), - this.getDeposits(), - this.getLamassuTransactions() ]) }, computed: { - isConfigFormValid() { - const data = this.configDialog.data - - // Basic database fields are required - const basicValid = data.host && data.database_name && data.username && data.selectedWallet - - // If SSH tunnel is enabled, validate SSH fields - if (data.use_ssh_tunnel) { - const sshValid = data.ssh_host && data.ssh_username && - (data.ssh_password || data.ssh_private_key) - return basicValid && sshValid - } - - return basicValid - }, - - clientOptions() { - return this.dcaClients.map(client => ({ - label: `${client.username || client.user_id.substring(0, 8) + '...'} (${client.dca_mode})`, - value: client.id - })) - }, - - totalDcaBalance() { - return this.deposits - .filter(d => d.status === 'confirmed') - .reduce((total, deposit) => total + deposit.amount, 0) - } } }) diff --git a/templates/satmachineclient/index.html b/templates/satmachineclient/index.html index c8c60f3..117e5bf 100644 --- a/templates/satmachineclient/index.html +++ b/templates/satmachineclient/index.html @@ -7,765 +7,6 @@ {% endblock %} {% block page %}
-
- - - -
-
-
DCA Deposit Management
-

Manage fiat deposits for existing DCA clients

-
-
-
-
- - - - -
-
-
Registered DCA Clients
-

Clients registered via the DCA client extension

-
-
- Export to CSV -
-
- - - -
-
- - - - -
Quick Add Deposit
-

Add a new deposit for an existing client

- -
- - - No DCA clients registered yet. Clients must first install and configure the DCA client extension. - - -
- - -
-
- -
-
- -
-
- Add Deposit -
-
-
-
- -
-
-
-
-
- - - - -
-
-
Recent Deposits
-
-
- Export to CSV -
-
- - - -
-
- - - - -
-
-
Processed Lamassu Transactions
-

ATM transactions processed through DCA distribution

-
-
- Export to CSV -
-
- - - -
-
- -
- -
- - -
- {{SITE_TITLE}} DCA Client Extension -
-

- Dollar Cost Averaging administration for Lamassu ATM integration.
- Manage client deposits and DCA distribution settings. -

-
- - - - - -
-
Active Clients:
-
${ dcaClients.filter(c => c.status === 'active').length }
-
-
-
Pending Deposits:
-
${ deposits.filter(d => d.status === 'pending').length }
-
-
-
Total DCA Balance:
-
${ formatCurrency(totalDcaBalance) }
-
-
-
- - - -
-

Database: ${ lamassuConfig.host }:${ lamassuConfig.port }/${ lamassuConfig.database_name }

-

Status: - Connected - Failed - Not tested -

-

Last Poll: ${ lamassuConfig.last_poll_time ? formatDateTime(lamassuConfig.last_poll_time) : 'Not yet run' }

-

Last Success: ${ lamassuConfig.last_successful_poll ? formatDateTime(lamassuConfig.last_successful_poll) : 'Never' }

-
-
-

Status: Not configured

-
- -
- - Configure Database - - - Test Connection - - - Manual Poll - - - Test Transaction - -
-
-
- - {% include "satmachineclient/_api_docs.html" %} -
-
-
-
- - - - - - - - - -
- Deposit for: ${ depositFormDialog.data.client_name } -
- - - -
- Update Deposit - Create Deposit - Cancel -
-
-
-
- - - - - - - -
Client Details
-
- - - - Username - ${ clientDetailsDialog.data.username } - - - - - User ID - ${ clientDetailsDialog.data.user_id } - - - - - Wallet ID - ${ clientDetailsDialog.data.wallet_id } - - - - - DCA Mode - ${ clientDetailsDialog.data.dca_mode } - - - - - Daily Limit - ${ formatCurrency(clientDetailsDialog.data.fixed_mode_daily_limit) } - - - - - Balance Summary - - Deposits: ${ formatCurrency(clientDetailsDialog.balance.total_deposits) } | - Payments: ${ formatCurrency(clientDetailsDialog.balance.total_payments) } | - Remaining: ${ formatCurrency(clientDetailsDialog.balance.remaining_balance) } - - - - -
-
- Close -
-
-
- - - - - - - -
Lamassu Database Configuration
- - - - - - - - - - - - - -
DCA Source Wallet
- - - - - - - -
SSH Tunnel (Recommended)
- -
- - Use SSH Tunnel -
- -
- - - - - - - - - - - - - SSH tunneling keeps your database secure by avoiding direct internet exposure. - The database connection will be routed through the SSH server. - -
- - - - This configuration will be securely stored and used for hourly polling. - Only read access to the Lamassu database is required. - - -
- Save Configuration - Cancel -
-
-
-
- - - - - - - -
Transaction Distribution Details
- -
- - - - Lamassu Transaction ID - ${ distributionDialog.transaction.lamassu_transaction_id } - - - - - Transaction Time - ${ formatDateTime(distributionDialog.transaction.transaction_time) } - - - - - Total Amount - - ${ formatCurrency(distributionDialog.transaction.fiat_amount) } - (${ formatSats(distributionDialog.transaction.crypto_amount) }) - - - - - - Commission - - ${ (distributionDialog.transaction.commission_percentage * 100).toFixed(1) }% - - (with ${ distributionDialog.transaction.discount }% discount = ${ (distributionDialog.transaction.effective_commission * 100).toFixed(1) }% effective) - - = ${ formatSats(distributionDialog.transaction.commission_amount_sats) } - - - - - - Available for Distribution - ${ formatSats(distributionDialog.transaction.base_amount_sats) } - - - - - Total Distributed - ${ formatSats(distributionDialog.transaction.distributions_total_sats) } to ${ distributionDialog.transaction.clients_count } clients - - - -
- - - -
Client Distributions
- - - - - -
- Close -
-
-
{% endblock %} diff --git a/views_api.py b/views_api.py index 31b6473..aac2b08 100644 --- a/views_api.py +++ b/views_api.py @@ -7,40 +7,23 @@ from fastapi import APIRouter, Depends, Request from lnbits.core.crud import get_user from lnbits.core.models import WalletTypeInfo from lnbits.core.services import create_invoice -from lnbits.decorators import require_admin_key, require_invoice_key +from lnbits.decorators import require_admin_key from starlette.exceptions import HTTPException from .crud import ( # DCA CRUD operations create_dca_client, - get_dca_clients, get_dca_client, - update_dca_client, - delete_dca_client, - create_deposit, get_all_deposits, 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, - # Lamassu transaction CRUD operations - get_all_lamassu_transactions, - get_lamassu_transaction ) from .models import ( # DCA models - CreateDcaClientData, DcaClient, UpdateDcaClientData, - CreateDepositData, DcaDeposit, UpdateDepositStatusData, + CreateDcaClientData, + DcaClient, + DcaDeposit, ClientBalanceSummary, - CreateLamassuConfigData, LamassuConfig, UpdateLamassuConfigData, - StoredLamassuTransaction ) satmachineclient_api_router = APIRouter() @@ -51,46 +34,21 @@ satmachineclient_api_router = APIRouter() ################################################### # DCA Client Endpoints - -@satmachineclient_api_router.get("/api/v1/dca/clients") -async def api_get_dca_clients( - wallet: WalletTypeInfo = Depends(require_invoice_key), -) -> list[DcaClient]: - """Get all DCA clients""" - return await get_dca_clients() +# Note: Client creation/update -@satmachineclient_api_router.get("/api/v1/dca/clients/{client_id}") -async def api_get_dca_client( - client_id: str, - wallet: WalletTypeInfo = Depends(require_invoice_key), -) -> DcaClient: - """Get a specific DCA client""" - client = await get_dca_client(client_id) - if not client: - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="DCA client not found." - ) - return client - - -# Note: Client creation/update/delete will be handled by the DCA client extension -# Admin extension only reads existing clients and manages their deposits - -# TEMPORARY: Test client creation endpoint (remove in production) @satmachineclient_api_router.post("/api/v1/dca/clients", status_code=HTTPStatus.CREATED) async def api_create_test_dca_client( data: CreateDcaClientData, wallet: WalletTypeInfo = Depends(require_admin_key), ) -> DcaClient: - """Create a test DCA client (temporary for testing)""" return await create_dca_client(data) @satmachineclient_api_router.get("/api/v1/dca/clients/{client_id}/balance") async def api_get_client_balance( client_id: str, - wallet: WalletTypeInfo = Depends(require_invoice_key), + wallet: WalletTypeInfo = Depends(require_admin_key), ) -> ClientBalanceSummary: """Get client balance summary""" client = await get_dca_client(client_id) @@ -98,24 +56,27 @@ async def api_get_client_balance( raise HTTPException( status_code=HTTPStatus.NOT_FOUND, detail="DCA client not found." ) - + return await get_client_balance_summary(client_id) # DCA Deposit Endpoints + +# NOTE: to Claude - modify this so it only gets the deposits for the user! important security @satmachineclient_api_router.get("/api/v1/dca/deposits") async def api_get_deposits( - wallet: WalletTypeInfo = Depends(require_invoice_key), + wallet: WalletTypeInfo = Depends(require_admin_key), ) -> list[DcaDeposit]: """Get all deposits""" return await get_all_deposits() +# NOTE: does the client have any need to get sepcific deposits? @satmachineclient_api_router.get("/api/v1/dca/deposits/{deposit_id}") async def api_get_deposit( deposit_id: str, - wallet: WalletTypeInfo = Depends(require_invoice_key), + wallet: WalletTypeInfo = Depends(require_admin_key), ) -> DcaDeposit: """Get a specific deposit""" deposit = await get_deposit(deposit_id) @@ -124,287 +85,3 @@ async def api_get_deposit( status_code=HTTPStatus.NOT_FOUND, detail="Deposit not found." ) return deposit - - -@satmachineclient_api_router.post("/api/v1/dca/deposits", status_code=HTTPStatus.CREATED) -async def api_create_deposit( - data: CreateDepositData, - wallet: WalletTypeInfo = Depends(require_admin_key), -) -> DcaDeposit: - """Create a new deposit""" - # Verify client exists - client = await get_dca_client(data.client_id) - if not client: - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="DCA client not found." - ) - - return await create_deposit(data) - - -@satmachineclient_api_router.put("/api/v1/dca/deposits/{deposit_id}/status") -async def api_update_deposit_status( - deposit_id: str, - data: UpdateDepositStatusData, - wallet: WalletTypeInfo = Depends(require_admin_key), -) -> DcaDeposit: - """Update deposit status (e.g., confirm deposit)""" - deposit = await get_deposit(deposit_id) - if not deposit: - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="Deposit not found." - ) - - updated_deposit = await update_deposit_status(deposit_id, data) - if not updated_deposit: - raise HTTPException( - status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Failed to update deposit." - ) - return updated_deposit - - -# Transaction Polling Endpoints - -@satmachineclient_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 with detailed reporting""" - try: - from .transaction_processor import transaction_processor - - # Use the detailed test method - result = await transaction_processor.test_connection_detailed() - return result - - except Exception as e: - return { - "success": False, - "message": f"Test connection error: {str(e)}", - "steps": [f"❌ Unexpected error: {str(e)}"], - "ssh_tunnel_used": False, - "ssh_tunnel_success": False, - "database_connection_success": False - } - - -@satmachineclient_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 - from .crud import update_poll_start_time, update_poll_success_time - - # Get database configuration - db_config = await transaction_processor.connect_to_lamassu_db() - if not db_config: - raise HTTPException( - status_code=HTTPStatus.SERVICE_UNAVAILABLE, - detail="Could not get Lamassu database configuration" - ) - - config_id = db_config["config_id"] - - # Record manual poll start time - await update_poll_start_time(config_id) - - # Fetch and process transactions via SSH - new_transactions = await transaction_processor.fetch_new_transactions(db_config) - - transactions_processed = 0 - for transaction in new_transactions: - await transaction_processor.process_transaction(transaction) - transactions_processed += 1 - - # Record successful manual poll completion - await update_poll_success_time(config_id) - - return { - "success": True, - "transactions_processed": transactions_processed, - "message": f"Processed {transactions_processed} new transactions since last poll" - } - - except Exception as e: - raise HTTPException( - status_code=HTTPStatus.INTERNAL_SERVER_ERROR, - detail=f"Error during manual poll: {str(e)}" - ) - - -@satmachineclient_api_router.post("/api/v1/dca/test-transaction") -async def api_test_transaction( - wallet: WalletTypeInfo = Depends(require_admin_key), - crypto_atoms: int = 103, - commission_percentage: float = 0.03, - discount: float = 0.0, -) -> dict: - """Test transaction processing with simulated Lamassu transaction data""" - try: - from .transaction_processor import transaction_processor - import uuid - from datetime import datetime, timezone - - # Create a mock transaction that mimics Lamassu database structure - mock_transaction = { - "transaction_id": str(uuid.uuid4())[:8], # Short ID for testing - "crypto_amount": crypto_atoms, # Total sats including commission - "fiat_amount": 100, # Mock fiat amount (100 centavos = 1 GTQ) - "commission_percentage": commission_percentage, # Already as decimal - "discount": discount, - "transaction_time": datetime.now(timezone.utc), - "crypto_code": "BTC", - "fiat_code": "GTQ", - "device_id": "test_device", - "status": "confirmed" - } - - # Process the mock transaction through the complete DCA flow - await transaction_processor.process_transaction(mock_transaction) - - # Calculate commission for response - if commission_percentage > 0: - effective_commission = commission_percentage * (100 - discount) / 100 - base_crypto_atoms = int(crypto_atoms / (1 + effective_commission)) - commission_amount_sats = crypto_atoms - base_crypto_atoms - else: - base_crypto_atoms = crypto_atoms - commission_amount_sats = 0 - - return { - "success": True, - "message": "Test transaction processed successfully", - "transaction_details": { - "transaction_id": mock_transaction["transaction_id"], - "total_amount_sats": crypto_atoms, - "base_amount_sats": base_crypto_atoms, - "commission_amount_sats": commission_amount_sats, - "commission_percentage": commission_percentage * 100, # Show as percentage - "effective_commission": effective_commission * 100 if commission_percentage > 0 else 0, - "discount": discount - } - } - - except Exception as e: - raise HTTPException( - status_code=HTTPStatus.INTERNAL_SERVER_ERROR, - detail=f"Error processing test transaction: {str(e)}" - ) - - -# Lamassu Transaction Endpoints - -@satmachineclient_api_router.get("/api/v1/dca/transactions") -async def api_get_lamassu_transactions( - wallet: WalletTypeInfo = Depends(require_invoice_key), -) -> list[StoredLamassuTransaction]: - """Get all processed Lamassu transactions""" - return await get_all_lamassu_transactions() - - -@satmachineclient_api_router.get("/api/v1/dca/transactions/{transaction_id}") -async def api_get_lamassu_transaction( - transaction_id: str, - wallet: WalletTypeInfo = Depends(require_invoice_key), -) -> StoredLamassuTransaction: - """Get a specific Lamassu transaction with details""" - transaction = await get_lamassu_transaction(transaction_id) - if not transaction: - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="Lamassu transaction not found." - ) - return transaction - - -@satmachineclient_api_router.get("/api/v1/dca/transactions/{transaction_id}/distributions") -async def api_get_transaction_distributions( - transaction_id: str, - wallet: WalletTypeInfo = Depends(require_invoice_key), -) -> list[dict]: - """Get distribution details for a specific Lamassu transaction""" - # Get the stored transaction - transaction = await get_lamassu_transaction(transaction_id) - if not transaction: - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="Lamassu transaction not found." - ) - - # Get all DCA payments for this Lamassu transaction - from .crud import get_payments_by_lamassu_transaction, get_dca_client - payments = await get_payments_by_lamassu_transaction(transaction.lamassu_transaction_id) - - # Enhance payments with client information - distributions = [] - for payment in payments: - client = await get_dca_client(payment.client_id) - distributions.append({ - "payment_id": payment.id, - "client_id": payment.client_id, - "client_username": client.username if client else None, - "client_user_id": client.user_id if client else None, - "amount_sats": payment.amount_sats, - "amount_fiat": payment.amount_fiat, - "exchange_rate": payment.exchange_rate, - "status": payment.status, - "created_at": payment.created_at - }) - - return distributions - - -# Lamassu Configuration Endpoints - -@satmachineclient_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() - - -@satmachineclient_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) - - -@satmachineclient_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 - - -@satmachineclient_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"}