@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.
- package/.env.example +14 -0
- package/README.md +223 -0
- package/dist/__tests__/errors.test.d.ts +5 -0
- package/dist/__tests__/errors.test.js +147 -0
- package/dist/__tests__/errors.test.js.map +1 -0
- package/dist/__tests__/prompts.test.d.ts +1 -0
- package/dist/__tests__/prompts.test.js +73 -0
- package/dist/__tests__/prompts.test.js.map +1 -0
- package/dist/__tests__/resources.test.d.ts +1 -0
- package/dist/__tests__/resources.test.js +79 -0
- package/dist/__tests__/resources.test.js.map +1 -0
- package/dist/__tests__/validation.test.d.ts +15 -0
- package/dist/__tests__/validation.test.js +64 -0
- package/dist/__tests__/validation.test.js.map +1 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.js +81 -0
- package/dist/config.js.map +1 -0
- package/dist/http/httpClient.d.ts +40 -0
- package/dist/http/httpClient.js +341 -0
- package/dist/http/httpClient.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +60 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/errors.d.ts +21 -0
- package/dist/mcp/errors.js +214 -0
- package/dist/mcp/errors.js.map +1 -0
- package/dist/mcp/logging.d.ts +21 -0
- package/dist/mcp/logging.js +241 -0
- package/dist/mcp/logging.js.map +1 -0
- package/dist/mcp/prompts.d.ts +9 -0
- package/dist/mcp/prompts.js +165 -0
- package/dist/mcp/prompts.js.map +1 -0
- package/dist/mcp/resources.d.ts +9 -0
- package/dist/mcp/resources.js +125 -0
- package/dist/mcp/resources.js.map +1 -0
- package/dist/mcp/tools_futures.d.ts +5 -0
- package/dist/mcp/tools_futures.js +694 -0
- package/dist/mcp/tools_futures.js.map +1 -0
- package/dist/mcp/tools_spot.d.ts +11 -0
- package/dist/mcp/tools_spot.js +2225 -0
- package/dist/mcp/tools_spot.js.map +1 -0
- package/dist/private/FuturesClient.d.ts +57 -0
- package/dist/private/FuturesClient.js +181 -0
- package/dist/private/FuturesClient.js.map +1 -0
- package/dist/private/SpotClient.d.ts +44 -0
- package/dist/private/SpotClient.js +201 -0
- package/dist/private/SpotClient.js.map +1 -0
- package/dist/private/ZebpayAPI.d.ts +19 -0
- package/dist/private/ZebpayAPI.js +172 -0
- package/dist/private/ZebpayAPI.js.map +1 -0
- package/dist/public/PublicClient.d.ts +79 -0
- package/dist/public/PublicClient.js +283 -0
- package/dist/public/PublicClient.js.map +1 -0
- package/dist/public/PublicFuturesClient.d.ts +27 -0
- package/dist/public/PublicFuturesClient.js +187 -0
- package/dist/public/PublicFuturesClient.js.map +1 -0
- package/dist/security/credentials.d.ts +42 -0
- package/dist/security/credentials.js +80 -0
- package/dist/security/credentials.js.map +1 -0
- package/dist/security/signing.d.ts +33 -0
- package/dist/security/signing.js +56 -0
- package/dist/security/signing.js.map +1 -0
- package/dist/types/responses.d.ts +130 -0
- package/dist/types/responses.js +6 -0
- package/dist/types/responses.js.map +1 -0
- package/dist/utils/cache.d.ts +29 -0
- package/dist/utils/cache.js +72 -0
- package/dist/utils/cache.js.map +1 -0
- package/dist/utils/fileLogger.d.ts +10 -0
- package/dist/utils/fileLogger.js +81 -0
- package/dist/utils/fileLogger.js.map +1 -0
- package/dist/utils/metrics.d.ts +35 -0
- package/dist/utils/metrics.js +94 -0
- package/dist/utils/metrics.js.map +1 -0
- package/dist/utils/responseFormatter.d.ts +93 -0
- package/dist/utils/responseFormatter.js +268 -0
- package/dist/utils/responseFormatter.js.map +1 -0
- package/dist/validation/schemas.d.ts +70 -0
- package/dist/validation/schemas.js +48 -0
- package/dist/validation/schemas.js.map +1 -0
- package/dist/validation/validators.d.ts +28 -0
- package/dist/validation/validators.js +129 -0
- package/dist/validation/validators.js.map +1 -0
- package/docs/LOGGING.md +371 -0
- package/docs/zebpay-ai-trading-beginner.png +0 -0
- package/mcp-config.json.example +20 -0
- package/package.json +54 -0
- package/scripts/README.md +103 -0
- package/scripts/clear-logs.js +52 -0
- package/scripts/log-stats.js +264 -0
- package/scripts/log-viewer.js +288 -0
- package/server.json +31 -0
- package/src/__tests__/errors.test.ts +180 -0
- package/src/__tests__/prompts.test.ts +89 -0
- package/src/__tests__/resources.test.ts +95 -0
- package/src/__tests__/validation.test.ts +88 -0
- package/src/config.ts +108 -0
- package/src/http/httpClient.ts +398 -0
- package/src/index.ts +71 -0
- package/src/mcp/errors.ts +262 -0
- package/src/mcp/logging.ts +284 -0
- package/src/mcp/prompts.ts +206 -0
- package/src/mcp/resources.ts +163 -0
- package/src/mcp/tools_futures.ts +874 -0
- package/src/mcp/tools_spot.ts +2702 -0
- package/src/private/FuturesClient.ts +189 -0
- package/src/private/SpotClient.ts +250 -0
- package/src/private/ZebpayAPI.ts +205 -0
- package/src/public/PublicClient.ts +381 -0
- package/src/public/PublicFuturesClient.ts +228 -0
- package/src/security/credentials.ts +114 -0
- package/src/security/signing.ts +98 -0
- package/src/types/responses.ts +146 -0
- package/src/utils/cache.ts +90 -0
- package/src/utils/fileLogger.ts +88 -0
- package/src/utils/metrics.ts +135 -0
- package/src/utils/responseFormatter.ts +361 -0
- package/src/validation/schemas.ts +66 -0
- package/src/validation/validators.ts +189 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Credential provider abstraction. Current implementation reads from env vars.
|
|
3
|
+
Secrets are never logged directly; use redact() for any visibility.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { redact } from "../config.js";
|
|
7
|
+
|
|
8
|
+
export interface CredentialsProvider {
|
|
9
|
+
getApiKey(): string;
|
|
10
|
+
getSecretKey(): string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class EnvCredentialsProvider implements CredentialsProvider {
|
|
14
|
+
private readonly apiKeyEnv: string;
|
|
15
|
+
private readonly secretKeyEnv: string;
|
|
16
|
+
|
|
17
|
+
constructor(
|
|
18
|
+
apiKeyEnvName: string = "ZEBPAY_API_KEY",
|
|
19
|
+
secretKeyEnvName: string = "ZEBPAY_API_SECRET"
|
|
20
|
+
) {
|
|
21
|
+
this.apiKeyEnv = apiKeyEnvName;
|
|
22
|
+
this.secretKeyEnv = secretKeyEnvName;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
getApiKey(): string {
|
|
26
|
+
const value = process.env[this.apiKeyEnv];
|
|
27
|
+
if (!value) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
`Missing API key in env ${this.apiKeyEnv}. Configure securely.`
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
getSecretKey(): string {
|
|
36
|
+
const value = process.env[this.secretKeyEnv];
|
|
37
|
+
if (!value) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`Missing API secret in env ${this.secretKeyEnv}. Configure securely.`
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Returns safe-to-log shapes for debugging without exposing secrets.
|
|
47
|
+
*/
|
|
48
|
+
describeRedacted(): { apiKey: string; secretKey: string } {
|
|
49
|
+
return {
|
|
50
|
+
apiKey: redact(process.env[this.apiKeyEnv] ?? ""),
|
|
51
|
+
secretKey: redact(process.env[this.secretKeyEnv] ?? ""),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Credentials provider that stores credentials in memory.
|
|
58
|
+
* Used for accepting credentials from client during initialization.
|
|
59
|
+
* Credentials are optional - if not provided, authenticated tools will fail with a clear error.
|
|
60
|
+
*/
|
|
61
|
+
export class InMemoryCredentialsProvider implements CredentialsProvider {
|
|
62
|
+
private apiKey: string | undefined;
|
|
63
|
+
private secretKey: string | undefined;
|
|
64
|
+
|
|
65
|
+
constructor(apiKey?: string, secretKey?: string) {
|
|
66
|
+
this.apiKey = apiKey;
|
|
67
|
+
this.secretKey = secretKey;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
setCredentials(apiKey: string, secretKey: string): void {
|
|
71
|
+
if (!apiKey || !secretKey) {
|
|
72
|
+
throw new Error("API key and secret are required.");
|
|
73
|
+
}
|
|
74
|
+
this.apiKey = apiKey;
|
|
75
|
+
this.secretKey = secretKey;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
getApiKey(): string {
|
|
79
|
+
if (!this.apiKey) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
"API credentials are required for this operation. Please provide ZEBPAY_API_KEY and ZEBPAY_API_SECRET headers or include credentials in initialization params."
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
return this.apiKey;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
getSecretKey(): string {
|
|
88
|
+
if (!this.secretKey) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
"API credentials are required for this operation. Please provide ZEBPAY_API_KEY and ZEBPAY_API_SECRET headers or include credentials in initialization params."
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
return this.secretKey;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Returns safe-to-log shapes for debugging without exposing secrets.
|
|
98
|
+
*/
|
|
99
|
+
describeRedacted(): { apiKey: string; secretKey: string } {
|
|
100
|
+
return {
|
|
101
|
+
apiKey: redact(this.apiKey),
|
|
102
|
+
secretKey: redact(this.secretKey),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Check if credentials are set
|
|
108
|
+
*/
|
|
109
|
+
hasCredentials(): boolean {
|
|
110
|
+
return !!(this.apiKey && this.secretKey);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/*
|
|
2
|
+
HMAC signing utilities for ZebPay.
|
|
3
|
+
The exact prehash format and header names are configurable via config to match ZebPay docs.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import crypto from "node:crypto";
|
|
7
|
+
import { SigningHeaderNames } from "../config.js";
|
|
8
|
+
|
|
9
|
+
export interface SignatureResult {
|
|
10
|
+
signature: string;
|
|
11
|
+
timestamp: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface SigningOptions {
|
|
15
|
+
headers: SigningHeaderNames;
|
|
16
|
+
/** If true, include body in prehash; set false for GETs without body. */
|
|
17
|
+
includeBody?: boolean;
|
|
18
|
+
/** Allows overriding timestamp for testing. */
|
|
19
|
+
now?: () => number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function stableStringify(value: unknown): string {
|
|
23
|
+
// Deterministic JSON stringify for signing.
|
|
24
|
+
return JSON.stringify(value, Object.keys(value as object).sort());
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Sign a query string using HMAC-SHA256 (Zebpay API format).
|
|
29
|
+
* This matches the Postman script format where query parameters are signed directly.
|
|
30
|
+
*/
|
|
31
|
+
export function signQueryString(
|
|
32
|
+
queryString: string,
|
|
33
|
+
secretKey: string
|
|
34
|
+
): string {
|
|
35
|
+
return crypto.createHmac("sha256", secretKey).update(queryString).digest("hex");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Sign a JSON payload string using HMAC-SHA256 (Zebpay API format for POST requests).
|
|
40
|
+
* This matches the Postman script format where the JSON body string is signed directly.
|
|
41
|
+
*/
|
|
42
|
+
export function signPayloadString(
|
|
43
|
+
payloadString: string,
|
|
44
|
+
secretKey: string
|
|
45
|
+
): string {
|
|
46
|
+
return crypto.createHmac("sha256", secretKey).update(payloadString).digest("hex");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Produce an HMAC-SHA256 signature. Default prehash: `${method}\n${path}\n${timestamp}\n${body}`.
|
|
51
|
+
*/
|
|
52
|
+
export function signRequest(
|
|
53
|
+
method: string,
|
|
54
|
+
path: string,
|
|
55
|
+
body: unknown,
|
|
56
|
+
secretKey: string,
|
|
57
|
+
opts: SigningOptions
|
|
58
|
+
): SignatureResult {
|
|
59
|
+
const timestamp = String((opts.now ? opts.now() : Date.now()));
|
|
60
|
+
const useBody = opts.includeBody ?? (method.toUpperCase() !== "GET" && body !== undefined && body !== null);
|
|
61
|
+
const bodyString = useBody ? (typeof body === "string" ? body : stableStringify(body)) : "";
|
|
62
|
+
const prehash = `${method.toUpperCase()}\n${path}\n${timestamp}\n${bodyString}`;
|
|
63
|
+
const signature = crypto.createHmac("sha256", secretKey).update(prehash).digest("hex");
|
|
64
|
+
return { signature, timestamp };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function buildAuthHeaders(
|
|
68
|
+
apiKey: string,
|
|
69
|
+
sig: SignatureResult,
|
|
70
|
+
headerNames: SigningHeaderNames
|
|
71
|
+
): Record<string, string> {
|
|
72
|
+
const headers: Record<string, string> = {
|
|
73
|
+
[headerNames.apiKeyHeader]: apiKey,
|
|
74
|
+
[headerNames.signatureHeader]: sig.signature,
|
|
75
|
+
};
|
|
76
|
+
// Only include timestamp header if it's configured (for non-query-string signing)
|
|
77
|
+
if (headerNames.timestampHeader) {
|
|
78
|
+
headers[headerNames.timestampHeader] = sig.timestamp;
|
|
79
|
+
}
|
|
80
|
+
return headers;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Build auth headers for query-string based signing (Zebpay format).
|
|
85
|
+
* Only includes API key and signature headers, no timestamp header.
|
|
86
|
+
*/
|
|
87
|
+
export function buildQueryStringAuthHeaders(
|
|
88
|
+
apiKey: string,
|
|
89
|
+
signature: string,
|
|
90
|
+
headerNames: SigningHeaderNames
|
|
91
|
+
): Record<string, string> {
|
|
92
|
+
return {
|
|
93
|
+
[headerNames.apiKeyHeader]: apiKey,
|
|
94
|
+
[headerNames.signatureHeader]: signature,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/*
|
|
2
|
+
TypeScript type definitions for API responses.
|
|
3
|
+
These help LLMs understand the structure of tool responses.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Common response structure for order operations
|
|
8
|
+
*/
|
|
9
|
+
export interface OrderResponse {
|
|
10
|
+
orderId?: string;
|
|
11
|
+
clientOrderId?: string;
|
|
12
|
+
symbol: string;
|
|
13
|
+
side: "buy" | "sell";
|
|
14
|
+
type: "market" | "limit";
|
|
15
|
+
quantity: string;
|
|
16
|
+
price?: string;
|
|
17
|
+
status: string;
|
|
18
|
+
executedPrice?: string;
|
|
19
|
+
executedQuantity?: string;
|
|
20
|
+
timestamp?: number;
|
|
21
|
+
[key: string]: unknown; // Allow additional fields from API
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Balance information for a single currency
|
|
26
|
+
*/
|
|
27
|
+
export interface CurrencyBalance {
|
|
28
|
+
currency: string;
|
|
29
|
+
available: string;
|
|
30
|
+
locked?: string;
|
|
31
|
+
total?: string;
|
|
32
|
+
[key: string]: unknown; // Allow additional fields from API
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Balance response structure
|
|
37
|
+
*/
|
|
38
|
+
export interface BalanceResponse {
|
|
39
|
+
balances?: CurrencyBalance[];
|
|
40
|
+
currencies?: Record<string, CurrencyBalance>;
|
|
41
|
+
[key: string]: unknown; // Allow additional fields from API
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Ticker information for a trading pair
|
|
46
|
+
*/
|
|
47
|
+
export interface TickerResponse {
|
|
48
|
+
symbol: string;
|
|
49
|
+
lastPrice: string;
|
|
50
|
+
bidPrice?: string;
|
|
51
|
+
askPrice?: string;
|
|
52
|
+
high24h?: string;
|
|
53
|
+
low24h?: string;
|
|
54
|
+
volume24h?: string;
|
|
55
|
+
priceChange24h?: string;
|
|
56
|
+
priceChangePercent24h?: string;
|
|
57
|
+
timestamp?: number;
|
|
58
|
+
[key: string]: unknown; // Allow additional fields from API
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Order book entry
|
|
63
|
+
*/
|
|
64
|
+
export interface OrderBookEntry {
|
|
65
|
+
price: string;
|
|
66
|
+
quantity: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Order book response
|
|
71
|
+
*/
|
|
72
|
+
export interface OrderBookResponse {
|
|
73
|
+
symbol: string;
|
|
74
|
+
bids: OrderBookEntry[];
|
|
75
|
+
asks: OrderBookEntry[];
|
|
76
|
+
timestamp?: number;
|
|
77
|
+
[key: string]: unknown; // Allow additional fields from API
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Trade information
|
|
82
|
+
*/
|
|
83
|
+
export interface TradeResponse {
|
|
84
|
+
symbol: string;
|
|
85
|
+
price: string;
|
|
86
|
+
quantity: string;
|
|
87
|
+
side: "buy" | "sell";
|
|
88
|
+
timestamp: number;
|
|
89
|
+
[key: string]: unknown; // Allow additional fields from API
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Kline/Candlestick data
|
|
94
|
+
*/
|
|
95
|
+
export interface KlineResponse {
|
|
96
|
+
openTime: number;
|
|
97
|
+
open: string;
|
|
98
|
+
high: string;
|
|
99
|
+
low: string;
|
|
100
|
+
close: string;
|
|
101
|
+
volume: string;
|
|
102
|
+
closeTime: number;
|
|
103
|
+
[key: string]: unknown; // Allow additional fields from API
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Exchange information
|
|
108
|
+
*/
|
|
109
|
+
export interface ExchangeInfoResponse {
|
|
110
|
+
symbols?: Array<{
|
|
111
|
+
symbol: string;
|
|
112
|
+
baseAsset: string;
|
|
113
|
+
quoteAsset: string;
|
|
114
|
+
status: string;
|
|
115
|
+
[key: string]: unknown;
|
|
116
|
+
}>;
|
|
117
|
+
[key: string]: unknown; // Allow additional fields from API
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Currency information
|
|
122
|
+
*/
|
|
123
|
+
export interface CurrencyInfo {
|
|
124
|
+
currency: string;
|
|
125
|
+
name?: string;
|
|
126
|
+
fullName?: string;
|
|
127
|
+
precision?: number;
|
|
128
|
+
type?: string;
|
|
129
|
+
chains?: Array<{
|
|
130
|
+
chainName: string;
|
|
131
|
+
withdrawalFee?: string;
|
|
132
|
+
depositMinSize?: string;
|
|
133
|
+
withdrawalMinSize?: string;
|
|
134
|
+
[key: string]: unknown;
|
|
135
|
+
}>;
|
|
136
|
+
[key: string]: unknown; // Allow additional fields from API
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Currencies response
|
|
141
|
+
*/
|
|
142
|
+
export interface CurrenciesResponse {
|
|
143
|
+
currencies?: CurrencyInfo[];
|
|
144
|
+
[key: string]: unknown; // Allow additional fields from API
|
|
145
|
+
}
|
|
146
|
+
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Simple in-memory cache for public endpoints to reduce API calls.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
interface CacheEntry<T> {
|
|
6
|
+
data: T;
|
|
7
|
+
timestamp: number;
|
|
8
|
+
ttl: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class SimpleCache<T = unknown> {
|
|
12
|
+
private cache = new Map<string, CacheEntry<T>>();
|
|
13
|
+
private defaultTtl: number;
|
|
14
|
+
|
|
15
|
+
constructor(defaultTtlMs: number = 5000) {
|
|
16
|
+
this.defaultTtl = defaultTtlMs;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get cached value if not expired
|
|
21
|
+
*/
|
|
22
|
+
get(key: string): T | null {
|
|
23
|
+
const entry = this.cache.get(key);
|
|
24
|
+
if (!entry) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const now = Date.now();
|
|
29
|
+
if (now - entry.timestamp > entry.ttl) {
|
|
30
|
+
this.cache.delete(key);
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return entry.data;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Set cached value with optional TTL
|
|
39
|
+
*/
|
|
40
|
+
set(key: string, data: T, ttlMs?: number): void {
|
|
41
|
+
this.cache.set(key, {
|
|
42
|
+
data,
|
|
43
|
+
timestamp: Date.now(),
|
|
44
|
+
ttl: ttlMs || this.defaultTtl,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Clear expired entries
|
|
50
|
+
*/
|
|
51
|
+
clearExpired(): void {
|
|
52
|
+
const now = Date.now();
|
|
53
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
54
|
+
if (now - entry.timestamp > entry.ttl) {
|
|
55
|
+
this.cache.delete(key);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Clear all cache entries
|
|
62
|
+
*/
|
|
63
|
+
clear(): void {
|
|
64
|
+
this.cache.clear();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get cache size
|
|
69
|
+
*/
|
|
70
|
+
size(): number {
|
|
71
|
+
return this.cache.size;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Cache instances for different endpoint types
|
|
76
|
+
export const tickerCache = new SimpleCache(5000); // 5 seconds for tickers
|
|
77
|
+
export const orderBookCache = new SimpleCache(2000); // 2 seconds for order books
|
|
78
|
+
export const exchangeInfoCache = new SimpleCache(60000); // 1 minute for exchange info
|
|
79
|
+
export const currenciesCache = new SimpleCache(300000); // 5 minutes for currencies
|
|
80
|
+
|
|
81
|
+
// Clean up expired entries periodically
|
|
82
|
+
if (typeof setInterval !== "undefined") {
|
|
83
|
+
setInterval(() => {
|
|
84
|
+
tickerCache.clearExpired();
|
|
85
|
+
orderBookCache.clearExpired();
|
|
86
|
+
exchangeInfoCache.clearExpired();
|
|
87
|
+
currenciesCache.clearExpired();
|
|
88
|
+
}, 10000); // Clean every 10 seconds
|
|
89
|
+
}
|
|
90
|
+
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/*
|
|
2
|
+
File logging utility for writing logs to both console and file.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createWriteStream, WriteStream } from "node:fs";
|
|
6
|
+
import { mkdir } from "node:fs/promises";
|
|
7
|
+
import { dirname } from "node:path";
|
|
8
|
+
|
|
9
|
+
class FileLogger {
|
|
10
|
+
private stream: WriteStream | null = null;
|
|
11
|
+
private logPath: string | null = null;
|
|
12
|
+
|
|
13
|
+
async initialize(logPath?: string): Promise<void> {
|
|
14
|
+
if (!logPath) {
|
|
15
|
+
// Default log path
|
|
16
|
+
this.logPath = null;
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
// Ensure log directory exists
|
|
22
|
+
const logDir = dirname(logPath);
|
|
23
|
+
await mkdir(logDir, { recursive: true });
|
|
24
|
+
|
|
25
|
+
// Create write stream in append mode
|
|
26
|
+
this.stream = createWriteStream(logPath, { flags: "a" });
|
|
27
|
+
this.logPath = logPath;
|
|
28
|
+
|
|
29
|
+
// Write initial log entry
|
|
30
|
+
this.writeToFile(JSON.stringify({
|
|
31
|
+
level: "info",
|
|
32
|
+
type: "log_init",
|
|
33
|
+
timestamp: new Date().toISOString(),
|
|
34
|
+
message: `Logging initialized to file: ${logPath}`,
|
|
35
|
+
}) + "\n");
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.error(JSON.stringify({
|
|
38
|
+
level: "error",
|
|
39
|
+
type: "log_init_error",
|
|
40
|
+
timestamp: new Date().toISOString(),
|
|
41
|
+
error: error instanceof Error ? error.message : String(error),
|
|
42
|
+
}));
|
|
43
|
+
// Continue without file logging if file can't be opened
|
|
44
|
+
this.stream = null;
|
|
45
|
+
this.logPath = null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
log(message: string): void {
|
|
50
|
+
// Always write to console.error
|
|
51
|
+
console.error(message);
|
|
52
|
+
|
|
53
|
+
// Also write to file if initialized
|
|
54
|
+
if (this.stream) {
|
|
55
|
+
this.writeToFile(message + "\n");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private writeToFile(message: string): void {
|
|
60
|
+
if (this.stream) {
|
|
61
|
+
try {
|
|
62
|
+
this.stream.write(message);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
// If file write fails, log to console but don't crash
|
|
65
|
+
console.error(JSON.stringify({
|
|
66
|
+
level: "error",
|
|
67
|
+
type: "log_write_error",
|
|
68
|
+
timestamp: new Date().toISOString(),
|
|
69
|
+
error: error instanceof Error ? error.message : String(error),
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async close(): Promise<void> {
|
|
76
|
+
if (this.stream) {
|
|
77
|
+
return new Promise((resolve) => {
|
|
78
|
+
this.stream!.end(() => {
|
|
79
|
+
this.stream = null;
|
|
80
|
+
resolve();
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export const fileLogger = new FileLogger();
|
|
88
|
+
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Basic metrics collection for monitoring tool usage and performance.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
interface Metric {
|
|
6
|
+
toolName: string;
|
|
7
|
+
timestamp: number;
|
|
8
|
+
durationMs: number;
|
|
9
|
+
success: boolean;
|
|
10
|
+
errorType?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
class MetricsCollector {
|
|
14
|
+
private metrics: Metric[] = [];
|
|
15
|
+
private maxMetrics = 1000; // Keep last 1000 metrics
|
|
16
|
+
|
|
17
|
+
record(
|
|
18
|
+
toolName: string,
|
|
19
|
+
durationMs: number,
|
|
20
|
+
success: boolean,
|
|
21
|
+
errorType?: string
|
|
22
|
+
): void {
|
|
23
|
+
this.metrics.push({
|
|
24
|
+
toolName,
|
|
25
|
+
timestamp: Date.now(),
|
|
26
|
+
durationMs,
|
|
27
|
+
success,
|
|
28
|
+
errorType,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Keep only last N metrics
|
|
32
|
+
if (this.metrics.length > this.maxMetrics) {
|
|
33
|
+
this.metrics = this.metrics.slice(-this.maxMetrics);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Get metrics summary for a tool
|
|
39
|
+
*/
|
|
40
|
+
getToolStats(toolName: string, timeWindowMs?: number): {
|
|
41
|
+
count: number;
|
|
42
|
+
successCount: number;
|
|
43
|
+
errorCount: number;
|
|
44
|
+
avgDurationMs: number;
|
|
45
|
+
minDurationMs: number;
|
|
46
|
+
maxDurationMs: number;
|
|
47
|
+
} {
|
|
48
|
+
const now = Date.now();
|
|
49
|
+
const window = timeWindowMs || Infinity;
|
|
50
|
+
const relevant = this.metrics.filter(
|
|
51
|
+
(m) =>
|
|
52
|
+
m.toolName === toolName && now - m.timestamp < window
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
if (relevant.length === 0) {
|
|
56
|
+
return {
|
|
57
|
+
count: 0,
|
|
58
|
+
successCount: 0,
|
|
59
|
+
errorCount: 0,
|
|
60
|
+
avgDurationMs: 0,
|
|
61
|
+
minDurationMs: 0,
|
|
62
|
+
maxDurationMs: 0,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const successCount = relevant.filter((m) => m.success).length;
|
|
67
|
+
const durations = relevant.map((m) => m.durationMs);
|
|
68
|
+
const sum = durations.reduce((a, b) => a + b, 0);
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
count: relevant.length,
|
|
72
|
+
successCount,
|
|
73
|
+
errorCount: relevant.length - successCount,
|
|
74
|
+
avgDurationMs: sum / relevant.length,
|
|
75
|
+
minDurationMs: Math.min(...durations),
|
|
76
|
+
maxDurationMs: Math.max(...durations),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get all tool names
|
|
82
|
+
*/
|
|
83
|
+
getToolNames(): string[] {
|
|
84
|
+
return Array.from(new Set(this.metrics.map((m) => m.toolName)));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get overall stats
|
|
89
|
+
*/
|
|
90
|
+
getOverallStats(timeWindowMs?: number): {
|
|
91
|
+
totalRequests: number;
|
|
92
|
+
successRate: number;
|
|
93
|
+
avgDurationMs: number;
|
|
94
|
+
toolStats: Record<string, ReturnType<MetricsCollector["getToolStats"]>>;
|
|
95
|
+
} {
|
|
96
|
+
const now = Date.now();
|
|
97
|
+
const window = timeWindowMs || Infinity;
|
|
98
|
+
const relevant = this.metrics.filter((m) => now - m.timestamp < window);
|
|
99
|
+
|
|
100
|
+
if (relevant.length === 0) {
|
|
101
|
+
return {
|
|
102
|
+
totalRequests: 0,
|
|
103
|
+
successRate: 0,
|
|
104
|
+
avgDurationMs: 0,
|
|
105
|
+
toolStats: {},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const successCount = relevant.filter((m) => m.success).length;
|
|
110
|
+
const durations = relevant.map((m) => m.durationMs);
|
|
111
|
+
const sum = durations.reduce((a, b) => a + b, 0);
|
|
112
|
+
|
|
113
|
+
const toolStats: Record<string, ReturnType<typeof this.getToolStats>> = {};
|
|
114
|
+
for (const toolName of this.getToolNames()) {
|
|
115
|
+
toolStats[toolName] = this.getToolStats(toolName, timeWindowMs);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
totalRequests: relevant.length,
|
|
120
|
+
successRate: successCount / relevant.length,
|
|
121
|
+
avgDurationMs: sum / relevant.length,
|
|
122
|
+
toolStats,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Clear all metrics
|
|
128
|
+
*/
|
|
129
|
+
clear(): void {
|
|
130
|
+
this.metrics = [];
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export const metricsCollector = new MetricsCollector();
|
|
135
|
+
|