@sip-protocol/api 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/dist/routes/index.js +1164 -145
- package/dist/server.d.ts +29 -1
- package/dist/server.js +1514 -173
- package/package.json +25 -11
- package/src/config.ts +121 -0
- package/src/logger.ts +99 -0
- package/src/middleware/auth.ts +128 -0
- package/src/middleware/cors.ts +163 -0
- package/src/middleware/error-handler.ts +18 -7
- package/src/middleware/index.ts +13 -1
- package/src/middleware/rate-limit.ts +203 -0
- package/src/middleware/request-id.ts +66 -0
- package/src/middleware/validation.ts +100 -11
- package/src/monitoring/index.ts +36 -0
- package/src/monitoring/metrics.ts +163 -0
- package/src/monitoring/sentry.ts +179 -0
- package/src/routes/commitment.ts +1 -1
- package/src/routes/health.ts +35 -3
- package/src/routes/index.ts +2 -0
- package/src/routes/metrics.ts +27 -0
- package/src/routes/proof.ts +68 -1
- package/src/routes/swap.ts +116 -47
- package/src/routes/webhook.ts +156 -0
- package/src/server.ts +142 -19
- package/src/services/helius-listener.ts +104 -0
- package/src/services/index.ts +15 -0
- package/src/services/token-metadata.ts +91 -0
- package/src/services/webhook-delivery.ts +146 -0
- package/src/shutdown.ts +119 -0
- package/src/stores/index.ts +2 -0
- package/src/stores/swap-store.ts +158 -0
- package/src/stores/webhook-store.ts +120 -0
- package/src/types/api.ts +67 -1
package/dist/server.js
CHANGED
|
@@ -30,69 +30,615 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/server.ts
|
|
31
31
|
var server_exports = {};
|
|
32
32
|
__export(server_exports, {
|
|
33
|
-
default: () => server_default
|
|
33
|
+
default: () => server_default,
|
|
34
|
+
isServerShuttingDown: () => isServerShuttingDown
|
|
34
35
|
});
|
|
35
36
|
module.exports = __toCommonJS(server_exports);
|
|
36
|
-
var
|
|
37
|
-
var import_cors = __toESM(require("cors"));
|
|
37
|
+
var import_express9 = __toESM(require("express"));
|
|
38
38
|
var import_helmet = __toESM(require("helmet"));
|
|
39
39
|
var import_compression = __toESM(require("compression"));
|
|
40
|
-
var import_morgan = __toESM(require("morgan"));
|
|
41
40
|
|
|
42
41
|
// src/routes/index.ts
|
|
43
|
-
var
|
|
42
|
+
var import_express7 = require("express");
|
|
44
43
|
|
|
45
44
|
// src/routes/health.ts
|
|
46
|
-
var
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
45
|
+
var import_express2 = require("express");
|
|
46
|
+
|
|
47
|
+
// src/logger.ts
|
|
48
|
+
var import_pino = __toESM(require("pino"));
|
|
49
|
+
var import_pino_http = __toESM(require("pino-http"));
|
|
50
|
+
var import_crypto = __toESM(require("crypto"));
|
|
51
|
+
|
|
52
|
+
// src/config.ts
|
|
53
|
+
var import_envalid = require("envalid");
|
|
54
|
+
var env = (0, import_envalid.cleanEnv)(process.env, {
|
|
55
|
+
// Server configuration
|
|
56
|
+
NODE_ENV: (0, import_envalid.str)({
|
|
57
|
+
choices: ["development", "production", "test"],
|
|
58
|
+
default: "development",
|
|
59
|
+
desc: "Application environment"
|
|
60
|
+
}),
|
|
61
|
+
PORT: (0, import_envalid.port)({
|
|
62
|
+
default: 3e3,
|
|
63
|
+
desc: "Server port"
|
|
64
|
+
}),
|
|
65
|
+
// CORS configuration
|
|
66
|
+
CORS_ORIGINS: (0, import_envalid.str)({
|
|
67
|
+
default: "",
|
|
68
|
+
desc: "Comma-separated list of allowed origins (empty = localhost only in dev)"
|
|
69
|
+
}),
|
|
70
|
+
// Authentication
|
|
71
|
+
API_KEYS: (0, import_envalid.str)({
|
|
72
|
+
default: "",
|
|
73
|
+
desc: "Comma-separated list of valid API keys (empty = auth disabled in dev)"
|
|
74
|
+
}),
|
|
75
|
+
// Rate limiting
|
|
76
|
+
RATE_LIMIT_MAX: (0, import_envalid.num)({
|
|
77
|
+
default: 100,
|
|
78
|
+
desc: "Maximum requests per window"
|
|
79
|
+
}),
|
|
80
|
+
RATE_LIMIT_WINDOW_MS: (0, import_envalid.num)({
|
|
81
|
+
default: 6e4,
|
|
82
|
+
desc: "Rate limit window in milliseconds"
|
|
83
|
+
}),
|
|
84
|
+
// Proxy trust configuration (for X-Forwarded-For header)
|
|
85
|
+
// Set to number of trusted proxies (e.g., 1 for single nginx)
|
|
86
|
+
// or 'loopback' for local proxies, or 'uniquelocal' for private IPs
|
|
87
|
+
TRUST_PROXY: (0, import_envalid.str)({
|
|
88
|
+
default: "1",
|
|
89
|
+
desc: 'Express trust proxy setting (number of hops, "loopback", "uniquelocal", or "false")'
|
|
90
|
+
}),
|
|
91
|
+
// Logging
|
|
92
|
+
LOG_LEVEL: (0, import_envalid.str)({
|
|
93
|
+
choices: ["trace", "debug", "info", "warn", "error", "fatal"],
|
|
94
|
+
default: "info",
|
|
95
|
+
desc: "Logging level"
|
|
96
|
+
}),
|
|
97
|
+
// Graceful shutdown
|
|
98
|
+
SHUTDOWN_TIMEOUT_MS: (0, import_envalid.num)({
|
|
99
|
+
default: 3e4,
|
|
100
|
+
desc: "Graceful shutdown timeout in milliseconds"
|
|
101
|
+
}),
|
|
102
|
+
// Monitoring
|
|
103
|
+
SENTRY_DSN: (0, import_envalid.str)({
|
|
104
|
+
default: "",
|
|
105
|
+
desc: "Sentry DSN for error tracking (optional)"
|
|
106
|
+
}),
|
|
107
|
+
METRICS_ENABLED: (0, import_envalid.str)({
|
|
108
|
+
choices: ["true", "false"],
|
|
109
|
+
default: "true",
|
|
110
|
+
desc: "Enable Prometheus metrics endpoint"
|
|
111
|
+
}),
|
|
112
|
+
// Webhook configuration
|
|
113
|
+
HELIUS_WEBHOOK_SECRET: (0, import_envalid.str)({
|
|
114
|
+
default: "",
|
|
115
|
+
desc: "Helius webhook HMAC secret for signature verification (optional)"
|
|
116
|
+
}),
|
|
117
|
+
WEBHOOK_DELIVERY_MAX_RETRIES: (0, import_envalid.num)({
|
|
118
|
+
default: 3,
|
|
119
|
+
desc: "Maximum delivery attempts for webhook notifications"
|
|
120
|
+
}),
|
|
121
|
+
WEBHOOK_STORE_MAX_SIZE: (0, import_envalid.num)({
|
|
122
|
+
default: 1e3,
|
|
123
|
+
desc: "Maximum number of registered webhooks"
|
|
124
|
+
})
|
|
125
|
+
});
|
|
126
|
+
function logConfigWarnings(logger2) {
|
|
127
|
+
if (env.isProduction) {
|
|
128
|
+
if (!env.API_KEYS) {
|
|
129
|
+
logger2.warn("API_KEYS not set in production - authentication disabled");
|
|
130
|
+
}
|
|
131
|
+
if (!env.CORS_ORIGINS) {
|
|
132
|
+
logger2.warn("CORS_ORIGINS not set in production - only localhost allowed");
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
var isProduction = env.isProduction;
|
|
137
|
+
var isDevelopment = env.isDevelopment;
|
|
138
|
+
var isTest = env.isTest;
|
|
139
|
+
|
|
140
|
+
// src/logger.ts
|
|
141
|
+
var loggerConfig = {
|
|
142
|
+
level: env.LOG_LEVEL,
|
|
143
|
+
base: {
|
|
144
|
+
service: "sip-api",
|
|
145
|
+
version: "0.1.0"
|
|
146
|
+
},
|
|
147
|
+
timestamp: import_pino.default.stdTimeFunctions.isoTime
|
|
148
|
+
};
|
|
149
|
+
if (env.isDevelopment) {
|
|
150
|
+
loggerConfig.transport = {
|
|
151
|
+
target: "pino-pretty",
|
|
152
|
+
options: {
|
|
153
|
+
colorize: true,
|
|
154
|
+
translateTime: "HH:MM:ss",
|
|
155
|
+
ignore: "pid,hostname,service,version"
|
|
57
156
|
}
|
|
58
157
|
};
|
|
59
|
-
|
|
158
|
+
}
|
|
159
|
+
var logger = (0, import_pino.default)(loggerConfig);
|
|
160
|
+
var requestLogger = (0, import_pino_http.default)({
|
|
161
|
+
logger,
|
|
162
|
+
// Use requestId from requestIdMiddleware (set earlier in chain)
|
|
163
|
+
// Falls back to header or generates new if middleware didn't run
|
|
164
|
+
genReqId: (req) => {
|
|
165
|
+
const augmentedReq = req;
|
|
166
|
+
if (augmentedReq.requestId) {
|
|
167
|
+
return augmentedReq.requestId;
|
|
168
|
+
}
|
|
169
|
+
const existingId = req.headers["x-request-id"];
|
|
170
|
+
if (existingId && typeof existingId === "string") {
|
|
171
|
+
return existingId;
|
|
172
|
+
}
|
|
173
|
+
return import_crypto.default.randomUUID();
|
|
174
|
+
},
|
|
175
|
+
customLogLevel: (_req, res, err) => {
|
|
176
|
+
if (res.statusCode >= 500 || err) return "error";
|
|
177
|
+
if (res.statusCode >= 400) return "warn";
|
|
178
|
+
return "info";
|
|
179
|
+
},
|
|
180
|
+
customSuccessMessage: (req, res) => {
|
|
181
|
+
return `${req.method} ${req.url} ${res.statusCode}`;
|
|
182
|
+
},
|
|
183
|
+
customErrorMessage: (req, res, err) => {
|
|
184
|
+
return `${req.method} ${req.url} ${res.statusCode} - ${err.message}`;
|
|
185
|
+
},
|
|
186
|
+
// Don't log health checks in production
|
|
187
|
+
autoLogging: {
|
|
188
|
+
ignore: (req) => {
|
|
189
|
+
return req.url === "/api/v1/health" && env.isProduction;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
60
192
|
});
|
|
61
|
-
var health_default = router;
|
|
62
193
|
|
|
63
|
-
// src/
|
|
64
|
-
var
|
|
194
|
+
// src/shutdown.ts
|
|
195
|
+
var isShuttingDown = false;
|
|
196
|
+
function isServerShuttingDown() {
|
|
197
|
+
return isShuttingDown;
|
|
198
|
+
}
|
|
199
|
+
function setupGracefulShutdown(server, cleanup) {
|
|
200
|
+
const shutdown = async (signal) => {
|
|
201
|
+
if (isShuttingDown) {
|
|
202
|
+
logger.warn({ signal }, "Shutdown already in progress, ignoring signal");
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
isShuttingDown = true;
|
|
206
|
+
logger.info({ signal }, "Received shutdown signal, starting graceful shutdown...");
|
|
207
|
+
server.close(async (err) => {
|
|
208
|
+
if (err) {
|
|
209
|
+
logger.error({ err }, "Error closing HTTP server");
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
212
|
+
logger.info("HTTP server closed, no longer accepting connections");
|
|
213
|
+
if (cleanup) {
|
|
214
|
+
try {
|
|
215
|
+
logger.info("Running cleanup tasks...");
|
|
216
|
+
await cleanup();
|
|
217
|
+
logger.info("Cleanup completed successfully");
|
|
218
|
+
} catch (cleanupErr) {
|
|
219
|
+
logger.error({ err: cleanupErr }, "Error during cleanup");
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
logger.info("Graceful shutdown complete, exiting");
|
|
223
|
+
process.exit(0);
|
|
224
|
+
});
|
|
225
|
+
setTimeout(() => {
|
|
226
|
+
logger.error(
|
|
227
|
+
{ timeoutMs: env.SHUTDOWN_TIMEOUT_MS },
|
|
228
|
+
"Graceful shutdown timeout exceeded, forcing exit"
|
|
229
|
+
);
|
|
230
|
+
process.exit(1);
|
|
231
|
+
}, env.SHUTDOWN_TIMEOUT_MS);
|
|
232
|
+
};
|
|
233
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
234
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
235
|
+
process.on("uncaughtException", (err) => {
|
|
236
|
+
logger.fatal({ err }, "Uncaught exception, shutting down");
|
|
237
|
+
shutdown("uncaughtException");
|
|
238
|
+
});
|
|
239
|
+
process.on("unhandledRejection", (reason) => {
|
|
240
|
+
logger.fatal({ reason }, "Unhandled rejection, shutting down");
|
|
241
|
+
shutdown("unhandledRejection");
|
|
242
|
+
});
|
|
243
|
+
logger.debug("Graceful shutdown handlers registered");
|
|
244
|
+
}
|
|
245
|
+
function shutdownMiddleware(req, res, next) {
|
|
246
|
+
if (isShuttingDown) {
|
|
247
|
+
if (req.path === "/api/v1/health") {
|
|
248
|
+
return next();
|
|
249
|
+
}
|
|
250
|
+
res.status(503).json({
|
|
251
|
+
success: false,
|
|
252
|
+
error: {
|
|
253
|
+
code: "SERVICE_UNAVAILABLE",
|
|
254
|
+
message: "Server is shutting down"
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
next();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// src/stores/swap-store.ts
|
|
263
|
+
var import_lru_cache = require("lru-cache");
|
|
264
|
+
var DEFAULT_MAX_SIZE = 1e4;
|
|
265
|
+
var DEFAULT_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
266
|
+
var SwapStore = class {
|
|
267
|
+
cache;
|
|
268
|
+
maxSize;
|
|
269
|
+
ttlMs;
|
|
270
|
+
constructor(config = {}) {
|
|
271
|
+
this.maxSize = config.maxSize ?? DEFAULT_MAX_SIZE;
|
|
272
|
+
this.ttlMs = config.ttlMs ?? DEFAULT_TTL_MS;
|
|
273
|
+
this.cache = new import_lru_cache.LRUCache({
|
|
274
|
+
max: this.maxSize,
|
|
275
|
+
ttl: this.ttlMs,
|
|
276
|
+
updateAgeOnGet: false,
|
|
277
|
+
// Don't reset TTL on read
|
|
278
|
+
updateAgeOnHas: false,
|
|
279
|
+
// Log when items are evicted or expired
|
|
280
|
+
disposeAfter: (_value, key, reason) => {
|
|
281
|
+
if (reason === "evict") {
|
|
282
|
+
logger.debug({ swapId: key, reason }, "Swap evicted from cache (LRU)");
|
|
283
|
+
} else if (reason === "expire") {
|
|
284
|
+
logger.debug({ swapId: key, reason }, "Swap expired from cache (TTL)");
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
logger.info(
|
|
289
|
+
{ maxSize: this.maxSize, ttlMs: this.ttlMs },
|
|
290
|
+
"SwapStore initialized"
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Get a swap by ID
|
|
295
|
+
*/
|
|
296
|
+
get(id) {
|
|
297
|
+
return this.cache.get(id);
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Check if a swap exists
|
|
301
|
+
*/
|
|
302
|
+
has(id) {
|
|
303
|
+
return this.cache.has(id);
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Set/update a swap
|
|
307
|
+
*/
|
|
308
|
+
set(id, data) {
|
|
309
|
+
this.cache.set(id, data);
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Delete a swap
|
|
313
|
+
*/
|
|
314
|
+
delete(id) {
|
|
315
|
+
return this.cache.delete(id);
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Update swap status
|
|
319
|
+
*/
|
|
320
|
+
updateStatus(id, status, updates) {
|
|
321
|
+
const existing = this.cache.get(id);
|
|
322
|
+
if (!existing) {
|
|
323
|
+
return void 0;
|
|
324
|
+
}
|
|
325
|
+
const updated = {
|
|
326
|
+
...existing,
|
|
327
|
+
...updates,
|
|
328
|
+
status,
|
|
329
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
330
|
+
};
|
|
331
|
+
this.cache.set(id, updated);
|
|
332
|
+
return updated;
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Get store metrics for monitoring
|
|
336
|
+
*/
|
|
337
|
+
getMetrics() {
|
|
338
|
+
const size = this.cache.size;
|
|
339
|
+
return {
|
|
340
|
+
size,
|
|
341
|
+
maxSize: this.maxSize,
|
|
342
|
+
ttlMs: this.ttlMs,
|
|
343
|
+
utilizationPercent: Math.round(size / this.maxSize * 100)
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Clear all swaps (for testing)
|
|
348
|
+
*/
|
|
349
|
+
clear() {
|
|
350
|
+
this.cache.clear();
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Purge stale entries (normally handled automatically by LRU cache)
|
|
354
|
+
*/
|
|
355
|
+
purgeStale() {
|
|
356
|
+
this.cache.purgeStale();
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
var swapStore = new SwapStore();
|
|
360
|
+
|
|
361
|
+
// src/stores/webhook-store.ts
|
|
362
|
+
var import_crypto2 = require("crypto");
|
|
363
|
+
var WebhookStore = class {
|
|
364
|
+
registrations = /* @__PURE__ */ new Map();
|
|
365
|
+
maxSize;
|
|
366
|
+
constructor(maxSize) {
|
|
367
|
+
this.maxSize = maxSize ?? env.WEBHOOK_STORE_MAX_SIZE;
|
|
368
|
+
logger.info({ maxSize: this.maxSize }, "WebhookStore initialized");
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Register a new webhook
|
|
372
|
+
*
|
|
373
|
+
* @returns The registration with secret (shown once only)
|
|
374
|
+
*/
|
|
375
|
+
register(url, viewingPrivateKey, spendingPublicKey) {
|
|
376
|
+
if (this.registrations.size >= this.maxSize) {
|
|
377
|
+
throw new Error("WEBHOOK_STORE_FULL");
|
|
378
|
+
}
|
|
379
|
+
const id = (0, import_crypto2.randomBytes)(16).toString("hex");
|
|
380
|
+
const secret = (0, import_crypto2.randomBytes)(32).toString("hex");
|
|
381
|
+
const registration = {
|
|
382
|
+
id,
|
|
383
|
+
url,
|
|
384
|
+
viewingPrivateKey,
|
|
385
|
+
spendingPublicKey,
|
|
386
|
+
secret,
|
|
387
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
388
|
+
active: true
|
|
389
|
+
};
|
|
390
|
+
this.registrations.set(id, registration);
|
|
391
|
+
logger.info({ webhookId: id, url }, "Webhook registered");
|
|
392
|
+
return registration;
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Unregister a webhook by ID
|
|
396
|
+
*
|
|
397
|
+
* @returns true if found and removed, false if not found
|
|
398
|
+
*/
|
|
399
|
+
unregister(id) {
|
|
400
|
+
const existed = this.registrations.delete(id);
|
|
401
|
+
if (existed) {
|
|
402
|
+
logger.info({ webhookId: id }, "Webhook unregistered");
|
|
403
|
+
}
|
|
404
|
+
return existed;
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Get a registration by ID
|
|
408
|
+
*/
|
|
409
|
+
get(id) {
|
|
410
|
+
return this.registrations.get(id);
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Get all active registrations
|
|
414
|
+
*/
|
|
415
|
+
getAll() {
|
|
416
|
+
return Array.from(this.registrations.values()).filter((r) => r.active);
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Get all registrations (including inactive) for listing
|
|
420
|
+
*/
|
|
421
|
+
getAllForList() {
|
|
422
|
+
return Array.from(this.registrations.values()).map((r) => ({
|
|
423
|
+
id: r.id,
|
|
424
|
+
url: r.url,
|
|
425
|
+
active: r.active,
|
|
426
|
+
createdAt: r.createdAt
|
|
427
|
+
}));
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Current store size
|
|
431
|
+
*/
|
|
432
|
+
get size() {
|
|
433
|
+
return this.registrations.size;
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Clear all registrations (for testing)
|
|
437
|
+
*/
|
|
438
|
+
clear() {
|
|
439
|
+
this.registrations.clear();
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
var webhookStore = new WebhookStore();
|
|
443
|
+
|
|
444
|
+
// src/routes/proof.ts
|
|
445
|
+
var import_express = require("express");
|
|
65
446
|
var import_sdk2 = require("@sip-protocol/sdk");
|
|
447
|
+
var import_utils = require("@noble/hashes/utils");
|
|
66
448
|
|
|
67
449
|
// src/middleware/error-handler.ts
|
|
68
450
|
var import_sdk = require("@sip-protocol/sdk");
|
|
451
|
+
|
|
452
|
+
// src/monitoring/sentry.ts
|
|
453
|
+
var Sentry = __toESM(require("@sentry/node"));
|
|
454
|
+
var isInitialized = false;
|
|
455
|
+
function initSentry() {
|
|
456
|
+
if (isInitialized) return;
|
|
457
|
+
if (!env.SENTRY_DSN) {
|
|
458
|
+
if (env.isProduction) {
|
|
459
|
+
console.warn("[Sentry] SENTRY_DSN not set - error monitoring disabled");
|
|
460
|
+
}
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
Sentry.init({
|
|
464
|
+
dsn: env.SENTRY_DSN,
|
|
465
|
+
environment: env.NODE_ENV,
|
|
466
|
+
release: `sip-api@${process.env.npm_package_version || "0.1.0"}`,
|
|
467
|
+
// Performance monitoring
|
|
468
|
+
tracesSampleRate: env.isProduction ? 0.1 : 1,
|
|
469
|
+
// Filter out noisy errors
|
|
470
|
+
ignoreErrors: [
|
|
471
|
+
// Expected client errors
|
|
472
|
+
"Request validation failed",
|
|
473
|
+
"Unauthorized",
|
|
474
|
+
// Network issues
|
|
475
|
+
"ECONNRESET",
|
|
476
|
+
"EPIPE"
|
|
477
|
+
],
|
|
478
|
+
// Add extra context
|
|
479
|
+
beforeSend(event) {
|
|
480
|
+
if (env.NODE_ENV === "test") {
|
|
481
|
+
return null;
|
|
482
|
+
}
|
|
483
|
+
const requestId = event.request?.headers?.["x-request-id"];
|
|
484
|
+
if (requestId && typeof requestId === "string") {
|
|
485
|
+
event.tags = { ...event.tags, requestId };
|
|
486
|
+
}
|
|
487
|
+
if (event.request?.headers) {
|
|
488
|
+
delete event.request.headers["authorization"];
|
|
489
|
+
delete event.request.headers["x-api-key"];
|
|
490
|
+
delete event.request.headers["cookie"];
|
|
491
|
+
}
|
|
492
|
+
return event;
|
|
493
|
+
},
|
|
494
|
+
// Capture breadcrumbs for debugging
|
|
495
|
+
beforeBreadcrumb(breadcrumb) {
|
|
496
|
+
if (breadcrumb.category === "http" && breadcrumb.data?.url?.includes("/health")) {
|
|
497
|
+
return null;
|
|
498
|
+
}
|
|
499
|
+
return breadcrumb;
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
isInitialized = true;
|
|
503
|
+
console.log("[Sentry] Initialized for environment:", env.NODE_ENV);
|
|
504
|
+
}
|
|
505
|
+
function isSentryEnabled() {
|
|
506
|
+
return isInitialized && !!env.SENTRY_DSN;
|
|
507
|
+
}
|
|
508
|
+
function captureException2(error, context) {
|
|
509
|
+
if (!isSentryEnabled()) return void 0;
|
|
510
|
+
return Sentry.captureException(error, {
|
|
511
|
+
extra: context
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
function setupSentryErrorHandler(app2) {
|
|
515
|
+
if (!isSentryEnabled()) return;
|
|
516
|
+
Sentry.setupExpressErrorHandler(app2);
|
|
517
|
+
}
|
|
518
|
+
async function flushSentry(timeout = 2e3) {
|
|
519
|
+
if (!isSentryEnabled()) return true;
|
|
520
|
+
return Sentry.close(timeout);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// src/monitoring/metrics.ts
|
|
524
|
+
var import_prom_client = require("prom-client");
|
|
525
|
+
var register = new import_prom_client.Registry();
|
|
526
|
+
(0, import_prom_client.collectDefaultMetrics)({
|
|
527
|
+
register,
|
|
528
|
+
prefix: "sip_api_"
|
|
529
|
+
});
|
|
530
|
+
var httpRequestsTotal = new import_prom_client.Counter({
|
|
531
|
+
name: "sip_api_http_requests_total",
|
|
532
|
+
help: "Total number of HTTP requests",
|
|
533
|
+
labelNames: ["method", "path", "status"],
|
|
534
|
+
registers: [register]
|
|
535
|
+
});
|
|
536
|
+
var httpRequestDuration = new import_prom_client.Histogram({
|
|
537
|
+
name: "sip_api_http_request_duration_seconds",
|
|
538
|
+
help: "Duration of HTTP requests in seconds",
|
|
539
|
+
labelNames: ["method", "path", "status"],
|
|
540
|
+
buckets: [1e-3, 5e-3, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10],
|
|
541
|
+
registers: [register]
|
|
542
|
+
});
|
|
543
|
+
var stealthAddressGenerations = new import_prom_client.Counter({
|
|
544
|
+
name: "sip_stealth_address_generations_total",
|
|
545
|
+
help: "Total number of stealth address generations",
|
|
546
|
+
labelNames: ["chain"],
|
|
547
|
+
registers: [register]
|
|
548
|
+
});
|
|
549
|
+
var commitmentCreations = new import_prom_client.Counter({
|
|
550
|
+
name: "sip_commitment_creations_total",
|
|
551
|
+
help: "Total number of commitment creations",
|
|
552
|
+
registers: [register]
|
|
553
|
+
});
|
|
554
|
+
var proofGenerations = new import_prom_client.Counter({
|
|
555
|
+
name: "sip_proof_generations_total",
|
|
556
|
+
help: "Total number of proof generations",
|
|
557
|
+
labelNames: ["type"],
|
|
558
|
+
registers: [register]
|
|
559
|
+
});
|
|
560
|
+
var proofGenerationDuration = new import_prom_client.Histogram({
|
|
561
|
+
name: "sip_proof_generation_duration_seconds",
|
|
562
|
+
help: "Duration of proof generation in seconds",
|
|
563
|
+
labelNames: ["type"],
|
|
564
|
+
buckets: [0.1, 0.5, 1, 2.5, 5, 10, 30, 60],
|
|
565
|
+
registers: [register]
|
|
566
|
+
});
|
|
567
|
+
var activeConnections = new import_prom_client.Gauge({
|
|
568
|
+
name: "sip_api_active_connections",
|
|
569
|
+
help: "Number of active connections",
|
|
570
|
+
registers: [register]
|
|
571
|
+
});
|
|
572
|
+
var swapRequests = new import_prom_client.Counter({
|
|
573
|
+
name: "sip_swap_requests_total",
|
|
574
|
+
help: "Total number of swap requests",
|
|
575
|
+
labelNames: ["from_chain", "to_chain", "status"],
|
|
576
|
+
registers: [register]
|
|
577
|
+
});
|
|
578
|
+
var quoteRequests = new import_prom_client.Counter({
|
|
579
|
+
name: "sip_quote_requests_total",
|
|
580
|
+
help: "Total number of quote requests",
|
|
581
|
+
labelNames: ["from_chain", "to_chain"],
|
|
582
|
+
registers: [register]
|
|
583
|
+
});
|
|
584
|
+
function normalizePath(path) {
|
|
585
|
+
return path.replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, ":id").replace(/0x[0-9a-fA-F]{40,}/g, ":address").replace(/\/\d+/g, "/:id").replace(/\/+/g, "/");
|
|
586
|
+
}
|
|
587
|
+
function metricsMiddleware(req, res, next) {
|
|
588
|
+
if (req.path === "/metrics") {
|
|
589
|
+
return next();
|
|
590
|
+
}
|
|
591
|
+
const startTime2 = process.hrtime.bigint();
|
|
592
|
+
activeConnections.inc();
|
|
593
|
+
res.on("finish", () => {
|
|
594
|
+
const endTime = process.hrtime.bigint();
|
|
595
|
+
const durationSeconds = Number(endTime - startTime2) / 1e9;
|
|
596
|
+
const path = normalizePath(req.route?.path || req.path);
|
|
597
|
+
const labels = {
|
|
598
|
+
method: req.method,
|
|
599
|
+
path,
|
|
600
|
+
status: res.statusCode.toString()
|
|
601
|
+
};
|
|
602
|
+
httpRequestsTotal.inc(labels);
|
|
603
|
+
httpRequestDuration.observe(labels, durationSeconds);
|
|
604
|
+
activeConnections.dec();
|
|
605
|
+
});
|
|
606
|
+
next();
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// src/middleware/error-handler.ts
|
|
69
610
|
function errorHandler(err, req, res, next) {
|
|
70
|
-
|
|
611
|
+
logger.error({
|
|
71
612
|
path: req.path,
|
|
72
613
|
method: req.method,
|
|
73
614
|
error: err.message,
|
|
74
615
|
stack: err.stack
|
|
75
|
-
});
|
|
616
|
+
}, "API Error");
|
|
76
617
|
if ((0, import_sdk.isSIPError)(err)) {
|
|
77
618
|
const sipError = err;
|
|
619
|
+
const isValidationError = err instanceof import_sdk.ValidationError;
|
|
78
620
|
return res.status(400).json({
|
|
79
621
|
success: false,
|
|
80
622
|
error: {
|
|
81
623
|
code: sipError.code,
|
|
82
624
|
message: sipError.message,
|
|
83
625
|
details: {
|
|
84
|
-
field:
|
|
85
|
-
|
|
626
|
+
...isValidationError && { field: err.field },
|
|
627
|
+
...sipError.context
|
|
86
628
|
}
|
|
87
629
|
}
|
|
88
630
|
});
|
|
89
631
|
}
|
|
632
|
+
captureException2(err, {
|
|
633
|
+
path: req.path,
|
|
634
|
+
method: req.method
|
|
635
|
+
});
|
|
90
636
|
res.status(500).json({
|
|
91
637
|
success: false,
|
|
92
638
|
error: {
|
|
93
639
|
code: "INTERNAL_SERVER_ERROR",
|
|
94
640
|
message: "An unexpected error occurred",
|
|
95
|
-
details:
|
|
641
|
+
details: env.isDevelopment ? err.message : void 0
|
|
96
642
|
}
|
|
97
643
|
});
|
|
98
644
|
}
|
|
@@ -108,6 +654,40 @@ function notFoundHandler(req, res) {
|
|
|
108
654
|
|
|
109
655
|
// src/middleware/validation.ts
|
|
110
656
|
var import_zod = require("zod");
|
|
657
|
+
var MAX_UINT256 = 2n ** 256n - 1n;
|
|
658
|
+
var amountSchema = import_zod.z.string().regex(/^[1-9]\d*$/, "Amount must be positive integer without leading zeros").max(78, "Amount exceeds maximum uint256 length").refine(
|
|
659
|
+
(v) => {
|
|
660
|
+
try {
|
|
661
|
+
const n = BigInt(v);
|
|
662
|
+
return n > 0n && n <= MAX_UINT256;
|
|
663
|
+
} catch {
|
|
664
|
+
return false;
|
|
665
|
+
}
|
|
666
|
+
},
|
|
667
|
+
{ message: "Invalid amount: must be positive integer <= 2^256-1" }
|
|
668
|
+
);
|
|
669
|
+
var minAmountSchema = import_zod.z.string().regex(/^(0|[1-9]\d*)$/, "Amount must be non-negative integer without leading zeros").max(78, "Amount exceeds maximum uint256 length").refine(
|
|
670
|
+
(v) => {
|
|
671
|
+
try {
|
|
672
|
+
const n = BigInt(v);
|
|
673
|
+
return n >= 0n && n <= MAX_UINT256;
|
|
674
|
+
} catch {
|
|
675
|
+
return false;
|
|
676
|
+
}
|
|
677
|
+
},
|
|
678
|
+
{ message: "Invalid amount: must be non-negative integer <= 2^256-1" }
|
|
679
|
+
);
|
|
680
|
+
function calculateMinAmount(input, slippageBps) {
|
|
681
|
+
if (slippageBps < 0 || slippageBps > 1e4) {
|
|
682
|
+
throw new Error("Invalid slippage: must be 0-10000 basis points");
|
|
683
|
+
}
|
|
684
|
+
const bps = BigInt(Math.floor(slippageBps));
|
|
685
|
+
const multiplier = 10000n - bps;
|
|
686
|
+
return input * multiplier / 10000n;
|
|
687
|
+
}
|
|
688
|
+
function percentToBps(percent) {
|
|
689
|
+
return Math.floor(percent * 100);
|
|
690
|
+
}
|
|
111
691
|
function validateRequest(schema) {
|
|
112
692
|
return async (req, res, next) => {
|
|
113
693
|
try {
|
|
@@ -118,7 +698,9 @@ function validateRequest(schema) {
|
|
|
118
698
|
req.query = await schema.query.parseAsync(req.query);
|
|
119
699
|
}
|
|
120
700
|
if (schema.params) {
|
|
121
|
-
req.params = await schema.params.parseAsync(
|
|
701
|
+
req.params = await schema.params.parseAsync(
|
|
702
|
+
req.params
|
|
703
|
+
);
|
|
122
704
|
}
|
|
123
705
|
next();
|
|
124
706
|
} catch (error) {
|
|
@@ -128,7 +710,8 @@ function validateRequest(schema) {
|
|
|
128
710
|
error: {
|
|
129
711
|
code: "VALIDATION_ERROR",
|
|
130
712
|
message: "Invalid request data",
|
|
131
|
-
|
|
713
|
+
// Zod 4 renamed 'errors' to 'issues'
|
|
714
|
+
details: error.issues
|
|
132
715
|
}
|
|
133
716
|
});
|
|
134
717
|
}
|
|
@@ -147,19 +730,19 @@ var schemas = {
|
|
|
147
730
|
})
|
|
148
731
|
}),
|
|
149
732
|
createCommitment: import_zod.z.object({
|
|
150
|
-
value:
|
|
151
|
-
// bigint as string
|
|
733
|
+
value: amountSchema,
|
|
152
734
|
blindingFactor: import_zod.z.string().regex(/^0x[0-9a-fA-F]+$/).optional()
|
|
153
735
|
}),
|
|
154
736
|
generateFundingProof: import_zod.z.object({
|
|
155
|
-
balance:
|
|
156
|
-
minRequired:
|
|
737
|
+
balance: amountSchema,
|
|
738
|
+
minRequired: minAmountSchema,
|
|
739
|
+
// Zero allowed: "prove I have >= 0" is valid
|
|
157
740
|
balanceBlinding: import_zod.z.string().regex(/^0x[0-9a-fA-F]+$/)
|
|
158
741
|
}),
|
|
159
742
|
getQuote: import_zod.z.object({
|
|
160
743
|
inputChain: import_zod.z.enum(["solana", "ethereum", "near", "zcash", "polygon", "arbitrum", "optimism", "base", "bitcoin", "aptos", "sui", "cosmos", "osmosis", "injective", "celestia", "sei", "dydx"]),
|
|
161
744
|
inputToken: import_zod.z.string().min(1),
|
|
162
|
-
inputAmount:
|
|
745
|
+
inputAmount: amountSchema,
|
|
163
746
|
outputChain: import_zod.z.enum(["solana", "ethereum", "near", "zcash", "polygon", "arbitrum", "optimism", "base", "bitcoin", "aptos", "sui", "cosmos", "osmosis", "injective", "celestia", "sei", "dydx"]),
|
|
164
747
|
outputToken: import_zod.z.string().min(1),
|
|
165
748
|
slippageTolerance: import_zod.z.number().min(0).max(100).optional()
|
|
@@ -175,132 +758,476 @@ var schemas = {
|
|
|
175
758
|
})
|
|
176
759
|
};
|
|
177
760
|
|
|
178
|
-
// src/
|
|
179
|
-
var
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
761
|
+
// src/middleware/rate-limit.ts
|
|
762
|
+
var import_express_rate_limit = __toESM(require("express-rate-limit"));
|
|
763
|
+
var import_rate_limit_redis = require("rate-limit-redis");
|
|
764
|
+
var import_ioredis = require("ioredis");
|
|
765
|
+
var WINDOW_MS = parseInt(process.env.RATE_LIMIT_WINDOW_MS || "60000", 10);
|
|
766
|
+
var MAX_REQUESTS = parseInt(process.env.RATE_LIMIT_MAX_REQUESTS || "100", 10);
|
|
767
|
+
var SKIP_FAILED = process.env.RATE_LIMIT_SKIP_FAILED === "true";
|
|
768
|
+
var STORE_TYPE = process.env.RATE_LIMIT_STORE || "memory";
|
|
769
|
+
var REDIS_URL = process.env.REDIS_URL;
|
|
770
|
+
var redisClient = null;
|
|
771
|
+
var redisConnectionFailed = false;
|
|
772
|
+
function getRedisClient() {
|
|
773
|
+
if (redisConnectionFailed) {
|
|
774
|
+
return null;
|
|
775
|
+
}
|
|
776
|
+
if (!redisClient && REDIS_URL) {
|
|
777
|
+
redisClient = new import_ioredis.Redis(REDIS_URL, {
|
|
778
|
+
maxRetriesPerRequest: 3,
|
|
779
|
+
retryStrategy: (times) => {
|
|
780
|
+
if (times > 3) {
|
|
781
|
+
console.warn("[rate-limit] Redis connection failed, falling back to memory store");
|
|
782
|
+
redisConnectionFailed = true;
|
|
783
|
+
return null;
|
|
784
|
+
}
|
|
785
|
+
return Math.min(times * 100, 1e3);
|
|
786
|
+
},
|
|
787
|
+
lazyConnect: true
|
|
788
|
+
});
|
|
789
|
+
redisClient.on("error", (err) => {
|
|
790
|
+
console.warn("[rate-limit] Redis error:", err.message);
|
|
791
|
+
});
|
|
792
|
+
redisClient.on("connect", () => {
|
|
793
|
+
console.log("[rate-limit] Redis connected successfully");
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
return redisClient;
|
|
797
|
+
}
|
|
798
|
+
function getRedisStatus() {
|
|
799
|
+
const client = getRedisClient();
|
|
800
|
+
return {
|
|
801
|
+
storeType: client && !redisConnectionFailed ? "redis" : "memory",
|
|
802
|
+
configured: !!REDIS_URL,
|
|
803
|
+
connected: client?.status === "ready"
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
function createStore(prefix) {
|
|
807
|
+
if (STORE_TYPE === "redis" || REDIS_URL) {
|
|
808
|
+
const client = getRedisClient();
|
|
809
|
+
if (client && !redisConnectionFailed) {
|
|
810
|
+
const sendCommand = async (...args) => {
|
|
811
|
+
return client.call(args[0], ...args.slice(1));
|
|
812
|
+
};
|
|
813
|
+
return new import_rate_limit_redis.RedisStore({
|
|
814
|
+
sendCommand,
|
|
815
|
+
prefix: `rl:${prefix}:`
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
return void 0;
|
|
820
|
+
}
|
|
821
|
+
var rateLimiter = (0, import_express_rate_limit.default)({
|
|
822
|
+
windowMs: WINDOW_MS,
|
|
823
|
+
max: MAX_REQUESTS,
|
|
824
|
+
skipFailedRequests: SKIP_FAILED,
|
|
825
|
+
standardHeaders: true,
|
|
826
|
+
// Return rate limit info in `RateLimit-*` headers
|
|
827
|
+
legacyHeaders: false,
|
|
828
|
+
// Disable `X-RateLimit-*` headers
|
|
829
|
+
// Use Redis store if available, otherwise memory
|
|
830
|
+
store: createStore("api"),
|
|
831
|
+
handler: (_req, res) => {
|
|
832
|
+
res.status(429).json({
|
|
833
|
+
success: false,
|
|
834
|
+
error: {
|
|
835
|
+
code: "RATE_LIMIT_EXCEEDED",
|
|
836
|
+
message: "Too many requests, please try again later",
|
|
837
|
+
details: {
|
|
838
|
+
retryAfter: Math.ceil(WINDOW_MS / 1e3),
|
|
839
|
+
limit: MAX_REQUESTS,
|
|
840
|
+
windowMs: WINDOW_MS
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
});
|
|
844
|
+
},
|
|
845
|
+
skip: (req) => {
|
|
846
|
+
return req.path === "/api/v1/health" || req.path === "/";
|
|
847
|
+
}
|
|
848
|
+
});
|
|
849
|
+
var strictRateLimiter = (0, import_express_rate_limit.default)({
|
|
850
|
+
windowMs: 6e4,
|
|
851
|
+
// 1 minute
|
|
852
|
+
max: 10,
|
|
853
|
+
// 10 requests per minute
|
|
854
|
+
skipFailedRequests: false,
|
|
855
|
+
standardHeaders: true,
|
|
856
|
+
legacyHeaders: false,
|
|
857
|
+
// Use Redis store if available with different prefix
|
|
858
|
+
store: createStore("strict"),
|
|
859
|
+
handler: (_req, res) => {
|
|
860
|
+
res.status(429).json({
|
|
861
|
+
success: false,
|
|
862
|
+
error: {
|
|
863
|
+
code: "RATE_LIMIT_EXCEEDED",
|
|
864
|
+
message: "Rate limit exceeded for sensitive endpoint",
|
|
865
|
+
details: {
|
|
866
|
+
retryAfter: 60,
|
|
867
|
+
limit: 10,
|
|
868
|
+
windowMs: 6e4
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
// src/middleware/auth.ts
|
|
876
|
+
var import_crypto3 = require("crypto");
|
|
877
|
+
var API_KEYS = (process.env.API_KEYS || "").split(",").filter(Boolean);
|
|
878
|
+
var NODE_ENV = process.env.NODE_ENV || "development";
|
|
879
|
+
var AUTH_ENABLED = process.env.AUTH_ENABLED !== "false" && NODE_ENV === "production";
|
|
880
|
+
var SKIP_PATHS = (process.env.AUTH_SKIP_PATHS || "/health,/,/webhooks/internal/helius").split(",").map((p) => p.trim());
|
|
881
|
+
function safeCompare(a, b) {
|
|
882
|
+
if (a.length !== b.length) {
|
|
883
|
+
return false;
|
|
884
|
+
}
|
|
885
|
+
try {
|
|
886
|
+
return (0, import_crypto3.timingSafeEqual)(Buffer.from(a), Buffer.from(b));
|
|
887
|
+
} catch {
|
|
888
|
+
return false;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
function isValidApiKey(key) {
|
|
892
|
+
return API_KEYS.some((validKey) => safeCompare(key, validKey));
|
|
893
|
+
}
|
|
894
|
+
function extractApiKey(req) {
|
|
895
|
+
const apiKeyHeader = req.headers["x-api-key"];
|
|
896
|
+
if (typeof apiKeyHeader === "string" && apiKeyHeader) {
|
|
897
|
+
return apiKeyHeader;
|
|
898
|
+
}
|
|
899
|
+
const authHeader = req.headers.authorization;
|
|
900
|
+
if (authHeader?.startsWith("Bearer ")) {
|
|
901
|
+
return authHeader.slice(7);
|
|
902
|
+
}
|
|
903
|
+
return null;
|
|
904
|
+
}
|
|
905
|
+
function authenticate(req, res, next) {
|
|
906
|
+
if (!AUTH_ENABLED) {
|
|
907
|
+
return next();
|
|
908
|
+
}
|
|
909
|
+
const path = req.path.replace("/api/v1", "");
|
|
910
|
+
if (SKIP_PATHS.some((skipPath) => path === skipPath || path.startsWith(skipPath + "/"))) {
|
|
911
|
+
return next();
|
|
912
|
+
}
|
|
913
|
+
if (API_KEYS.length === 0) {
|
|
914
|
+
console.warn("[Auth] No API keys configured. Set API_KEYS environment variable.");
|
|
915
|
+
return res.status(500).json({
|
|
916
|
+
success: false,
|
|
917
|
+
error: {
|
|
918
|
+
code: "AUTH_NOT_CONFIGURED",
|
|
919
|
+
message: "Authentication is enabled but no API keys are configured"
|
|
920
|
+
}
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
const apiKey = extractApiKey(req);
|
|
924
|
+
if (!apiKey) {
|
|
925
|
+
return res.status(401).json({
|
|
926
|
+
success: false,
|
|
927
|
+
error: {
|
|
928
|
+
code: "UNAUTHORIZED",
|
|
929
|
+
message: "API key required. Provide via X-API-Key header or Authorization: Bearer <key>"
|
|
930
|
+
}
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
if (!isValidApiKey(apiKey)) {
|
|
934
|
+
return res.status(401).json({
|
|
935
|
+
success: false,
|
|
936
|
+
error: {
|
|
937
|
+
code: "INVALID_API_KEY",
|
|
938
|
+
message: "Invalid API key"
|
|
939
|
+
}
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
next();
|
|
943
|
+
}
|
|
944
|
+
function isAuthEnabled() {
|
|
945
|
+
return AUTH_ENABLED;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// src/middleware/cors.ts
|
|
949
|
+
var import_cors = __toESM(require("cors"));
|
|
950
|
+
var NODE_ENV2 = process.env.NODE_ENV || "development";
|
|
951
|
+
var CORS_ORIGINS = process.env.CORS_ORIGINS?.split(",").map((o) => o.trim()).filter(Boolean) || [];
|
|
952
|
+
var CORS_CREDENTIALS = process.env.CORS_CREDENTIALS !== "false";
|
|
953
|
+
var CORS_MAX_AGE = parseInt(process.env.CORS_MAX_AGE || "86400", 10);
|
|
954
|
+
var DEV_ORIGINS = [
|
|
955
|
+
"http://localhost:3000",
|
|
956
|
+
"http://localhost:3001",
|
|
957
|
+
"http://localhost:4000",
|
|
958
|
+
"http://localhost:5173",
|
|
959
|
+
// Vite
|
|
960
|
+
"http://127.0.0.1:3000",
|
|
961
|
+
"http://127.0.0.1:3001",
|
|
962
|
+
"http://127.0.0.1:4000",
|
|
963
|
+
"http://127.0.0.1:5173"
|
|
964
|
+
];
|
|
965
|
+
function getAllowedOrigins() {
|
|
966
|
+
if (CORS_ORIGINS.length > 0) {
|
|
967
|
+
return CORS_ORIGINS;
|
|
968
|
+
}
|
|
969
|
+
if (NODE_ENV2 === "development" || NODE_ENV2 === "test") {
|
|
970
|
+
return DEV_ORIGINS;
|
|
971
|
+
}
|
|
972
|
+
return [];
|
|
973
|
+
}
|
|
974
|
+
function isOriginAllowed(origin) {
|
|
975
|
+
if (!origin) {
|
|
976
|
+
return true;
|
|
977
|
+
}
|
|
978
|
+
const allowedOrigins = getAllowedOrigins();
|
|
979
|
+
if (allowedOrigins.includes(origin)) {
|
|
980
|
+
return true;
|
|
981
|
+
}
|
|
982
|
+
let originUrl;
|
|
983
|
+
try {
|
|
984
|
+
originUrl = new URL(origin);
|
|
985
|
+
} catch {
|
|
986
|
+
console.warn(`[CORS] Rejected malformed origin: ${origin}`);
|
|
987
|
+
return false;
|
|
988
|
+
}
|
|
989
|
+
for (const allowed of allowedOrigins) {
|
|
990
|
+
if (allowed.startsWith("*.")) {
|
|
991
|
+
const baseDomain = allowed.slice(2);
|
|
992
|
+
const originHost = originUrl.host;
|
|
993
|
+
if (NODE_ENV2 === "production" && originUrl.protocol !== "https:") {
|
|
994
|
+
console.warn(`[CORS] Rejected non-HTTPS origin in production: ${origin}`);
|
|
995
|
+
continue;
|
|
996
|
+
}
|
|
997
|
+
if (originHost === baseDomain || originHost.endsWith("." + baseDomain)) {
|
|
998
|
+
return true;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
return false;
|
|
1003
|
+
}
|
|
1004
|
+
var corsOptionsDelegate = (req, callback) => {
|
|
1005
|
+
const origin = req.headers.origin;
|
|
1006
|
+
const allowed = isOriginAllowed(origin);
|
|
1007
|
+
const options = {
|
|
1008
|
+
origin: allowed ? origin : false,
|
|
1009
|
+
credentials: CORS_CREDENTIALS,
|
|
1010
|
+
maxAge: CORS_MAX_AGE,
|
|
1011
|
+
methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"],
|
|
1012
|
+
allowedHeaders: [
|
|
1013
|
+
"Content-Type",
|
|
1014
|
+
"Authorization",
|
|
1015
|
+
"X-API-Key",
|
|
1016
|
+
"X-Request-ID",
|
|
1017
|
+
"X-Forwarded-For"
|
|
1018
|
+
],
|
|
1019
|
+
exposedHeaders: [
|
|
1020
|
+
"RateLimit-Limit",
|
|
1021
|
+
"RateLimit-Remaining",
|
|
1022
|
+
"RateLimit-Reset",
|
|
1023
|
+
"X-Request-ID"
|
|
1024
|
+
]
|
|
1025
|
+
};
|
|
1026
|
+
if (!allowed && origin) {
|
|
1027
|
+
console.warn(`[CORS] Blocked request from origin: ${origin}`);
|
|
1028
|
+
}
|
|
1029
|
+
callback(null, options);
|
|
1030
|
+
};
|
|
1031
|
+
var secureCors = (0, import_cors.default)(corsOptionsDelegate);
|
|
1032
|
+
function getCorsConfig() {
|
|
1033
|
+
return {
|
|
1034
|
+
origins: getAllowedOrigins(),
|
|
1035
|
+
credentials: CORS_CREDENTIALS,
|
|
1036
|
+
maxAge: CORS_MAX_AGE,
|
|
1037
|
+
environment: NODE_ENV2
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// src/middleware/request-id.ts
|
|
1042
|
+
var import_crypto4 = __toESM(require("crypto"));
|
|
1043
|
+
var requestIdMiddleware = (req, res, next) => {
|
|
1044
|
+
const clientId = req.headers["x-request-id"];
|
|
1045
|
+
const requestId = typeof clientId === "string" && clientId.length > 0 ? clientId : import_crypto4.default.randomUUID();
|
|
1046
|
+
req.requestId = requestId;
|
|
1047
|
+
res.setHeader("X-Request-ID", requestId);
|
|
1048
|
+
next();
|
|
1049
|
+
};
|
|
1050
|
+
|
|
1051
|
+
// src/routes/proof.ts
|
|
1052
|
+
var router = (0, import_express.Router)();
|
|
1053
|
+
var proofProvider = new import_sdk2.MockProofProvider();
|
|
1054
|
+
var proofProviderReady = false;
|
|
1055
|
+
var proofInitError = null;
|
|
1056
|
+
var MAX_INIT_RETRIES = 3;
|
|
1057
|
+
var RETRY_DELAY_MS = 2e3;
|
|
1058
|
+
async function initializeProofProvider() {
|
|
1059
|
+
for (let attempt = 1; attempt <= MAX_INIT_RETRIES; attempt++) {
|
|
1060
|
+
try {
|
|
1061
|
+
await proofProvider.initialize();
|
|
1062
|
+
proofProviderReady = true;
|
|
1063
|
+
proofInitError = null;
|
|
1064
|
+
logger.info({ attempt }, "Proof provider initialized successfully");
|
|
1065
|
+
return;
|
|
1066
|
+
} catch (err) {
|
|
1067
|
+
proofInitError = err instanceof Error ? err : new Error(String(err));
|
|
1068
|
+
logger.warn(
|
|
1069
|
+
{ attempt, maxRetries: MAX_INIT_RETRIES, error: proofInitError.message },
|
|
1070
|
+
"Proof provider initialization failed, retrying..."
|
|
1071
|
+
);
|
|
1072
|
+
if (attempt < MAX_INIT_RETRIES) {
|
|
1073
|
+
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS * attempt));
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
logger.error(
|
|
1078
|
+
{ error: proofInitError?.message },
|
|
1079
|
+
"Proof provider initialization failed after all retries"
|
|
1080
|
+
);
|
|
1081
|
+
}
|
|
1082
|
+
initializeProofProvider();
|
|
1083
|
+
function isProofProviderReady() {
|
|
1084
|
+
return proofProviderReady;
|
|
1085
|
+
}
|
|
1086
|
+
function getProofInitError() {
|
|
1087
|
+
return proofInitError;
|
|
1088
|
+
}
|
|
1089
|
+
router.post(
|
|
1090
|
+
"/funding",
|
|
1091
|
+
validateRequest({ body: schemas.generateFundingProof }),
|
|
183
1092
|
async (req, res) => {
|
|
184
|
-
|
|
185
|
-
|
|
1093
|
+
if (!proofProviderReady) {
|
|
1094
|
+
const errorMsg = proofInitError?.message || "Proof provider is initializing";
|
|
1095
|
+
logger.warn({ error: errorMsg }, "Proof request rejected - provider not ready");
|
|
1096
|
+
return res.status(503).json({
|
|
1097
|
+
success: false,
|
|
1098
|
+
error: {
|
|
1099
|
+
code: "PROOF_PROVIDER_NOT_READY",
|
|
1100
|
+
message: "Proof generation service is not ready",
|
|
1101
|
+
details: { reason: errorMsg }
|
|
1102
|
+
}
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
const { balance, minRequired, balanceBlinding } = req.body;
|
|
1106
|
+
const balanceBigInt = BigInt(balance);
|
|
1107
|
+
const minRequiredBigInt = BigInt(minRequired);
|
|
1108
|
+
const balanceBlindingBytes = (0, import_utils.hexToBytes)(balanceBlinding.replace(/^0x/, ""));
|
|
1109
|
+
const result = await proofProvider.generateFundingProof({
|
|
1110
|
+
balance: balanceBigInt,
|
|
1111
|
+
minimumRequired: minRequiredBigInt,
|
|
1112
|
+
blindingFactor: balanceBlindingBytes,
|
|
1113
|
+
assetId: "SOL",
|
|
1114
|
+
// Default asset
|
|
1115
|
+
userAddress: "0x0000000000000000000000000000000000000000",
|
|
1116
|
+
ownershipSignature: new Uint8Array(64)
|
|
1117
|
+
});
|
|
186
1118
|
const response = {
|
|
187
1119
|
success: true,
|
|
188
1120
|
data: {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
ephemeralPublicKey: result.stealthAddress.ephemeralPublicKey,
|
|
192
|
-
viewTag: result.stealthAddress.viewTag
|
|
193
|
-
}
|
|
1121
|
+
proof: result.proof.proof,
|
|
1122
|
+
publicInputs: result.proof.publicInputs
|
|
194
1123
|
}
|
|
195
1124
|
};
|
|
196
1125
|
res.json(response);
|
|
197
1126
|
}
|
|
198
1127
|
);
|
|
199
|
-
var
|
|
1128
|
+
var proof_default = router;
|
|
200
1129
|
|
|
201
|
-
// src/routes/
|
|
1130
|
+
// src/routes/health.ts
|
|
1131
|
+
var router2 = (0, import_express2.Router)();
|
|
1132
|
+
var startTime = Date.now();
|
|
1133
|
+
router2.get("/", (req, res) => {
|
|
1134
|
+
const shuttingDown = isServerShuttingDown();
|
|
1135
|
+
const status = shuttingDown ? "shutting_down" : "healthy";
|
|
1136
|
+
const statusCode = shuttingDown ? 503 : 200;
|
|
1137
|
+
const cacheMetrics = swapStore.getMetrics();
|
|
1138
|
+
const proofReady = isProofProviderReady();
|
|
1139
|
+
const proofError = getProofInitError();
|
|
1140
|
+
const redisStatus = getRedisStatus();
|
|
1141
|
+
const response = {
|
|
1142
|
+
success: !shuttingDown,
|
|
1143
|
+
data: {
|
|
1144
|
+
status,
|
|
1145
|
+
version: process.env.npm_package_version || "0.1.0",
|
|
1146
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1147
|
+
uptime: Math.floor((Date.now() - startTime) / 1e3),
|
|
1148
|
+
services: {
|
|
1149
|
+
proofProvider: {
|
|
1150
|
+
ready: proofReady,
|
|
1151
|
+
error: proofError?.message || null
|
|
1152
|
+
},
|
|
1153
|
+
rateLimiter: {
|
|
1154
|
+
store: redisStatus.storeType,
|
|
1155
|
+
redisConfigured: redisStatus.configured,
|
|
1156
|
+
redisConnected: redisStatus.connected
|
|
1157
|
+
}
|
|
1158
|
+
},
|
|
1159
|
+
cache: {
|
|
1160
|
+
swaps: {
|
|
1161
|
+
size: cacheMetrics.size,
|
|
1162
|
+
maxSize: cacheMetrics.maxSize,
|
|
1163
|
+
utilizationPercent: cacheMetrics.utilizationPercent
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
};
|
|
1168
|
+
res.status(statusCode).json(response);
|
|
1169
|
+
});
|
|
1170
|
+
var health_default = router2;
|
|
1171
|
+
|
|
1172
|
+
// src/routes/stealth.ts
|
|
202
1173
|
var import_express3 = require("express");
|
|
203
1174
|
var import_sdk3 = require("@sip-protocol/sdk");
|
|
204
|
-
|
|
205
|
-
// ../../../../node_modules/@noble/hashes/esm/utils.js
|
|
206
|
-
var hasHexBuiltin = /* @__PURE__ */ (() => (
|
|
207
|
-
// @ts-ignore
|
|
208
|
-
typeof Uint8Array.from([]).toHex === "function" && typeof Uint8Array.fromHex === "function"
|
|
209
|
-
))();
|
|
210
|
-
var asciis = { _0: 48, _9: 57, A: 65, F: 70, a: 97, f: 102 };
|
|
211
|
-
function asciiToBase16(ch) {
|
|
212
|
-
if (ch >= asciis._0 && ch <= asciis._9)
|
|
213
|
-
return ch - asciis._0;
|
|
214
|
-
if (ch >= asciis.A && ch <= asciis.F)
|
|
215
|
-
return ch - (asciis.A - 10);
|
|
216
|
-
if (ch >= asciis.a && ch <= asciis.f)
|
|
217
|
-
return ch - (asciis.a - 10);
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
function hexToBytes(hex) {
|
|
221
|
-
if (typeof hex !== "string")
|
|
222
|
-
throw new Error("hex string expected, got " + typeof hex);
|
|
223
|
-
if (hasHexBuiltin)
|
|
224
|
-
return Uint8Array.fromHex(hex);
|
|
225
|
-
const hl = hex.length;
|
|
226
|
-
const al = hl / 2;
|
|
227
|
-
if (hl % 2)
|
|
228
|
-
throw new Error("hex string expected, got unpadded hex of length " + hl);
|
|
229
|
-
const array = new Uint8Array(al);
|
|
230
|
-
for (let ai = 0, hi = 0; ai < al; ai++, hi += 2) {
|
|
231
|
-
const n1 = asciiToBase16(hex.charCodeAt(hi));
|
|
232
|
-
const n2 = asciiToBase16(hex.charCodeAt(hi + 1));
|
|
233
|
-
if (n1 === void 0 || n2 === void 0) {
|
|
234
|
-
const char = hex[hi] + hex[hi + 1];
|
|
235
|
-
throw new Error('hex string expected, got non-hex character "' + char + '" at index ' + hi);
|
|
236
|
-
}
|
|
237
|
-
array[ai] = n1 * 16 + n2;
|
|
238
|
-
}
|
|
239
|
-
return array;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// src/routes/commitment.ts
|
|
243
1175
|
var router3 = (0, import_express3.Router)();
|
|
244
1176
|
router3.post(
|
|
245
|
-
"/
|
|
246
|
-
validateRequest({ body: schemas.
|
|
1177
|
+
"/generate",
|
|
1178
|
+
validateRequest({ body: schemas.generateStealth }),
|
|
247
1179
|
async (req, res) => {
|
|
248
|
-
const {
|
|
249
|
-
const
|
|
250
|
-
const blindingBytes = blindingFactor ? hexToBytes(blindingFactor) : void 0;
|
|
251
|
-
const result = (0, import_sdk3.commit)(valueBigInt, blindingBytes);
|
|
1180
|
+
const { chain, recipientMetaAddress } = req.body;
|
|
1181
|
+
const result = (0, import_sdk3.generateStealthAddress)(recipientMetaAddress);
|
|
252
1182
|
const response = {
|
|
253
1183
|
success: true,
|
|
254
1184
|
data: {
|
|
255
|
-
|
|
256
|
-
|
|
1185
|
+
stealthAddress: {
|
|
1186
|
+
address: result.stealthAddress.address,
|
|
1187
|
+
ephemeralPublicKey: result.stealthAddress.ephemeralPublicKey,
|
|
1188
|
+
viewTag: result.stealthAddress.viewTag
|
|
1189
|
+
}
|
|
257
1190
|
}
|
|
258
1191
|
};
|
|
259
1192
|
res.json(response);
|
|
260
1193
|
}
|
|
261
1194
|
);
|
|
262
|
-
var
|
|
1195
|
+
var stealth_default = router3;
|
|
263
1196
|
|
|
264
|
-
// src/routes/
|
|
1197
|
+
// src/routes/commitment.ts
|
|
265
1198
|
var import_express4 = require("express");
|
|
266
1199
|
var import_sdk4 = require("@sip-protocol/sdk");
|
|
1200
|
+
var import_utils2 = require("@noble/hashes/utils");
|
|
267
1201
|
var router4 = (0, import_express4.Router)();
|
|
268
|
-
var proofProvider = new import_sdk4.MockProofProvider();
|
|
269
1202
|
router4.post(
|
|
270
|
-
"/
|
|
271
|
-
validateRequest({ body: schemas.
|
|
1203
|
+
"/create",
|
|
1204
|
+
validateRequest({ body: schemas.createCommitment }),
|
|
272
1205
|
async (req, res) => {
|
|
273
|
-
const {
|
|
274
|
-
const
|
|
275
|
-
const
|
|
276
|
-
const
|
|
277
|
-
const result = await proofProvider.generateFundingProof({
|
|
278
|
-
balance: balanceBigInt,
|
|
279
|
-
minimumRequired: minRequiredBigInt,
|
|
280
|
-
blindingFactor: balanceBlindingBytes,
|
|
281
|
-
assetId: "SOL",
|
|
282
|
-
// Default asset
|
|
283
|
-
userAddress: "0x0000000000000000000000000000000000000000",
|
|
284
|
-
ownershipSignature: new Uint8Array(64)
|
|
285
|
-
});
|
|
1206
|
+
const { value, blindingFactor } = req.body;
|
|
1207
|
+
const valueBigInt = BigInt(value);
|
|
1208
|
+
const blindingBytes = blindingFactor ? (0, import_utils2.hexToBytes)(blindingFactor.replace(/^0x/, "")) : void 0;
|
|
1209
|
+
const result = (0, import_sdk4.commit)(valueBigInt, blindingBytes);
|
|
286
1210
|
const response = {
|
|
287
1211
|
success: true,
|
|
288
1212
|
data: {
|
|
289
|
-
|
|
290
|
-
|
|
1213
|
+
commitment: result.commitment,
|
|
1214
|
+
blindingFactor: result.blinding
|
|
291
1215
|
}
|
|
292
1216
|
};
|
|
293
1217
|
res.json(response);
|
|
294
1218
|
}
|
|
295
1219
|
);
|
|
296
|
-
var
|
|
1220
|
+
var commitment_default = router4;
|
|
297
1221
|
|
|
298
1222
|
// src/routes/swap.ts
|
|
299
1223
|
var import_express5 = require("express");
|
|
300
1224
|
var import_sdk5 = require("@sip-protocol/sdk");
|
|
301
1225
|
var router5 = (0, import_express5.Router)();
|
|
302
1226
|
var sip = new import_sdk5.SIP({ network: "testnet" });
|
|
303
|
-
var
|
|
1227
|
+
var MOCK_MODE_ENABLED = env.NODE_ENV !== "production";
|
|
1228
|
+
if (!MOCK_MODE_ENABLED) {
|
|
1229
|
+
logger.warn("Production mode: Mock quotes and swaps are DISABLED");
|
|
1230
|
+
}
|
|
304
1231
|
router5.post(
|
|
305
1232
|
"/",
|
|
306
1233
|
validateRequest({ body: schemas.getQuote }),
|
|
@@ -313,37 +1240,66 @@ router5.post(
|
|
|
313
1240
|
outputToken,
|
|
314
1241
|
slippageTolerance
|
|
315
1242
|
} = req.body;
|
|
1243
|
+
if (!MOCK_MODE_ENABLED) {
|
|
1244
|
+
return res.status(503).json({
|
|
1245
|
+
success: false,
|
|
1246
|
+
error: {
|
|
1247
|
+
code: "QUOTE_SERVICE_UNAVAILABLE",
|
|
1248
|
+
message: "Real quote aggregator not configured. This API is not ready for production use.",
|
|
1249
|
+
details: {
|
|
1250
|
+
hint: "Configure QUOTE_AGGREGATOR_URL or use development mode"
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1255
|
+
if (!(0, import_sdk5.isKnownToken)(inputToken, inputChain)) {
|
|
1256
|
+
return res.status(400).json({
|
|
1257
|
+
success: false,
|
|
1258
|
+
error: {
|
|
1259
|
+
code: "UNKNOWN_TOKEN",
|
|
1260
|
+
message: `Unknown input token: ${inputToken} on ${inputChain}`
|
|
1261
|
+
}
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1264
|
+
if (!(0, import_sdk5.isKnownToken)(outputToken, outputChain)) {
|
|
1265
|
+
return res.status(400).json({
|
|
1266
|
+
success: false,
|
|
1267
|
+
error: {
|
|
1268
|
+
code: "UNKNOWN_TOKEN",
|
|
1269
|
+
message: `Unknown output token: ${outputToken} on ${outputChain}`
|
|
1270
|
+
}
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1273
|
+
const inputAsset = (0, import_sdk5.getAsset)(inputToken, inputChain);
|
|
1274
|
+
const outputAsset = (0, import_sdk5.getAsset)(outputToken, outputChain);
|
|
1275
|
+
const inputAmountBigInt = BigInt(inputAmount);
|
|
1276
|
+
const slippagePercent = slippageTolerance ?? 1;
|
|
1277
|
+
const slippageBps = percentToBps(slippagePercent);
|
|
316
1278
|
const intent = await sip.createIntent({
|
|
317
1279
|
input: {
|
|
318
|
-
asset:
|
|
319
|
-
|
|
320
|
-
address: null,
|
|
321
|
-
// Native token
|
|
322
|
-
symbol: inputToken,
|
|
323
|
-
decimals: 9
|
|
324
|
-
},
|
|
325
|
-
amount: BigInt(inputAmount)
|
|
1280
|
+
asset: inputAsset,
|
|
1281
|
+
amount: inputAmountBigInt
|
|
326
1282
|
},
|
|
327
1283
|
output: {
|
|
328
|
-
asset:
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
// Native token
|
|
332
|
-
symbol: outputToken,
|
|
333
|
-
decimals: 9
|
|
334
|
-
},
|
|
335
|
-
minAmount: BigInt(inputAmount) * 95n / 100n,
|
|
336
|
-
// 5% slippage
|
|
337
|
-
maxSlippage: (slippageTolerance || 1) / 100
|
|
1284
|
+
asset: outputAsset,
|
|
1285
|
+
minAmount: calculateMinAmount(inputAmountBigInt, slippageBps),
|
|
1286
|
+
maxSlippage: slippagePercent / 100
|
|
338
1287
|
},
|
|
339
1288
|
privacy: import_sdk5.PrivacyLevel.TRANSPARENT
|
|
340
1289
|
// Default to transparent for quote
|
|
341
1290
|
});
|
|
1291
|
+
logger.warn({
|
|
1292
|
+
inputChain,
|
|
1293
|
+
inputToken,
|
|
1294
|
+
outputChain,
|
|
1295
|
+
outputToken,
|
|
1296
|
+
inputAmount
|
|
1297
|
+
}, "Returning MOCK quote - not for production use");
|
|
342
1298
|
const mockQuote = {
|
|
343
1299
|
quoteId: `quote-${Date.now()}`,
|
|
344
1300
|
inputAmount,
|
|
1301
|
+
// Mock: 5% fee (clearly fake rate)
|
|
345
1302
|
outputAmount: (BigInt(inputAmount) * 95n / 100n).toString(),
|
|
346
|
-
// Mock 5% fee
|
|
347
1303
|
rate: "0.95",
|
|
348
1304
|
estimatedTime: 30,
|
|
349
1305
|
fees: {
|
|
@@ -363,7 +1319,10 @@ router5.post(
|
|
|
363
1319
|
};
|
|
364
1320
|
const response = {
|
|
365
1321
|
success: true,
|
|
366
|
-
data:
|
|
1322
|
+
data: {
|
|
1323
|
+
...mockQuote,
|
|
1324
|
+
_warning: "MOCK_DATA: This quote uses simulated pricing. Do not use for real transactions."
|
|
1325
|
+
}
|
|
367
1326
|
};
|
|
368
1327
|
res.json(response);
|
|
369
1328
|
}
|
|
@@ -372,23 +1331,45 @@ router5.post(
|
|
|
372
1331
|
"/swap",
|
|
373
1332
|
validateRequest({ body: schemas.executeSwap }),
|
|
374
1333
|
async (req, res) => {
|
|
375
|
-
const { intentId, quoteId,
|
|
1334
|
+
const { intentId, quoteId, inputAmount } = req.body;
|
|
1335
|
+
if (!MOCK_MODE_ENABLED) {
|
|
1336
|
+
return res.status(503).json({
|
|
1337
|
+
success: false,
|
|
1338
|
+
error: {
|
|
1339
|
+
code: "SWAP_SERVICE_UNAVAILABLE",
|
|
1340
|
+
message: "Real swap executor not configured. This API is not ready for production use.",
|
|
1341
|
+
details: {
|
|
1342
|
+
hint: "Configure SWAP_EXECUTOR_URL or use development mode"
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
376
1347
|
const swapId = `swap-${Date.now()}`;
|
|
1348
|
+
const actualInputAmount = inputAmount || "0";
|
|
1349
|
+
if (!inputAmount) {
|
|
1350
|
+
logger.warn({ swapId, quoteId, intentId }, "Swap created without inputAmount - using 0");
|
|
1351
|
+
}
|
|
377
1352
|
const swap = {
|
|
378
1353
|
id: swapId,
|
|
379
1354
|
status: "pending",
|
|
380
|
-
inputAmount:
|
|
381
|
-
// Mock value
|
|
1355
|
+
inputAmount: actualInputAmount,
|
|
382
1356
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
383
1357
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
384
1358
|
};
|
|
385
|
-
|
|
1359
|
+
swapStore.set(swapId, swap);
|
|
1360
|
+
logger.warn({
|
|
1361
|
+
swapId,
|
|
1362
|
+
quoteId,
|
|
1363
|
+
intentId,
|
|
1364
|
+
inputAmount: actualInputAmount
|
|
1365
|
+
}, "Creating MOCK swap - not for production use");
|
|
386
1366
|
const response = {
|
|
387
1367
|
success: true,
|
|
388
1368
|
data: {
|
|
389
1369
|
swapId,
|
|
390
1370
|
status: "pending",
|
|
391
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1371
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1372
|
+
_warning: "MOCK_DATA: This swap is simulated. No real transaction will be executed."
|
|
392
1373
|
}
|
|
393
1374
|
};
|
|
394
1375
|
res.json(response);
|
|
@@ -399,13 +1380,14 @@ router5.get(
|
|
|
399
1380
|
validateRequest({ params: schemas.swapStatus }),
|
|
400
1381
|
async (req, res) => {
|
|
401
1382
|
const { id } = req.params;
|
|
402
|
-
const
|
|
1383
|
+
const swapId = Array.isArray(id) ? id[0] : id;
|
|
1384
|
+
const swap = swapStore.get(swapId);
|
|
403
1385
|
if (!swap) {
|
|
404
1386
|
return res.status(404).json({
|
|
405
1387
|
success: false,
|
|
406
1388
|
error: {
|
|
407
1389
|
code: "SWAP_NOT_FOUND",
|
|
408
|
-
message: `Swap ${
|
|
1390
|
+
message: `Swap ${swapId} not found`
|
|
409
1391
|
}
|
|
410
1392
|
});
|
|
411
1393
|
}
|
|
@@ -418,32 +1400,344 @@ router5.get(
|
|
|
418
1400
|
);
|
|
419
1401
|
var swap_default = router5;
|
|
420
1402
|
|
|
421
|
-
// src/routes/
|
|
1403
|
+
// src/routes/webhook.ts
|
|
1404
|
+
var import_express6 = require("express");
|
|
1405
|
+
var import_zod2 = require("zod");
|
|
1406
|
+
|
|
1407
|
+
// src/services/helius-listener.ts
|
|
1408
|
+
var import_sdk6 = require("@sip-protocol/sdk");
|
|
1409
|
+
|
|
1410
|
+
// src/services/webhook-delivery.ts
|
|
1411
|
+
var import_hmac = require("@noble/hashes/hmac");
|
|
1412
|
+
var import_sha256 = require("@noble/hashes/sha256");
|
|
1413
|
+
var import_utils3 = require("@noble/hashes/utils");
|
|
1414
|
+
function computeHmacSignature(secret, body) {
|
|
1415
|
+
const encoder = new TextEncoder();
|
|
1416
|
+
const sig = (0, import_utils3.bytesToHex)((0, import_hmac.hmac)(import_sha256.sha256, encoder.encode(secret), encoder.encode(body)));
|
|
1417
|
+
return `sha256=${sig}`;
|
|
1418
|
+
}
|
|
1419
|
+
function sleep(ms) {
|
|
1420
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1421
|
+
}
|
|
1422
|
+
var WebhookDeliveryService = class {
|
|
1423
|
+
maxRetries;
|
|
1424
|
+
pendingDeliveries = /* @__PURE__ */ new Set();
|
|
1425
|
+
constructor(maxRetries) {
|
|
1426
|
+
this.maxRetries = maxRetries ?? env.WEBHOOK_DELIVERY_MAX_RETRIES;
|
|
1427
|
+
}
|
|
1428
|
+
/**
|
|
1429
|
+
* Build the delivery payload from a scan result
|
|
1430
|
+
*/
|
|
1431
|
+
buildPayload(registration, payment) {
|
|
1432
|
+
return {
|
|
1433
|
+
event: "payment.received",
|
|
1434
|
+
webhookId: registration.id,
|
|
1435
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1436
|
+
data: {
|
|
1437
|
+
txSignature: payment.txSignature,
|
|
1438
|
+
stealthAddress: payment.stealthAddress,
|
|
1439
|
+
ephemeralPublicKey: payment.ephemeralPublicKey,
|
|
1440
|
+
amount: payment.amount.toString(),
|
|
1441
|
+
mint: payment.mint,
|
|
1442
|
+
tokenSymbol: payment.tokenSymbol,
|
|
1443
|
+
slot: payment.slot,
|
|
1444
|
+
blockTime: payment.timestamp
|
|
1445
|
+
}
|
|
1446
|
+
};
|
|
1447
|
+
}
|
|
1448
|
+
/**
|
|
1449
|
+
* Deliver a payment notification to a registered webhook
|
|
1450
|
+
*
|
|
1451
|
+
* Retries with exponential backoff on failure.
|
|
1452
|
+
*/
|
|
1453
|
+
async deliver(registration, payment) {
|
|
1454
|
+
const payload = this.buildPayload(registration, payment);
|
|
1455
|
+
const body = JSON.stringify(payload);
|
|
1456
|
+
const signature = computeHmacSignature(registration.secret, body);
|
|
1457
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
1458
|
+
try {
|
|
1459
|
+
const response = await fetch(registration.url, {
|
|
1460
|
+
method: "POST",
|
|
1461
|
+
headers: {
|
|
1462
|
+
"Content-Type": "application/json",
|
|
1463
|
+
"X-SIP-Signature": signature,
|
|
1464
|
+
"X-SIP-Webhook-Id": registration.id
|
|
1465
|
+
},
|
|
1466
|
+
body,
|
|
1467
|
+
signal: AbortSignal.timeout(1e4)
|
|
1468
|
+
});
|
|
1469
|
+
if (response.ok) {
|
|
1470
|
+
logger.info(
|
|
1471
|
+
{ webhookId: registration.id, attempt, status: response.status },
|
|
1472
|
+
"Webhook delivered"
|
|
1473
|
+
);
|
|
1474
|
+
return true;
|
|
1475
|
+
}
|
|
1476
|
+
logger.warn(
|
|
1477
|
+
{ webhookId: registration.id, attempt, status: response.status },
|
|
1478
|
+
"Webhook delivery failed (non-2xx)"
|
|
1479
|
+
);
|
|
1480
|
+
} catch (error) {
|
|
1481
|
+
logger.warn(
|
|
1482
|
+
{ webhookId: registration.id, attempt, error: error.message },
|
|
1483
|
+
"Webhook delivery error"
|
|
1484
|
+
);
|
|
1485
|
+
}
|
|
1486
|
+
if (attempt < this.maxRetries) {
|
|
1487
|
+
const backoffMs = Math.pow(4, attempt) * 1e3;
|
|
1488
|
+
await sleep(backoffMs);
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
logger.error(
|
|
1492
|
+
{ webhookId: registration.id, maxRetries: this.maxRetries },
|
|
1493
|
+
"Webhook delivery exhausted all retries"
|
|
1494
|
+
);
|
|
1495
|
+
return false;
|
|
1496
|
+
}
|
|
1497
|
+
/**
|
|
1498
|
+
* Queue a delivery (fire-and-forget with tracking)
|
|
1499
|
+
*/
|
|
1500
|
+
queueDelivery(registration, payment) {
|
|
1501
|
+
const promise = this.deliver(registration, payment).then(() => {
|
|
1502
|
+
}).catch((err) => {
|
|
1503
|
+
logger.error({ webhookId: registration.id, err }, "Unhandled webhook delivery error");
|
|
1504
|
+
}).finally(() => {
|
|
1505
|
+
this.pendingDeliveries.delete(promise);
|
|
1506
|
+
});
|
|
1507
|
+
this.pendingDeliveries.add(promise);
|
|
1508
|
+
}
|
|
1509
|
+
/**
|
|
1510
|
+
* Wait for all pending deliveries to complete (graceful shutdown)
|
|
1511
|
+
*/
|
|
1512
|
+
async drainPending() {
|
|
1513
|
+
if (this.pendingDeliveries.size > 0) {
|
|
1514
|
+
logger.info({ pending: this.pendingDeliveries.size }, "Draining pending webhook deliveries");
|
|
1515
|
+
await Promise.allSettled(this.pendingDeliveries);
|
|
1516
|
+
logger.info("All webhook deliveries drained");
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
};
|
|
1520
|
+
var webhookDeliveryService = new WebhookDeliveryService();
|
|
1521
|
+
|
|
1522
|
+
// src/services/helius-listener.ts
|
|
1523
|
+
var HeliusListenerService = class {
|
|
1524
|
+
/**
|
|
1525
|
+
* Process an incoming Helius webhook payload
|
|
1526
|
+
*
|
|
1527
|
+
* 1. Verify Helius signature (if configured)
|
|
1528
|
+
* 2. For each active registration, check if payment is for them
|
|
1529
|
+
* 3. On match, queue delivery to the agent's URL
|
|
1530
|
+
*
|
|
1531
|
+
* @returns Number of matched deliveries queued
|
|
1532
|
+
*/
|
|
1533
|
+
async processIncoming(payload, headers) {
|
|
1534
|
+
if (env.HELIUS_WEBHOOK_SECRET && headers.rawBody) {
|
|
1535
|
+
const valid = (0, import_sdk6.verifyWebhookSignature)(
|
|
1536
|
+
headers.rawBody,
|
|
1537
|
+
headers.signature,
|
|
1538
|
+
env.HELIUS_WEBHOOK_SECRET
|
|
1539
|
+
);
|
|
1540
|
+
if (!valid) {
|
|
1541
|
+
logger.warn("Helius webhook signature verification failed");
|
|
1542
|
+
throw new Error("HELIUS_SIGNATURE_INVALID");
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
const transactions = Array.isArray(payload) ? payload : [payload];
|
|
1546
|
+
const registrations = webhookStore.getAll();
|
|
1547
|
+
if (registrations.length === 0) {
|
|
1548
|
+
logger.debug("No active webhook registrations, skipping processing");
|
|
1549
|
+
return 0;
|
|
1550
|
+
}
|
|
1551
|
+
let matchCount = 0;
|
|
1552
|
+
for (const tx of transactions) {
|
|
1553
|
+
for (const registration of registrations) {
|
|
1554
|
+
try {
|
|
1555
|
+
const payment = await (0, import_sdk6.processWebhookTransaction)(
|
|
1556
|
+
tx,
|
|
1557
|
+
registration.viewingPrivateKey,
|
|
1558
|
+
registration.spendingPublicKey
|
|
1559
|
+
);
|
|
1560
|
+
if (payment) {
|
|
1561
|
+
matchCount++;
|
|
1562
|
+
webhookDeliveryService.queueDelivery(registration, payment);
|
|
1563
|
+
logger.info(
|
|
1564
|
+
{
|
|
1565
|
+
webhookId: registration.id,
|
|
1566
|
+
txSignature: payment.txSignature
|
|
1567
|
+
},
|
|
1568
|
+
"Payment matched, delivery queued"
|
|
1569
|
+
);
|
|
1570
|
+
}
|
|
1571
|
+
} catch (error) {
|
|
1572
|
+
logger.warn(
|
|
1573
|
+
{
|
|
1574
|
+
webhookId: registration.id,
|
|
1575
|
+
error: error.message
|
|
1576
|
+
},
|
|
1577
|
+
"Error checking transaction against registration"
|
|
1578
|
+
);
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
logger.info(
|
|
1583
|
+
{
|
|
1584
|
+
transactions: transactions.length,
|
|
1585
|
+
registrations: registrations.length,
|
|
1586
|
+
matches: matchCount
|
|
1587
|
+
},
|
|
1588
|
+
"Helius webhook processed"
|
|
1589
|
+
);
|
|
1590
|
+
return matchCount;
|
|
1591
|
+
}
|
|
1592
|
+
};
|
|
1593
|
+
var heliusListenerService = new HeliusListenerService();
|
|
1594
|
+
|
|
1595
|
+
// src/routes/webhook.ts
|
|
422
1596
|
var router6 = (0, import_express6.Router)();
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
1597
|
+
var webhookSchemas = {
|
|
1598
|
+
register: import_zod2.z.object({
|
|
1599
|
+
url: import_zod2.z.string().url("Must be a valid URL"),
|
|
1600
|
+
viewingPrivateKey: import_zod2.z.string().regex(/^0x[0-9a-fA-F]{64}$/, "Must be 0x-prefixed 32-byte hex"),
|
|
1601
|
+
spendingPublicKey: import_zod2.z.string().regex(/^0x[0-9a-fA-F]{64}$/, "Must be 0x-prefixed 32-byte hex")
|
|
1602
|
+
}),
|
|
1603
|
+
unregister: import_zod2.z.object({
|
|
1604
|
+
id: import_zod2.z.string().min(1)
|
|
1605
|
+
})
|
|
1606
|
+
};
|
|
1607
|
+
router6.post(
|
|
1608
|
+
"/register",
|
|
1609
|
+
validateRequest({ body: webhookSchemas.register }),
|
|
1610
|
+
(req, res) => {
|
|
1611
|
+
const { url, viewingPrivateKey, spendingPublicKey } = req.body;
|
|
1612
|
+
try {
|
|
1613
|
+
const registration = webhookStore.register(url, viewingPrivateKey, spendingPublicKey);
|
|
1614
|
+
const response = {
|
|
1615
|
+
success: true,
|
|
1616
|
+
data: {
|
|
1617
|
+
id: registration.id,
|
|
1618
|
+
url: registration.url,
|
|
1619
|
+
secret: registration.secret,
|
|
1620
|
+
createdAt: registration.createdAt
|
|
1621
|
+
}
|
|
1622
|
+
};
|
|
1623
|
+
res.status(201).json(response);
|
|
1624
|
+
} catch (error) {
|
|
1625
|
+
if (error.message === "WEBHOOK_STORE_FULL") {
|
|
1626
|
+
res.status(503).json({
|
|
1627
|
+
success: false,
|
|
1628
|
+
error: {
|
|
1629
|
+
code: "WEBHOOK_STORE_FULL",
|
|
1630
|
+
message: "Maximum webhook registrations reached"
|
|
1631
|
+
}
|
|
1632
|
+
});
|
|
1633
|
+
return;
|
|
1634
|
+
}
|
|
1635
|
+
throw error;
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
);
|
|
1639
|
+
router6.delete(
|
|
1640
|
+
"/:id",
|
|
1641
|
+
validateRequest({ params: webhookSchemas.unregister }),
|
|
1642
|
+
(req, res) => {
|
|
1643
|
+
const removed = webhookStore.unregister(req.params.id);
|
|
1644
|
+
if (!removed) {
|
|
1645
|
+
res.status(404).json({
|
|
1646
|
+
success: false,
|
|
1647
|
+
error: {
|
|
1648
|
+
code: "WEBHOOK_NOT_FOUND",
|
|
1649
|
+
message: "Webhook not found"
|
|
1650
|
+
}
|
|
1651
|
+
});
|
|
1652
|
+
return;
|
|
1653
|
+
}
|
|
1654
|
+
res.status(204).send();
|
|
1655
|
+
}
|
|
1656
|
+
);
|
|
1657
|
+
router6.get("/", (_req, res) => {
|
|
1658
|
+
const webhooks = webhookStore.getAllForList();
|
|
1659
|
+
const response = {
|
|
1660
|
+
success: true,
|
|
1661
|
+
data: { webhooks }
|
|
1662
|
+
};
|
|
1663
|
+
res.json(response);
|
|
1664
|
+
});
|
|
1665
|
+
router6.post("/internal/helius", async (req, res) => {
|
|
1666
|
+
const headers = {
|
|
1667
|
+
signature: req.headers["x-helius-signature"],
|
|
1668
|
+
rawBody: typeof req.body === "string" ? req.body : JSON.stringify(req.body)
|
|
1669
|
+
};
|
|
1670
|
+
try {
|
|
1671
|
+
const matchCount = await heliusListenerService.processIncoming(req.body, headers);
|
|
1672
|
+
res.status(200).json({ success: true, matched: matchCount });
|
|
1673
|
+
} catch (error) {
|
|
1674
|
+
if (error.message === "HELIUS_SIGNATURE_INVALID") {
|
|
1675
|
+
res.status(401).json({
|
|
1676
|
+
success: false,
|
|
1677
|
+
error: {
|
|
1678
|
+
code: "HELIUS_SIGNATURE_INVALID",
|
|
1679
|
+
message: "Invalid Helius webhook signature"
|
|
1680
|
+
}
|
|
1681
|
+
});
|
|
1682
|
+
return;
|
|
1683
|
+
}
|
|
1684
|
+
throw error;
|
|
1685
|
+
}
|
|
1686
|
+
});
|
|
1687
|
+
var webhook_default = router6;
|
|
1688
|
+
|
|
1689
|
+
// src/routes/index.ts
|
|
1690
|
+
var router7 = (0, import_express7.Router)();
|
|
1691
|
+
router7.use("/health", health_default);
|
|
1692
|
+
router7.use("/stealth", stealth_default);
|
|
1693
|
+
router7.use("/commitment", commitment_default);
|
|
1694
|
+
router7.use("/proof", proof_default);
|
|
1695
|
+
router7.use("/quote", swap_default);
|
|
1696
|
+
router7.use("/swap", swap_default);
|
|
1697
|
+
router7.use("/webhooks", webhook_default);
|
|
1698
|
+
var routes_default = router7;
|
|
1699
|
+
|
|
1700
|
+
// src/routes/metrics.ts
|
|
1701
|
+
var import_express8 = require("express");
|
|
1702
|
+
var router8 = (0, import_express8.Router)();
|
|
1703
|
+
router8.get("/", async (req, res) => {
|
|
1704
|
+
try {
|
|
1705
|
+
res.set("Content-Type", register.contentType);
|
|
1706
|
+
res.end(await register.metrics());
|
|
1707
|
+
} catch (err) {
|
|
1708
|
+
res.status(500).end();
|
|
1709
|
+
}
|
|
1710
|
+
});
|
|
1711
|
+
var metrics_default = router8;
|
|
430
1712
|
|
|
431
1713
|
// src/server.ts
|
|
432
|
-
|
|
433
|
-
var
|
|
434
|
-
var
|
|
435
|
-
|
|
1714
|
+
initSentry();
|
|
1715
|
+
var app = (0, import_express9.default)();
|
|
1716
|
+
var trustProxy = env.TRUST_PROXY;
|
|
1717
|
+
if (trustProxy !== "false") {
|
|
1718
|
+
const parsedValue = /^\d+$/.test(trustProxy) ? parseInt(trustProxy, 10) : trustProxy;
|
|
1719
|
+
app.set("trust proxy", parsedValue);
|
|
1720
|
+
logger.info({ trustProxy: parsedValue }, "Proxy trust configured");
|
|
1721
|
+
}
|
|
1722
|
+
if (env.METRICS_ENABLED === "true") {
|
|
1723
|
+
app.use(metricsMiddleware);
|
|
1724
|
+
}
|
|
1725
|
+
app.use(shutdownMiddleware);
|
|
1726
|
+
app.use(requestIdMiddleware);
|
|
436
1727
|
app.use((0, import_helmet.default)());
|
|
437
|
-
app.use(
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
}));
|
|
441
|
-
app.use(
|
|
442
|
-
app.use(import_express7.default.urlencoded({ extended: true, limit: "1mb" }));
|
|
1728
|
+
app.use(secureCors);
|
|
1729
|
+
app.use(rateLimiter);
|
|
1730
|
+
app.use(authenticate);
|
|
1731
|
+
app.use(import_express9.default.json({ limit: "1mb" }));
|
|
1732
|
+
app.use(import_express9.default.urlencoded({ extended: true, limit: "1mb" }));
|
|
443
1733
|
app.use((0, import_compression.default)());
|
|
444
|
-
app.use(
|
|
1734
|
+
app.use(requestLogger);
|
|
445
1735
|
app.use("/api/v1", routes_default);
|
|
1736
|
+
if (env.METRICS_ENABLED === "true") {
|
|
1737
|
+
app.use("/metrics", metrics_default);
|
|
1738
|
+
}
|
|
446
1739
|
app.get("/", (req, res) => {
|
|
1740
|
+
const corsConfig = getCorsConfig();
|
|
447
1741
|
res.json({
|
|
448
1742
|
name: "@sip-protocol/api",
|
|
449
1743
|
version: "0.1.0",
|
|
@@ -456,28 +1750,75 @@ app.get("/", (req, res) => {
|
|
|
456
1750
|
proof: "POST /api/v1/proof/funding",
|
|
457
1751
|
quote: "POST /api/v1/quote",
|
|
458
1752
|
swap: "POST /api/v1/swap",
|
|
459
|
-
swapStatus: "GET /api/v1/swap/:id/status"
|
|
1753
|
+
swapStatus: "GET /api/v1/swap/:id/status",
|
|
1754
|
+
webhookRegister: "POST /api/v1/webhooks/register",
|
|
1755
|
+
webhookUnregister: "DELETE /api/v1/webhooks/:id",
|
|
1756
|
+
webhookList: "GET /api/v1/webhooks",
|
|
1757
|
+
heliusIngest: "POST /api/v1/webhooks/internal/helius"
|
|
1758
|
+
},
|
|
1759
|
+
security: {
|
|
1760
|
+
authentication: isAuthEnabled() ? "enabled" : "disabled",
|
|
1761
|
+
cors: {
|
|
1762
|
+
origins: corsConfig.origins.length > 0 ? corsConfig.origins.length + " configured" : "none (blocked)",
|
|
1763
|
+
credentials: corsConfig.credentials
|
|
1764
|
+
},
|
|
1765
|
+
rateLimit: "enabled"
|
|
460
1766
|
}
|
|
461
1767
|
});
|
|
462
1768
|
});
|
|
463
1769
|
app.use(notFoundHandler);
|
|
1770
|
+
setupSentryErrorHandler(app);
|
|
464
1771
|
app.use(errorHandler);
|
|
465
1772
|
if (require.main === module) {
|
|
466
|
-
|
|
467
|
-
|
|
1773
|
+
logConfigWarnings(logger);
|
|
1774
|
+
const corsConfig = getCorsConfig();
|
|
1775
|
+
const server = app.listen(env.PORT, () => {
|
|
1776
|
+
logger.info({
|
|
1777
|
+
port: env.PORT,
|
|
1778
|
+
environment: env.NODE_ENV,
|
|
1779
|
+
auth: isAuthEnabled() ? "enabled" : "disabled",
|
|
1780
|
+
corsOrigins: corsConfig.origins.length,
|
|
1781
|
+
logLevel: env.LOG_LEVEL,
|
|
1782
|
+
sentry: isSentryEnabled() ? "enabled" : "disabled",
|
|
1783
|
+
metrics: env.METRICS_ENABLED === "true" ? "enabled" : "disabled"
|
|
1784
|
+
}, "SIP Protocol API started");
|
|
1785
|
+
if (env.isDevelopment) {
|
|
1786
|
+
console.log(`
|
|
468
1787
|
\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
|
|
469
1788
|
\u2551 SIP Protocol REST API \u2551
|
|
470
1789
|
\u2551 Version: 0.1.0 \u2551
|
|
471
|
-
\
|
|
472
|
-
\u2551
|
|
473
|
-
\u2551
|
|
1790
|
+
\u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563
|
|
1791
|
+
\u2551 Port: ${String(env.PORT).padEnd(43)}\u2551
|
|
1792
|
+
\u2551 Environment: ${env.NODE_ENV.padEnd(37)}\u2551
|
|
1793
|
+
\u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563
|
|
1794
|
+
\u2551 Security: \u2551
|
|
1795
|
+
\u2551 \u2022 Auth: ${(isAuthEnabled() ? "ENABLED" : "disabled (dev mode)").padEnd(42)}\u2551
|
|
1796
|
+
\u2551 \u2022 CORS: ${(corsConfig.origins.length + " origins").padEnd(42)}\u2551
|
|
1797
|
+
\u2551 \u2022 Rate Limit: enabled \u2551
|
|
1798
|
+
\u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563
|
|
1799
|
+
\u2551 Monitoring: \u2551
|
|
1800
|
+
\u2551 \u2022 Sentry: ${(isSentryEnabled() ? "ENABLED" : "disabled").padEnd(40)}\u2551
|
|
1801
|
+
\u2551 \u2022 Metrics: ${(env.METRICS_ENABLED === "true" ? "/metrics" : "disabled").padEnd(39)}\u2551
|
|
1802
|
+
\u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563
|
|
1803
|
+
\u2551 Logging: ${env.LOG_LEVEL.padEnd(41)}\u2551
|
|
1804
|
+
\u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563
|
|
1805
|
+
\u2551 Documentation: http://localhost:${String(env.PORT).padEnd(17)}\u2551
|
|
474
1806
|
\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
|
|
475
|
-
|
|
1807
|
+
`);
|
|
1808
|
+
}
|
|
1809
|
+
});
|
|
1810
|
+
setupGracefulShutdown(server, async () => {
|
|
1811
|
+
logger.info("Draining webhook deliveries...");
|
|
1812
|
+
await webhookDeliveryService.drainPending();
|
|
1813
|
+
if (isSentryEnabled()) {
|
|
1814
|
+
logger.info("Flushing Sentry events...");
|
|
1815
|
+
await flushSentry(2e3);
|
|
1816
|
+
}
|
|
1817
|
+
logger.info("Flushing logs...");
|
|
476
1818
|
});
|
|
477
1819
|
}
|
|
478
1820
|
var server_default = app;
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
*/
|
|
1821
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1822
|
+
0 && (module.exports = {
|
|
1823
|
+
isServerShuttingDown
|
|
1824
|
+
});
|