@cfxdevkit/executor 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.
package/dist/index.cjs ADDED
@@ -0,0 +1,860 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
+ DEFAULT_SAFETY_CONFIG: () => DEFAULT_SAFETY_CONFIG,
24
+ Executor: () => Executor,
25
+ KeeperClientImpl: () => KeeperClientImpl,
26
+ PriceChecker: () => PriceChecker,
27
+ RetryQueue: () => RetryQueue,
28
+ SafetyGuard: () => SafetyGuard,
29
+ noopLogger: () => noopLogger
30
+ });
31
+ module.exports = __toCommonJS(src_exports);
32
+
33
+ // src/executor.ts
34
+ var import_core = require("@cfxdevkit/core");
35
+ var Executor = class {
36
+ priceChecker;
37
+ safetyGuard;
38
+ retryQueue;
39
+ keeperClient;
40
+ jobStore;
41
+ dryRun;
42
+ constructor(priceChecker, safetyGuard, retryQueue, keeperClient, jobStore, options = {}) {
43
+ this.priceChecker = priceChecker;
44
+ this.safetyGuard = safetyGuard;
45
+ this.retryQueue = retryQueue;
46
+ this.keeperClient = keeperClient;
47
+ this.jobStore = jobStore;
48
+ this.dryRun = options.dryRun ?? false;
49
+ }
50
+ /**
51
+ * Process a single job tick.
52
+ */
53
+ async processTick(job) {
54
+ if (job.expiresAt !== null && Date.now() >= job.expiresAt) {
55
+ import_core.logger.info(`[Executor] job ${job.id} expired`);
56
+ await this.jobStore.markExpired(job.id);
57
+ return;
58
+ }
59
+ try {
60
+ if (job.status === "pending") {
61
+ await this.jobStore.markActive(job.id);
62
+ }
63
+ if (job.type === "limit_order") {
64
+ await this._processLimitOrder(job);
65
+ } else if (job.type === "dca") {
66
+ await this._processDCA(job);
67
+ }
68
+ } catch (err) {
69
+ const message = err instanceof Error ? err.message : String(err);
70
+ if (message.includes("PriceConditionNotMet")) {
71
+ import_core.logger.debug(
72
+ `[Executor] job ${job.id}: price condition no longer met at execution time \u2014 will retry next tick`
73
+ );
74
+ return;
75
+ }
76
+ if (message.includes("DCAIntervalNotReached")) {
77
+ import_core.logger.debug(
78
+ `[Executor] job ${job.id}: DCA interval not yet reached at execution time \u2014 will retry next tick`
79
+ );
80
+ return;
81
+ }
82
+ if (message.includes("could not be found") || message.includes("TransactionReceiptNotFoundError")) {
83
+ import_core.logger.debug(
84
+ `[Executor] job ${job.id}: receipt not yet indexed \u2014 will retry next tick`
85
+ );
86
+ return;
87
+ }
88
+ if (message.includes("Slippage exceeded")) {
89
+ import_core.logger.debug(
90
+ `[Executor] job ${job.id}: slippage exceeded at execution time \u2014 will retry next tick`
91
+ );
92
+ await this.jobStore.incrementRetry(job.id);
93
+ await this.jobStore.updateLastError(job.id, message);
94
+ return;
95
+ }
96
+ if (err instanceof Error && err.cause) {
97
+ import_core.logger.error(
98
+ { cause: err.cause },
99
+ "[Executor] raw error cause"
100
+ );
101
+ }
102
+ if (message.includes("JobNotFound")) {
103
+ const ocId = job.onChainJobId;
104
+ import_core.logger.warn(
105
+ { jobId: job.id, onChainJobId: ocId },
106
+ "[Executor] JobNotFound \u2014 on_chain_job_id not found on current contract (likely left over from an old deployment) \u2014 marking cancelled"
107
+ );
108
+ await this.jobStore.markCancelled(job.id);
109
+ return;
110
+ }
111
+ if (message.includes("JobNotActive")) {
112
+ const ocId = job.onChainJobId;
113
+ let onChainStatus = "cancelled";
114
+ if (ocId) {
115
+ try {
116
+ onChainStatus = await this.keeperClient.getOnChainStatus(ocId);
117
+ } catch (statusErr) {
118
+ import_core.logger.warn(
119
+ { jobId: job.id, err: statusErr },
120
+ "[Executor] could not read on-chain status \u2014 defaulting to cancelled"
121
+ );
122
+ }
123
+ }
124
+ if (onChainStatus === "executed") {
125
+ import_core.logger.warn(
126
+ { jobId: job.id, onChainJobId: ocId },
127
+ "[Executor] job EXECUTED on-chain but DB was out of sync \u2014 marking executed"
128
+ );
129
+ await this.jobStore.markExecuted(job.id, "chain-sync");
130
+ } else {
131
+ import_core.logger.warn(
132
+ { jobId: job.id, onChainJobId: ocId, onChainStatus },
133
+ "[Executor] job is CANCELLED/EXPIRED on-chain \u2014 marking cancelled in DB"
134
+ );
135
+ await this.jobStore.markCancelled(job.id);
136
+ }
137
+ return;
138
+ }
139
+ import_core.logger.error(`[Executor] job ${job.id} failed: ${message}`);
140
+ const nextRetries = job.retries + 1;
141
+ if (job.retries < job.maxRetries) {
142
+ await this.jobStore.incrementRetry(job.id);
143
+ }
144
+ if (nextRetries < job.maxRetries) {
145
+ this.retryQueue.enqueue({ ...job, retries: nextRetries });
146
+ }
147
+ await this.jobStore.markFailed(job.id, message);
148
+ }
149
+ }
150
+ /**
151
+ * Process all active jobs + due retries in one tick.
152
+ */
153
+ async runAllTicks() {
154
+ const activeJobs = await this.jobStore.getActiveJobs();
155
+ const retries = this.retryQueue.drainDue();
156
+ const all = [...activeJobs, ...retries];
157
+ import_core.logger.info(
158
+ `[Executor] tick \u2013 ${activeJobs.length} active, ${retries.length} retries`
159
+ );
160
+ await Promise.allSettled(all.map((job) => this.processTick(job)));
161
+ }
162
+ // ─── Private ────────────────────────────────────────────────────────────────
163
+ async _processLimitOrder(job) {
164
+ const priceResult = await this.priceChecker.checkLimitOrder(job);
165
+ if (!priceResult.conditionMet) {
166
+ import_core.logger.info(
167
+ {
168
+ jobId: job.id,
169
+ currentPrice: priceResult.currentPrice.toString(),
170
+ targetPrice: priceResult.targetPrice.toString(),
171
+ direction: job.params.direction
172
+ },
173
+ `[Executor] limit-order ${job.id}: condition not met \u2013 waiting`
174
+ );
175
+ return;
176
+ }
177
+ const safetyResult = this.safetyGuard.check(job, {
178
+ swapUsd: priceResult.swapUsd
179
+ });
180
+ if (!safetyResult.ok) {
181
+ import_core.logger.warn(
182
+ { violation: safetyResult.violation },
183
+ `[Executor] limit-order ${job.id} blocked by safety guard`
184
+ );
185
+ return;
186
+ }
187
+ if (this.dryRun) {
188
+ import_core.logger.info(`[Executor][dryRun] would execute limit-order ${job.id}`);
189
+ return;
190
+ }
191
+ import_core.logger.info(`[Executor] executing limit-order ${job.id}`);
192
+ const onChainJobId = job.onChainJobId;
193
+ if (!onChainJobId) {
194
+ import_core.logger.warn(
195
+ `[Executor] limit-order ${job.id} has no onChainJobId \u2013 skipping until registered on-chain`
196
+ );
197
+ return;
198
+ }
199
+ const { txHash, amountOut } = await this.keeperClient.executeLimitOrder(
200
+ onChainJobId,
201
+ job.owner,
202
+ job.params
203
+ );
204
+ await this.jobStore.markExecuted(job.id, txHash, amountOut);
205
+ import_core.logger.info(`[Executor] limit-order ${job.id} executed \u2013 tx ${txHash}`);
206
+ }
207
+ async _processDCA(job) {
208
+ const priceResult = await this.priceChecker.checkDCA(job);
209
+ if (!priceResult.conditionMet) {
210
+ import_core.logger.info(
211
+ {
212
+ jobId: job.id,
213
+ nextExecution: job.params.nextExecution,
214
+ now: Date.now(),
215
+ secsRemaining: Math.round(
216
+ (job.params.nextExecution - Date.now()) / 1e3
217
+ )
218
+ },
219
+ `[Executor] DCA ${job.id}: interval not reached`
220
+ );
221
+ return;
222
+ }
223
+ const safetyResult = this.safetyGuard.check(job, {
224
+ swapUsd: priceResult.swapUsd
225
+ });
226
+ if (!safetyResult.ok) {
227
+ import_core.logger.warn(
228
+ { violation: safetyResult.violation },
229
+ `[Executor] DCA ${job.id} blocked by safety guard`
230
+ );
231
+ return;
232
+ }
233
+ if (this.dryRun) {
234
+ import_core.logger.info(`[Executor][dryRun] would execute DCA tick for ${job.id}`);
235
+ return;
236
+ }
237
+ import_core.logger.info(`[Executor] executing DCA tick ${job.id}`);
238
+ const onChainJobId = job.onChainJobId;
239
+ if (!onChainJobId) {
240
+ import_core.logger.warn(
241
+ `[Executor] DCA job ${job.id} has no onChainJobId \u2013 skipping until registered on-chain`
242
+ );
243
+ return;
244
+ }
245
+ const { txHash, amountOut, nextExecutionSec } = await this.keeperClient.executeDCATick(
246
+ onChainJobId,
247
+ job.owner,
248
+ job.params
249
+ );
250
+ const newSwapsCompleted = job.params.swapsCompleted + 1;
251
+ const nextExecutionMs = nextExecutionSec * 1e3;
252
+ await this.jobStore.markDCATick(
253
+ job.id,
254
+ txHash,
255
+ newSwapsCompleted,
256
+ nextExecutionMs,
257
+ amountOut
258
+ );
259
+ import_core.logger.info(
260
+ {
261
+ jobId: job.id,
262
+ txHash,
263
+ swapsCompleted: newSwapsCompleted,
264
+ total: job.params.totalSwaps
265
+ },
266
+ `[Executor] DCA tick executed \u2013 ${newSwapsCompleted}/${job.params.totalSwaps}`
267
+ );
268
+ }
269
+ };
270
+
271
+ // src/keeper-client.ts
272
+ var import_core2 = require("@cfxdevkit/core");
273
+ var import_protocol = require("@cfxdevkit/protocol");
274
+ var import_viem = require("viem");
275
+ var import_accounts = require("viem/accounts");
276
+ function decodeAmountOut(logs, owner) {
277
+ const TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
278
+ const ownerLower = owner.toLowerCase();
279
+ for (let i = logs.length - 1; i >= 0; i--) {
280
+ const log = logs[i];
281
+ if (log.topics[0]?.toLowerCase() === TRANSFER_TOPIC && log.topics[2] && `0x${log.topics[2].slice(26)}`.toLowerCase() === ownerLower) {
282
+ try {
283
+ return BigInt(log.data).toString();
284
+ } catch {
285
+ return null;
286
+ }
287
+ }
288
+ }
289
+ return null;
290
+ }
291
+ var KeeperClientImpl = class {
292
+ walletClient;
293
+ publicClient;
294
+ contractAddress;
295
+ swappiRouter;
296
+ maxGasPriceGwei;
297
+ rpcTimeoutMs;
298
+ account;
299
+ constructor(config) {
300
+ this.account = (0, import_accounts.privateKeyToAccount)(config.privateKey);
301
+ this.contractAddress = config.contractAddress;
302
+ this.swappiRouter = config.swappiRouter;
303
+ this.maxGasPriceGwei = config.maxGasPriceGwei ?? 1000n;
304
+ this.rpcTimeoutMs = config.rpcTimeoutMs ?? 12e4;
305
+ this.publicClient = (0, import_viem.createPublicClient)({
306
+ chain: config.chain,
307
+ transport: (0, import_viem.http)(config.rpcUrl)
308
+ });
309
+ this.walletClient = (0, import_viem.createWalletClient)({
310
+ chain: config.chain,
311
+ transport: (0, import_viem.http)(config.rpcUrl),
312
+ account: this.account
313
+ });
314
+ }
315
+ withTimeout(fn, timeoutMs = this.rpcTimeoutMs) {
316
+ const controller = new AbortController();
317
+ const id = setTimeout(() => controller.abort(), timeoutMs);
318
+ return fn(controller.signal).finally(() => clearTimeout(id));
319
+ }
320
+ // --------------------------------------------------------------------------
321
+ // Helpers
322
+ // --------------------------------------------------------------------------
323
+ /**
324
+ * Query the on-chain status of a job.
325
+ * Returns one of: 'active' | 'executed' | 'cancelled' | 'expired'.
326
+ * Throws if the contract call fails (e.g. job not found).
327
+ */
328
+ async getOnChainStatus(onChainJobId) {
329
+ const job = await this.withTimeout(
330
+ (_signal) => this.publicClient.readContract({
331
+ address: this.contractAddress,
332
+ abi: import_protocol.AUTOMATION_MANAGER_ABI,
333
+ functionName: "getJob",
334
+ args: [onChainJobId]
335
+ })
336
+ );
337
+ switch (job.status) {
338
+ case 0:
339
+ return "active";
340
+ case 1:
341
+ return "executed";
342
+ case 2:
343
+ return "cancelled";
344
+ case 3:
345
+ return "expired";
346
+ default:
347
+ return "cancelled";
348
+ }
349
+ }
350
+ /** Guard: abort if chain is paused or gas price is too high */
351
+ async preflightCheck() {
352
+ const isPaused = await this.withTimeout(
353
+ (_signal) => this.publicClient.readContract({
354
+ address: this.contractAddress,
355
+ abi: import_protocol.AUTOMATION_MANAGER_ABI,
356
+ functionName: "paused"
357
+ })
358
+ );
359
+ if (isPaused) {
360
+ throw new Error("AutomationManager is paused on-chain");
361
+ }
362
+ const gasPrice = await Promise.race([
363
+ this.publicClient.getGasPrice(),
364
+ new Promise(
365
+ (_, rej) => setTimeout(
366
+ () => rej(new Error("getGasPrice timeout")),
367
+ this.rpcTimeoutMs
368
+ )
369
+ )
370
+ ]);
371
+ const gasPriceGwei = gasPrice / 1000000000n;
372
+ if (gasPriceGwei > this.maxGasPriceGwei) {
373
+ throw new Error(
374
+ `Gas price ${gasPriceGwei} gwei exceeds cap ${this.maxGasPriceGwei} gwei`
375
+ );
376
+ }
377
+ }
378
+ /**
379
+ * Build minimal swap calldata for a token-in/token-out pair via Swappi.
380
+ *
381
+ * In a production keeper the calldata would be built via a DEX aggregator
382
+ * (e.g. OKX DEX API) to get optimal routing. For the MVP we encode the
383
+ * simplest Swappi path: `swapExactTokensForTokens(amountIn, minOut, path, to, deadline)`.
384
+ *
385
+ * NOTE: The AutomationManager does NOT use this calldata for custody;
386
+ * it only uses it to call the router on behalf of the user's pre-approved allowance.
387
+ */
388
+ buildSwapCalldata(tokenIn, tokenOut, amountIn, minAmountOut, recipient) {
389
+ const selector = "38ed1739";
390
+ const deadline = BigInt(Math.floor(Date.now() / 1e3) + 1800);
391
+ const encode256 = (n) => n.toString(16).padStart(64, "0");
392
+ const encodeAddr = (a) => a.slice(2).toLowerCase().padStart(64, "0");
393
+ const pathOffset = (5 * 32).toString(16).padStart(64, "0");
394
+ const pathLength = encode256(2n);
395
+ const pathData = encodeAddr(tokenIn) + encodeAddr(tokenOut);
396
+ const calldata = "0x" + selector + encode256(amountIn) + encode256(minAmountOut) + pathOffset + encodeAddr(recipient) + encode256(deadline) + pathLength + pathData;
397
+ return calldata;
398
+ }
399
+ // --------------------------------------------------------------------------
400
+ // IKeeperClient interface implementation
401
+ // --------------------------------------------------------------------------
402
+ async executeLimitOrder(jobId, owner, params) {
403
+ await this.preflightCheck();
404
+ const amountIn = BigInt(params.amountIn);
405
+ const minAmountOut = 0n;
406
+ const swapCalldata = this.buildSwapCalldata(
407
+ params.tokenIn,
408
+ params.tokenOut,
409
+ amountIn,
410
+ minAmountOut,
411
+ owner
412
+ );
413
+ import_core2.logger.info(
414
+ {
415
+ jobId,
416
+ tokenIn: params.tokenIn,
417
+ tokenOut: params.tokenOut,
418
+ amountIn: params.amountIn
419
+ },
420
+ "[KeeperClient] executeLimitOrder"
421
+ );
422
+ try {
423
+ await this.withTimeout(
424
+ (_signal) => this.publicClient.simulateContract({
425
+ address: this.contractAddress,
426
+ abi: import_protocol.AUTOMATION_MANAGER_ABI,
427
+ functionName: "executeLimitOrder",
428
+ args: [jobId, this.swappiRouter, swapCalldata],
429
+ account: this.account.address
430
+ })
431
+ );
432
+ } catch (simErr) {
433
+ const msg = simErr instanceof Error ? simErr.message : String(simErr);
434
+ import_core2.logger.error(
435
+ { jobId, error: msg.slice(0, 500) },
436
+ "[KeeperClient] executeLimitOrder simulation reverted"
437
+ );
438
+ throw simErr;
439
+ }
440
+ const hash = await this.withTimeout(
441
+ (_signal) => this.walletClient.writeContract({
442
+ address: this.contractAddress,
443
+ abi: import_protocol.AUTOMATION_MANAGER_ABI,
444
+ functionName: "executeLimitOrder",
445
+ args: [jobId, this.swappiRouter, swapCalldata],
446
+ chain: void 0,
447
+ account: this.account
448
+ })
449
+ );
450
+ import_core2.logger.info({ jobId, hash }, "[KeeperClient] limitOrder tx submitted");
451
+ const receipt = await Promise.race([
452
+ this.publicClient.waitForTransactionReceipt({ hash }),
453
+ new Promise(
454
+ (_, rej) => setTimeout(
455
+ () => rej(new Error("waitForTransactionReceipt timeout")),
456
+ this.rpcTimeoutMs
457
+ )
458
+ )
459
+ ]);
460
+ if (receipt.status !== "success") {
461
+ let reason = hash ?? "unknown";
462
+ try {
463
+ await this.withTimeout(
464
+ (_signal) => this.publicClient.simulateContract({
465
+ address: this.contractAddress,
466
+ abi: import_protocol.AUTOMATION_MANAGER_ABI,
467
+ functionName: "executeLimitOrder",
468
+ args: [jobId, this.swappiRouter, swapCalldata],
469
+ account: this.account.address
470
+ })
471
+ );
472
+ } catch (simErr) {
473
+ if (simErr instanceof Error) reason = simErr.message;
474
+ }
475
+ throw new Error(`Limit order tx reverted: ${reason}`);
476
+ }
477
+ const amountOut = decodeAmountOut(receipt.logs, owner);
478
+ import_core2.logger.info(
479
+ { jobId, hash, blockNumber: receipt.blockNumber.toString(), amountOut },
480
+ "[KeeperClient] limitOrder confirmed"
481
+ );
482
+ return { txHash: hash, amountOut };
483
+ }
484
+ async executeDCATick(jobId, owner, params) {
485
+ await this.preflightCheck();
486
+ const onChainParams = await this.publicClient.readContract({
487
+ address: this.contractAddress,
488
+ abi: import_protocol.AUTOMATION_MANAGER_ABI,
489
+ functionName: "getDCAJob",
490
+ args: [jobId]
491
+ });
492
+ const nowSec = BigInt(Math.floor(Date.now() / 1e3));
493
+ if (nowSec < onChainParams.nextExecution) {
494
+ throw new Error(
495
+ `DCAIntervalNotReached(${onChainParams.nextExecution}) \u2014 on-chain interval not reached yet (now=${nowSec}, next=${onChainParams.nextExecution}, remaining=${onChainParams.nextExecution - nowSec}s)`
496
+ );
497
+ }
498
+ const amountPerTick = BigInt(params.amountPerSwap);
499
+ const minAmountOut = 0n;
500
+ const swapCalldata = this.buildSwapCalldata(
501
+ params.tokenIn,
502
+ params.tokenOut,
503
+ amountPerTick,
504
+ minAmountOut,
505
+ owner
506
+ );
507
+ import_core2.logger.info(
508
+ {
509
+ jobId,
510
+ tokenIn: params.tokenIn,
511
+ tokenOut: params.tokenOut,
512
+ amountPerTick: params.amountPerSwap
513
+ },
514
+ "[KeeperClient] executeDCATick"
515
+ );
516
+ try {
517
+ await this.withTimeout(
518
+ (_signal) => this.publicClient.simulateContract({
519
+ address: this.contractAddress,
520
+ abi: import_protocol.AUTOMATION_MANAGER_ABI,
521
+ functionName: "executeDCATick",
522
+ args: [jobId, this.swappiRouter, swapCalldata],
523
+ account: this.account.address
524
+ })
525
+ );
526
+ } catch (simErr) {
527
+ const msg = simErr instanceof Error ? simErr.message : String(simErr);
528
+ import_core2.logger.error(
529
+ { jobId, error: msg.slice(0, 500) },
530
+ "[KeeperClient] executeDCATick simulation reverted"
531
+ );
532
+ throw simErr;
533
+ }
534
+ const hash = await this.withTimeout(
535
+ (_signal) => this.walletClient.writeContract({
536
+ address: this.contractAddress,
537
+ abi: import_protocol.AUTOMATION_MANAGER_ABI,
538
+ functionName: "executeDCATick",
539
+ args: [jobId, this.swappiRouter, swapCalldata],
540
+ chain: void 0,
541
+ account: this.account
542
+ })
543
+ );
544
+ import_core2.logger.info({ jobId, hash }, "[KeeperClient] dca tx submitted");
545
+ const receipt = await Promise.race([
546
+ this.publicClient.waitForTransactionReceipt({ hash }),
547
+ new Promise(
548
+ (_, rej) => setTimeout(
549
+ () => rej(new Error("waitForTransactionReceipt timeout")),
550
+ this.rpcTimeoutMs
551
+ )
552
+ )
553
+ ]);
554
+ if (receipt.status !== "success") {
555
+ let reason = hash ?? "unknown";
556
+ try {
557
+ await this.withTimeout(
558
+ (_signal) => this.publicClient.simulateContract({
559
+ address: this.contractAddress,
560
+ abi: import_protocol.AUTOMATION_MANAGER_ABI,
561
+ functionName: "executeDCATick",
562
+ args: [jobId, this.swappiRouter, swapCalldata],
563
+ account: this.account.address
564
+ })
565
+ );
566
+ } catch (simErr) {
567
+ if (simErr instanceof Error) reason = simErr.message;
568
+ }
569
+ throw new Error(`DCA tick tx reverted: ${reason}`);
570
+ }
571
+ const amountOut = decodeAmountOut(receipt.logs, owner);
572
+ const postTickParams = await this.publicClient.readContract({
573
+ address: this.contractAddress,
574
+ abi: import_protocol.AUTOMATION_MANAGER_ABI,
575
+ functionName: "getDCAJob",
576
+ args: [jobId]
577
+ });
578
+ const nextExecutionSec = Number(postTickParams.nextExecution);
579
+ import_core2.logger.info(
580
+ {
581
+ jobId,
582
+ hash,
583
+ blockNumber: receipt.blockNumber.toString(),
584
+ amountOut,
585
+ nextExecutionSec
586
+ },
587
+ "[KeeperClient] dca confirmed"
588
+ );
589
+ return { txHash: hash, amountOut, nextExecutionSec };
590
+ }
591
+ };
592
+
593
+ // src/logger.ts
594
+ var noopLogger = {
595
+ info: () => {
596
+ },
597
+ warn: () => {
598
+ },
599
+ debug: () => {
600
+ },
601
+ error: () => {
602
+ }
603
+ };
604
+
605
+ // src/price-checker.ts
606
+ var defaultDecimalsResolver = async () => 18;
607
+ var PriceChecker = class {
608
+ source;
609
+ tokenPricesUsd;
610
+ /**
611
+ * Resolves a token's decimal count. Called lazily when _estimateUsd needs
612
+ * to convert raw wei amounts into human-readable values. The resolver may
613
+ * query the chain, a static map, or simply return 18.
614
+ */
615
+ getDecimals;
616
+ log;
617
+ constructor(source, tokenPricesUsd = /* @__PURE__ */ new Map(), logger3 = noopLogger, getDecimals = defaultDecimalsResolver) {
618
+ this.source = source;
619
+ this.tokenPricesUsd = tokenPricesUsd;
620
+ this.getDecimals = getDecimals;
621
+ this.log = logger3;
622
+ }
623
+ async checkLimitOrder(job) {
624
+ const params = job.params;
625
+ const currentPrice = await this.source.getPrice(
626
+ params.tokenIn,
627
+ params.tokenOut
628
+ );
629
+ const targetPrice = BigInt(params.targetPrice);
630
+ let conditionMet;
631
+ if (params.direction === "gte") {
632
+ conditionMet = currentPrice >= targetPrice;
633
+ } else {
634
+ conditionMet = currentPrice <= targetPrice;
635
+ }
636
+ const swapUsd = await this._estimateUsd(params.tokenIn, params.amountIn);
637
+ this.log.debug(
638
+ {
639
+ jobId: job.id,
640
+ currentPrice: currentPrice.toString(),
641
+ targetPrice: targetPrice.toString(),
642
+ conditionMet,
643
+ swapUsd
644
+ },
645
+ "[PriceChecker] limit-order check"
646
+ );
647
+ return { conditionMet, currentPrice, targetPrice, swapUsd };
648
+ }
649
+ async checkDCA(job) {
650
+ const params = job.params;
651
+ const DCA_EXECUTION_BUFFER_MS = 15e3;
652
+ const conditionMet = Date.now() >= params.nextExecution + DCA_EXECUTION_BUFFER_MS;
653
+ const currentPrice = await this.source.getPrice(
654
+ params.tokenIn,
655
+ params.tokenOut
656
+ );
657
+ const swapUsd = await this._estimateUsd(
658
+ params.tokenIn,
659
+ params.amountPerSwap
660
+ );
661
+ this.log.debug(
662
+ {
663
+ jobId: job.id,
664
+ nextExecution: params.nextExecution,
665
+ conditionMet,
666
+ swapUsd
667
+ },
668
+ "[PriceChecker] DCA check"
669
+ );
670
+ return { conditionMet, currentPrice, targetPrice: 0n, swapUsd };
671
+ }
672
+ updateTokenPrice(token, usdPrice) {
673
+ this.tokenPricesUsd.set(token.toLowerCase(), usdPrice);
674
+ }
675
+ async _estimateUsd(token, amountWei) {
676
+ const usdPerToken = this.tokenPricesUsd.get(token.toLowerCase()) ?? 0;
677
+ const decimals = await this.getDecimals(token);
678
+ const divisor = 10 ** decimals;
679
+ const amount = Number(BigInt(amountWei)) / divisor;
680
+ return amount * usdPerToken;
681
+ }
682
+ };
683
+
684
+ // src/retry-queue.ts
685
+ var RetryQueue = class {
686
+ baseDelayMs;
687
+ maxDelayMs;
688
+ jitter;
689
+ log;
690
+ queue = /* @__PURE__ */ new Map();
691
+ constructor(options, logger3 = noopLogger) {
692
+ this.baseDelayMs = options?.baseDelayMs ?? 5e3;
693
+ this.maxDelayMs = options?.maxDelayMs ?? 3e5;
694
+ this.jitter = options?.jitter ?? 0.2;
695
+ this.log = logger3;
696
+ }
697
+ /** Enqueue a job for retry after a calculated backoff delay. */
698
+ enqueue(job) {
699
+ const existing = this.queue.get(job.id);
700
+ const attempt = existing ? existing.attempt + 1 : 0;
701
+ const delay = this._backoffDelay(attempt);
702
+ const nextRetryAt = Date.now() + delay;
703
+ this.queue.set(job.id, { job, nextRetryAt, attempt });
704
+ this.log.info(
705
+ `[RetryQueue] job ${job.id} enqueued, attempt=${attempt}, delay=${delay}ms`
706
+ );
707
+ }
708
+ /** Remove a job from the queue (e.g. after success or manual cancel). */
709
+ remove(jobId) {
710
+ this.queue.delete(jobId);
711
+ }
712
+ /** Return all jobs whose retry time has arrived; removes them from the queue. */
713
+ drainDue(now = Date.now()) {
714
+ const due = [];
715
+ for (const [jobId, entry] of this.queue) {
716
+ if (now >= entry.nextRetryAt) {
717
+ due.push(entry.job);
718
+ this.queue.delete(jobId);
719
+ }
720
+ }
721
+ return due;
722
+ }
723
+ size() {
724
+ return this.queue.size;
725
+ }
726
+ // ─── Private ──────────────────────────────────────────────────────
727
+ _backoffDelay(attempt) {
728
+ const exponential = this.baseDelayMs * 2 ** attempt;
729
+ const capped = Math.min(exponential, this.maxDelayMs);
730
+ const withJitter = capped * (1 + this.jitter * Math.random());
731
+ return Math.floor(withJitter);
732
+ }
733
+ };
734
+
735
+ // src/safety-guard.ts
736
+ var DEFAULT_SAFETY_CONFIG = {
737
+ maxSwapUsd: 1e4,
738
+ maxSlippageBps: 500,
739
+ maxRetries: 5,
740
+ minExecutionIntervalSeconds: 30,
741
+ globalPause: false
742
+ };
743
+ var SafetyGuard = class {
744
+ config;
745
+ violations = [];
746
+ log;
747
+ constructor(config = {}, logger3 = noopLogger) {
748
+ this.config = { ...DEFAULT_SAFETY_CONFIG, ...config };
749
+ this.log = logger3;
750
+ this.log.info(
751
+ {
752
+ maxSwapUsd: this.config.maxSwapUsd,
753
+ maxSlippageBps: this.config.maxSlippageBps,
754
+ maxRetries: this.config.maxRetries,
755
+ globalPause: this.config.globalPause
756
+ },
757
+ "[SafetyGuard] initialized"
758
+ );
759
+ }
760
+ // ─── Core check ───────────────────────────────────────────────────
761
+ /**
762
+ * Run all configured safety checks against a job.
763
+ * Returns `{ ok: true }` if all pass, or `{ ok: false, violation }` on first failure.
764
+ */
765
+ check(job, context) {
766
+ const now = context.currentTime ?? Date.now();
767
+ if (this.config.globalPause) {
768
+ return this._fail(
769
+ job.id,
770
+ "globalPause",
771
+ "Global pause is active \u2014 all execution halted"
772
+ );
773
+ }
774
+ if (job.status !== "active" && job.status !== "pending") {
775
+ return this._fail(
776
+ job.id,
777
+ "globalPause",
778
+ `Job ${job.id} cannot be executed (status: ${job.status})`
779
+ );
780
+ }
781
+ if (job.retries >= job.maxRetries) {
782
+ return this._fail(
783
+ job.id,
784
+ "maxRetries",
785
+ `Job ${job.id} has exhausted retries (${job.retries}/${job.maxRetries})`
786
+ );
787
+ }
788
+ if (context.swapUsd > this.config.maxSwapUsd) {
789
+ return this._fail(
790
+ job.id,
791
+ "maxSwapUsd",
792
+ `Swap value $${context.swapUsd.toFixed(2)} exceeds limit $${this.config.maxSwapUsd}`
793
+ );
794
+ }
795
+ if (job.expiresAt !== null && now >= job.expiresAt) {
796
+ return this._fail(job.id, "globalPause", `Job ${job.id} has expired`);
797
+ }
798
+ if (job.type === "dca") {
799
+ const dcaParams = job.params;
800
+ if (now < dcaParams.nextExecution) {
801
+ const waitSec = Math.ceil((dcaParams.nextExecution - now) / 1e3);
802
+ return this._fail(
803
+ job.id,
804
+ "minExecutionIntervalSeconds",
805
+ `DCA interval not yet reached (${waitSec}s remaining)`
806
+ );
807
+ }
808
+ }
809
+ return { ok: true };
810
+ }
811
+ // ─── Config management ────────────────────────────────────────────
812
+ updateConfig(patch) {
813
+ this.config = { ...this.config, ...patch };
814
+ this.log.info(patch, "[SafetyGuard] config updated");
815
+ }
816
+ getConfig() {
817
+ return { ...this.config };
818
+ }
819
+ pauseAll() {
820
+ this.config.globalPause = true;
821
+ this.log.warn("[SafetyGuard] GLOBAL PAUSE ACTIVATED");
822
+ }
823
+ resumeAll() {
824
+ this.config.globalPause = false;
825
+ this.log.info("[SafetyGuard] global pause lifted");
826
+ }
827
+ isPaused() {
828
+ return this.config.globalPause;
829
+ }
830
+ // ─── Audit log ────────────────────────────────────────────────────
831
+ getViolations() {
832
+ return this.violations;
833
+ }
834
+ clearViolations() {
835
+ this.violations = [];
836
+ }
837
+ // ─── Private ──────────────────────────────────────────────────────
838
+ _fail(jobId, rule, detail) {
839
+ const violation = {
840
+ jobId,
841
+ rule,
842
+ detail,
843
+ timestamp: Date.now()
844
+ };
845
+ this.violations.push(violation);
846
+ this.log.warn({ jobId, detail }, `[SafetyGuard] violation \u2013 ${rule}`);
847
+ return { ok: false, violation };
848
+ }
849
+ };
850
+ // Annotate the CommonJS export names for ESM import in node:
851
+ 0 && (module.exports = {
852
+ DEFAULT_SAFETY_CONFIG,
853
+ Executor,
854
+ KeeperClientImpl,
855
+ PriceChecker,
856
+ RetryQueue,
857
+ SafetyGuard,
858
+ noopLogger
859
+ });
860
+ //# sourceMappingURL=index.cjs.map