@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.
@@ -0,0 +1,425 @@
1
+ import { Address, createWalletClient } from 'viem';
2
+
3
+ /**
4
+ * Minimal injectable logger interface for the automation module.
5
+ *
6
+ * The SDK ships with NO logging dependency. Consumers can pass any compatible
7
+ * logger (pino, winston, console wrapper …) or omit it to get silent behaviour.
8
+ */
9
+ interface AutomationLogger {
10
+ info(msg: string | object, ...args: unknown[]): void;
11
+ warn(msg: string | object, ...args: unknown[]): void;
12
+ debug(msg: string | object, ...args: unknown[]): void;
13
+ error(msg: string | object, ...args: unknown[]): void;
14
+ }
15
+ /** Default no-op logger — used when no logger is supplied. */
16
+ declare const noopLogger: AutomationLogger;
17
+
18
+ type JobStatus = 'pending' | 'active' | 'executed' | 'cancelled' | 'failed' | 'paused';
19
+ type JobType = 'limit_order' | 'dca' | 'twap' | 'swap';
20
+ interface BaseJob {
21
+ id: string;
22
+ owner: string;
23
+ type: JobType;
24
+ status: JobStatus;
25
+ /** bytes32 hex jobId from the on-chain JobCreated event (0x-prefixed) */
26
+ onChainJobId: string | null;
27
+ createdAt: number;
28
+ updatedAt: number;
29
+ expiresAt: number | null;
30
+ retries: number;
31
+ maxRetries: number;
32
+ lastError: string | null;
33
+ }
34
+ interface LimitOrderJob extends BaseJob {
35
+ type: 'limit_order';
36
+ params: LimitOrderParams;
37
+ }
38
+ interface DCAJob extends BaseJob {
39
+ type: 'dca';
40
+ params: DCAParams;
41
+ }
42
+ interface TWAPJob extends BaseJob {
43
+ type: 'twap';
44
+ params: TWAPParams;
45
+ }
46
+ interface SwapJob extends BaseJob {
47
+ type: 'swap';
48
+ params: SwapParams;
49
+ }
50
+ type Job = LimitOrderJob | DCAJob | TWAPJob | SwapJob;
51
+ interface LimitOrderParams {
52
+ tokenIn: string;
53
+ tokenOut: string;
54
+ amountIn: string;
55
+ minAmountOut: string;
56
+ targetPrice: string;
57
+ direction: 'gte' | 'lte';
58
+ /** User-configured slippage in basis points (e.g. 50 = 0.5%) — stored for display */
59
+ slippageBps?: number;
60
+ }
61
+ interface DCAParams {
62
+ tokenIn: string;
63
+ tokenOut: string;
64
+ amountPerSwap: string;
65
+ intervalSeconds: number;
66
+ totalSwaps: number;
67
+ swapsCompleted: number;
68
+ nextExecution: number;
69
+ }
70
+ interface TWAPParams {
71
+ tokenIn: string;
72
+ tokenOut: string;
73
+ totalAmountIn: string;
74
+ numberOfTranches: number;
75
+ intervalSeconds: number;
76
+ slippageBps?: number;
77
+ }
78
+ interface SwapParams {
79
+ tokenIn: string;
80
+ tokenOut: string;
81
+ amountIn: string;
82
+ minAmountOut: string;
83
+ slippageBps?: number;
84
+ }
85
+ interface SafetyConfig {
86
+ /** Maximum USD value per single swap */
87
+ maxSwapUsd: number;
88
+ /** Maximum slippage in basis points (e.g. 200 = 2%) */
89
+ maxSlippageBps: number;
90
+ /** Maximum retries before a job is marked failed */
91
+ maxRetries: number;
92
+ /** Minimum seconds between consecutive executions for the same job */
93
+ minExecutionIntervalSeconds: number;
94
+ /** If true, all job execution is halted (circuit-breaker) */
95
+ globalPause: boolean;
96
+ }
97
+ interface SafetyViolation {
98
+ jobId: string;
99
+ rule: keyof SafetyConfig;
100
+ detail: string;
101
+ timestamp: number;
102
+ }
103
+ type SafetyCheckResult = {
104
+ ok: true;
105
+ } | {
106
+ ok: false;
107
+ violation: SafetyViolation;
108
+ };
109
+
110
+ /**
111
+ * Price source adapter interface.
112
+ *
113
+ * Returns the spot price of `tokenIn` denominated in `tokenOut`, scaled by
114
+ * 1e18. Implementations may call a DEX (Swappi, etc.), an oracle, or a mock.
115
+ */
116
+ interface PriceSource {
117
+ /**
118
+ * Returns 0n if the pair is unknown or price cannot be fetched.
119
+ */
120
+ getPrice(tokenIn: string, tokenOut: string): Promise<bigint>;
121
+ }
122
+ interface PriceCheckResult {
123
+ conditionMet: boolean;
124
+ currentPrice: bigint;
125
+ targetPrice: bigint;
126
+ swapUsd: number;
127
+ }
128
+ /**
129
+ * Callback that resolves a token address to its ERC-20 decimal count.
130
+ * Implementations should cache the result for performance.
131
+ */
132
+ type DecimalsResolver = (token: string) => Promise<number>;
133
+ /**
134
+ * PriceChecker – queries a price source and evaluates whether a job's
135
+ * trigger condition is currently met.
136
+ */
137
+ declare class PriceChecker {
138
+ private source;
139
+ private tokenPricesUsd;
140
+ /**
141
+ * Resolves a token's decimal count. Called lazily when _estimateUsd needs
142
+ * to convert raw wei amounts into human-readable values. The resolver may
143
+ * query the chain, a static map, or simply return 18.
144
+ */
145
+ private getDecimals;
146
+ private readonly log;
147
+ constructor(source: PriceSource, tokenPricesUsd?: Map<string, number>, logger?: AutomationLogger, getDecimals?: DecimalsResolver);
148
+ checkLimitOrder(job: Job & {
149
+ type: 'limit_order';
150
+ }): Promise<PriceCheckResult>;
151
+ checkDCA(job: Job & {
152
+ type: 'dca';
153
+ }): Promise<PriceCheckResult>;
154
+ updateTokenPrice(token: string, usdPrice: number): void;
155
+ private _estimateUsd;
156
+ }
157
+
158
+ /**
159
+ * RetryQueue – wraps jobs for retry-with-exponential-backoff scheduling.
160
+ *
161
+ * Backoff formula:
162
+ * delay = min(base × 2^attempt, maxDelay) × (1 + jitter × rand)
163
+ */
164
+ declare class RetryQueue {
165
+ private readonly baseDelayMs;
166
+ private readonly maxDelayMs;
167
+ private readonly jitter;
168
+ private readonly log;
169
+ private queue;
170
+ constructor(options?: {
171
+ baseDelayMs?: number;
172
+ maxDelayMs?: number;
173
+ jitter?: number;
174
+ }, logger?: AutomationLogger);
175
+ /** Enqueue a job for retry after a calculated backoff delay. */
176
+ enqueue(job: Job): void;
177
+ /** Remove a job from the queue (e.g. after success or manual cancel). */
178
+ remove(jobId: string): void;
179
+ /** Return all jobs whose retry time has arrived; removes them from the queue. */
180
+ drainDue(now?: number): Job[];
181
+ size(): number;
182
+ private _backoffDelay;
183
+ }
184
+
185
+ /** Default on-chain safety limits. */
186
+ declare const DEFAULT_SAFETY_CONFIG: SafetyConfig;
187
+ /**
188
+ * SafetyGuard – off-chain complement to AutomationManager's on-chain checks.
189
+ *
190
+ * Responsibilities:
191
+ * 1. Validate a job satisfies configured safety bounds before the keeper
192
+ * submits a transaction.
193
+ * 2. Provide a global pause switch (circuit-breaker).
194
+ * 3. Log every violation for audit purposes.
195
+ *
196
+ * The logger is injected rather than imported — the SDK ships with no logging
197
+ * dependency. Pass your pino/winston/console instance, or omit for silence.
198
+ */
199
+ declare class SafetyGuard {
200
+ private config;
201
+ private violations;
202
+ private readonly log;
203
+ constructor(config?: Partial<SafetyConfig>, logger?: AutomationLogger);
204
+ /**
205
+ * Run all configured safety checks against a job.
206
+ * Returns `{ ok: true }` if all pass, or `{ ok: false, violation }` on first failure.
207
+ */
208
+ check(job: Job, context: {
209
+ swapUsd: number;
210
+ currentTime?: number;
211
+ }): SafetyCheckResult;
212
+ updateConfig(patch: Partial<SafetyConfig>): void;
213
+ getConfig(): Readonly<SafetyConfig>;
214
+ pauseAll(): void;
215
+ resumeAll(): void;
216
+ isPaused(): boolean;
217
+ getViolations(): readonly SafetyViolation[];
218
+ clearViolations(): void;
219
+ private _fail;
220
+ }
221
+
222
+ interface KeeperClient$1 {
223
+ executeLimitOrder(jobId: string, owner: string, params: LimitOrderJob['params']): Promise<{
224
+ txHash: string;
225
+ amountOut?: string | null;
226
+ }>;
227
+ executeDCATick(jobId: string, owner: string, params: DCAJob['params']): Promise<{
228
+ txHash: string;
229
+ amountOut?: string | null;
230
+ nextExecutionSec: number;
231
+ }>;
232
+ /** Read the terminal status of a job from the contract. */
233
+ getOnChainStatus(jobId: `0x${string}`): Promise<'active' | 'executed' | 'cancelled' | 'expired'>;
234
+ }
235
+ interface JobStore {
236
+ getActiveJobs(): Promise<Job[]>;
237
+ markActive(jobId: string): Promise<void>;
238
+ markExecuted(jobId: string, txHash: string, amountOut?: string | null): Promise<void>;
239
+ /**
240
+ * Record one completed DCA tick.
241
+ * Updates swapsCompleted + nextExecution in params_json and inserts an
242
+ * execution record. Sets status → 'executed' when all swaps are done,
243
+ * otherwise keeps the job 'active' so subsequent ticks can run.
244
+ */
245
+ markDCATick(jobId: string, txHash: string, newSwapsCompleted: number, nextExecution: number, amountOut?: string | null): Promise<void>;
246
+ markFailed(jobId: string, error: string): Promise<void>;
247
+ incrementRetry(jobId: string): Promise<void>;
248
+ markExpired(jobId: string): Promise<void>;
249
+ /** Mark a job cancelled — use when on-chain status is CANCELLED. */
250
+ markCancelled(jobId: string): Promise<void>;
251
+ /** Record the latest error message without changing status or incrementing retries. */
252
+ updateLastError(jobId: string, error: string): Promise<void>;
253
+ }
254
+ interface ExecutorOptions {
255
+ dryRun?: boolean;
256
+ }
257
+ /**
258
+ * Executor – evaluates active jobs and submits on-chain transactions when
259
+ * conditions are met and the SafetyGuard approves.
260
+ */
261
+ declare class Executor {
262
+ private priceChecker;
263
+ private safetyGuard;
264
+ private retryQueue;
265
+ private keeperClient;
266
+ private jobStore;
267
+ private dryRun;
268
+ constructor(priceChecker: PriceChecker, safetyGuard: SafetyGuard, retryQueue: RetryQueue, keeperClient: KeeperClient$1, jobStore: JobStore, options?: ExecutorOptions);
269
+ /**
270
+ * Process a single job tick.
271
+ */
272
+ processTick(job: Job): Promise<void>;
273
+ /**
274
+ * Process all active jobs + due retries in one tick.
275
+ */
276
+ runAllTicks(): Promise<void>;
277
+ private _processLimitOrder;
278
+ private _processDCA;
279
+ }
280
+
281
+ /**
282
+ * KeeperClient — viem implementation of the KeeperClient interface.
283
+ *
284
+ * Wraps AutomationManager contract calls for executeLimitOrder and executeDCATick.
285
+ * The executor wallet is a keeper (not a custodian) — it cannot move user funds
286
+ * unless the user has set an on-chain allowance for that specific swap.
287
+ */
288
+
289
+ interface KeeperClientConfig {
290
+ /** RPC endpoint for Conflux eSpace */
291
+ rpcUrl: string;
292
+ /** Hex private key (0x-prefixed) of the keeper wallet */
293
+ privateKey: `0x${string}`;
294
+ /** Deployed AutomationManager contract address */
295
+ contractAddress: Address;
296
+ /**
297
+ * Swappi router address.
298
+ * Testnet: 0x873789aaf553fd0b4252d0d2b72c6331c47aff2e
299
+ * Mainnet: 0xE37B52296b0bAA91412cD0Cd97975B0805037B84 (Swappi v2 — only address with deployed code;
300
+ * old 0x62B0873... has no bytecode)
301
+ */
302
+ swappiRouter: Address;
303
+ /**
304
+ * Maximum gas price in gwei before aborting execution (circuit breaker).
305
+ * Defaults to 1000 gwei.
306
+ */
307
+ maxGasPriceGwei?: bigint;
308
+ /** Chain definition — pass the viem chain object (espaceTestnet / espaceMainnet) */
309
+ chain: Parameters<typeof createWalletClient>[0]['chain'];
310
+ /** RPC request timeout in milliseconds (applies to read/simulate/write calls). */
311
+ rpcTimeoutMs?: number;
312
+ }
313
+ declare class KeeperClientImpl implements KeeperClient$1 {
314
+ private readonly walletClient;
315
+ private readonly publicClient;
316
+ private readonly contractAddress;
317
+ private readonly swappiRouter;
318
+ private readonly maxGasPriceGwei;
319
+ private readonly rpcTimeoutMs;
320
+ private readonly account;
321
+ constructor(config: KeeperClientConfig);
322
+ private withTimeout;
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
+ getOnChainStatus(onChainJobId: `0x${string}`): Promise<'active' | 'executed' | 'cancelled' | 'expired'>;
329
+ /** Guard: abort if chain is paused or gas price is too high */
330
+ private preflightCheck;
331
+ /**
332
+ * Build minimal swap calldata for a token-in/token-out pair via Swappi.
333
+ *
334
+ * In a production keeper the calldata would be built via a DEX aggregator
335
+ * (e.g. OKX DEX API) to get optimal routing. For the MVP we encode the
336
+ * simplest Swappi path: `swapExactTokensForTokens(amountIn, minOut, path, to, deadline)`.
337
+ *
338
+ * NOTE: The AutomationManager does NOT use this calldata for custody;
339
+ * it only uses it to call the router on behalf of the user's pre-approved allowance.
340
+ */
341
+ private buildSwapCalldata;
342
+ executeLimitOrder(jobId: string, owner: string, params: LimitOrderJob['params']): Promise<{
343
+ txHash: string;
344
+ amountOut: string | null;
345
+ }>;
346
+ executeDCATick(jobId: string, owner: string, params: DCAJob['params']): Promise<{
347
+ txHash: string;
348
+ amountOut: string | null;
349
+ nextExecutionSec: number;
350
+ }>;
351
+ }
352
+
353
+ /**
354
+ * Minimal interface that any concrete keeper/executor must satisfy.
355
+ *
356
+ * The SDK defines the contract here; implementations live in the CAS worker
357
+ * (KeeperClient) or in custom keeper deployments.
358
+ */
359
+ interface KeeperClient {
360
+ /**
361
+ * Submit an `executeLimitOrder` transaction to the AutomationManager
362
+ * contract and wait for it to be mined.
363
+ */
364
+ executeLimitOrder(jobId: `0x${string}`, owner: `0x${string}`, params: LimitOrderParams): Promise<{
365
+ txHash: `0x${string}`;
366
+ amountOut?: string;
367
+ }>;
368
+ /**
369
+ * Submit an `executeDCATick` transaction and wait for confirmation.
370
+ */
371
+ executeDCATick(jobId: `0x${string}`, owner: `0x${string}`, params: DCAParams): Promise<{
372
+ txHash: `0x${string}`;
373
+ amountOut?: string;
374
+ }>;
375
+ /**
376
+ * Read the current on-chain status of a job.
377
+ *
378
+ * Maps the contract `JobStatus` enum:
379
+ * `0 = ACTIVE → 'active'`
380
+ * `1 = EXECUTED → 'executed'`
381
+ * `2 = CANCELLED → 'cancelled'`
382
+ * `3 = EXPIRED → 'expired'`
383
+ */
384
+ getOnChainStatus(jobId: `0x${string}`): Promise<'active' | 'executed' | 'cancelled' | 'expired'>;
385
+ }
386
+
387
+ interface LimitOrderStrategy {
388
+ kind: 'limit_order';
389
+ tokenIn: string;
390
+ tokenOut: string;
391
+ amountIn: string;
392
+ targetPrice: string;
393
+ direction: 'gte' | 'lte';
394
+ slippageBps: number;
395
+ expiresInDays: number | null;
396
+ }
397
+ interface DCAStrategy {
398
+ kind: 'dca';
399
+ tokenIn: string;
400
+ tokenOut: string;
401
+ amountPerSwap: string;
402
+ intervalHours: number;
403
+ totalSwaps: number;
404
+ slippageBps: number;
405
+ }
406
+ interface TWAPStrategy {
407
+ kind: 'twap';
408
+ tokenIn: string;
409
+ tokenOut: string;
410
+ totalAmountIn: string;
411
+ numberOfTranches: number;
412
+ intervalHours: number;
413
+ slippageBps: number;
414
+ expiresInDays: number | null;
415
+ }
416
+ interface SwapStrategy {
417
+ kind: 'swap';
418
+ tokenIn: string;
419
+ tokenOut: string;
420
+ amountIn: string;
421
+ slippageBps: number;
422
+ }
423
+ type Strategy = LimitOrderStrategy | DCAStrategy | TWAPStrategy | SwapStrategy;
424
+
425
+ export { type AutomationLogger, type BaseJob, type DCAJob, type DCAParams, type DCAStrategy, DEFAULT_SAFETY_CONFIG, type DecimalsResolver, Executor, type ExecutorOptions, type KeeperClient as IKeeperClient, type Job, type JobStatus, type JobStore, type JobType, type KeeperClient$1 as KeeperClient, type KeeperClientConfig, KeeperClientImpl, type LimitOrderJob, type LimitOrderParams, type LimitOrderStrategy, type PriceCheckResult, PriceChecker, type PriceSource, RetryQueue, type SafetyCheckResult, type SafetyConfig, SafetyGuard, type SafetyViolation, type Strategy, type SwapJob, type SwapParams, type SwapStrategy, type TWAPJob, type TWAPParams, type TWAPStrategy, noopLogger };