@pioneer-platform/markets 8.12.0 → 8.15.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.
@@ -0,0 +1,87 @@
1
+ import { Db } from 'mongodb';
2
+ export interface TokenBucketConfig {
3
+ apiName: string;
4
+ capacity: number;
5
+ refillAmount: number;
6
+ refillInterval: number;
7
+ tokensPerRequest: number;
8
+ costPerToken: number;
9
+ monthlyBudget: number;
10
+ }
11
+ export interface TokenBucketDocument {
12
+ apiName: string;
13
+ capacity: number;
14
+ currentTokens: number;
15
+ lastRefill: Date;
16
+ nextRefill: Date;
17
+ refillAmount: number;
18
+ refillInterval: number;
19
+ tokensPerRequest: number;
20
+ costPerToken: number;
21
+ monthlyCost: number;
22
+ monthlyBudget: number;
23
+ enabled: boolean;
24
+ exhausted: boolean;
25
+ exhaustedAt?: Date;
26
+ createdAt: Date;
27
+ updatedAt: Date;
28
+ }
29
+ export declare class TokenBucket {
30
+ private config;
31
+ private db;
32
+ private collection;
33
+ private cachedTokens;
34
+ private lastSync;
35
+ private syncInterval;
36
+ constructor(config: TokenBucketConfig, db: Db);
37
+ /**
38
+ * Initialize bucket (call on startup)
39
+ */
40
+ initialize(): Promise<void>;
41
+ /**
42
+ * Try to consume tokens (returns true if successful)
43
+ */
44
+ tryConsume(count?: number): Promise<boolean>;
45
+ /**
46
+ * Get current token count
47
+ */
48
+ getTokenCount(): Promise<number>;
49
+ /**
50
+ * Get bucket status
51
+ */
52
+ getStatus(): Promise<{
53
+ available: number;
54
+ capacity: number;
55
+ exhausted: boolean;
56
+ nextRefill: Date;
57
+ utilizationRate: number;
58
+ }>;
59
+ /**
60
+ * Force refill (admin use or scheduled job)
61
+ */
62
+ refill(): Promise<void>;
63
+ /**
64
+ * Check if refill is needed (call on startup and periodically)
65
+ */
66
+ private checkAndRefill;
67
+ /**
68
+ * Sync cached tokens to database
69
+ */
70
+ private syncToDB;
71
+ /**
72
+ * Sync from database to cache
73
+ */
74
+ private syncFromDB;
75
+ /**
76
+ * Get next midnight UTC
77
+ */
78
+ private getNextMidnightUTC;
79
+ /**
80
+ * Add tokens (for testing or manual adjustments)
81
+ */
82
+ addTokens(count: number): Promise<void>;
83
+ /**
84
+ * Get remaining tokens without consuming
85
+ */
86
+ getRemainingTokens(): Promise<number>;
87
+ }
@@ -0,0 +1,341 @@
1
+ "use strict";
2
+ /*
3
+ Token Bucket Rate Limiting System
4
+
5
+ Implements per-API token bucket rate limiting with:
6
+ - Daily token budgets (refill at midnight UTC)
7
+ - MongoDB persistence (survives restarts)
8
+ - In-memory caching (fast checks)
9
+ - Cost tracking
10
+ */
11
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
12
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
13
+ return new (P || (P = Promise))(function (resolve, reject) {
14
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
15
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
16
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
17
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
18
+ });
19
+ };
20
+ var __generator = (this && this.__generator) || function (thisArg, body) {
21
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
22
+ return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
23
+ function verb(n) { return function (v) { return step([n, v]); }; }
24
+ function step(op) {
25
+ if (f) throw new TypeError("Generator is already executing.");
26
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
27
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
28
+ if (y = 0, t) op = [op[0] & 2, t.value];
29
+ switch (op[0]) {
30
+ case 0: case 1: t = op; break;
31
+ case 4: _.label++; return { value: op[1], done: false };
32
+ case 5: _.label++; y = op[1]; op = [0]; continue;
33
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
34
+ default:
35
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
36
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
37
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
38
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
39
+ if (t[2]) _.ops.pop();
40
+ _.trys.pop(); continue;
41
+ }
42
+ op = body.call(thisArg, _);
43
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
44
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
45
+ }
46
+ };
47
+ Object.defineProperty(exports, "__esModule", { value: true });
48
+ exports.TokenBucket = void 0;
49
+ var log = require('@pioneer-platform/loggerdog')();
50
+ var TAG = ' | TokenBucket | ';
51
+ var TokenBucket = /** @class */ (function () {
52
+ function TokenBucket(config, db) {
53
+ this.syncInterval = 5000; // Sync to DB every 5 seconds
54
+ this.config = config;
55
+ this.db = db;
56
+ this.collection = db.collection('api_token_buckets');
57
+ this.cachedTokens = 0;
58
+ this.lastSync = new Date();
59
+ }
60
+ /**
61
+ * Initialize bucket (call on startup)
62
+ */
63
+ TokenBucket.prototype.initialize = function () {
64
+ return __awaiter(this, void 0, void 0, function () {
65
+ var tag, bucket, newBucket;
66
+ return __generator(this, function (_a) {
67
+ switch (_a.label) {
68
+ case 0:
69
+ tag = TAG + ' | initialize | ';
70
+ return [4 /*yield*/, this.collection.findOne({ apiName: this.config.apiName })];
71
+ case 1:
72
+ bucket = _a.sent();
73
+ if (!!bucket) return [3 /*break*/, 3];
74
+ // Create new bucket
75
+ log.info(tag, "Creating new token bucket for ".concat(this.config.apiName));
76
+ newBucket = {
77
+ apiName: this.config.apiName,
78
+ capacity: this.config.capacity,
79
+ currentTokens: this.config.capacity, // Start full
80
+ lastRefill: new Date(),
81
+ nextRefill: this.getNextMidnightUTC(),
82
+ refillAmount: this.config.refillAmount,
83
+ refillInterval: this.config.refillInterval,
84
+ tokensPerRequest: this.config.tokensPerRequest,
85
+ costPerToken: this.config.costPerToken,
86
+ monthlyCost: 0,
87
+ monthlyBudget: this.config.monthlyBudget,
88
+ enabled: true,
89
+ exhausted: false,
90
+ createdAt: new Date(),
91
+ updatedAt: new Date()
92
+ };
93
+ return [4 /*yield*/, this.collection.insertOne(newBucket)];
94
+ case 2:
95
+ _a.sent();
96
+ return [3 /*break*/, 5];
97
+ case 3:
98
+ // Check if refill is needed
99
+ return [4 /*yield*/, this.checkAndRefill()];
100
+ case 4:
101
+ // Check if refill is needed
102
+ _a.sent();
103
+ _a.label = 5;
104
+ case 5: return [4 /*yield*/, this.collection.findOne({ apiName: this.config.apiName })];
105
+ case 6:
106
+ // Load current tokens into memory
107
+ bucket = _a.sent();
108
+ this.cachedTokens = bucket.currentTokens;
109
+ log.info(tag, "Initialized ".concat(this.config.apiName, ": ").concat(this.cachedTokens, "/").concat(this.config.capacity, " tokens"));
110
+ return [2 /*return*/];
111
+ }
112
+ });
113
+ });
114
+ };
115
+ /**
116
+ * Try to consume tokens (returns true if successful)
117
+ */
118
+ TokenBucket.prototype.tryConsume = function () {
119
+ return __awaiter(this, arguments, void 0, function (count) {
120
+ var tag;
121
+ if (count === void 0) { count = 1; }
122
+ return __generator(this, function (_a) {
123
+ switch (_a.label) {
124
+ case 0:
125
+ tag = TAG + ' | tryConsume | ';
126
+ if (!(this.cachedTokens >= count)) return [3 /*break*/, 3];
127
+ this.cachedTokens -= count;
128
+ if (!(Date.now() - this.lastSync.getTime() > this.syncInterval)) return [3 /*break*/, 2];
129
+ return [4 /*yield*/, this.syncToDB()];
130
+ case 1:
131
+ _a.sent();
132
+ _a.label = 2;
133
+ case 2: return [2 /*return*/, true];
134
+ case 3:
135
+ // Cache might be stale, check DB
136
+ return [4 /*yield*/, this.syncFromDB()];
137
+ case 4:
138
+ // Cache might be stale, check DB
139
+ _a.sent();
140
+ if (!(this.cachedTokens >= count)) return [3 /*break*/, 6];
141
+ this.cachedTokens -= count;
142
+ return [4 /*yield*/, this.syncToDB()];
143
+ case 5:
144
+ _a.sent();
145
+ return [2 /*return*/, true];
146
+ case 6:
147
+ // Not enough tokens
148
+ log.warn(tag, "".concat(this.config.apiName, " bucket exhausted! ").concat(this.cachedTokens, "/").concat(count, " needed"));
149
+ // Mark as exhausted in DB
150
+ return [4 /*yield*/, this.collection.updateOne({ apiName: this.config.apiName }, {
151
+ $set: {
152
+ exhausted: true,
153
+ exhaustedAt: new Date(),
154
+ updatedAt: new Date()
155
+ }
156
+ })];
157
+ case 7:
158
+ // Mark as exhausted in DB
159
+ _a.sent();
160
+ return [2 /*return*/, false];
161
+ }
162
+ });
163
+ });
164
+ };
165
+ /**
166
+ * Get current token count
167
+ */
168
+ TokenBucket.prototype.getTokenCount = function () {
169
+ return __awaiter(this, void 0, void 0, function () {
170
+ return __generator(this, function (_a) {
171
+ switch (_a.label) {
172
+ case 0: return [4 /*yield*/, this.syncFromDB()];
173
+ case 1:
174
+ _a.sent();
175
+ return [2 /*return*/, this.cachedTokens];
176
+ }
177
+ });
178
+ });
179
+ };
180
+ /**
181
+ * Get bucket status
182
+ */
183
+ TokenBucket.prototype.getStatus = function () {
184
+ return __awaiter(this, void 0, void 0, function () {
185
+ var bucket;
186
+ return __generator(this, function (_a) {
187
+ switch (_a.label) {
188
+ case 0: return [4 /*yield*/, this.collection.findOne({ apiName: this.config.apiName })];
189
+ case 1:
190
+ bucket = _a.sent();
191
+ if (!bucket) {
192
+ throw new Error("Bucket not found: ".concat(this.config.apiName));
193
+ }
194
+ return [2 /*return*/, {
195
+ available: bucket.currentTokens,
196
+ capacity: bucket.capacity,
197
+ exhausted: bucket.exhausted,
198
+ nextRefill: bucket.nextRefill,
199
+ utilizationRate: 1 - (bucket.currentTokens / bucket.capacity)
200
+ }];
201
+ }
202
+ });
203
+ });
204
+ };
205
+ /**
206
+ * Force refill (admin use or scheduled job)
207
+ */
208
+ TokenBucket.prototype.refill = function () {
209
+ return __awaiter(this, void 0, void 0, function () {
210
+ var tag;
211
+ return __generator(this, function (_a) {
212
+ switch (_a.label) {
213
+ case 0:
214
+ tag = TAG + ' | refill | ';
215
+ return [4 /*yield*/, this.collection.updateOne({ apiName: this.config.apiName }, {
216
+ $set: {
217
+ currentTokens: this.config.refillAmount,
218
+ lastRefill: new Date(),
219
+ nextRefill: this.getNextMidnightUTC(),
220
+ exhausted: false,
221
+ exhaustedAt: undefined,
222
+ updatedAt: new Date()
223
+ }
224
+ })];
225
+ case 1:
226
+ _a.sent();
227
+ this.cachedTokens = this.config.refillAmount;
228
+ log.info(tag, "Refilled ".concat(this.config.apiName, " to ").concat(this.config.refillAmount, " tokens"));
229
+ return [2 /*return*/];
230
+ }
231
+ });
232
+ });
233
+ };
234
+ /**
235
+ * Check if refill is needed (call on startup and periodically)
236
+ */
237
+ TokenBucket.prototype.checkAndRefill = function () {
238
+ return __awaiter(this, void 0, void 0, function () {
239
+ var bucket;
240
+ return __generator(this, function (_a) {
241
+ switch (_a.label) {
242
+ case 0: return [4 /*yield*/, this.collection.findOne({ apiName: this.config.apiName })];
243
+ case 1:
244
+ bucket = _a.sent();
245
+ if (!bucket)
246
+ return [2 /*return*/];
247
+ if (!(new Date() >= bucket.nextRefill)) return [3 /*break*/, 3];
248
+ return [4 /*yield*/, this.refill()];
249
+ case 2:
250
+ _a.sent();
251
+ _a.label = 3;
252
+ case 3: return [2 /*return*/];
253
+ }
254
+ });
255
+ });
256
+ };
257
+ /**
258
+ * Sync cached tokens to database
259
+ */
260
+ TokenBucket.prototype.syncToDB = function () {
261
+ return __awaiter(this, void 0, void 0, function () {
262
+ return __generator(this, function (_a) {
263
+ switch (_a.label) {
264
+ case 0: return [4 /*yield*/, this.collection.updateOne({ apiName: this.config.apiName }, {
265
+ $set: {
266
+ currentTokens: this.cachedTokens,
267
+ updatedAt: new Date()
268
+ }
269
+ })];
270
+ case 1:
271
+ _a.sent();
272
+ this.lastSync = new Date();
273
+ return [2 /*return*/];
274
+ }
275
+ });
276
+ });
277
+ };
278
+ /**
279
+ * Sync from database to cache
280
+ */
281
+ TokenBucket.prototype.syncFromDB = function () {
282
+ return __awaiter(this, void 0, void 0, function () {
283
+ var bucket;
284
+ return __generator(this, function (_a) {
285
+ switch (_a.label) {
286
+ case 0: return [4 /*yield*/, this.collection.findOne({ apiName: this.config.apiName })];
287
+ case 1:
288
+ bucket = _a.sent();
289
+ if (bucket) {
290
+ this.cachedTokens = bucket.currentTokens;
291
+ this.lastSync = new Date();
292
+ }
293
+ return [2 /*return*/];
294
+ }
295
+ });
296
+ });
297
+ };
298
+ /**
299
+ * Get next midnight UTC
300
+ */
301
+ TokenBucket.prototype.getNextMidnightUTC = function () {
302
+ var now = new Date();
303
+ var midnight = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1, // Next day
304
+ 0, 0, 0, 0));
305
+ return midnight;
306
+ };
307
+ /**
308
+ * Add tokens (for testing or manual adjustments)
309
+ */
310
+ TokenBucket.prototype.addTokens = function (count) {
311
+ return __awaiter(this, void 0, void 0, function () {
312
+ return __generator(this, function (_a) {
313
+ switch (_a.label) {
314
+ case 0:
315
+ this.cachedTokens = Math.min(this.cachedTokens + count, this.config.capacity);
316
+ return [4 /*yield*/, this.syncToDB()];
317
+ case 1:
318
+ _a.sent();
319
+ return [2 /*return*/];
320
+ }
321
+ });
322
+ });
323
+ };
324
+ /**
325
+ * Get remaining tokens without consuming
326
+ */
327
+ TokenBucket.prototype.getRemainingTokens = function () {
328
+ return __awaiter(this, void 0, void 0, function () {
329
+ return __generator(this, function (_a) {
330
+ switch (_a.label) {
331
+ case 0: return [4 /*yield*/, this.syncFromDB()];
332
+ case 1:
333
+ _a.sent();
334
+ return [2 /*return*/, this.cachedTokens];
335
+ }
336
+ });
337
+ });
338
+ };
339
+ return TokenBucket;
340
+ }());
341
+ exports.TokenBucket = TokenBucket;
package/package.json CHANGED
@@ -1,20 +1,22 @@
1
1
  {
2
2
  "name": "@pioneer-platform/markets",
3
- "version": "8.12.0",
3
+ "version": "8.15.0",
4
4
  "main": "./lib/index.js",
5
5
  "types": "./lib/index.d.ts",
6
6
  "dependencies": {
7
7
  "@pioneer-platform/default-redis": "^8.11.7",
8
8
  "@pioneer-platform/loggerdog": "^8.11.0",
9
9
  "@pioneer-platform/pioneer-coins": "^9.11.0",
10
- "@pioneer-platform/pioneer-discovery": "^8.12.0",
10
+ "@pioneer-platform/pioneer-discovery": "^8.14.1",
11
11
  "@pioneer-platform/pioneer-types": "^8.11.0",
12
12
  "@pioneer-platform/pro-token": "^0.9.0",
13
13
  "@shapeshiftoss/caip": "^9.0.0-alpha.0",
14
14
  "axios": "^1.6.0",
15
15
  "axios-retry": "^3.2.0",
16
16
  "bottleneck": "^2.19.5",
17
- "dotenv": "^8.2.0"
17
+ "ccxt": "^4.5.18",
18
+ "dotenv": "^8.2.0",
19
+ "node-schedule": "^2.1.1"
18
20
  },
19
21
  "scripts": {
20
22
  "npm": "pnpm i",
@@ -27,6 +29,7 @@
27
29
  "devDependencies": {
28
30
  "@types/jest": "^25.2.3",
29
31
  "@types/node": "^18.16.0",
32
+ "@types/node-schedule": "^2.1.8",
30
33
  "@types/source-map-support": "^0.5.3",
31
34
  "jest": "^26.4.2",
32
35
  "onchange": "^7.0.2",