Skip to content
Open
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
2 changes: 1 addition & 1 deletion coordinator/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ async function main(): Promise<void> {

const app = createApp({
log,
corsOrigins: parseCorsOrigins(cfg.corsOrigins),
corsOrigin: cfg.corsOrigins,
maxRequestBodyBytes: cfg.maxRequestBodyBytes,
orders,
secrets,
Expand Down
42 changes: 16 additions & 26 deletions coordinator/src/persistence/orders-repo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,9 @@ export class OrdersRepository {
this.metricsByStatus = db.prepare(
"SELECT status, COUNT(*) as count FROM orders GROUP BY status"
);
this.metricsTotal = db.prepare("SELECT COUNT(*) as count FROM orders");
this.metricsTotal = db.prepare(
"SELECT COUNT(*) as count FROM orders"
);
this.metricsLastUpdated = db.prepare(
"SELECT MAX(updated_at) as ts FROM orders"
);
Expand Down Expand Up @@ -420,34 +422,22 @@ export class OrdersRepository {
}

async getMetrics(): Promise<OrderMetrics> {
const byStatus = (await this.all<{ status: string; count: string }>(
this.metricsByStatus
)) as { status: string; count: string }[];
const totalRow = (await this.get<{ count: string }>(this.metricsTotal)) as
| { count: string }
| undefined;
const lastUpdatedRow = (await this.get<{ ts: number | null }>(
this.metricsLastUpdated
)) as { ts: number | null } | undefined;

const byStatusMap: Record<string, number> = {};
for (const r of byStatus) {
byStatusMap[r.status] = Number(r.count);
}
const byStatus = await this.all<{ status: string; count: number }>(this.metricsByStatus);
const totalRow = await this.get<{ count: number }>(this.metricsTotal);
const lastUpdatedRow = await this.get<{ ts: number | null }>(this.metricsLastUpdated);

const totalOrders = Number(totalRow?.count ?? 0);
const completedOrders = byStatusMap["completed"] ?? 0;
const refundedOrders = byStatusMap["refunded"] ?? 0;
const staleExpiredOrders =
(byStatusMap["expired"] ?? 0) + (byStatusMap["failed"] ?? 0);
const statusMap: Record<string, number> = {};
for (const row of byStatus) {
statusMap[row.status] = Number(row.count);
}

return {
totalOrders,
byStatus: byStatusMap,
completedOrders,
refundedOrders,
staleExpiredOrders,
lastUpdatedTimestamp: lastUpdatedRow?.ts ?? null
totalOrders: Number(totalRow?.count ?? 0),
byStatus: statusMap,
completedOrders: statusMap["completed"] ?? 0,
refundedOrders: statusMap["refunded"] ?? 0,
staleExpiredOrders: (statusMap["expired"] ?? 0) + (statusMap["failed"] ?? 0),
lastUpdatedTimestamp: lastUpdatedRow?.ts != null ? Number(lastUpdatedRow.ts) : null
};
}

Expand Down
18 changes: 16 additions & 2 deletions coordinator/src/server/app.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import express, { type Express } from "express";
import cors from "cors";
import pinoHttp from "pino-http";
import type { Logger } from "pino";
import { healthRoutes } from "./routes/health.js";
Expand All @@ -14,7 +15,7 @@ import type { QuoteService } from "../services/quote-service.js";

export interface AppDeps {
log: Logger;
corsOrigins: string[];
corsOrigin: string;
/** Maximum allowed JSON request body size in bytes. Default: 65536 (64 KiB). */
maxRequestBodyBytes: number;
orders: OrderService;
Expand All @@ -26,8 +27,21 @@ export function createApp(deps: AppDeps): Express {
const { maxRequestBodyBytes } = deps;
const app = express();
app.use(pinoHttp({ logger: deps.log }));
// Reject bodies exceeding the configured limit before any route logic runs.
app.use(express.json({ limit: maxRequestBodyBytes }));
app.use(createCorsMiddleware(deps.corsOrigins));
app.use(
cors({
origin: (origin, callback) => {
if (!origin || !deps.corsOrigin || deps.corsOrigin === "*") {
callback(null, true);
} else {
const allowedOrigins = deps.corsOrigin.split(",");
callback(null, allowedOrigins.includes(origin));
}
},
credentials: true
})
);

// Prometheus HTTP duration instrumentation
app.use((req, res, next) => {
Expand Down
70 changes: 52 additions & 18 deletions coordinator/src/server/routes/orders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,58 @@ export function ordersRoutes(orders: OrderService): Router {
}
});

router.get("/orders/history", async (req, res, next) => {
// Support both single address and multiple addresses (eth + stellar)
const address = (req.query.address as string | undefined);
const ethAddress = (req.query.eth as string | undefined);
const stellarAddress = (req.query.stellar as string | undefined);

const addresses: string[] = [];
if (address) {
addresses.push(address);
}
if (ethAddress) {
addresses.push(ethAddress);
}
if (stellarAddress) {
addresses.push(stellarAddress);
}

if (addresses.length === 0) {
res.status(400).json({ error: "address_required" });
return;
}

const limit = Math.min(Number(req.query.limit ?? 50), 200);
const offset = Math.max(Number(req.query.offset ?? 0), 0);

try {
// Fetch orders for each address and deduplicate by publicId
const allOrders = await Promise.all(
addresses.map(addr => orders.history(addr, limit, offset))
);

const seen = new Set<string>();
const deduped: typeof allOrders[0] = [];

for (const orderList of allOrders) {
for (const order of orderList) {
if (!seen.has(order.publicId)) {
seen.add(order.publicId);
deduped.push(order);
}
}
}

res.json({
transactions: deduped.map((o) => serialiseOrder(o)).filter(Boolean),
pagination: { limit, offset, count: deduped.length }
});
} catch (err) {
next(err);
}
});

router.get("/orders/:id", async (req, res, next) => {
const id = req.params.id;
try {
Expand Down Expand Up @@ -94,24 +146,6 @@ export function ordersRoutes(orders: OrderService): Router {
}
});

router.get("/orders/history", async (req, res, next) => {
const address = (req.query.address as string | undefined) ?? "";
if (!address) {
res.status(400).json({ error: "address_required" });
return;
}
const limit = Math.min(Number(req.query.limit ?? 50), 200);
const offset = Math.max(Number(req.query.offset ?? 0), 0);
try {
const list = await orders.history(address, limit, offset);
res.json({
transactions: list.map((o) => serialiseOrder(o)).filter(Boolean),
pagination: { limit, offset, count: list.length }
});
} catch (err) {
next(err);
}
});

const lockSchema = z.object({
orderId: z.string().min(1),
Expand Down
1 change: 1 addition & 0 deletions coordinator/src/server/routes/quotes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export function quotesRoutes(quotes: QuoteService): Router {
source: quote.source,
issuedAt: quote.issuedAt,
expiresAt: quote.expiresAt,
fetchedAt: quote.issuedAt,
/** Convenience: milliseconds remaining until expiry (negative when expired). */
freshMs: quote.expiresAt - Date.now()
});
Expand Down
Loading