Skip to content
2 changes: 1 addition & 1 deletion src/controllers/adminController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@

// Log audit event before deletion
const adminInfo = extractAdminInfo(req);
await logAuditEvent({
const deleteAuditPayload: Parameters<typeof logAuditEvent>[0] = {
eventType: "RELAYER_REGISTRY_DELETED",
actionType: "RELAYER_REGISTRY",
relatedId: existing.id,
Expand All @@ -305,7 +305,7 @@
}),
...(adminInfo.ipAddress !== undefined ? { ipAddress: adminInfo.ipAddress } : {}),
...(adminInfo.userAgent !== undefined ? { userAgent: adminInfo.userAgent } : {}),
});

Check failure on line 308 in src/controllers/adminController.ts

View workflow job for this annotation

GitHub Actions / build-and-test

',' expected.

await prisma.relayerRegistry.delete({
where: { relayerId },
Expand Down
25 changes: 25 additions & 0 deletions src/metrics/index.ts
Original file line number Diff line number Diff line change
@@ -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"],
});
5 changes: 5 additions & 0 deletions src/services/marketRate/middleValuePriceService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/services/multiSigService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
failedSubmissions,
gasUsagePerAsset,
submissionDuration,
} from "../metrics";
} from "../metrics/index.js";

dotenv.config();

Expand Down
3 changes: 3 additions & 0 deletions src/services/priceAggregatorService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
4 changes: 4 additions & 0 deletions src/services/secretManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
18 changes: 18 additions & 0 deletions src/services/sorobanEventListener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down
35 changes: 31 additions & 4 deletions src/services/stellarService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
}

Expand Down
62 changes: 62 additions & 0 deletions src/services/webhook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(
Expand Down
Loading