@zebpay_rajesh/zebpay-mcp-server 0.1.0

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.

Potentially problematic release.


This version of @zebpay_rajesh/zebpay-mcp-server might be problematic. Click here for more details.

Files changed (120) hide show
  1. package/.env.example +14 -0
  2. package/README.md +223 -0
  3. package/dist/__tests__/errors.test.d.ts +5 -0
  4. package/dist/__tests__/errors.test.js +147 -0
  5. package/dist/__tests__/errors.test.js.map +1 -0
  6. package/dist/__tests__/prompts.test.d.ts +1 -0
  7. package/dist/__tests__/prompts.test.js +73 -0
  8. package/dist/__tests__/prompts.test.js.map +1 -0
  9. package/dist/__tests__/resources.test.d.ts +1 -0
  10. package/dist/__tests__/resources.test.js +79 -0
  11. package/dist/__tests__/resources.test.js.map +1 -0
  12. package/dist/__tests__/validation.test.d.ts +15 -0
  13. package/dist/__tests__/validation.test.js +64 -0
  14. package/dist/__tests__/validation.test.js.map +1 -0
  15. package/dist/config.d.ts +19 -0
  16. package/dist/config.js +81 -0
  17. package/dist/config.js.map +1 -0
  18. package/dist/http/httpClient.d.ts +40 -0
  19. package/dist/http/httpClient.js +341 -0
  20. package/dist/http/httpClient.js.map +1 -0
  21. package/dist/index.d.ts +1 -0
  22. package/dist/index.js +60 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/mcp/errors.d.ts +21 -0
  25. package/dist/mcp/errors.js +214 -0
  26. package/dist/mcp/errors.js.map +1 -0
  27. package/dist/mcp/logging.d.ts +21 -0
  28. package/dist/mcp/logging.js +241 -0
  29. package/dist/mcp/logging.js.map +1 -0
  30. package/dist/mcp/prompts.d.ts +9 -0
  31. package/dist/mcp/prompts.js +165 -0
  32. package/dist/mcp/prompts.js.map +1 -0
  33. package/dist/mcp/resources.d.ts +9 -0
  34. package/dist/mcp/resources.js +125 -0
  35. package/dist/mcp/resources.js.map +1 -0
  36. package/dist/mcp/tools_futures.d.ts +5 -0
  37. package/dist/mcp/tools_futures.js +694 -0
  38. package/dist/mcp/tools_futures.js.map +1 -0
  39. package/dist/mcp/tools_spot.d.ts +11 -0
  40. package/dist/mcp/tools_spot.js +2225 -0
  41. package/dist/mcp/tools_spot.js.map +1 -0
  42. package/dist/private/FuturesClient.d.ts +57 -0
  43. package/dist/private/FuturesClient.js +181 -0
  44. package/dist/private/FuturesClient.js.map +1 -0
  45. package/dist/private/SpotClient.d.ts +44 -0
  46. package/dist/private/SpotClient.js +201 -0
  47. package/dist/private/SpotClient.js.map +1 -0
  48. package/dist/private/ZebpayAPI.d.ts +19 -0
  49. package/dist/private/ZebpayAPI.js +172 -0
  50. package/dist/private/ZebpayAPI.js.map +1 -0
  51. package/dist/public/PublicClient.d.ts +79 -0
  52. package/dist/public/PublicClient.js +283 -0
  53. package/dist/public/PublicClient.js.map +1 -0
  54. package/dist/public/PublicFuturesClient.d.ts +27 -0
  55. package/dist/public/PublicFuturesClient.js +187 -0
  56. package/dist/public/PublicFuturesClient.js.map +1 -0
  57. package/dist/security/credentials.d.ts +42 -0
  58. package/dist/security/credentials.js +80 -0
  59. package/dist/security/credentials.js.map +1 -0
  60. package/dist/security/signing.d.ts +33 -0
  61. package/dist/security/signing.js +56 -0
  62. package/dist/security/signing.js.map +1 -0
  63. package/dist/types/responses.d.ts +130 -0
  64. package/dist/types/responses.js +6 -0
  65. package/dist/types/responses.js.map +1 -0
  66. package/dist/utils/cache.d.ts +29 -0
  67. package/dist/utils/cache.js +72 -0
  68. package/dist/utils/cache.js.map +1 -0
  69. package/dist/utils/fileLogger.d.ts +10 -0
  70. package/dist/utils/fileLogger.js +81 -0
  71. package/dist/utils/fileLogger.js.map +1 -0
  72. package/dist/utils/metrics.d.ts +35 -0
  73. package/dist/utils/metrics.js +94 -0
  74. package/dist/utils/metrics.js.map +1 -0
  75. package/dist/utils/responseFormatter.d.ts +93 -0
  76. package/dist/utils/responseFormatter.js +268 -0
  77. package/dist/utils/responseFormatter.js.map +1 -0
  78. package/dist/validation/schemas.d.ts +70 -0
  79. package/dist/validation/schemas.js +48 -0
  80. package/dist/validation/schemas.js.map +1 -0
  81. package/dist/validation/validators.d.ts +28 -0
  82. package/dist/validation/validators.js +129 -0
  83. package/dist/validation/validators.js.map +1 -0
  84. package/docs/LOGGING.md +371 -0
  85. package/docs/zebpay-ai-trading-beginner.png +0 -0
  86. package/mcp-config.json.example +20 -0
  87. package/package.json +54 -0
  88. package/scripts/README.md +103 -0
  89. package/scripts/clear-logs.js +52 -0
  90. package/scripts/log-stats.js +264 -0
  91. package/scripts/log-viewer.js +288 -0
  92. package/server.json +31 -0
  93. package/src/__tests__/errors.test.ts +180 -0
  94. package/src/__tests__/prompts.test.ts +89 -0
  95. package/src/__tests__/resources.test.ts +95 -0
  96. package/src/__tests__/validation.test.ts +88 -0
  97. package/src/config.ts +108 -0
  98. package/src/http/httpClient.ts +398 -0
  99. package/src/index.ts +71 -0
  100. package/src/mcp/errors.ts +262 -0
  101. package/src/mcp/logging.ts +284 -0
  102. package/src/mcp/prompts.ts +206 -0
  103. package/src/mcp/resources.ts +163 -0
  104. package/src/mcp/tools_futures.ts +874 -0
  105. package/src/mcp/tools_spot.ts +2702 -0
  106. package/src/private/FuturesClient.ts +189 -0
  107. package/src/private/SpotClient.ts +250 -0
  108. package/src/private/ZebpayAPI.ts +205 -0
  109. package/src/public/PublicClient.ts +381 -0
  110. package/src/public/PublicFuturesClient.ts +228 -0
  111. package/src/security/credentials.ts +114 -0
  112. package/src/security/signing.ts +98 -0
  113. package/src/types/responses.ts +146 -0
  114. package/src/utils/cache.ts +90 -0
  115. package/src/utils/fileLogger.ts +88 -0
  116. package/src/utils/metrics.ts +135 -0
  117. package/src/utils/responseFormatter.ts +361 -0
  118. package/src/validation/schemas.ts +66 -0
  119. package/src/validation/validators.ts +189 -0
  120. package/tsconfig.json +21 -0
@@ -0,0 +1,361 @@
1
+ /*
2
+ Response formatting utilities for better LLM parsing and understanding.
3
+ */
4
+
5
+ import { randomUUID } from "node:crypto";
6
+
7
+ /**
8
+ * Generates a correlation ID for request tracing
9
+ */
10
+ export function generateCorrelationId(): string {
11
+ return randomUUID();
12
+ }
13
+
14
+ /**
15
+ * Formats API response for better LLM understanding
16
+ */
17
+ export function formatResponse(
18
+ data: unknown,
19
+ metadata?: {
20
+ toolName?: string;
21
+ correlationId?: string;
22
+ executionTimeMs?: number;
23
+ timestamp?: string;
24
+ }
25
+ ): {
26
+ content: Array<{ type: "text"; text: string }>;
27
+ } {
28
+ const timestamp = metadata?.timestamp || new Date().toISOString();
29
+ const response: Record<string, unknown> = {
30
+ data,
31
+ ...(metadata?.correlationId && { correlationId: metadata.correlationId }),
32
+ ...(metadata?.executionTimeMs !== undefined && { executionTimeMs: metadata.executionTimeMs }),
33
+ timestamp,
34
+ };
35
+
36
+ // Format response text with metadata
37
+ let text = JSON.stringify(data, null, 2);
38
+
39
+ // Add metadata as comments for LLM context
40
+ if (metadata?.correlationId || metadata?.executionTimeMs !== undefined) {
41
+ const metadataLines: string[] = [];
42
+ if (metadata.correlationId) {
43
+ metadataLines.push(`// Request ID: ${metadata.correlationId}`);
44
+ }
45
+ if (metadata.executionTimeMs !== undefined) {
46
+ metadataLines.push(`// Execution time: ${metadata.executionTimeMs}ms`);
47
+ }
48
+ if (metadata.toolName) {
49
+ metadataLines.push(`// Tool: ${metadata.toolName}`);
50
+ }
51
+ text = metadataLines.join("\n") + "\n" + text;
52
+ }
53
+
54
+ return {
55
+ content: [
56
+ {
57
+ type: "text",
58
+ text,
59
+ },
60
+ ],
61
+ };
62
+ }
63
+
64
+ /**
65
+ * Formats balance response for better readability
66
+ */
67
+ export function formatBalanceResponse(
68
+ data: unknown,
69
+ metadata?: {
70
+ correlationId?: string;
71
+ executionTimeMs?: number;
72
+ }
73
+ ): {
74
+ content: Array<{ type: "text"; text: string }>;
75
+ } {
76
+ const response = formatResponse(data, metadata);
77
+
78
+ // If data is a balance response, add a summary
79
+ if (data && typeof data === "object") {
80
+ const obj = data as Record<string, unknown>;
81
+ if (obj.balances || obj.currencies) {
82
+ let summary = "## Balance Summary\n\n";
83
+
84
+ if (Array.isArray(obj.balances)) {
85
+ summary += "Available balances:\n";
86
+ for (const balance of obj.balances) {
87
+ if (balance && typeof balance === "object") {
88
+ const bal = balance as Record<string, unknown>;
89
+ const currency = bal.currency || bal.asset || "Unknown";
90
+ const available = bal.available || bal.free || "0";
91
+ const locked = bal.locked || "0";
92
+ summary += `- ${currency}: ${available} available${locked !== "0" ? `, ${locked} locked` : ""}\n`;
93
+ }
94
+ }
95
+ } else if (obj.currencies && typeof obj.currencies === "object") {
96
+ summary += "Available balances:\n";
97
+ for (const [currency, balance] of Object.entries(obj.currencies)) {
98
+ if (balance && typeof balance === "object") {
99
+ const bal = balance as Record<string, unknown>;
100
+ const available = bal.available || bal.free || "0";
101
+ const locked = bal.locked || "0";
102
+ summary += `- ${currency}: ${available} available${locked !== "0" ? `, ${locked} locked` : ""}\n`;
103
+ }
104
+ }
105
+ }
106
+
107
+ summary += "\n---\n\n";
108
+ response.content[0].text = summary + response.content[0].text;
109
+ }
110
+ }
111
+
112
+ return response;
113
+ }
114
+
115
+ /**
116
+ * Formats ticker response for better readability
117
+ */
118
+ export function formatTickerResponse(
119
+ data: unknown,
120
+ metadata?: {
121
+ correlationId?: string;
122
+ executionTimeMs?: number;
123
+ }
124
+ ): {
125
+ content: Array<{ type: "text"; text: string }>;
126
+ } {
127
+ const response = formatResponse(data, metadata);
128
+
129
+ // If data is a ticker response, add a summary
130
+ if (data && typeof data === "object") {
131
+ const obj = data as Record<string, unknown>;
132
+ if (obj.lastPrice || obj.price) {
133
+ let summary = "## Price Information\n\n";
134
+ const symbol = obj.symbol || "Unknown";
135
+ const price = obj.lastPrice || obj.price || "N/A";
136
+ const change24h = obj.priceChange24h || obj.change24h;
137
+ const changePercent24h = obj.priceChangePercent24h || obj.changePercent24h;
138
+
139
+ summary += `**Symbol:** ${symbol}\n`;
140
+ summary += `**Current Price:** ${price}\n`;
141
+ if (change24h) {
142
+ summary += `**24h Change:** ${change24h}`;
143
+ if (changePercent24h) {
144
+ summary += ` (${changePercent24h}%)`;
145
+ }
146
+ summary += "\n";
147
+ }
148
+ if (obj.volume24h) {
149
+ summary += `**24h Volume:** ${obj.volume24h}\n`;
150
+ }
151
+
152
+ summary += "\n---\n\n";
153
+ response.content[0].text = summary + response.content[0].text;
154
+ }
155
+ }
156
+
157
+ return response;
158
+ }
159
+
160
+ /**
161
+ * Formats order response for better readability
162
+ */
163
+ export function formatOrderResponse(
164
+ data: unknown,
165
+ metadata?: {
166
+ correlationId?: string;
167
+ executionTimeMs?: number;
168
+ }
169
+ ): {
170
+ content: Array<{ type: "text"; text: string }>;
171
+ } {
172
+ const response = formatResponse(data, metadata);
173
+
174
+ // If data is an order response, add a summary
175
+ if (data && typeof data === "object") {
176
+ const obj = data as Record<string, unknown>;
177
+ if (obj.orderId || obj.clientOrderId || obj.symbol) {
178
+ let summary = "## Order Status\n\n";
179
+
180
+ if (obj.orderId) summary += `**Order ID:** ${obj.orderId}\n`;
181
+ if (obj.clientOrderId) summary += `**Client Order ID:** ${obj.clientOrderId}\n`;
182
+ if (obj.symbol) summary += `**Symbol:** ${obj.symbol}\n`;
183
+ if (obj.side) summary += `**Side:** ${obj.side}\n`;
184
+ if (obj.status) summary += `**Status:** ${obj.status}\n`;
185
+ if (obj.executedPrice) summary += `**Executed Price:** ${obj.executedPrice}\n`;
186
+ if (obj.executedQuantity) summary += `**Executed Quantity:** ${obj.executedQuantity}\n`;
187
+
188
+ summary += "\n---\n\n";
189
+ response.content[0].text = summary + response.content[0].text;
190
+ }
191
+ }
192
+
193
+ return response;
194
+ }
195
+
196
+ /**
197
+ * Formats order book response for better readability
198
+ */
199
+ export function formatOrderBookResponse(
200
+ data: unknown,
201
+ metadata?: {
202
+ toolName?: string;
203
+ correlationId?: string;
204
+ executionTimeMs?: number;
205
+ }
206
+ ): {
207
+ content: Array<{ type: "text"; text: string }>;
208
+ } {
209
+ const response = formatResponse(data, metadata);
210
+
211
+ if (data && typeof data === "object") {
212
+ const obj = data as Record<string, unknown>;
213
+ if (obj.bids || obj.asks || obj.symbol) {
214
+ let summary = "## Order Book Summary\n\n";
215
+
216
+ if (obj.symbol) summary += `**Symbol:** ${obj.symbol}\n`;
217
+
218
+ if (Array.isArray(obj.bids) && obj.bids.length > 0) {
219
+ const bestBid = obj.bids[0];
220
+ if (bestBid && typeof bestBid === "object") {
221
+ const bid = bestBid as Record<string, unknown>;
222
+ summary += `**Best Bid:** ${bid[0] || bid.price || "N/A"} (Quantity: ${bid[1] || bid.quantity || "N/A"})\n`;
223
+ }
224
+ summary += `**Total Bids:** ${obj.bids.length} price levels\n`;
225
+ }
226
+
227
+ if (Array.isArray(obj.asks) && obj.asks.length > 0) {
228
+ const bestAsk = obj.asks[0];
229
+ if (bestAsk && typeof bestAsk === "object") {
230
+ const ask = bestAsk as Record<string, unknown>;
231
+ summary += `**Best Ask:** ${ask[0] || ask.price || "N/A"} (Quantity: ${ask[1] || ask.quantity || "N/A"})\n`;
232
+ }
233
+ summary += `**Total Asks:** ${obj.asks.length} price levels\n`;
234
+ }
235
+
236
+ if (obj.spread) {
237
+ summary += `**Spread:** ${obj.spread}\n`;
238
+ }
239
+
240
+ summary += "\n---\n\n";
241
+ response.content[0].text = summary + response.content[0].text;
242
+ }
243
+ }
244
+
245
+ return response;
246
+ }
247
+
248
+ /**
249
+ * Formats trades response for better readability
250
+ */
251
+ export function formatTradesResponse(
252
+ data: unknown,
253
+ metadata?: {
254
+ toolName?: string;
255
+ correlationId?: string;
256
+ executionTimeMs?: number;
257
+ }
258
+ ): {
259
+ content: Array<{ type: "text"; text: string }>;
260
+ } {
261
+ const response = formatResponse(data, metadata);
262
+
263
+ if (data && typeof data === "object") {
264
+ const obj = data as Record<string, unknown>;
265
+ if (Array.isArray(obj.trades) || Array.isArray(obj.data)) {
266
+ const trades = (obj.trades || obj.data) as unknown[];
267
+ let summary = "## Recent Trades Summary\n\n";
268
+
269
+ if (obj.symbol) summary += `**Symbol:** ${obj.symbol}\n`;
270
+ summary += `**Total Trades:** ${trades.length}\n\n`;
271
+
272
+ if (trades.length > 0) {
273
+ summary += "**Recent Activity:**\n";
274
+ const displayCount = Math.min(5, trades.length);
275
+ for (let i = 0; i < displayCount; i++) {
276
+ const trade = trades[i];
277
+ if (trade && typeof trade === "object") {
278
+ const t = trade as Record<string, unknown>;
279
+ const price = t.price || t[0] || "N/A";
280
+ const quantity = t.quantity || t.qty || t[1] || "N/A";
281
+ const side = t.side || (t[2] === "buy" ? "BUY" : t[2] === "sell" ? "SELL" : "N/A");
282
+ const time = t.time || t.timestamp || t[3] || "N/A";
283
+ summary += `${i + 1}. ${side} ${quantity} @ ${price} (Time: ${time})\n`;
284
+ }
285
+ }
286
+ if (trades.length > displayCount) {
287
+ summary += `... and ${trades.length - displayCount} more trades\n`;
288
+ }
289
+ }
290
+
291
+ summary += "\n---\n\n";
292
+ response.content[0].text = summary + response.content[0].text;
293
+ }
294
+ }
295
+
296
+ return response;
297
+ }
298
+
299
+ /**
300
+ * Formats klines/candlestick response for better readability
301
+ */
302
+ export function formatKlinesResponse(
303
+ data: unknown,
304
+ metadata?: {
305
+ toolName?: string;
306
+ correlationId?: string;
307
+ executionTimeMs?: number;
308
+ }
309
+ ): {
310
+ content: Array<{ type: "text"; text: string }>;
311
+ } {
312
+ const response = formatResponse(data, metadata);
313
+
314
+ if (data && typeof data === "object") {
315
+ const obj = data as Record<string, unknown>;
316
+ if (Array.isArray(obj.klines) || Array.isArray(obj.data) || Array.isArray(obj)) {
317
+ const klines = (obj.klines || obj.data || obj) as unknown[];
318
+ let summary = "## Candlestick Data Summary\n\n";
319
+
320
+ if (obj.symbol) summary += `**Symbol:** ${obj.symbol}\n`;
321
+ if (obj.interval) summary += `**Interval:** ${obj.interval}\n`;
322
+ summary += `**Total Candles:** ${klines.length}\n\n`;
323
+
324
+ if (klines.length > 0) {
325
+ summary += "**Price Range:**\n";
326
+ // Extract OHLC from first and last candles
327
+ const firstCandle = klines[0];
328
+ const lastCandle = klines[klines.length - 1];
329
+
330
+ if (Array.isArray(firstCandle) && firstCandle.length >= 4) {
331
+ summary += `- **Open (First):** ${firstCandle[0]}\n`;
332
+ summary += `- **High (First):** ${firstCandle[1]}\n`;
333
+ summary += `- **Low (First):** ${firstCandle[2]}\n`;
334
+ summary += `- **Close (First):** ${firstCandle[3]}\n`;
335
+ }
336
+
337
+ if (Array.isArray(lastCandle) && lastCandle.length >= 4) {
338
+ summary += `- **Close (Last):** ${lastCandle[3]}\n`;
339
+ }
340
+
341
+ // Calculate price change if possible
342
+ if (Array.isArray(firstCandle) && Array.isArray(lastCandle) &&
343
+ firstCandle.length >= 4 && lastCandle.length >= 4) {
344
+ const openPrice = parseFloat(String(firstCandle[0]));
345
+ const closePrice = parseFloat(String(lastCandle[3]));
346
+ if (!isNaN(openPrice) && !isNaN(closePrice)) {
347
+ const change = closePrice - openPrice;
348
+ const changePercent = ((change / openPrice) * 100).toFixed(2);
349
+ summary += `- **Price Change:** ${change >= 0 ? "+" : ""}${change} (${changePercent}%)\n`;
350
+ }
351
+ }
352
+ }
353
+
354
+ summary += "\n---\n\n";
355
+ response.content[0].text = summary + response.content[0].text;
356
+ }
357
+ }
358
+
359
+ return response;
360
+ }
361
+
@@ -0,0 +1,66 @@
1
+ /*
2
+ Zod schemas for inputs/outputs. These validate tool params and normalize outputs.
3
+ */
4
+
5
+ import { z } from "zod";
6
+
7
+ // Shared enums
8
+ export const sideEnum = z.enum(["buy", "sell"]);
9
+ export const orderTypeEnum = z.enum(["market", "limit"]);
10
+
11
+ // Spot
12
+ export const spotPlaceOrderInput = z.object({
13
+ symbol: z.string().min(1),
14
+ side: sideEnum,
15
+ type: orderTypeEnum,
16
+ quantity: z.string().min(1),
17
+ price: z.string().optional(),
18
+ clientOrderId: z.string().optional(),
19
+ });
20
+
21
+ export const spotCancelOrderInput = z.object({
22
+ orderId: z.string().min(1),
23
+ symbol: z.string().optional(),
24
+ });
25
+
26
+ export const spotBalancesOutput = z.object({
27
+ balances: z.array(
28
+ z.object({ asset: z.string(), free: z.string(), locked: z.string() })
29
+ ),
30
+ });
31
+
32
+ // Futures
33
+ export const futuresPlaceOrderInput = z.object({
34
+ symbol: z.string().min(1),
35
+ side: sideEnum,
36
+ type: orderTypeEnum,
37
+ quantity: z.string().min(1),
38
+ price: z.string().optional(),
39
+ leverage: z.number().int().positive().max(125).optional(),
40
+ clientOrderId: z.string().optional(),
41
+ });
42
+
43
+ export const futuresCancelOrderInput = z.object({
44
+ orderId: z.string().min(1),
45
+ symbol: z.string().optional(),
46
+ });
47
+
48
+ export const futuresPositionsOutput = z.object({
49
+ positions: z.array(
50
+ z.object({
51
+ symbol: z.string(),
52
+ positionSide: z.enum(["long", "short"]).optional(),
53
+ quantity: z.string(),
54
+ entryPrice: z.string().optional(),
55
+ unrealizedPnl: z.string().optional(),
56
+ leverage: z.number().int().positive().optional(),
57
+ })
58
+ ),
59
+ });
60
+
61
+ export type SpotPlaceOrderInput = z.infer<typeof spotPlaceOrderInput>;
62
+ export type SpotCancelOrderInput = z.infer<typeof spotCancelOrderInput>;
63
+ export type FuturesPlaceOrderInput = z.infer<typeof futuresPlaceOrderInput>;
64
+ export type FuturesCancelOrderInput = z.infer<typeof futuresCancelOrderInput>;
65
+
66
+
@@ -0,0 +1,189 @@
1
+ /*
2
+ Input validation helpers with helpful error messages for LLMs.
3
+ */
4
+
5
+ import { z } from "zod";
6
+ import { createInvalidParamsError } from "../mcp/errors.js";
7
+
8
+ /**
9
+ * Validates trading pair symbol format (BASE-QUOTE)
10
+ * Examples: BTC-INR, ETH-INR, BTC-USDT
11
+ */
12
+ export function validateSymbol(symbol: string): void {
13
+ if (!symbol || typeof symbol !== "string") {
14
+ throw createInvalidParamsError(
15
+ `Invalid symbol: must be a non-empty string. Expected format: BASE-QUOTE (e.g., "BTC-INR", "ETH-INR")`
16
+ );
17
+ }
18
+
19
+ const trimmed = symbol.trim().toUpperCase();
20
+ const parts = trimmed.split("-");
21
+
22
+ if (parts.length !== 2) {
23
+ throw createInvalidParamsError(
24
+ `Invalid symbol format: "${symbol}". Expected format: BASE-QUOTE (e.g., "BTC-INR", "ETH-INR", "BTC-USDT"). Use uppercase currency codes separated by a single hyphen.`
25
+ );
26
+ }
27
+
28
+ const [base, quote] = parts;
29
+
30
+ if (!base || base.length === 0) {
31
+ throw createInvalidParamsError(
32
+ `Invalid symbol: base currency is missing. Expected format: BASE-QUOTE (e.g., "BTC-INR")`
33
+ );
34
+ }
35
+
36
+ if (!quote || quote.length === 0) {
37
+ throw createInvalidParamsError(
38
+ `Invalid symbol: quote currency is missing. Expected format: BASE-QUOTE (e.g., "BTC-INR")`
39
+ );
40
+ }
41
+
42
+ // Check for valid currency code format (alphanumeric, typically 2-10 characters)
43
+ const currencyCodeRegex = /^[A-Z0-9]{2,10}$/;
44
+ if (!currencyCodeRegex.test(base)) {
45
+ throw createInvalidParamsError(
46
+ `Invalid base currency code: "${base}". Currency codes should be 2-10 uppercase alphanumeric characters (e.g., "BTC", "ETH", "USDT")`
47
+ );
48
+ }
49
+ if (!currencyCodeRegex.test(quote)) {
50
+ throw createInvalidParamsError(
51
+ `Invalid quote currency code: "${quote}". Currency codes should be 2-10 uppercase alphanumeric characters (e.g., "INR", "USDT", "BTC")`
52
+ );
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Validates quantity string format
58
+ */
59
+ export function validateQuantity(quantity: string): void {
60
+ if (!quantity || typeof quantity !== "string") {
61
+ throw createInvalidParamsError(
62
+ `Invalid quantity: must be a non-empty string. This prevents floating-point precision issues. Example: "0.001" or "100"`
63
+ );
64
+ }
65
+
66
+ const trimmed = quantity.trim();
67
+ if (trimmed.length === 0) {
68
+ throw createInvalidParamsError(
69
+ `Invalid quantity: cannot be empty. Provide a positive number as a string (e.g., "0.001", "100", "0.5")`
70
+ );
71
+ }
72
+
73
+ // Check if it's a valid number format
74
+ const numberRegex = /^-?\d+(\.\d+)?$/;
75
+ if (!numberRegex.test(trimmed)) {
76
+ throw createInvalidParamsError(
77
+ `Invalid quantity format: "${quantity}". Expected a positive number as a string. Examples: "0.001", "100", "0.5". Do not include commas or other formatting.`
78
+ );
79
+ }
80
+
81
+ // Check if it's positive
82
+ const numValue = parseFloat(trimmed);
83
+ if (isNaN(numValue)) {
84
+ throw createInvalidParamsError(
85
+ `Invalid quantity: "${quantity}" is not a valid number. Provide a positive number as a string (e.g., "0.001")`
86
+ );
87
+ }
88
+
89
+ if (numValue <= 0) {
90
+ throw createInvalidParamsError(
91
+ `Invalid quantity: "${quantity}" must be greater than zero. Provide a positive number as a string (e.g., "0.001", "100")`
92
+ );
93
+ }
94
+
95
+ // Check for reasonable precision (typically up to 8 decimal places for crypto)
96
+ const decimalParts = trimmed.split(".");
97
+ if (decimalParts.length === 2 && decimalParts[1].length > 18) {
98
+ throw createInvalidParamsError(
99
+ `Invalid quantity precision: "${quantity}" has too many decimal places (max 18). Example: "0.001" or "100.123456789012345678"`
100
+ );
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Zod schema for symbol validation
106
+ */
107
+ export const symbolSchema = z.string().min(1).refine(
108
+ (val) => {
109
+ try {
110
+ validateSymbol(val);
111
+ return true;
112
+ } catch {
113
+ return false;
114
+ }
115
+ },
116
+ {
117
+ message: 'Invalid symbol format. Expected BASE-QUOTE (e.g., "BTC-INR", "ETH-INR")',
118
+ }
119
+ );
120
+
121
+ /**
122
+ * Zod schema for quantity validation
123
+ */
124
+ export const quantitySchema = z.string().min(1).refine(
125
+ (val) => {
126
+ try {
127
+ validateQuantity(val);
128
+ return true;
129
+ } catch {
130
+ return false;
131
+ }
132
+ },
133
+ {
134
+ message: 'Invalid quantity format. Expected a positive number as a string (e.g., "0.001", "100")',
135
+ }
136
+ );
137
+
138
+ /**
139
+ * Validates clientOrderId format
140
+ * Must contain only letters, numbers, dots, colons, slashes, underscores, and hyphens
141
+ * Length must be between 1-36 characters
142
+ */
143
+ export function validateClientOrderId(clientOrderId: string): void {
144
+ if (!clientOrderId || typeof clientOrderId !== "string") {
145
+ throw createInvalidParamsError(
146
+ `Invalid clientOrderId: must be a non-empty string`
147
+ );
148
+ }
149
+
150
+ const trimmed = clientOrderId.trim();
151
+
152
+ if (trimmed.length === 0) {
153
+ throw createInvalidParamsError(
154
+ `Invalid clientOrderId: cannot be empty`
155
+ );
156
+ }
157
+
158
+ if (trimmed.length > 36) {
159
+ throw createInvalidParamsError(
160
+ `Invalid clientOrderId: "${clientOrderId}" exceeds maximum length of 36 characters (current: ${trimmed.length})`
161
+ );
162
+ }
163
+
164
+ // Must contain only letters, numbers, dots, colons, slashes, underscores, and hyphens
165
+ const validFormat = /^[a-zA-Z0-9.:/_\-]+$/;
166
+ if (!validFormat.test(trimmed)) {
167
+ throw createInvalidParamsError(
168
+ `Invalid clientOrderId format: "${clientOrderId}". Must contain only letters, numbers, dots, colons, slashes, underscores, and hyphens. Examples: "my-order-123", "order:2024/01/15_001", "trade.abc-123"`
169
+ );
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Zod schema for clientOrderId validation
175
+ */
176
+ export const clientOrderIdSchema = z.string().min(1).max(36).refine(
177
+ (val) => {
178
+ try {
179
+ validateClientOrderId(val);
180
+ return true;
181
+ } catch {
182
+ return false;
183
+ }
184
+ },
185
+ {
186
+ message: 'Invalid clientOrderId format. Must be 1-36 characters and contain only letters, numbers, dots, colons, slashes, underscores, and hyphens',
187
+ }
188
+ );
189
+
package/tsconfig.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Node",
6
+ "outDir": "dist",
7
+ "rootDir": "src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "resolveJsonModule": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "lib": ["ES2020"],
14
+ "types": ["node"],
15
+ "sourceMap": true,
16
+ "declaration": true
17
+ },
18
+ "include": ["src/**/*"],
19
+ "exclude": ["node_modules", "dist"]
20
+ }
21
+