Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/workflows/monitoring-dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ on:
paths-ignore:
- 'frontend/**'
workflow_dispatch:
inputs:
reset_database:
description: 'Reset Database (DESTRUCTIVE - drops all data)'
required: false
type: boolean
default: false

env:
DOCKER_TAGS: dfxswiss/deuro-monitoring:beta
Expand Down Expand Up @@ -48,6 +54,22 @@ jobs:
inlineScript: |
az containerapp update --resource-group ${{ env.AZURE_RESOURCE_GROUP }} --name ${{ env.AZURE_CONTAINER_APP }} --image ${{ env.DOCKER_TAGS }} --set-env-vars DEPLOY_INFO=${{ env.DEPLOY_INFO }}

- name: Reset Database (if requested)
if: inputs.reset_database == true
run: |
set -euo pipefail
sudo apt-get update && sudo apt-get install -y postgresql-client
echo "DATABASE RESET INITIATED"
psql "$DATABASE_URL" <<'EOF'
DROP SCHEMA public CASCADE;
CREATE SCHEMA public;
GRANT ALL ON SCHEMA public TO PUBLIC;
EOF
psql "$DATABASE_URL" -f database/schema.sql
echo "Database reset complete"
env:
DATABASE_URL: ${{ secrets.DATABASE_URL_DEV }}

- name: Logout from Azure
run: az logout
if: always()
37 changes: 36 additions & 1 deletion .github/workflows/monitoring-prd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ on:
paths-ignore:
- 'frontend/**'
workflow_dispatch:
inputs:
reset_database:
description: 'Reset Database (DESTRUCTIVE - drops all data)'
required: false
type: boolean
default: false

env:
DOCKER_TAGS: dfxswiss/deuro-monitoring:latest
Expand Down Expand Up @@ -50,4 +56,33 @@ jobs:

- name: Logout from Azure
run: az logout
if: always()
if: always()

reset-db:
name: Reset PRD Database
needs: build-and-deploy
runs-on: ubuntu-latest
timeout-minutes: 15
if: inputs.reset_database == true
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install PostgreSQL Client
run: |
sudo apt-get update
sudo apt-get install -y postgresql-client

- name: Reset Database
run: |
set -euo pipefail
echo "PRODUCTION DATABASE RESET INITIATED"
psql "$DATABASE_URL" <<'EOF'
DROP SCHEMA public CASCADE;
CREATE SCHEMA public;
GRANT ALL ON SCHEMA public TO PUBLIC;
EOF
psql "$DATABASE_URL" -f database/schema.sql
echo "Production database reset complete"
env:
DATABASE_URL: ${{ secrets.DATABASE_URL_PRD }}
91 changes: 45 additions & 46 deletions database/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -163,50 +163,49 @@ CREATE INDEX IF NOT EXISTS idx_minter_states_status ON minter_states(status);
CREATE INDEX IF NOT EXISTS idx_minter_states_bridge_token ON minter_states(bridge_token) WHERE bridge_token IS NOT NULL;

-- System State (single row for global metrics)
-- CREATE TABLE IF NOT EXISTS system_state (
-- id INTEGER PRIMARY KEY DEFAULT 1,
-- -- Token supplies
-- deuro_total_supply NUMERIC(78, 0) NOT NULL DEFAULT 0, -- dynamic: DecentralizedEURO.totalSupply
-- deps_total_supply NUMERIC(78, 0) NOT NULL DEFAULT 0, -- dynamic: DEPSWrapper.totalSupply
CREATE TABLE IF NOT EXISTS deuro_state (
id INTEGER PRIMARY KEY DEFAULT 1,
-- Token supplies
deuro_total_supply NUMERIC(78, 0) NOT NULL DEFAULT 0, -- dynamic: DecentralizedEURO.totalSupply
deps_total_supply NUMERIC(78, 0) NOT NULL DEFAULT 0, -- dynamic: DEPSWrapper.totalSupply

-- -- Equity metrics
-- equity_shares NUMERIC(78, 0) NOT NULL DEFAULT 0, -- dynamic: Equity.totalSupply
-- equity_price NUMERIC(78, 0) NOT NULL DEFAULT 0, -- dynamic: Equity.price

-- -- Reserve metrics
-- reserve_total NUMERIC(78, 0) NOT NULL DEFAULT 0, -- dynamic: DecentralizedEURO.reserve
-- reserve_minter NUMERIC(78, 0) NOT NULL DEFAULT 0, -- dynamic: DecentralizedEURO.minterReserve
-- reserve_equity NUMERIC(78, 0) NOT NULL DEFAULT 0, -- dynamic: DecentralizedEURO.equity

-- -- Savings metrics
-- savings_total NUMERIC(78, 0) NOT NULL DEFAULT 0, -- dynamic: DecentralizedEURO.balanceOf(SavingsGateway.address)
-- savings_interest_collected NUMERIC(78, 0) NOT NULL DEFAULT 0, -- dynamic: sum over SavingsGateway.InterestCollected events
-- savings_rate NUMERIC(78, 0) NOT NULL DEFAULT 0, -- dynamic: SavingsGateway.currentRatePPM

-- -- Profit/Loss tracking
-- deuro_loss NUMERIC(78, 0) DEFAULT 0 NOT NULL, -- dynamic: sum over DecentralizedEURO.Loss events
-- deuro_profit NUMERIC(78, 0) DEFAULT 0 NOT NULL, -- dynamic: sum over DecentralizedEURO.Profit events
-- deuro_profit_distributed NUMERIC(78, 0) DEFAULT 0 NOT NULL, -- dynamic: sum over DecentralizedEURO.ProfitDistributed events

-- -- Frontend metrics
-- frontend_fees_collected NUMERIC(78, 0) DEFAULT 0 NOT NULL, -- dynamic: sum over FrontendGateway.FrontendCodeRewardsWithdrawn
-- frontends_active INTEGER DEFAULT 0 NOT NULL, -- dynamic: count over FrontendCodeRegistered events

-- -- Currency rates
-- usd_to_eur_rate NUMERIC(10, 6) DEFAULT 0 NOT NULL, -- dynamic: getExchangeRate('USD', 'EUR')
-- usd_to_chf_rate NUMERIC(10, 6) DEFAULT 0 NOT NULL, -- dynamic: getExchangeRate('USD', 'CHF')

-- -- metadata
-- block_number BIGINT NOT NULL DEFAULT 0,
-- timestamp TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
-- CONSTRAINT system_single_row CHECK (id = 1)
-- );

-- =============================================================================
-- INITIALIZATION
-- =============================================================================

-- Initialize system state (required for UPDATE queries to work)
-- INSERT INTO system_state (id)
-- VALUES (1)
-- ON CONFLICT (id) DO NOTHING;
-- Equity metrics
equity_shares NUMERIC(78, 0) NOT NULL DEFAULT 0, -- dynamic: Equity.totalSupply
equity_price NUMERIC(78, 0) NOT NULL DEFAULT 0, -- dynamic: Equity.price

-- Reserve metrics
reserve_total NUMERIC(78, 0) NOT NULL DEFAULT 0, -- dynamic: DecentralizedEURO.reserve
reserve_minter NUMERIC(78, 0) NOT NULL DEFAULT 0, -- dynamic: DecentralizedEURO.minterReserve
reserve_equity NUMERIC(78, 0) NOT NULL DEFAULT 0, -- dynamic: DecentralizedEURO.equity

-- Savings metrics
savings_total NUMERIC(78, 0) NOT NULL DEFAULT 0, -- dynamic: DecentralizedEURO.balanceOf(SavingsGateway.address)
savings_interest_collected NUMERIC(78, 0) NOT NULL DEFAULT 0, -- dynamic: sum over SavingsGateway.InterestCollected events
savings_rate INTEGER NOT NULL DEFAULT 0, -- dynamic: SavingsGateway.currentRatePPM

-- Profit/Loss tracking
deuro_loss NUMERIC(78, 0) DEFAULT 0 NOT NULL, -- dynamic: sum over DecentralizedEURO.Loss events
deuro_profit NUMERIC(78, 0) DEFAULT 0 NOT NULL, -- dynamic: sum over DecentralizedEURO.Profit events
deuro_profit_distributed NUMERIC(78, 0) DEFAULT 0 NOT NULL, -- dynamic: sum over DecentralizedEURO.ProfitDistributed events

-- Frontend metrics
frontend_fees_collected NUMERIC(78, 0) DEFAULT 0 NOT NULL, -- dynamic: sum over FrontendGateway.FrontendCodeRewardsWithdrawn
frontends_active INTEGER DEFAULT 0 NOT NULL, -- dynamic: count over FrontendCodeRegistered events

-- Currency rates
usd_to_eur_rate NUMERIC(10, 6) DEFAULT 0 NOT NULL, -- dynamic: getExchangeRate('USD', 'EUR')
usd_to_chf_rate NUMERIC(10, 6) DEFAULT 0 NOT NULL, -- dynamic: getExchangeRate('USD', 'CHF')

-- 24h metrics
savings_interest_collected_24h NUMERIC(78, 0) DEFAULT 0 NOT NULL, -- dynamic: sum over last 24h InterestCollected events
savings_added_24h NUMERIC(78, 0) DEFAULT 0 NOT NULL, -- dynamic: sum over last 24h Saved events
savings_withdrawn_24h NUMERIC(78, 0) DEFAULT 0 NOT NULL, -- dynamic: sum over last 24h Withdrawn events
equity_trade_volume_24h NUMERIC(78, 0) DEFAULT 0 NOT NULL, -- dynamic: sum over last 24h Trade events (totPrice)
equity_trade_count_24h INTEGER DEFAULT 0 NOT NULL, -- dynamic: count over last 24h Trade events
equity_delegations_24h INTEGER DEFAULT 0 NOT NULL, -- dynamic: count over last 24h Delegation events

-- metadata
block_number BIGINT NOT NULL DEFAULT 0,
timestamp TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
CONSTRAINT system_single_row CHECK (id = 1)
);
1 change: 1 addition & 0 deletions frontend/src/components/MintersTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export function MintersTable({ data }: MinterTableProps) {
columns={columns}
getRowKey={(minter) => minter.address}
hidden={(minter) => minter.status === MinterStatus.DENIED || minter.status === MinterStatus.EXPIRED}
sort={(a, b) => Number(b.applicationTimestamp) - Number(a.applicationTimestamp)}
emptyMessage="No minters found"
/>
);
Expand Down
62 changes: 36 additions & 26 deletions frontend/src/components/SystemOverview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,54 +7,64 @@ export function SystemOverview({ data, error }: DataState<DeuroState>) {
if (error) return <div className={colors.critical}>{error}</div>;
if (!data) return null;

// 300'000 dEURO manually added to Equity contract during liquidiation of WFPS postions (26.06.2025-29.06.2025)
const deuroProfit = BigInt(data.deuroProfit) + 300_000n * 10n ** 18n;
const netProfit = deuroProfit - BigInt(data.deuroLoss);
const deuroProfit = parseFloat(data.deuroProfit);
const netProfit = deuroProfit - parseFloat(data.deuroLoss);

return (
<div className={`${colors.background} ${colors.table.border} border rounded-xl p-4`}>
<h2 className={`text-sm uppercase tracking-wider ${colors.text.primary} mb-4`}>SYSTEM OVERVIEW</h2>

<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-8">
<Section title="SUPPLY">
<Metric label="dEURO" value={formatNumber(data.deuroTotalSupply, 18, 2)} valueClass={colors.text.primary} />
<Metric label="nDEPS" value={formatNumber(data.equityShares, 18, 2)} />
<Metric label="DEPS" value={formatNumber(data.depsTotalSupply, 18, 2)} />
<Metric label="dEURO" value={formatNumber(data.deuroTotalSupply, 0, 2)} valueClass={colors.text.primary} />
<Metric label="nDEPS" value={formatNumber(data.equityShares, 0, 2)} />
<Metric label="DEPS" value={formatNumber(data.depsTotalSupply, 0, 2)} />
</Section>

<Section title="RESERVES">
<Metric label="Total" value={formatNumber(data.reserveTotal, 18, 2)} />
<Metric label="Minter" value={formatNumber(data.reserveMinter, 18, 2)} />
<Metric label="Equity" value={formatNumber(data.reserveEquity, 18, 2)} valueClass={colors.success} />
<Metric label="Total" value={formatNumber(data.reserveTotal, 0, 2)} />
<Metric label="Minter" value={formatNumber(data.reserveMinter, 0, 2)} />
<Metric label="Equity" value={formatNumber(data.reserveEquity, 0, 2)} valueClass={colors.success} />
</Section>

<Section title="24H ACTIVITY (dEURO)">
<Metric label="Volume" value={formatNumber(data.deuroVolume24h, 18, 2)} />
<Metric label="Transfers" value={data.deuroTransferCount24h.toLocaleString()} />
<Metric label="Addresses" value={data.deuroUniqueAddresses24h.toLocaleString()} />
</Section>
{data.deuroVolume24h && (
<Section title="24H ACTIVITY (dEURO)">
<Metric label="Volume" value={formatNumber(data.deuroVolume24h, 0, 2)} />
{data.deuroTransferCount24h !== undefined && <Metric label="Transfers" value={data.deuroTransferCount24h.toLocaleString()} />}
{data.deuroUniqueAddresses24h !== undefined && <Metric label="Addresses" value={data.deuroUniqueAddresses24h.toLocaleString()} />}
</Section>
)}

<Section title="EQUITY">
<Metric label="Price" value={`${formatNumber(data.equityPrice, 18, 4)}`} valueClass={colors.text.primary} />
<Metric label="Profit" value={formatNumber(netProfit, 18, 2)} valueClass={colors.success} />
<Metric label="24h Vol" value={formatNumber(data.equityTradeVolume24h, 18, 2) + ` (${data.equityTradeCount24h.toLocaleString()})`} />
<Metric label="Price" value={`${formatNumber(data.equityPrice, 0, 4)}`} valueClass={colors.text.primary} />
<Metric label="Profit" value={formatNumber(netProfit, 0, 2)} valueClass={colors.success} />
<Metric label="24h Vol" value={formatNumber(data.equityTradeVolume24h, 0, 2) + ` (${data.equityTradeCount24h.toLocaleString()})`} />
<Metric label="24h Delegations" value={data.equityDelegations24h.toLocaleString()} />
</Section>

<Section title="SAVINGS">
<Metric label="Total" value={formatNumber(data.savingsTotal, 18, 2)} valueClass={colors.text.primary} />
<Metric label="Interest" value={formatNumber(data.savingsInterestCollected, 18, 2)} />
<Metric label="Total" value={formatNumber(data.savingsTotal, 0, 2)} valueClass={colors.text.primary} />
<Metric label="Interest" value={formatNumber(data.savingsInterestCollected, 0, 2)} />
<Metric label="Rate" value={formatPercent(Number(data.savingsRate) / 10_000, 2)} />
</Section>

<Section title="24H MINT/BURN (dEURO)">
<Metric label="Minted" value={formatNumber(data.deuroMinted24h || '0', 18, 2)} />
<Metric label="Burned" value={formatNumber(data.deuroBurned24h || '0', 18, 2)} />
<Metric
label="Net"
value={formatNumber((BigInt(data.deuroMinted24h || '0') - BigInt(data.deuroBurned24h || '0')).toString(), 18, 2)}
/>
<Section title="SAVINGS 24H">
<Metric label="Interest" value={formatNumber(data.savingsInterestCollected24h, 0, 2)} />
<Metric label="Added" value={formatNumber(data.savingsAdded24h, 0, 2)} />
<Metric label="Withdrawn" value={formatNumber(data.savingsWithdrawn24h, 0, 2)} />
</Section>

{(data.deuroMinted24h || data.deuroBurned24h) && (
<Section title="24H MINT/BURN (dEURO)">
<Metric label="Minted" value={formatNumber(data.deuroMinted24h || '0', 0, 2)} />
<Metric label="Burned" value={formatNumber(data.deuroBurned24h || '0', 0, 2)} />
<Metric
label="Net"
value={formatNumber((parseFloat(data.deuroMinted24h || '0') - parseFloat(data.deuroBurned24h || '0')), 0, 2)}
/>
</Section>
)}

{(data.usdToEurRate || data.usdToChfRate) && (
<Section title="CURRENCY RATES">
{data.usdToEurRate && <Metric label="USD/EUR" value={formatNumber(1 / data.usdToEurRate, 0, 4)} />}
Expand Down
6 changes: 2 additions & 4 deletions frontend/src/lib/api.hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const REFRESH_INTERVAL = 60000; // 1 minute

export function useApi(): UseApiResult {
const [health, setHealth] = useState<DataState<HealthResponse>>();
const [deuro, _setDeuro] = useState<DataState<DeuroState>>();
const [deuro, setDeuro] = useState<DataState<DeuroState>>();
const [positions, setPositions] = useState<DataState<PositionResponse[]>>();
const [collateral, setCollateral] = useState<DataState<CollateralResponse[]>>();
const [challenges, setChallenges] = useState<DataState<ChallengeResponse[]>>();
Expand All @@ -37,8 +37,7 @@ export function useApi(): UseApiResult {
if (!healthResult) return;

await Promise.all([
// TODO: Uncomment when backend tables are ready
// fetchData('deuro', setDeuro),
fetchData('deuro', setDeuro),
fetchData('positions', setPositions),
fetchData('collateral', setCollateral),
fetchData('challenges', setChallenges),
Expand All @@ -58,7 +57,6 @@ export function useApi(): UseApiResult {
}

async function fetchApi<T>(endpoint: string): Promise<T> {
console.log(`Fetching ${endpoint}...`);
const response = await fetch(`${API_BASE_URL}${endpoint}`);
if (!response?.ok) throw new Error(`API Error: ${response.statusText}`);
return await response.json();
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/lib/formatters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,11 +113,11 @@ export function getStatusColor(status: PositionStatus | MinterStatus | Challenge
case MinterStatus.PROPOSED:
case ChallengeStatus.AUCTION:
return colors.critical;
case PositionStatus.EXPIRED:
case PositionStatus.DENIED:
case ChallengeStatus.AVERTING:
return colors.highlight;
case PositionStatus.CLOSED:
case PositionStatus.EXPIRED:
case MinterStatus.DENIED:
case ChallengeStatus.ENDED:
return colors.text.secondary;
Expand Down
16 changes: 8 additions & 8 deletions shared/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,12 +102,12 @@ export interface DeuroState {
reserveTotal: string;
reserveMinter: string;
reserveEquity: string;
deuroVolume24h: string;
deuroTransferCount24h: number;
deuroUniqueAddresses24h: number;
depsVolume24h: string;
depsTransferCount24h: number;
depsUniqueAddresses24h: number;
deuroVolume24h?: string;
deuroTransferCount24h?: number;
deuroUniqueAddresses24h?: number;
depsVolume24h?: string;
depsTransferCount24h?: number;
depsUniqueAddresses24h?: number;
equityTradeVolume24h: string;
equityTradeCount24h: number;
equityDelegations24h: number;
Expand All @@ -119,8 +119,8 @@ export interface DeuroState {
savingsAdded24h: string;
savingsWithdrawn24h: string;
savingsInterestCollected24h: string;
deuroMinted24h: string;
deuroBurned24h: string;
deuroMinted24h?: string;
deuroBurned24h?: string;
savingsInterestCollected: string;
frontendFeesCollected: string;
frontendsActive: number;
Expand Down
4 changes: 4 additions & 0 deletions src/config/config.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ export class AppConfigService {
return this.monitoringConfig.priceCacheTtlMs || 120000;
}

get rpcTimeoutMs(): number {
return this.monitoringConfig.rpcTimeoutMs || 60000;
}

get telegramBotToken(): string | undefined {
return this.monitoringConfig.telegramBotToken;
}
Expand Down
8 changes: 8 additions & 0 deletions src/config/monitoring.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ export class MonitoringConfig {
@Min(1)
blockPerBatch?: number;

@Transform(({ value }) => parseInt(value))
@IsOptional()
@IsNumber()
@Min(5000)
@Max(300000)
rpcTimeoutMs?: number;

@IsOptional()
@IsString()
telegramBotToken?: string;
Expand Down Expand Up @@ -74,6 +81,7 @@ export default registerAs('monitoring', () => {
config.pgMaxClients = parseInt(process.env.PG_MAX_CLIENTS || '10');
config.priceCacheTtlMs = parseInt(process.env.PRICE_CACHE_TTL_MS || '120000');
config.blockPerBatch = parseInt(process.env.MAX_BLOCKS_PER_BATCH || '500');
config.rpcTimeoutMs = parseInt(process.env.RPC_TIMEOUT_MS || '60000');

config.telegramBotToken = process.env.TELEGRAM_BOT_TOKEN || '';
config.telegramChatId = process.env.TELEGRAM_CHAT_ID || '';
Expand Down
Loading
Loading