@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/routes/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
|
|
20
30
|
// src/routes/index.ts
|
|
@@ -23,35 +33,445 @@ __export(routes_exports, {
|
|
|
23
33
|
default: () => routes_default
|
|
24
34
|
});
|
|
25
35
|
module.exports = __toCommonJS(routes_exports);
|
|
26
|
-
var
|
|
36
|
+
var import_express7 = require("express");
|
|
27
37
|
|
|
28
38
|
// src/routes/health.ts
|
|
29
|
-
var
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
var import_express2 = require("express");
|
|
40
|
+
|
|
41
|
+
// src/logger.ts
|
|
42
|
+
var import_pino = __toESM(require("pino"));
|
|
43
|
+
var import_pino_http = __toESM(require("pino-http"));
|
|
44
|
+
var import_crypto = __toESM(require("crypto"));
|
|
45
|
+
|
|
46
|
+
// src/config.ts
|
|
47
|
+
var import_envalid = require("envalid");
|
|
48
|
+
var env = (0, import_envalid.cleanEnv)(process.env, {
|
|
49
|
+
// Server configuration
|
|
50
|
+
NODE_ENV: (0, import_envalid.str)({
|
|
51
|
+
choices: ["development", "production", "test"],
|
|
52
|
+
default: "development",
|
|
53
|
+
desc: "Application environment"
|
|
54
|
+
}),
|
|
55
|
+
PORT: (0, import_envalid.port)({
|
|
56
|
+
default: 3e3,
|
|
57
|
+
desc: "Server port"
|
|
58
|
+
}),
|
|
59
|
+
// CORS configuration
|
|
60
|
+
CORS_ORIGINS: (0, import_envalid.str)({
|
|
61
|
+
default: "",
|
|
62
|
+
desc: "Comma-separated list of allowed origins (empty = localhost only in dev)"
|
|
63
|
+
}),
|
|
64
|
+
// Authentication
|
|
65
|
+
API_KEYS: (0, import_envalid.str)({
|
|
66
|
+
default: "",
|
|
67
|
+
desc: "Comma-separated list of valid API keys (empty = auth disabled in dev)"
|
|
68
|
+
}),
|
|
69
|
+
// Rate limiting
|
|
70
|
+
RATE_LIMIT_MAX: (0, import_envalid.num)({
|
|
71
|
+
default: 100,
|
|
72
|
+
desc: "Maximum requests per window"
|
|
73
|
+
}),
|
|
74
|
+
RATE_LIMIT_WINDOW_MS: (0, import_envalid.num)({
|
|
75
|
+
default: 6e4,
|
|
76
|
+
desc: "Rate limit window in milliseconds"
|
|
77
|
+
}),
|
|
78
|
+
// Proxy trust configuration (for X-Forwarded-For header)
|
|
79
|
+
// Set to number of trusted proxies (e.g., 1 for single nginx)
|
|
80
|
+
// or 'loopback' for local proxies, or 'uniquelocal' for private IPs
|
|
81
|
+
TRUST_PROXY: (0, import_envalid.str)({
|
|
82
|
+
default: "1",
|
|
83
|
+
desc: 'Express trust proxy setting (number of hops, "loopback", "uniquelocal", or "false")'
|
|
84
|
+
}),
|
|
85
|
+
// Logging
|
|
86
|
+
LOG_LEVEL: (0, import_envalid.str)({
|
|
87
|
+
choices: ["trace", "debug", "info", "warn", "error", "fatal"],
|
|
88
|
+
default: "info",
|
|
89
|
+
desc: "Logging level"
|
|
90
|
+
}),
|
|
91
|
+
// Graceful shutdown
|
|
92
|
+
SHUTDOWN_TIMEOUT_MS: (0, import_envalid.num)({
|
|
93
|
+
default: 3e4,
|
|
94
|
+
desc: "Graceful shutdown timeout in milliseconds"
|
|
95
|
+
}),
|
|
96
|
+
// Monitoring
|
|
97
|
+
SENTRY_DSN: (0, import_envalid.str)({
|
|
98
|
+
default: "",
|
|
99
|
+
desc: "Sentry DSN for error tracking (optional)"
|
|
100
|
+
}),
|
|
101
|
+
METRICS_ENABLED: (0, import_envalid.str)({
|
|
102
|
+
choices: ["true", "false"],
|
|
103
|
+
default: "true",
|
|
104
|
+
desc: "Enable Prometheus metrics endpoint"
|
|
105
|
+
}),
|
|
106
|
+
// Webhook configuration
|
|
107
|
+
HELIUS_WEBHOOK_SECRET: (0, import_envalid.str)({
|
|
108
|
+
default: "",
|
|
109
|
+
desc: "Helius webhook HMAC secret for signature verification (optional)"
|
|
110
|
+
}),
|
|
111
|
+
WEBHOOK_DELIVERY_MAX_RETRIES: (0, import_envalid.num)({
|
|
112
|
+
default: 3,
|
|
113
|
+
desc: "Maximum delivery attempts for webhook notifications"
|
|
114
|
+
}),
|
|
115
|
+
WEBHOOK_STORE_MAX_SIZE: (0, import_envalid.num)({
|
|
116
|
+
default: 1e3,
|
|
117
|
+
desc: "Maximum number of registered webhooks"
|
|
118
|
+
})
|
|
119
|
+
});
|
|
120
|
+
var isProduction = env.isProduction;
|
|
121
|
+
var isDevelopment = env.isDevelopment;
|
|
122
|
+
var isTest = env.isTest;
|
|
123
|
+
|
|
124
|
+
// src/logger.ts
|
|
125
|
+
var loggerConfig = {
|
|
126
|
+
level: env.LOG_LEVEL,
|
|
127
|
+
base: {
|
|
128
|
+
service: "sip-api",
|
|
129
|
+
version: "0.1.0"
|
|
130
|
+
},
|
|
131
|
+
timestamp: import_pino.default.stdTimeFunctions.isoTime
|
|
132
|
+
};
|
|
133
|
+
if (env.isDevelopment) {
|
|
134
|
+
loggerConfig.transport = {
|
|
135
|
+
target: "pino-pretty",
|
|
136
|
+
options: {
|
|
137
|
+
colorize: true,
|
|
138
|
+
translateTime: "HH:MM:ss",
|
|
139
|
+
ignore: "pid,hostname,service,version"
|
|
40
140
|
}
|
|
41
141
|
};
|
|
42
|
-
|
|
142
|
+
}
|
|
143
|
+
var logger = (0, import_pino.default)(loggerConfig);
|
|
144
|
+
var requestLogger = (0, import_pino_http.default)({
|
|
145
|
+
logger,
|
|
146
|
+
// Use requestId from requestIdMiddleware (set earlier in chain)
|
|
147
|
+
// Falls back to header or generates new if middleware didn't run
|
|
148
|
+
genReqId: (req) => {
|
|
149
|
+
const augmentedReq = req;
|
|
150
|
+
if (augmentedReq.requestId) {
|
|
151
|
+
return augmentedReq.requestId;
|
|
152
|
+
}
|
|
153
|
+
const existingId = req.headers["x-request-id"];
|
|
154
|
+
if (existingId && typeof existingId === "string") {
|
|
155
|
+
return existingId;
|
|
156
|
+
}
|
|
157
|
+
return import_crypto.default.randomUUID();
|
|
158
|
+
},
|
|
159
|
+
customLogLevel: (_req, res, err) => {
|
|
160
|
+
if (res.statusCode >= 500 || err) return "error";
|
|
161
|
+
if (res.statusCode >= 400) return "warn";
|
|
162
|
+
return "info";
|
|
163
|
+
},
|
|
164
|
+
customSuccessMessage: (req, res) => {
|
|
165
|
+
return `${req.method} ${req.url} ${res.statusCode}`;
|
|
166
|
+
},
|
|
167
|
+
customErrorMessage: (req, res, err) => {
|
|
168
|
+
return `${req.method} ${req.url} ${res.statusCode} - ${err.message}`;
|
|
169
|
+
},
|
|
170
|
+
// Don't log health checks in production
|
|
171
|
+
autoLogging: {
|
|
172
|
+
ignore: (req) => {
|
|
173
|
+
return req.url === "/api/v1/health" && env.isProduction;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
43
176
|
});
|
|
44
|
-
var health_default = router;
|
|
45
177
|
|
|
46
|
-
// src/
|
|
47
|
-
var
|
|
178
|
+
// src/shutdown.ts
|
|
179
|
+
var isShuttingDown = false;
|
|
180
|
+
function isServerShuttingDown() {
|
|
181
|
+
return isShuttingDown;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// src/stores/swap-store.ts
|
|
185
|
+
var import_lru_cache = require("lru-cache");
|
|
186
|
+
var DEFAULT_MAX_SIZE = 1e4;
|
|
187
|
+
var DEFAULT_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
188
|
+
var SwapStore = class {
|
|
189
|
+
cache;
|
|
190
|
+
maxSize;
|
|
191
|
+
ttlMs;
|
|
192
|
+
constructor(config = {}) {
|
|
193
|
+
this.maxSize = config.maxSize ?? DEFAULT_MAX_SIZE;
|
|
194
|
+
this.ttlMs = config.ttlMs ?? DEFAULT_TTL_MS;
|
|
195
|
+
this.cache = new import_lru_cache.LRUCache({
|
|
196
|
+
max: this.maxSize,
|
|
197
|
+
ttl: this.ttlMs,
|
|
198
|
+
updateAgeOnGet: false,
|
|
199
|
+
// Don't reset TTL on read
|
|
200
|
+
updateAgeOnHas: false,
|
|
201
|
+
// Log when items are evicted or expired
|
|
202
|
+
disposeAfter: (_value, key, reason) => {
|
|
203
|
+
if (reason === "evict") {
|
|
204
|
+
logger.debug({ swapId: key, reason }, "Swap evicted from cache (LRU)");
|
|
205
|
+
} else if (reason === "expire") {
|
|
206
|
+
logger.debug({ swapId: key, reason }, "Swap expired from cache (TTL)");
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
logger.info(
|
|
211
|
+
{ maxSize: this.maxSize, ttlMs: this.ttlMs },
|
|
212
|
+
"SwapStore initialized"
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Get a swap by ID
|
|
217
|
+
*/
|
|
218
|
+
get(id) {
|
|
219
|
+
return this.cache.get(id);
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Check if a swap exists
|
|
223
|
+
*/
|
|
224
|
+
has(id) {
|
|
225
|
+
return this.cache.has(id);
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Set/update a swap
|
|
229
|
+
*/
|
|
230
|
+
set(id, data) {
|
|
231
|
+
this.cache.set(id, data);
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Delete a swap
|
|
235
|
+
*/
|
|
236
|
+
delete(id) {
|
|
237
|
+
return this.cache.delete(id);
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Update swap status
|
|
241
|
+
*/
|
|
242
|
+
updateStatus(id, status, updates) {
|
|
243
|
+
const existing = this.cache.get(id);
|
|
244
|
+
if (!existing) {
|
|
245
|
+
return void 0;
|
|
246
|
+
}
|
|
247
|
+
const updated = {
|
|
248
|
+
...existing,
|
|
249
|
+
...updates,
|
|
250
|
+
status,
|
|
251
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
252
|
+
};
|
|
253
|
+
this.cache.set(id, updated);
|
|
254
|
+
return updated;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Get store metrics for monitoring
|
|
258
|
+
*/
|
|
259
|
+
getMetrics() {
|
|
260
|
+
const size = this.cache.size;
|
|
261
|
+
return {
|
|
262
|
+
size,
|
|
263
|
+
maxSize: this.maxSize,
|
|
264
|
+
ttlMs: this.ttlMs,
|
|
265
|
+
utilizationPercent: Math.round(size / this.maxSize * 100)
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Clear all swaps (for testing)
|
|
270
|
+
*/
|
|
271
|
+
clear() {
|
|
272
|
+
this.cache.clear();
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Purge stale entries (normally handled automatically by LRU cache)
|
|
276
|
+
*/
|
|
277
|
+
purgeStale() {
|
|
278
|
+
this.cache.purgeStale();
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
var swapStore = new SwapStore();
|
|
282
|
+
|
|
283
|
+
// src/stores/webhook-store.ts
|
|
284
|
+
var import_crypto2 = require("crypto");
|
|
285
|
+
var WebhookStore = class {
|
|
286
|
+
registrations = /* @__PURE__ */ new Map();
|
|
287
|
+
maxSize;
|
|
288
|
+
constructor(maxSize) {
|
|
289
|
+
this.maxSize = maxSize ?? env.WEBHOOK_STORE_MAX_SIZE;
|
|
290
|
+
logger.info({ maxSize: this.maxSize }, "WebhookStore initialized");
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Register a new webhook
|
|
294
|
+
*
|
|
295
|
+
* @returns The registration with secret (shown once only)
|
|
296
|
+
*/
|
|
297
|
+
register(url, viewingPrivateKey, spendingPublicKey) {
|
|
298
|
+
if (this.registrations.size >= this.maxSize) {
|
|
299
|
+
throw new Error("WEBHOOK_STORE_FULL");
|
|
300
|
+
}
|
|
301
|
+
const id = (0, import_crypto2.randomBytes)(16).toString("hex");
|
|
302
|
+
const secret = (0, import_crypto2.randomBytes)(32).toString("hex");
|
|
303
|
+
const registration = {
|
|
304
|
+
id,
|
|
305
|
+
url,
|
|
306
|
+
viewingPrivateKey,
|
|
307
|
+
spendingPublicKey,
|
|
308
|
+
secret,
|
|
309
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
310
|
+
active: true
|
|
311
|
+
};
|
|
312
|
+
this.registrations.set(id, registration);
|
|
313
|
+
logger.info({ webhookId: id, url }, "Webhook registered");
|
|
314
|
+
return registration;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Unregister a webhook by ID
|
|
318
|
+
*
|
|
319
|
+
* @returns true if found and removed, false if not found
|
|
320
|
+
*/
|
|
321
|
+
unregister(id) {
|
|
322
|
+
const existed = this.registrations.delete(id);
|
|
323
|
+
if (existed) {
|
|
324
|
+
logger.info({ webhookId: id }, "Webhook unregistered");
|
|
325
|
+
}
|
|
326
|
+
return existed;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Get a registration by ID
|
|
330
|
+
*/
|
|
331
|
+
get(id) {
|
|
332
|
+
return this.registrations.get(id);
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Get all active registrations
|
|
336
|
+
*/
|
|
337
|
+
getAll() {
|
|
338
|
+
return Array.from(this.registrations.values()).filter((r) => r.active);
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Get all registrations (including inactive) for listing
|
|
342
|
+
*/
|
|
343
|
+
getAllForList() {
|
|
344
|
+
return Array.from(this.registrations.values()).map((r) => ({
|
|
345
|
+
id: r.id,
|
|
346
|
+
url: r.url,
|
|
347
|
+
active: r.active,
|
|
348
|
+
createdAt: r.createdAt
|
|
349
|
+
}));
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Current store size
|
|
353
|
+
*/
|
|
354
|
+
get size() {
|
|
355
|
+
return this.registrations.size;
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Clear all registrations (for testing)
|
|
359
|
+
*/
|
|
360
|
+
clear() {
|
|
361
|
+
this.registrations.clear();
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
var webhookStore = new WebhookStore();
|
|
365
|
+
|
|
366
|
+
// src/routes/proof.ts
|
|
367
|
+
var import_express = require("express");
|
|
48
368
|
var import_sdk2 = require("@sip-protocol/sdk");
|
|
369
|
+
var import_utils = require("@noble/hashes/utils");
|
|
49
370
|
|
|
50
371
|
// src/middleware/error-handler.ts
|
|
51
372
|
var import_sdk = require("@sip-protocol/sdk");
|
|
52
373
|
|
|
374
|
+
// src/monitoring/sentry.ts
|
|
375
|
+
var Sentry = __toESM(require("@sentry/node"));
|
|
376
|
+
|
|
377
|
+
// src/monitoring/metrics.ts
|
|
378
|
+
var import_prom_client = require("prom-client");
|
|
379
|
+
var register = new import_prom_client.Registry();
|
|
380
|
+
(0, import_prom_client.collectDefaultMetrics)({
|
|
381
|
+
register,
|
|
382
|
+
prefix: "sip_api_"
|
|
383
|
+
});
|
|
384
|
+
var httpRequestsTotal = new import_prom_client.Counter({
|
|
385
|
+
name: "sip_api_http_requests_total",
|
|
386
|
+
help: "Total number of HTTP requests",
|
|
387
|
+
labelNames: ["method", "path", "status"],
|
|
388
|
+
registers: [register]
|
|
389
|
+
});
|
|
390
|
+
var httpRequestDuration = new import_prom_client.Histogram({
|
|
391
|
+
name: "sip_api_http_request_duration_seconds",
|
|
392
|
+
help: "Duration of HTTP requests in seconds",
|
|
393
|
+
labelNames: ["method", "path", "status"],
|
|
394
|
+
buckets: [1e-3, 5e-3, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10],
|
|
395
|
+
registers: [register]
|
|
396
|
+
});
|
|
397
|
+
var stealthAddressGenerations = new import_prom_client.Counter({
|
|
398
|
+
name: "sip_stealth_address_generations_total",
|
|
399
|
+
help: "Total number of stealth address generations",
|
|
400
|
+
labelNames: ["chain"],
|
|
401
|
+
registers: [register]
|
|
402
|
+
});
|
|
403
|
+
var commitmentCreations = new import_prom_client.Counter({
|
|
404
|
+
name: "sip_commitment_creations_total",
|
|
405
|
+
help: "Total number of commitment creations",
|
|
406
|
+
registers: [register]
|
|
407
|
+
});
|
|
408
|
+
var proofGenerations = new import_prom_client.Counter({
|
|
409
|
+
name: "sip_proof_generations_total",
|
|
410
|
+
help: "Total number of proof generations",
|
|
411
|
+
labelNames: ["type"],
|
|
412
|
+
registers: [register]
|
|
413
|
+
});
|
|
414
|
+
var proofGenerationDuration = new import_prom_client.Histogram({
|
|
415
|
+
name: "sip_proof_generation_duration_seconds",
|
|
416
|
+
help: "Duration of proof generation in seconds",
|
|
417
|
+
labelNames: ["type"],
|
|
418
|
+
buckets: [0.1, 0.5, 1, 2.5, 5, 10, 30, 60],
|
|
419
|
+
registers: [register]
|
|
420
|
+
});
|
|
421
|
+
var activeConnections = new import_prom_client.Gauge({
|
|
422
|
+
name: "sip_api_active_connections",
|
|
423
|
+
help: "Number of active connections",
|
|
424
|
+
registers: [register]
|
|
425
|
+
});
|
|
426
|
+
var swapRequests = new import_prom_client.Counter({
|
|
427
|
+
name: "sip_swap_requests_total",
|
|
428
|
+
help: "Total number of swap requests",
|
|
429
|
+
labelNames: ["from_chain", "to_chain", "status"],
|
|
430
|
+
registers: [register]
|
|
431
|
+
});
|
|
432
|
+
var quoteRequests = new import_prom_client.Counter({
|
|
433
|
+
name: "sip_quote_requests_total",
|
|
434
|
+
help: "Total number of quote requests",
|
|
435
|
+
labelNames: ["from_chain", "to_chain"],
|
|
436
|
+
registers: [register]
|
|
437
|
+
});
|
|
438
|
+
|
|
53
439
|
// src/middleware/validation.ts
|
|
54
440
|
var import_zod = require("zod");
|
|
441
|
+
var MAX_UINT256 = 2n ** 256n - 1n;
|
|
442
|
+
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(
|
|
443
|
+
(v) => {
|
|
444
|
+
try {
|
|
445
|
+
const n = BigInt(v);
|
|
446
|
+
return n > 0n && n <= MAX_UINT256;
|
|
447
|
+
} catch {
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
450
|
+
},
|
|
451
|
+
{ message: "Invalid amount: must be positive integer <= 2^256-1" }
|
|
452
|
+
);
|
|
453
|
+
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(
|
|
454
|
+
(v) => {
|
|
455
|
+
try {
|
|
456
|
+
const n = BigInt(v);
|
|
457
|
+
return n >= 0n && n <= MAX_UINT256;
|
|
458
|
+
} catch {
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
461
|
+
},
|
|
462
|
+
{ message: "Invalid amount: must be non-negative integer <= 2^256-1" }
|
|
463
|
+
);
|
|
464
|
+
function calculateMinAmount(input, slippageBps) {
|
|
465
|
+
if (slippageBps < 0 || slippageBps > 1e4) {
|
|
466
|
+
throw new Error("Invalid slippage: must be 0-10000 basis points");
|
|
467
|
+
}
|
|
468
|
+
const bps = BigInt(Math.floor(slippageBps));
|
|
469
|
+
const multiplier = 10000n - bps;
|
|
470
|
+
return input * multiplier / 10000n;
|
|
471
|
+
}
|
|
472
|
+
function percentToBps(percent) {
|
|
473
|
+
return Math.floor(percent * 100);
|
|
474
|
+
}
|
|
55
475
|
function validateRequest(schema) {
|
|
56
476
|
return async (req, res, next) => {
|
|
57
477
|
try {
|
|
@@ -62,7 +482,9 @@ function validateRequest(schema) {
|
|
|
62
482
|
req.query = await schema.query.parseAsync(req.query);
|
|
63
483
|
}
|
|
64
484
|
if (schema.params) {
|
|
65
|
-
req.params = await schema.params.parseAsync(
|
|
485
|
+
req.params = await schema.params.parseAsync(
|
|
486
|
+
req.params
|
|
487
|
+
);
|
|
66
488
|
}
|
|
67
489
|
next();
|
|
68
490
|
} catch (error) {
|
|
@@ -72,7 +494,8 @@ function validateRequest(schema) {
|
|
|
72
494
|
error: {
|
|
73
495
|
code: "VALIDATION_ERROR",
|
|
74
496
|
message: "Invalid request data",
|
|
75
|
-
|
|
497
|
+
// Zod 4 renamed 'errors' to 'issues'
|
|
498
|
+
details: error.issues
|
|
76
499
|
}
|
|
77
500
|
});
|
|
78
501
|
}
|
|
@@ -91,19 +514,19 @@ var schemas = {
|
|
|
91
514
|
})
|
|
92
515
|
}),
|
|
93
516
|
createCommitment: import_zod.z.object({
|
|
94
|
-
value:
|
|
95
|
-
// bigint as string
|
|
517
|
+
value: amountSchema,
|
|
96
518
|
blindingFactor: import_zod.z.string().regex(/^0x[0-9a-fA-F]+$/).optional()
|
|
97
519
|
}),
|
|
98
520
|
generateFundingProof: import_zod.z.object({
|
|
99
|
-
balance:
|
|
100
|
-
minRequired:
|
|
521
|
+
balance: amountSchema,
|
|
522
|
+
minRequired: minAmountSchema,
|
|
523
|
+
// Zero allowed: "prove I have >= 0" is valid
|
|
101
524
|
balanceBlinding: import_zod.z.string().regex(/^0x[0-9a-fA-F]+$/)
|
|
102
525
|
}),
|
|
103
526
|
getQuote: import_zod.z.object({
|
|
104
527
|
inputChain: import_zod.z.enum(["solana", "ethereum", "near", "zcash", "polygon", "arbitrum", "optimism", "base", "bitcoin", "aptos", "sui", "cosmos", "osmosis", "injective", "celestia", "sei", "dydx"]),
|
|
105
528
|
inputToken: import_zod.z.string().min(1),
|
|
106
|
-
inputAmount:
|
|
529
|
+
inputAmount: amountSchema,
|
|
107
530
|
outputChain: import_zod.z.enum(["solana", "ethereum", "near", "zcash", "polygon", "arbitrum", "optimism", "base", "bitcoin", "aptos", "sui", "cosmos", "osmosis", "injective", "celestia", "sei", "dydx"]),
|
|
108
531
|
outputToken: import_zod.z.string().min(1),
|
|
109
532
|
slippageTolerance: import_zod.z.number().min(0).max(100).optional()
|
|
@@ -119,105 +542,269 @@ var schemas = {
|
|
|
119
542
|
})
|
|
120
543
|
};
|
|
121
544
|
|
|
122
|
-
// src/
|
|
123
|
-
var
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
545
|
+
// src/middleware/rate-limit.ts
|
|
546
|
+
var import_express_rate_limit = __toESM(require("express-rate-limit"));
|
|
547
|
+
var import_rate_limit_redis = require("rate-limit-redis");
|
|
548
|
+
var import_ioredis = require("ioredis");
|
|
549
|
+
var WINDOW_MS = parseInt(process.env.RATE_LIMIT_WINDOW_MS || "60000", 10);
|
|
550
|
+
var MAX_REQUESTS = parseInt(process.env.RATE_LIMIT_MAX_REQUESTS || "100", 10);
|
|
551
|
+
var SKIP_FAILED = process.env.RATE_LIMIT_SKIP_FAILED === "true";
|
|
552
|
+
var STORE_TYPE = process.env.RATE_LIMIT_STORE || "memory";
|
|
553
|
+
var REDIS_URL = process.env.REDIS_URL;
|
|
554
|
+
var redisClient = null;
|
|
555
|
+
var redisConnectionFailed = false;
|
|
556
|
+
function getRedisClient() {
|
|
557
|
+
if (redisConnectionFailed) {
|
|
558
|
+
return null;
|
|
559
|
+
}
|
|
560
|
+
if (!redisClient && REDIS_URL) {
|
|
561
|
+
redisClient = new import_ioredis.Redis(REDIS_URL, {
|
|
562
|
+
maxRetriesPerRequest: 3,
|
|
563
|
+
retryStrategy: (times) => {
|
|
564
|
+
if (times > 3) {
|
|
565
|
+
console.warn("[rate-limit] Redis connection failed, falling back to memory store");
|
|
566
|
+
redisConnectionFailed = true;
|
|
567
|
+
return null;
|
|
568
|
+
}
|
|
569
|
+
return Math.min(times * 100, 1e3);
|
|
570
|
+
},
|
|
571
|
+
lazyConnect: true
|
|
572
|
+
});
|
|
573
|
+
redisClient.on("error", (err) => {
|
|
574
|
+
console.warn("[rate-limit] Redis error:", err.message);
|
|
575
|
+
});
|
|
576
|
+
redisClient.on("connect", () => {
|
|
577
|
+
console.log("[rate-limit] Redis connected successfully");
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
return redisClient;
|
|
581
|
+
}
|
|
582
|
+
function getRedisStatus() {
|
|
583
|
+
const client = getRedisClient();
|
|
584
|
+
return {
|
|
585
|
+
storeType: client && !redisConnectionFailed ? "redis" : "memory",
|
|
586
|
+
configured: !!REDIS_URL,
|
|
587
|
+
connected: client?.status === "ready"
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
function createStore(prefix) {
|
|
591
|
+
if (STORE_TYPE === "redis" || REDIS_URL) {
|
|
592
|
+
const client = getRedisClient();
|
|
593
|
+
if (client && !redisConnectionFailed) {
|
|
594
|
+
const sendCommand = async (...args) => {
|
|
595
|
+
return client.call(args[0], ...args.slice(1));
|
|
596
|
+
};
|
|
597
|
+
return new import_rate_limit_redis.RedisStore({
|
|
598
|
+
sendCommand,
|
|
599
|
+
prefix: `rl:${prefix}:`
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
return void 0;
|
|
604
|
+
}
|
|
605
|
+
var rateLimiter = (0, import_express_rate_limit.default)({
|
|
606
|
+
windowMs: WINDOW_MS,
|
|
607
|
+
max: MAX_REQUESTS,
|
|
608
|
+
skipFailedRequests: SKIP_FAILED,
|
|
609
|
+
standardHeaders: true,
|
|
610
|
+
// Return rate limit info in `RateLimit-*` headers
|
|
611
|
+
legacyHeaders: false,
|
|
612
|
+
// Disable `X-RateLimit-*` headers
|
|
613
|
+
// Use Redis store if available, otherwise memory
|
|
614
|
+
store: createStore("api"),
|
|
615
|
+
handler: (_req, res) => {
|
|
616
|
+
res.status(429).json({
|
|
617
|
+
success: false,
|
|
618
|
+
error: {
|
|
619
|
+
code: "RATE_LIMIT_EXCEEDED",
|
|
620
|
+
message: "Too many requests, please try again later",
|
|
621
|
+
details: {
|
|
622
|
+
retryAfter: Math.ceil(WINDOW_MS / 1e3),
|
|
623
|
+
limit: MAX_REQUESTS,
|
|
624
|
+
windowMs: WINDOW_MS
|
|
137
625
|
}
|
|
138
626
|
}
|
|
139
|
-
};
|
|
140
|
-
|
|
627
|
+
});
|
|
628
|
+
},
|
|
629
|
+
skip: (req) => {
|
|
630
|
+
return req.path === "/api/v1/health" || req.path === "/";
|
|
141
631
|
}
|
|
142
|
-
);
|
|
143
|
-
var
|
|
632
|
+
});
|
|
633
|
+
var strictRateLimiter = (0, import_express_rate_limit.default)({
|
|
634
|
+
windowMs: 6e4,
|
|
635
|
+
// 1 minute
|
|
636
|
+
max: 10,
|
|
637
|
+
// 10 requests per minute
|
|
638
|
+
skipFailedRequests: false,
|
|
639
|
+
standardHeaders: true,
|
|
640
|
+
legacyHeaders: false,
|
|
641
|
+
// Use Redis store if available with different prefix
|
|
642
|
+
store: createStore("strict"),
|
|
643
|
+
handler: (_req, res) => {
|
|
644
|
+
res.status(429).json({
|
|
645
|
+
success: false,
|
|
646
|
+
error: {
|
|
647
|
+
code: "RATE_LIMIT_EXCEEDED",
|
|
648
|
+
message: "Rate limit exceeded for sensitive endpoint",
|
|
649
|
+
details: {
|
|
650
|
+
retryAfter: 60,
|
|
651
|
+
limit: 10,
|
|
652
|
+
windowMs: 6e4
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
});
|
|
144
658
|
|
|
145
|
-
// src/
|
|
146
|
-
var
|
|
147
|
-
var
|
|
659
|
+
// src/middleware/auth.ts
|
|
660
|
+
var API_KEYS = (process.env.API_KEYS || "").split(",").filter(Boolean);
|
|
661
|
+
var NODE_ENV = process.env.NODE_ENV || "development";
|
|
662
|
+
var AUTH_ENABLED = process.env.AUTH_ENABLED !== "false" && NODE_ENV === "production";
|
|
663
|
+
var SKIP_PATHS = (process.env.AUTH_SKIP_PATHS || "/health,/,/webhooks/internal/helius").split(",").map((p) => p.trim());
|
|
148
664
|
|
|
149
|
-
//
|
|
150
|
-
var
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
var
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
665
|
+
// src/middleware/cors.ts
|
|
666
|
+
var import_cors = __toESM(require("cors"));
|
|
667
|
+
var NODE_ENV2 = process.env.NODE_ENV || "development";
|
|
668
|
+
var CORS_ORIGINS = process.env.CORS_ORIGINS?.split(",").map((o) => o.trim()).filter(Boolean) || [];
|
|
669
|
+
var CORS_CREDENTIALS = process.env.CORS_CREDENTIALS !== "false";
|
|
670
|
+
var CORS_MAX_AGE = parseInt(process.env.CORS_MAX_AGE || "86400", 10);
|
|
671
|
+
var DEV_ORIGINS = [
|
|
672
|
+
"http://localhost:3000",
|
|
673
|
+
"http://localhost:3001",
|
|
674
|
+
"http://localhost:4000",
|
|
675
|
+
"http://localhost:5173",
|
|
676
|
+
// Vite
|
|
677
|
+
"http://127.0.0.1:3000",
|
|
678
|
+
"http://127.0.0.1:3001",
|
|
679
|
+
"http://127.0.0.1:4000",
|
|
680
|
+
"http://127.0.0.1:5173"
|
|
681
|
+
];
|
|
682
|
+
function getAllowedOrigins() {
|
|
683
|
+
if (CORS_ORIGINS.length > 0) {
|
|
684
|
+
return CORS_ORIGINS;
|
|
685
|
+
}
|
|
686
|
+
if (NODE_ENV2 === "development" || NODE_ENV2 === "test") {
|
|
687
|
+
return DEV_ORIGINS;
|
|
688
|
+
}
|
|
689
|
+
return [];
|
|
163
690
|
}
|
|
164
|
-
function
|
|
165
|
-
if (
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
691
|
+
function isOriginAllowed(origin) {
|
|
692
|
+
if (!origin) {
|
|
693
|
+
return true;
|
|
694
|
+
}
|
|
695
|
+
const allowedOrigins = getAllowedOrigins();
|
|
696
|
+
if (allowedOrigins.includes(origin)) {
|
|
697
|
+
return true;
|
|
698
|
+
}
|
|
699
|
+
let originUrl;
|
|
700
|
+
try {
|
|
701
|
+
originUrl = new URL(origin);
|
|
702
|
+
} catch {
|
|
703
|
+
console.warn(`[CORS] Rejected malformed origin: ${origin}`);
|
|
704
|
+
return false;
|
|
705
|
+
}
|
|
706
|
+
for (const allowed of allowedOrigins) {
|
|
707
|
+
if (allowed.startsWith("*.")) {
|
|
708
|
+
const baseDomain = allowed.slice(2);
|
|
709
|
+
const originHost = originUrl.host;
|
|
710
|
+
if (NODE_ENV2 === "production" && originUrl.protocol !== "https:") {
|
|
711
|
+
console.warn(`[CORS] Rejected non-HTTPS origin in production: ${origin}`);
|
|
712
|
+
continue;
|
|
713
|
+
}
|
|
714
|
+
if (originHost === baseDomain || originHost.endsWith("." + baseDomain)) {
|
|
715
|
+
return true;
|
|
716
|
+
}
|
|
180
717
|
}
|
|
181
|
-
array[ai] = n1 * 16 + n2;
|
|
182
718
|
}
|
|
183
|
-
return
|
|
719
|
+
return false;
|
|
184
720
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
721
|
+
var corsOptionsDelegate = (req, callback) => {
|
|
722
|
+
const origin = req.headers.origin;
|
|
723
|
+
const allowed = isOriginAllowed(origin);
|
|
724
|
+
const options = {
|
|
725
|
+
origin: allowed ? origin : false,
|
|
726
|
+
credentials: CORS_CREDENTIALS,
|
|
727
|
+
maxAge: CORS_MAX_AGE,
|
|
728
|
+
methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"],
|
|
729
|
+
allowedHeaders: [
|
|
730
|
+
"Content-Type",
|
|
731
|
+
"Authorization",
|
|
732
|
+
"X-API-Key",
|
|
733
|
+
"X-Request-ID",
|
|
734
|
+
"X-Forwarded-For"
|
|
735
|
+
],
|
|
736
|
+
exposedHeaders: [
|
|
737
|
+
"RateLimit-Limit",
|
|
738
|
+
"RateLimit-Remaining",
|
|
739
|
+
"RateLimit-Reset",
|
|
740
|
+
"X-Request-ID"
|
|
741
|
+
]
|
|
742
|
+
};
|
|
743
|
+
if (!allowed && origin) {
|
|
744
|
+
console.warn(`[CORS] Blocked request from origin: ${origin}`);
|
|
204
745
|
}
|
|
205
|
-
);
|
|
206
|
-
|
|
746
|
+
callback(null, options);
|
|
747
|
+
};
|
|
748
|
+
var secureCors = (0, import_cors.default)(corsOptionsDelegate);
|
|
207
749
|
|
|
208
750
|
// src/routes/proof.ts
|
|
209
|
-
var
|
|
210
|
-
var
|
|
211
|
-
var
|
|
212
|
-
var
|
|
213
|
-
|
|
751
|
+
var router = (0, import_express.Router)();
|
|
752
|
+
var proofProvider = new import_sdk2.MockProofProvider();
|
|
753
|
+
var proofProviderReady = false;
|
|
754
|
+
var proofInitError = null;
|
|
755
|
+
var MAX_INIT_RETRIES = 3;
|
|
756
|
+
var RETRY_DELAY_MS = 2e3;
|
|
757
|
+
async function initializeProofProvider() {
|
|
758
|
+
for (let attempt = 1; attempt <= MAX_INIT_RETRIES; attempt++) {
|
|
759
|
+
try {
|
|
760
|
+
await proofProvider.initialize();
|
|
761
|
+
proofProviderReady = true;
|
|
762
|
+
proofInitError = null;
|
|
763
|
+
logger.info({ attempt }, "Proof provider initialized successfully");
|
|
764
|
+
return;
|
|
765
|
+
} catch (err) {
|
|
766
|
+
proofInitError = err instanceof Error ? err : new Error(String(err));
|
|
767
|
+
logger.warn(
|
|
768
|
+
{ attempt, maxRetries: MAX_INIT_RETRIES, error: proofInitError.message },
|
|
769
|
+
"Proof provider initialization failed, retrying..."
|
|
770
|
+
);
|
|
771
|
+
if (attempt < MAX_INIT_RETRIES) {
|
|
772
|
+
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS * attempt));
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
logger.error(
|
|
777
|
+
{ error: proofInitError?.message },
|
|
778
|
+
"Proof provider initialization failed after all retries"
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
initializeProofProvider();
|
|
782
|
+
function isProofProviderReady() {
|
|
783
|
+
return proofProviderReady;
|
|
784
|
+
}
|
|
785
|
+
function getProofInitError() {
|
|
786
|
+
return proofInitError;
|
|
787
|
+
}
|
|
788
|
+
router.post(
|
|
214
789
|
"/funding",
|
|
215
790
|
validateRequest({ body: schemas.generateFundingProof }),
|
|
216
791
|
async (req, res) => {
|
|
792
|
+
if (!proofProviderReady) {
|
|
793
|
+
const errorMsg = proofInitError?.message || "Proof provider is initializing";
|
|
794
|
+
logger.warn({ error: errorMsg }, "Proof request rejected - provider not ready");
|
|
795
|
+
return res.status(503).json({
|
|
796
|
+
success: false,
|
|
797
|
+
error: {
|
|
798
|
+
code: "PROOF_PROVIDER_NOT_READY",
|
|
799
|
+
message: "Proof generation service is not ready",
|
|
800
|
+
details: { reason: errorMsg }
|
|
801
|
+
}
|
|
802
|
+
});
|
|
803
|
+
}
|
|
217
804
|
const { balance, minRequired, balanceBlinding } = req.body;
|
|
218
805
|
const balanceBigInt = BigInt(balance);
|
|
219
806
|
const minRequiredBigInt = BigInt(minRequired);
|
|
220
|
-
const balanceBlindingBytes = hexToBytes(balanceBlinding);
|
|
807
|
+
const balanceBlindingBytes = (0, import_utils.hexToBytes)(balanceBlinding.replace(/^0x/, ""));
|
|
221
808
|
const result = await proofProvider.generateFundingProof({
|
|
222
809
|
balance: balanceBigInt,
|
|
223
810
|
minimumRequired: minRequiredBigInt,
|
|
@@ -237,14 +824,109 @@ router4.post(
|
|
|
237
824
|
res.json(response);
|
|
238
825
|
}
|
|
239
826
|
);
|
|
240
|
-
var proof_default =
|
|
827
|
+
var proof_default = router;
|
|
828
|
+
|
|
829
|
+
// src/routes/health.ts
|
|
830
|
+
var router2 = (0, import_express2.Router)();
|
|
831
|
+
var startTime = Date.now();
|
|
832
|
+
router2.get("/", (req, res) => {
|
|
833
|
+
const shuttingDown = isServerShuttingDown();
|
|
834
|
+
const status = shuttingDown ? "shutting_down" : "healthy";
|
|
835
|
+
const statusCode = shuttingDown ? 503 : 200;
|
|
836
|
+
const cacheMetrics = swapStore.getMetrics();
|
|
837
|
+
const proofReady = isProofProviderReady();
|
|
838
|
+
const proofError = getProofInitError();
|
|
839
|
+
const redisStatus = getRedisStatus();
|
|
840
|
+
const response = {
|
|
841
|
+
success: !shuttingDown,
|
|
842
|
+
data: {
|
|
843
|
+
status,
|
|
844
|
+
version: process.env.npm_package_version || "0.1.0",
|
|
845
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
846
|
+
uptime: Math.floor((Date.now() - startTime) / 1e3),
|
|
847
|
+
services: {
|
|
848
|
+
proofProvider: {
|
|
849
|
+
ready: proofReady,
|
|
850
|
+
error: proofError?.message || null
|
|
851
|
+
},
|
|
852
|
+
rateLimiter: {
|
|
853
|
+
store: redisStatus.storeType,
|
|
854
|
+
redisConfigured: redisStatus.configured,
|
|
855
|
+
redisConnected: redisStatus.connected
|
|
856
|
+
}
|
|
857
|
+
},
|
|
858
|
+
cache: {
|
|
859
|
+
swaps: {
|
|
860
|
+
size: cacheMetrics.size,
|
|
861
|
+
maxSize: cacheMetrics.maxSize,
|
|
862
|
+
utilizationPercent: cacheMetrics.utilizationPercent
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
};
|
|
867
|
+
res.status(statusCode).json(response);
|
|
868
|
+
});
|
|
869
|
+
var health_default = router2;
|
|
870
|
+
|
|
871
|
+
// src/routes/stealth.ts
|
|
872
|
+
var import_express3 = require("express");
|
|
873
|
+
var import_sdk3 = require("@sip-protocol/sdk");
|
|
874
|
+
var router3 = (0, import_express3.Router)();
|
|
875
|
+
router3.post(
|
|
876
|
+
"/generate",
|
|
877
|
+
validateRequest({ body: schemas.generateStealth }),
|
|
878
|
+
async (req, res) => {
|
|
879
|
+
const { chain, recipientMetaAddress } = req.body;
|
|
880
|
+
const result = (0, import_sdk3.generateStealthAddress)(recipientMetaAddress);
|
|
881
|
+
const response = {
|
|
882
|
+
success: true,
|
|
883
|
+
data: {
|
|
884
|
+
stealthAddress: {
|
|
885
|
+
address: result.stealthAddress.address,
|
|
886
|
+
ephemeralPublicKey: result.stealthAddress.ephemeralPublicKey,
|
|
887
|
+
viewTag: result.stealthAddress.viewTag
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
};
|
|
891
|
+
res.json(response);
|
|
892
|
+
}
|
|
893
|
+
);
|
|
894
|
+
var stealth_default = router3;
|
|
895
|
+
|
|
896
|
+
// src/routes/commitment.ts
|
|
897
|
+
var import_express4 = require("express");
|
|
898
|
+
var import_sdk4 = require("@sip-protocol/sdk");
|
|
899
|
+
var import_utils2 = require("@noble/hashes/utils");
|
|
900
|
+
var router4 = (0, import_express4.Router)();
|
|
901
|
+
router4.post(
|
|
902
|
+
"/create",
|
|
903
|
+
validateRequest({ body: schemas.createCommitment }),
|
|
904
|
+
async (req, res) => {
|
|
905
|
+
const { value, blindingFactor } = req.body;
|
|
906
|
+
const valueBigInt = BigInt(value);
|
|
907
|
+
const blindingBytes = blindingFactor ? (0, import_utils2.hexToBytes)(blindingFactor.replace(/^0x/, "")) : void 0;
|
|
908
|
+
const result = (0, import_sdk4.commit)(valueBigInt, blindingBytes);
|
|
909
|
+
const response = {
|
|
910
|
+
success: true,
|
|
911
|
+
data: {
|
|
912
|
+
commitment: result.commitment,
|
|
913
|
+
blindingFactor: result.blinding
|
|
914
|
+
}
|
|
915
|
+
};
|
|
916
|
+
res.json(response);
|
|
917
|
+
}
|
|
918
|
+
);
|
|
919
|
+
var commitment_default = router4;
|
|
241
920
|
|
|
242
921
|
// src/routes/swap.ts
|
|
243
922
|
var import_express5 = require("express");
|
|
244
923
|
var import_sdk5 = require("@sip-protocol/sdk");
|
|
245
924
|
var router5 = (0, import_express5.Router)();
|
|
246
925
|
var sip = new import_sdk5.SIP({ network: "testnet" });
|
|
247
|
-
var
|
|
926
|
+
var MOCK_MODE_ENABLED = env.NODE_ENV !== "production";
|
|
927
|
+
if (!MOCK_MODE_ENABLED) {
|
|
928
|
+
logger.warn("Production mode: Mock quotes and swaps are DISABLED");
|
|
929
|
+
}
|
|
248
930
|
router5.post(
|
|
249
931
|
"/",
|
|
250
932
|
validateRequest({ body: schemas.getQuote }),
|
|
@@ -257,37 +939,66 @@ router5.post(
|
|
|
257
939
|
outputToken,
|
|
258
940
|
slippageTolerance
|
|
259
941
|
} = req.body;
|
|
942
|
+
if (!MOCK_MODE_ENABLED) {
|
|
943
|
+
return res.status(503).json({
|
|
944
|
+
success: false,
|
|
945
|
+
error: {
|
|
946
|
+
code: "QUOTE_SERVICE_UNAVAILABLE",
|
|
947
|
+
message: "Real quote aggregator not configured. This API is not ready for production use.",
|
|
948
|
+
details: {
|
|
949
|
+
hint: "Configure QUOTE_AGGREGATOR_URL or use development mode"
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
if (!(0, import_sdk5.isKnownToken)(inputToken, inputChain)) {
|
|
955
|
+
return res.status(400).json({
|
|
956
|
+
success: false,
|
|
957
|
+
error: {
|
|
958
|
+
code: "UNKNOWN_TOKEN",
|
|
959
|
+
message: `Unknown input token: ${inputToken} on ${inputChain}`
|
|
960
|
+
}
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
if (!(0, import_sdk5.isKnownToken)(outputToken, outputChain)) {
|
|
964
|
+
return res.status(400).json({
|
|
965
|
+
success: false,
|
|
966
|
+
error: {
|
|
967
|
+
code: "UNKNOWN_TOKEN",
|
|
968
|
+
message: `Unknown output token: ${outputToken} on ${outputChain}`
|
|
969
|
+
}
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
const inputAsset = (0, import_sdk5.getAsset)(inputToken, inputChain);
|
|
973
|
+
const outputAsset = (0, import_sdk5.getAsset)(outputToken, outputChain);
|
|
974
|
+
const inputAmountBigInt = BigInt(inputAmount);
|
|
975
|
+
const slippagePercent = slippageTolerance ?? 1;
|
|
976
|
+
const slippageBps = percentToBps(slippagePercent);
|
|
260
977
|
const intent = await sip.createIntent({
|
|
261
978
|
input: {
|
|
262
|
-
asset:
|
|
263
|
-
|
|
264
|
-
address: null,
|
|
265
|
-
// Native token
|
|
266
|
-
symbol: inputToken,
|
|
267
|
-
decimals: 9
|
|
268
|
-
},
|
|
269
|
-
amount: BigInt(inputAmount)
|
|
979
|
+
asset: inputAsset,
|
|
980
|
+
amount: inputAmountBigInt
|
|
270
981
|
},
|
|
271
982
|
output: {
|
|
272
|
-
asset:
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
// Native token
|
|
276
|
-
symbol: outputToken,
|
|
277
|
-
decimals: 9
|
|
278
|
-
},
|
|
279
|
-
minAmount: BigInt(inputAmount) * 95n / 100n,
|
|
280
|
-
// 5% slippage
|
|
281
|
-
maxSlippage: (slippageTolerance || 1) / 100
|
|
983
|
+
asset: outputAsset,
|
|
984
|
+
minAmount: calculateMinAmount(inputAmountBigInt, slippageBps),
|
|
985
|
+
maxSlippage: slippagePercent / 100
|
|
282
986
|
},
|
|
283
987
|
privacy: import_sdk5.PrivacyLevel.TRANSPARENT
|
|
284
988
|
// Default to transparent for quote
|
|
285
989
|
});
|
|
990
|
+
logger.warn({
|
|
991
|
+
inputChain,
|
|
992
|
+
inputToken,
|
|
993
|
+
outputChain,
|
|
994
|
+
outputToken,
|
|
995
|
+
inputAmount
|
|
996
|
+
}, "Returning MOCK quote - not for production use");
|
|
286
997
|
const mockQuote = {
|
|
287
998
|
quoteId: `quote-${Date.now()}`,
|
|
288
999
|
inputAmount,
|
|
1000
|
+
// Mock: 5% fee (clearly fake rate)
|
|
289
1001
|
outputAmount: (BigInt(inputAmount) * 95n / 100n).toString(),
|
|
290
|
-
// Mock 5% fee
|
|
291
1002
|
rate: "0.95",
|
|
292
1003
|
estimatedTime: 30,
|
|
293
1004
|
fees: {
|
|
@@ -307,7 +1018,10 @@ router5.post(
|
|
|
307
1018
|
};
|
|
308
1019
|
const response = {
|
|
309
1020
|
success: true,
|
|
310
|
-
data:
|
|
1021
|
+
data: {
|
|
1022
|
+
...mockQuote,
|
|
1023
|
+
_warning: "MOCK_DATA: This quote uses simulated pricing. Do not use for real transactions."
|
|
1024
|
+
}
|
|
311
1025
|
};
|
|
312
1026
|
res.json(response);
|
|
313
1027
|
}
|
|
@@ -316,23 +1030,45 @@ router5.post(
|
|
|
316
1030
|
"/swap",
|
|
317
1031
|
validateRequest({ body: schemas.executeSwap }),
|
|
318
1032
|
async (req, res) => {
|
|
319
|
-
const { intentId, quoteId,
|
|
1033
|
+
const { intentId, quoteId, inputAmount } = req.body;
|
|
1034
|
+
if (!MOCK_MODE_ENABLED) {
|
|
1035
|
+
return res.status(503).json({
|
|
1036
|
+
success: false,
|
|
1037
|
+
error: {
|
|
1038
|
+
code: "SWAP_SERVICE_UNAVAILABLE",
|
|
1039
|
+
message: "Real swap executor not configured. This API is not ready for production use.",
|
|
1040
|
+
details: {
|
|
1041
|
+
hint: "Configure SWAP_EXECUTOR_URL or use development mode"
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
320
1046
|
const swapId = `swap-${Date.now()}`;
|
|
1047
|
+
const actualInputAmount = inputAmount || "0";
|
|
1048
|
+
if (!inputAmount) {
|
|
1049
|
+
logger.warn({ swapId, quoteId, intentId }, "Swap created without inputAmount - using 0");
|
|
1050
|
+
}
|
|
321
1051
|
const swap = {
|
|
322
1052
|
id: swapId,
|
|
323
1053
|
status: "pending",
|
|
324
|
-
inputAmount:
|
|
325
|
-
// Mock value
|
|
1054
|
+
inputAmount: actualInputAmount,
|
|
326
1055
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
327
1056
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
328
1057
|
};
|
|
329
|
-
|
|
1058
|
+
swapStore.set(swapId, swap);
|
|
1059
|
+
logger.warn({
|
|
1060
|
+
swapId,
|
|
1061
|
+
quoteId,
|
|
1062
|
+
intentId,
|
|
1063
|
+
inputAmount: actualInputAmount
|
|
1064
|
+
}, "Creating MOCK swap - not for production use");
|
|
330
1065
|
const response = {
|
|
331
1066
|
success: true,
|
|
332
1067
|
data: {
|
|
333
1068
|
swapId,
|
|
334
1069
|
status: "pending",
|
|
335
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1070
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1071
|
+
_warning: "MOCK_DATA: This swap is simulated. No real transaction will be executed."
|
|
336
1072
|
}
|
|
337
1073
|
};
|
|
338
1074
|
res.json(response);
|
|
@@ -343,13 +1079,14 @@ router5.get(
|
|
|
343
1079
|
validateRequest({ params: schemas.swapStatus }),
|
|
344
1080
|
async (req, res) => {
|
|
345
1081
|
const { id } = req.params;
|
|
346
|
-
const
|
|
1082
|
+
const swapId = Array.isArray(id) ? id[0] : id;
|
|
1083
|
+
const swap = swapStore.get(swapId);
|
|
347
1084
|
if (!swap) {
|
|
348
1085
|
return res.status(404).json({
|
|
349
1086
|
success: false,
|
|
350
1087
|
error: {
|
|
351
1088
|
code: "SWAP_NOT_FOUND",
|
|
352
|
-
message: `Swap ${
|
|
1089
|
+
message: `Swap ${swapId} not found`
|
|
353
1090
|
}
|
|
354
1091
|
});
|
|
355
1092
|
}
|
|
@@ -362,17 +1099,299 @@ router5.get(
|
|
|
362
1099
|
);
|
|
363
1100
|
var swap_default = router5;
|
|
364
1101
|
|
|
365
|
-
// src/routes/
|
|
1102
|
+
// src/routes/webhook.ts
|
|
1103
|
+
var import_express6 = require("express");
|
|
1104
|
+
var import_zod2 = require("zod");
|
|
1105
|
+
|
|
1106
|
+
// src/services/helius-listener.ts
|
|
1107
|
+
var import_sdk6 = require("@sip-protocol/sdk");
|
|
1108
|
+
|
|
1109
|
+
// src/services/webhook-delivery.ts
|
|
1110
|
+
var import_hmac = require("@noble/hashes/hmac");
|
|
1111
|
+
var import_sha256 = require("@noble/hashes/sha256");
|
|
1112
|
+
var import_utils3 = require("@noble/hashes/utils");
|
|
1113
|
+
function computeHmacSignature(secret, body) {
|
|
1114
|
+
const encoder = new TextEncoder();
|
|
1115
|
+
const sig = (0, import_utils3.bytesToHex)((0, import_hmac.hmac)(import_sha256.sha256, encoder.encode(secret), encoder.encode(body)));
|
|
1116
|
+
return `sha256=${sig}`;
|
|
1117
|
+
}
|
|
1118
|
+
function sleep(ms) {
|
|
1119
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1120
|
+
}
|
|
1121
|
+
var WebhookDeliveryService = class {
|
|
1122
|
+
maxRetries;
|
|
1123
|
+
pendingDeliveries = /* @__PURE__ */ new Set();
|
|
1124
|
+
constructor(maxRetries) {
|
|
1125
|
+
this.maxRetries = maxRetries ?? env.WEBHOOK_DELIVERY_MAX_RETRIES;
|
|
1126
|
+
}
|
|
1127
|
+
/**
|
|
1128
|
+
* Build the delivery payload from a scan result
|
|
1129
|
+
*/
|
|
1130
|
+
buildPayload(registration, payment) {
|
|
1131
|
+
return {
|
|
1132
|
+
event: "payment.received",
|
|
1133
|
+
webhookId: registration.id,
|
|
1134
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1135
|
+
data: {
|
|
1136
|
+
txSignature: payment.txSignature,
|
|
1137
|
+
stealthAddress: payment.stealthAddress,
|
|
1138
|
+
ephemeralPublicKey: payment.ephemeralPublicKey,
|
|
1139
|
+
amount: payment.amount.toString(),
|
|
1140
|
+
mint: payment.mint,
|
|
1141
|
+
tokenSymbol: payment.tokenSymbol,
|
|
1142
|
+
slot: payment.slot,
|
|
1143
|
+
blockTime: payment.timestamp
|
|
1144
|
+
}
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
/**
|
|
1148
|
+
* Deliver a payment notification to a registered webhook
|
|
1149
|
+
*
|
|
1150
|
+
* Retries with exponential backoff on failure.
|
|
1151
|
+
*/
|
|
1152
|
+
async deliver(registration, payment) {
|
|
1153
|
+
const payload = this.buildPayload(registration, payment);
|
|
1154
|
+
const body = JSON.stringify(payload);
|
|
1155
|
+
const signature = computeHmacSignature(registration.secret, body);
|
|
1156
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
1157
|
+
try {
|
|
1158
|
+
const response = await fetch(registration.url, {
|
|
1159
|
+
method: "POST",
|
|
1160
|
+
headers: {
|
|
1161
|
+
"Content-Type": "application/json",
|
|
1162
|
+
"X-SIP-Signature": signature,
|
|
1163
|
+
"X-SIP-Webhook-Id": registration.id
|
|
1164
|
+
},
|
|
1165
|
+
body,
|
|
1166
|
+
signal: AbortSignal.timeout(1e4)
|
|
1167
|
+
});
|
|
1168
|
+
if (response.ok) {
|
|
1169
|
+
logger.info(
|
|
1170
|
+
{ webhookId: registration.id, attempt, status: response.status },
|
|
1171
|
+
"Webhook delivered"
|
|
1172
|
+
);
|
|
1173
|
+
return true;
|
|
1174
|
+
}
|
|
1175
|
+
logger.warn(
|
|
1176
|
+
{ webhookId: registration.id, attempt, status: response.status },
|
|
1177
|
+
"Webhook delivery failed (non-2xx)"
|
|
1178
|
+
);
|
|
1179
|
+
} catch (error) {
|
|
1180
|
+
logger.warn(
|
|
1181
|
+
{ webhookId: registration.id, attempt, error: error.message },
|
|
1182
|
+
"Webhook delivery error"
|
|
1183
|
+
);
|
|
1184
|
+
}
|
|
1185
|
+
if (attempt < this.maxRetries) {
|
|
1186
|
+
const backoffMs = Math.pow(4, attempt) * 1e3;
|
|
1187
|
+
await sleep(backoffMs);
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
logger.error(
|
|
1191
|
+
{ webhookId: registration.id, maxRetries: this.maxRetries },
|
|
1192
|
+
"Webhook delivery exhausted all retries"
|
|
1193
|
+
);
|
|
1194
|
+
return false;
|
|
1195
|
+
}
|
|
1196
|
+
/**
|
|
1197
|
+
* Queue a delivery (fire-and-forget with tracking)
|
|
1198
|
+
*/
|
|
1199
|
+
queueDelivery(registration, payment) {
|
|
1200
|
+
const promise = this.deliver(registration, payment).then(() => {
|
|
1201
|
+
}).catch((err) => {
|
|
1202
|
+
logger.error({ webhookId: registration.id, err }, "Unhandled webhook delivery error");
|
|
1203
|
+
}).finally(() => {
|
|
1204
|
+
this.pendingDeliveries.delete(promise);
|
|
1205
|
+
});
|
|
1206
|
+
this.pendingDeliveries.add(promise);
|
|
1207
|
+
}
|
|
1208
|
+
/**
|
|
1209
|
+
* Wait for all pending deliveries to complete (graceful shutdown)
|
|
1210
|
+
*/
|
|
1211
|
+
async drainPending() {
|
|
1212
|
+
if (this.pendingDeliveries.size > 0) {
|
|
1213
|
+
logger.info({ pending: this.pendingDeliveries.size }, "Draining pending webhook deliveries");
|
|
1214
|
+
await Promise.allSettled(this.pendingDeliveries);
|
|
1215
|
+
logger.info("All webhook deliveries drained");
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
};
|
|
1219
|
+
var webhookDeliveryService = new WebhookDeliveryService();
|
|
1220
|
+
|
|
1221
|
+
// src/services/helius-listener.ts
|
|
1222
|
+
var HeliusListenerService = class {
|
|
1223
|
+
/**
|
|
1224
|
+
* Process an incoming Helius webhook payload
|
|
1225
|
+
*
|
|
1226
|
+
* 1. Verify Helius signature (if configured)
|
|
1227
|
+
* 2. For each active registration, check if payment is for them
|
|
1228
|
+
* 3. On match, queue delivery to the agent's URL
|
|
1229
|
+
*
|
|
1230
|
+
* @returns Number of matched deliveries queued
|
|
1231
|
+
*/
|
|
1232
|
+
async processIncoming(payload, headers) {
|
|
1233
|
+
if (env.HELIUS_WEBHOOK_SECRET && headers.rawBody) {
|
|
1234
|
+
const valid = (0, import_sdk6.verifyWebhookSignature)(
|
|
1235
|
+
headers.rawBody,
|
|
1236
|
+
headers.signature,
|
|
1237
|
+
env.HELIUS_WEBHOOK_SECRET
|
|
1238
|
+
);
|
|
1239
|
+
if (!valid) {
|
|
1240
|
+
logger.warn("Helius webhook signature verification failed");
|
|
1241
|
+
throw new Error("HELIUS_SIGNATURE_INVALID");
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
const transactions = Array.isArray(payload) ? payload : [payload];
|
|
1245
|
+
const registrations = webhookStore.getAll();
|
|
1246
|
+
if (registrations.length === 0) {
|
|
1247
|
+
logger.debug("No active webhook registrations, skipping processing");
|
|
1248
|
+
return 0;
|
|
1249
|
+
}
|
|
1250
|
+
let matchCount = 0;
|
|
1251
|
+
for (const tx of transactions) {
|
|
1252
|
+
for (const registration of registrations) {
|
|
1253
|
+
try {
|
|
1254
|
+
const payment = await (0, import_sdk6.processWebhookTransaction)(
|
|
1255
|
+
tx,
|
|
1256
|
+
registration.viewingPrivateKey,
|
|
1257
|
+
registration.spendingPublicKey
|
|
1258
|
+
);
|
|
1259
|
+
if (payment) {
|
|
1260
|
+
matchCount++;
|
|
1261
|
+
webhookDeliveryService.queueDelivery(registration, payment);
|
|
1262
|
+
logger.info(
|
|
1263
|
+
{
|
|
1264
|
+
webhookId: registration.id,
|
|
1265
|
+
txSignature: payment.txSignature
|
|
1266
|
+
},
|
|
1267
|
+
"Payment matched, delivery queued"
|
|
1268
|
+
);
|
|
1269
|
+
}
|
|
1270
|
+
} catch (error) {
|
|
1271
|
+
logger.warn(
|
|
1272
|
+
{
|
|
1273
|
+
webhookId: registration.id,
|
|
1274
|
+
error: error.message
|
|
1275
|
+
},
|
|
1276
|
+
"Error checking transaction against registration"
|
|
1277
|
+
);
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
logger.info(
|
|
1282
|
+
{
|
|
1283
|
+
transactions: transactions.length,
|
|
1284
|
+
registrations: registrations.length,
|
|
1285
|
+
matches: matchCount
|
|
1286
|
+
},
|
|
1287
|
+
"Helius webhook processed"
|
|
1288
|
+
);
|
|
1289
|
+
return matchCount;
|
|
1290
|
+
}
|
|
1291
|
+
};
|
|
1292
|
+
var heliusListenerService = new HeliusListenerService();
|
|
1293
|
+
|
|
1294
|
+
// src/routes/webhook.ts
|
|
366
1295
|
var router6 = (0, import_express6.Router)();
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
1296
|
+
var webhookSchemas = {
|
|
1297
|
+
register: import_zod2.z.object({
|
|
1298
|
+
url: import_zod2.z.string().url("Must be a valid URL"),
|
|
1299
|
+
viewingPrivateKey: import_zod2.z.string().regex(/^0x[0-9a-fA-F]{64}$/, "Must be 0x-prefixed 32-byte hex"),
|
|
1300
|
+
spendingPublicKey: import_zod2.z.string().regex(/^0x[0-9a-fA-F]{64}$/, "Must be 0x-prefixed 32-byte hex")
|
|
1301
|
+
}),
|
|
1302
|
+
unregister: import_zod2.z.object({
|
|
1303
|
+
id: import_zod2.z.string().min(1)
|
|
1304
|
+
})
|
|
1305
|
+
};
|
|
1306
|
+
router6.post(
|
|
1307
|
+
"/register",
|
|
1308
|
+
validateRequest({ body: webhookSchemas.register }),
|
|
1309
|
+
(req, res) => {
|
|
1310
|
+
const { url, viewingPrivateKey, spendingPublicKey } = req.body;
|
|
1311
|
+
try {
|
|
1312
|
+
const registration = webhookStore.register(url, viewingPrivateKey, spendingPublicKey);
|
|
1313
|
+
const response = {
|
|
1314
|
+
success: true,
|
|
1315
|
+
data: {
|
|
1316
|
+
id: registration.id,
|
|
1317
|
+
url: registration.url,
|
|
1318
|
+
secret: registration.secret,
|
|
1319
|
+
createdAt: registration.createdAt
|
|
1320
|
+
}
|
|
1321
|
+
};
|
|
1322
|
+
res.status(201).json(response);
|
|
1323
|
+
} catch (error) {
|
|
1324
|
+
if (error.message === "WEBHOOK_STORE_FULL") {
|
|
1325
|
+
res.status(503).json({
|
|
1326
|
+
success: false,
|
|
1327
|
+
error: {
|
|
1328
|
+
code: "WEBHOOK_STORE_FULL",
|
|
1329
|
+
message: "Maximum webhook registrations reached"
|
|
1330
|
+
}
|
|
1331
|
+
});
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1334
|
+
throw error;
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
);
|
|
1338
|
+
router6.delete(
|
|
1339
|
+
"/:id",
|
|
1340
|
+
validateRequest({ params: webhookSchemas.unregister }),
|
|
1341
|
+
(req, res) => {
|
|
1342
|
+
const removed = webhookStore.unregister(req.params.id);
|
|
1343
|
+
if (!removed) {
|
|
1344
|
+
res.status(404).json({
|
|
1345
|
+
success: false,
|
|
1346
|
+
error: {
|
|
1347
|
+
code: "WEBHOOK_NOT_FOUND",
|
|
1348
|
+
message: "Webhook not found"
|
|
1349
|
+
}
|
|
1350
|
+
});
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1353
|
+
res.status(204).send();
|
|
1354
|
+
}
|
|
1355
|
+
);
|
|
1356
|
+
router6.get("/", (_req, res) => {
|
|
1357
|
+
const webhooks = webhookStore.getAllForList();
|
|
1358
|
+
const response = {
|
|
1359
|
+
success: true,
|
|
1360
|
+
data: { webhooks }
|
|
1361
|
+
};
|
|
1362
|
+
res.json(response);
|
|
1363
|
+
});
|
|
1364
|
+
router6.post("/internal/helius", async (req, res) => {
|
|
1365
|
+
const headers = {
|
|
1366
|
+
signature: req.headers["x-helius-signature"],
|
|
1367
|
+
rawBody: typeof req.body === "string" ? req.body : JSON.stringify(req.body)
|
|
1368
|
+
};
|
|
1369
|
+
try {
|
|
1370
|
+
const matchCount = await heliusListenerService.processIncoming(req.body, headers);
|
|
1371
|
+
res.status(200).json({ success: true, matched: matchCount });
|
|
1372
|
+
} catch (error) {
|
|
1373
|
+
if (error.message === "HELIUS_SIGNATURE_INVALID") {
|
|
1374
|
+
res.status(401).json({
|
|
1375
|
+
success: false,
|
|
1376
|
+
error: {
|
|
1377
|
+
code: "HELIUS_SIGNATURE_INVALID",
|
|
1378
|
+
message: "Invalid Helius webhook signature"
|
|
1379
|
+
}
|
|
1380
|
+
});
|
|
1381
|
+
return;
|
|
1382
|
+
}
|
|
1383
|
+
throw error;
|
|
1384
|
+
}
|
|
1385
|
+
});
|
|
1386
|
+
var webhook_default = router6;
|
|
375
1387
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
1388
|
+
// src/routes/index.ts
|
|
1389
|
+
var router7 = (0, import_express7.Router)();
|
|
1390
|
+
router7.use("/health", health_default);
|
|
1391
|
+
router7.use("/stealth", stealth_default);
|
|
1392
|
+
router7.use("/commitment", commitment_default);
|
|
1393
|
+
router7.use("/proof", proof_default);
|
|
1394
|
+
router7.use("/quote", swap_default);
|
|
1395
|
+
router7.use("/swap", swap_default);
|
|
1396
|
+
router7.use("/webhooks", webhook_default);
|
|
1397
|
+
var routes_default = router7;
|