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
13 changes: 13 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,19 @@ RPC_BATCH_SIZE=25
PRICE_CACHE_TTL_MS=120000
PG_MAX_CLIENTS=10

# CoinGecko Configuration.
#
# COINGECKO_BASE_URL: required. The origin the service calls. Recommended is
# the in-cluster pricing-proxy (https://github.com/DFXswiss/pricing-proxy),
# which holds the upstream Pro key and serves a 60 s shared cache. Anything
# CoinGecko-compatible works (pro-api.coingecko.com, api.coingecko.com, …).
COINGECKO_BASE_URL=http://pricing-proxy:8080/coingecko
#
# COINGECKO_API_KEY: optional. If set, attached as `x-cg-pro-api-key` to every
# request. Leave unset when talking to the pricing-proxy (proxy injects its
# own key) or to the public host anonymously.
# COINGECKO_API_KEY=

# Telegram Bot Configuration (optional)
# TELEGRAM_BOT_TOKEN=5123456789:ABCdefGHIjklMNOpqrsTUVwxyz
# Path to the persisted subscribers file. Each operator subscribes themselves by
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/frontend-dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
uses: actions/checkout@v4

- name: Build Docker image
run: docker build -f frontend/Dockerfile -t ${{ env.DOCKER_TAGS }} .
run: docker build --build-arg VITE_DEPLOYMENT_ENV=dev -f frontend/Dockerfile -t ${{ env.DOCKER_TAGS }} .

deploy:
name: Deploy Frontend to DEV
Expand Down Expand Up @@ -54,6 +54,8 @@ jobs:
push: true
tags: ${{ env.DOCKER_TAGS }}
platforms: linux/arm64
build-args: |
VITE_DEPLOYMENT_ENV=dev

- name: Install cloudflared
run: |
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/frontend-prd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
uses: actions/checkout@v4

- name: Build Docker image
run: docker build -f frontend/Dockerfile -t ${{ env.DOCKER_TAGS }} .
run: docker build --build-arg VITE_DEPLOYMENT_ENV=prd -f frontend/Dockerfile -t ${{ env.DOCKER_TAGS }} .

deploy:
name: Deploy Frontend to PRD
Expand Down Expand Up @@ -54,6 +54,8 @@ jobs:
push: true
tags: ${{ env.DOCKER_TAGS }}
platforms: linux/arm64
build-args: |
VITE_DEPLOYMENT_ENV=prd

- name: Install cloudflared
run: |
Expand Down
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ cp .env.example .env
# - DATABASE_URL: PostgreSQL connection string
# - RPC_URL: https://rpc.citreascan.com (Citrea mainnet)
# - BLOCKCHAIN_ID: Must be 4114 (Citrea)
# - COINGECKO_BASE_URL: required, see "CoinGecko" section below

# Generate Prisma client
npm run prisma:generate
Expand Down Expand Up @@ -87,6 +88,37 @@ Swagger documentation available at: `http://localhost:3001/swagger`
| `/jusd` | JUSD supply and protocol stats |
| `/minters` | Registered minters |

## CoinGecko

The monitoring service needs a CoinGecko-compatible endpoint for BTC spot
prices (drives the WCBTC suspicious-liq-price watchdog) and the daily Pro
quota probe. Configuration is two env vars:

| Var | Required | Purpose |
|---|---|---|
| `COINGECKO_BASE_URL` | yes | Origin the service calls. |
| `COINGECKO_API_KEY` | no | Attached as the `x-cg-pro-api-key` header on every request when set. |

The recommended deployment is the
[**pricing-proxy**](https://github.com/DFXswiss/pricing-proxy) — a small
caching reverse-proxy in front of CoinGecko Pro. It holds the upstream key,
serves a 60 s shared cache, validates upstream error envelopes, and
coalesces concurrent identical requests. When you use the proxy:

```env
COINGECKO_BASE_URL=http://pricing-proxy:8080/coingecko
# COINGECKO_API_KEY left unset — the proxy injects its own key
```

Without the proxy you can talk to CoinGecko directly:

```env
COINGECKO_BASE_URL=https://pro-api.coingecko.com
COINGECKO_API_KEY=CG-xxxxxxxxxxxxxxxxxxxxxxxx
```

The service refuses to start without `COINGECKO_BASE_URL`.

## Deployment

- **Development**: Push to `develop` branch
Expand Down
2 changes: 2 additions & 0 deletions frontend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ COPY frontend/ .
COPY shared/ /app/shared/
ARG VITE_API_BASE_URL=/api
ENV VITE_API_BASE_URL=$VITE_API_BASE_URL
ARG VITE_DEPLOYMENT_ENV
ENV VITE_DEPLOYMENT_ENV=$VITE_DEPLOYMENT_ENV
RUN npm run build

FROM nginx:alpine
Expand Down
6 changes: 4 additions & 2 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ import { ChallengesTable } from './components/ChallengesTable';
import { BridgesTable } from './components/BridgesTable';
import { MintersTable } from './components/MintersTable';
import { HealthStatus } from './components/HealthStatus';
import { Footer } from './components/Footer';

function App() {
const { health, jusd, positions, collateral, challenges, minters } = useApi();

return (
<div className="min-h-screen bg-neutral-950 text-gray-100">
<div className="max-w-7xl mx-auto p-4 space-y-6 text-sm mb-8">
<div className="min-h-screen bg-neutral-950 text-gray-100 flex flex-col">
<div className="flex-1 max-w-7xl w-full mx-auto p-4 space-y-6 text-sm">
<HealthStatus {...health} />
<SystemOverview {...jusd} minters={minters} />
<PositionsTable data={positions} />
Expand All @@ -21,6 +22,7 @@ function App() {
<BridgesTable data={minters} />
<MintersTable data={minters} />
</div>
<Footer />
</div>
);
}
Expand Down
36 changes: 36 additions & 0 deletions frontend/src/components/Footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { DEPLOYMENT_ENV, resolveChain, TELEGRAM_BOT } from '../constants';

export function Footer() {
const botUrl = TELEGRAM_BOT[resolveChain()][DEPLOYMENT_ENV];

return (
<footer className="border-t border-neutral-800 mt-8">
<div className="max-w-7xl mx-auto px-4 py-6 flex items-center justify-end gap-3 text-sm">
<a
href={botUrl}
target="_blank"
rel="noreferrer noopener"
title="Telegram alerts bot"
aria-label="Telegram alerts bot"
className="inline-flex items-center gap-2 text-gray-400 hover:text-gray-100 transition-colors"
>
<TelegramIcon className="w-5 h-5" />
<span>Get alerts</span>
</a>
</div>
</footer>
);
}

function TelegramIcon({ className = '' }: { className?: string }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
className={`fill-current ${className}`}
aria-hidden="true"
>
<path d="M9.78 18.65l.28-4.23 7.68-6.92c.34-.31-.07-.46-.52-.19L7.74 13.3 3.64 12c-.88-.25-.89-.86.2-1.3l15.97-6.16c.73-.33 1.43.18 1.15 1.3l-2.72 12.81c-.19.91-.74 1.13-1.5.71L12.6 16.3l-1.99 1.93c-.23.23-.42.42-.83.42z" />
</svg>
);
}
26 changes: 26 additions & 0 deletions frontend/src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export type DeploymentEnv = 'prd' | 'dev';
export type Chain = 'mainnet' | 'testnet';

const rawDeploymentEnv = import.meta.env.VITE_DEPLOYMENT_ENV;
if (rawDeploymentEnv !== 'prd' && rawDeploymentEnv !== 'dev') {
throw new Error(`VITE_DEPLOYMENT_ENV must be "prd" or "dev" (got: "${rawDeploymentEnv}")`);
}
export const DEPLOYMENT_ENV: DeploymentEnv = rawDeploymentEnv;

export const TELEGRAM_BOT = {
mainnet: {
prd: 'https://t.me/juicedollar_monitor_prd_bot',
dev: 'https://t.me/juicedollar_monitor_dev_bot',
},
testnet: {
prd: 'https://t.me/juicedollar_monitor_tst_prd_bot',
dev: 'https://t.me/juicedollar_monitor_tst_dev_bot',
},
} as const;

export function resolveChain(): Chain {
if (typeof window === 'undefined') {
throw new Error('resolveChain() requires window.location');
}
return window.location.hostname.includes('testnet') ? 'testnet' : 'mainnet';
}
9 changes: 9 additions & 0 deletions frontend/src/vite-env.d.ts
Original file line number Diff line number Diff line change
@@ -1 +1,10 @@
/// <reference types="vite/client" />

interface ImportMetaEnv {
readonly VITE_API_BASE_URL?: string;
readonly VITE_DEPLOYMENT_ENV?: string;
}

interface ImportMeta {
readonly env: ImportMetaEnv;
}
4 changes: 4 additions & 0 deletions src/config/config.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ export class AppConfigService {
return this.monitoringConfig.coingeckoApiKey || undefined;
}

get coingeckoBaseUrl(): string | undefined {
return this.monitoringConfig.coingeckoBaseUrl || undefined;
}

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

@IsOptional()
@IsString()
coingeckoBaseUrl?: string;

@IsOptional()
@IsString()
coingeckoApiKey?: string;
Expand Down Expand Up @@ -99,6 +103,7 @@ export default registerAs('monitoring', () => {
config.telegramGroupsJson = process.env.TELEGRAM_GROUPS_JSON;
config.telegramAlertsEnabled = (process.env.TELEGRAM_ALERTS_ENABLED || 'false').toLowerCase() === 'true';
config.alertTimeframeHours = parseInt(process.env.ALERT_TIMEFRAME_HOURS || '12');
config.coingeckoBaseUrl = process.env.COINGECKO_BASE_URL || '';
config.coingeckoApiKey = process.env.COINGECKO_API_KEY || '';
config.environment = process.env.ENVIRONMENT?.toLowerCase();
config.chain = process.env.CHAIN;
Expand Down
Loading
Loading