diff --git a/src/controllers/adminController.ts b/src/controllers/adminController.ts index 8bc79463..a4004c2c 100644 --- a/src/controllers/adminController.ts +++ b/src/controllers/adminController.ts @@ -290,7 +290,7 @@ export const deleteRelayerRegistry = async (req: Request, res: Response) => { // Log audit event before deletion const adminInfo = extractAdminInfo(req); - await logAuditEvent({ + const deleteAuditPayload: Parameters[0] = { eventType: "RELAYER_REGISTRY_DELETED", actionType: "RELAYER_REGISTRY", relatedId: existing.id, diff --git a/src/metrics/index.ts b/src/metrics/index.ts new file mode 100644 index 00000000..a46c419a --- /dev/null +++ b/src/metrics/index.ts @@ -0,0 +1,25 @@ +import { Counter, Histogram } from "prom-client"; + +export const successfulSubmissions = new Counter({ + name: "multi_sig_successful_submissions_total", + help: "Total number of successfully submitted multi-signature updates", + labelNames: ["asset"], +}); + +export const failedSubmissions = new Counter({ + name: "multi_sig_failed_submissions_total", + help: "Total number of failed multi-signature submissions", + labelNames: ["asset", "reason"], +}); + +export const gasUsagePerAsset = new Histogram({ + name: "multi_sig_gas_usage_stroops", + help: "Gas usage per asset for multi-signature submissions", + labelNames: ["asset"], +}); + +export const submissionDuration = new Histogram({ + name: "multi_sig_submission_duration_seconds", + help: "Duration of multi-signature submission processing", + labelNames: ["asset"], +}); diff --git a/src/services/marketRate/middleValuePriceService.ts b/src/services/marketRate/middleValuePriceService.ts index 4d34bea5..12cd834f 100644 --- a/src/services/marketRate/middleValuePriceService.ts +++ b/src/services/marketRate/middleValuePriceService.ts @@ -115,6 +115,11 @@ export class MiddleValuePriceService { const middleValue = this.calculateMiddleValue(rates); // Use the most recent timestamp from successful responses + const firstResult = successfulResults[0]; + if (!firstResult) { + throw new Error("No successful price sources available for middle value calculation"); + } + const mostRecentTimestamp = successfulResults.reduce( (latest, result) => (result.timestamp > latest ? result.timestamp : latest), successfulResults[0]!.timestamp, diff --git a/src/services/multiSigService.ts b/src/services/multiSigService.ts index 4c67a74f..8ce585ff 100644 --- a/src/services/multiSigService.ts +++ b/src/services/multiSigService.ts @@ -8,7 +8,7 @@ import { failedSubmissions, gasUsagePerAsset, submissionDuration, -} from "../metrics"; +} from "../metrics/index.js"; dotenv.config(); diff --git a/src/services/priceAggregatorService.ts b/src/services/priceAggregatorService.ts index 8711a467..0b1efd18 100644 --- a/src/services/priceAggregatorService.ts +++ b/src/services/priceAggregatorService.ts @@ -274,6 +274,9 @@ export class PriceAggregatorService { const rates = ticks.map((t: any) => Number(t.rate)); + const open = rates[0] ?? 0; + const close = rates[rates.length - 1] ?? 0; + return { open: rates[0]!, high: Math.max(...rates), diff --git a/src/services/secretManager.ts b/src/services/secretManager.ts index 42af1587..7c2d1d3f 100644 --- a/src/services/secretManager.ts +++ b/src/services/secretManager.ts @@ -9,6 +9,10 @@ export type ReloadTrigger = "admin-endpoint" | "file-watcher" | "startup"; let reloadCount: number = 0; const KEY_SLOT = "stellar-secret"; +function shouldFailFast(): boolean { + return process.env.NODE_ENV === "production"; +} + /** * Validates a candidate Stellar secret key. * Throws with a safe message — never includes the candidate value. diff --git a/src/services/sorobanEventListener.ts b/src/services/sorobanEventListener.ts index 1af52b69..138d42cc 100644 --- a/src/services/sorobanEventListener.ts +++ b/src/services/sorobanEventListener.ts @@ -79,6 +79,24 @@ export class SorobanEventListener { await this.pollTransactions(); // Start periodic polling + this.startPollingTimer(); + } + + restart(newIntervalMs: number): void { + this.pollIntervalMs = newIntervalMs; + + if (!this.isRunning) { + return; + } + + if (this.pollTimer) { + clearInterval(this.pollTimer); + } + + this.startPollingTimer(); + } + + private startPollingTimer(): void { this.pollTimer = setInterval(() => { this.pollTransactions().catch((err) => { logger.networkError("[EventListener] Poll error:", { err }); diff --git a/src/services/stellarService.ts b/src/services/stellarService.ts index 8af52316..5c1b3e64 100644 --- a/src/services/stellarService.ts +++ b/src/services/stellarService.ts @@ -10,7 +10,10 @@ import { Account, } from "@stellar/stellar-sdk"; import stellarProvider from "../lib/stellarProvider"; -import { getStellarNetworkPassphrase } from "../lib/stellarNetwork"; +import { + getStellarNetwork, + getStellarNetworkPassphrase, +} from "../lib/stellarNetwork"; import { sequenceManager } from "./sequence-manager"; import { assertSigningAllowed } from "../state/appState"; import { signer } from "../signer"; @@ -110,7 +113,15 @@ export class StellarService { baseFee, ); - logger.networkInfo(`✅ Price update for ${currency} confirmed. Hash: ${result.hash}`, { hash: result.hash }); + const network = getStellarNetwork(); + const txURL = network === "TESTNET" + ? `https://testnet.stellarexpert.org/tx/${result.hash}` + : `https://stellarexpert.org/tx/${result.hash}`; + + logger.networkInfo( + `✅ Price update for ${currency} confirmed. Hash: ${result.hash} | StellarExpert: ${txURL}`, + { hash: result.hash, url: txURL }, + ); return result.hash; } @@ -154,7 +165,15 @@ export class StellarService { ); const currencies = updates.map((u) => u.currency).join(", "); - logger.networkInfo(`✅ Batched price update for [${currencies}] confirmed. Hash: ${result.hash}`, { hash: result.hash, currencies }); + const network = getStellarNetwork(); + const txURL = network === "TESTNET" + ? `https://testnet.stellarexpert.org/tx/${result.hash}` + : `https://stellarexpert.org/tx/${result.hash}`; + + logger.networkInfo( + `✅ Batched price update for [${currencies}] confirmed. Hash: ${result.hash} | StellarExpert: ${txURL}`, + { hash: result.hash, url: txURL, currencies }, + ); return result.hash; } @@ -191,7 +210,15 @@ export class StellarService { baseFee, ); - logger.networkInfo(`✅ Multi-signed price update for ${currency} confirmed. Hash: ${result.hash}`, { hash: result.hash }); + const network = getStellarNetwork(); + const txURL = network === "TESTNET" + ? `https://testnet.stellarexpert.org/tx/${result.hash}` + : `https://stellarexpert.org/tx/${result.hash}`; + + logger.networkInfo( + `✅ Multi-signed price update for ${currency} confirmed. Hash: ${result.hash} | StellarExpert: ${txURL}`, + { hash: result.hash, url: txURL }, + ); return result.hash; } diff --git a/src/services/webhook.ts b/src/services/webhook.ts index 6343df6a..b84c63e0 100644 --- a/src/services/webhook.ts +++ b/src/services/webhook.ts @@ -79,6 +79,15 @@ type MonitorFailureAlertDetails = { timestamp: Date; }; +type PriorityAlertDetails = { + currency: string; + rate: number; + zScore: number; + mean: number; + stdDev: number; + timestamp: Date; +}; + export class WebhookService { private webhookUrl: string | undefined; private platform: string; @@ -317,6 +326,59 @@ export class WebhookService { }; } + private formatPriorityAlert(alertDetails: PriorityAlertDetails): WebhookPayload { + const { currency, rate, zScore, mean, stdDev, timestamp } = alertDetails; + + if (this.platform === "discord") { + return { + embeds: [ + { + title: "⚠️ High Priority Market Anomaly Detected", + color: 0xff6b00, + fields: [ + { name: "Currency", value: currency, inline: true }, + { name: "Rate", value: rate.toString(), inline: true }, + { name: "Z-Score", value: zScore.toFixed(2), inline: true }, + { name: "Mean", value: mean.toString(), inline: true }, + { name: "Std Dev", value: stdDev.toString(), inline: true }, + { name: "Time", value: timestamp.toISOString() }, + ], + }, + ], + }; + } + + return { + blocks: [ + { + type: "header", + text: { type: "plain_text", text: "⚠️ High Priority Market Anomaly Detected" }, + }, + { + type: "section", + fields: [ + { type: "mrkdwn", text: `*Currency:* +${currency}` }, + { type: "mrkdwn", text: `*Rate:* +${rate}` }, + { type: "mrkdwn", text: `*Z-Score:* +${zScore.toFixed(2)}` }, + { type: "mrkdwn", text: `*Mean:* +${mean}` }, + { type: "mrkdwn", text: `*Std Dev:* +${stdDev}` }, + ], + }, + { + type: "context", + elements: [ + { type: "mrkdwn", text: `Detected at ${timestamp.toISOString()}` }, + ], + }, + ], + }; + } + // FIX 1: Added Slack branch — previously always returned a Discord embed, // which would be silently dropped or mangled when NOTIFICATION_PLATFORM=slack. private formatGasBalanceAlert(