@clonegod/ttd-sui-common 2.0.14 → 2.0.15

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.
@@ -25,7 +25,6 @@ export interface SwapSubmitResult {
25
25
  }
26
26
  export interface CentralExecutorOptions {
27
27
  gasBudget?: bigint;
28
- reconcileAfterTx?: boolean;
29
28
  onBroadcastResult?: (r: {
30
29
  digest: string;
31
30
  success: boolean;
@@ -51,7 +50,6 @@ export declare class CentralExecutor {
51
50
  private readonly core;
52
51
  private readonly cache;
53
52
  private readonly gasBudget;
54
- private readonly reconcileAfterTx;
55
53
  private readonly onBroadcastResult?;
56
54
  private tradeWallets;
57
55
  private sharedRefCache;
@@ -67,51 +65,40 @@ export declare class CentralExecutor {
67
65
  rebalanceWalletFunds(): Promise<void>;
68
66
  private rebalanceOne;
69
67
  private rebalanceBalanceMode;
70
- private rebalanceObjectMode;
71
68
  private sweepObjectsToBalance;
72
- private selectWalletWithBalance;
73
69
  private decimalsCache;
74
70
  private objectReader?;
75
71
  private getCoinDecimalsCached;
76
72
  private coinSymbol;
77
73
  private pairLabel;
78
74
  private fmtAmount;
79
- private walletBalancesLabel;
80
- private maintainCoinObjects;
81
- private execMaintenance;
82
75
  private poolGenericsCache;
83
76
  private getPoolGenerics;
84
77
  private canonicalizeReq;
85
78
  private getSharedRefCached;
86
79
  private chainIdentifier;
80
+ private chainIdInflight?;
87
81
  private epochCache;
82
+ private epochInflight?;
83
+ private cachedChainId;
84
+ private cachedEpoch;
88
85
  private getValidDuringExpiration;
89
86
  private getGasPrice;
90
87
  private fetchEpoch;
91
- private fundModeFor;
92
88
  private nextTag;
93
89
  private inputCoinType;
94
90
  private outputCoinType;
95
- private chooseTradeWallet;
96
91
  private registerShared;
97
92
  submitSwap(req: SwapExecRequest): Promise<SwapSubmitResult>;
98
93
  private submitSwapBalance;
99
94
  private broadcastBalanceAndCommit;
100
95
  private isInsufficientBalanceError;
101
96
  private rebuildWithObjects;
102
- private broadcastAndCommit;
103
- private isRebuildableObjectError;
104
- private rebuildSwap;
105
97
  private toCheckerReceipt;
106
- private reconcileAfterFailure;
107
98
  simulateSwap(req: SwapExecRequest): Promise<TxResponse>;
108
- private minSplitFor;
109
- private postTradeRebalance;
110
99
  private swapTxShell;
111
100
  private finishSwapTx;
112
- private buildSwapTxObject;
113
101
  private buildSwapTxBalance;
114
- private onSuccess;
115
102
  get coinCache(): InProcessCoinCache;
116
103
  get tradeWalletAddresses(): string[];
117
104
  get coreClient(): ExecutorCore;
@@ -45,7 +45,6 @@ class CentralExecutor {
45
45
  this.chainIdentifier = '';
46
46
  this.epochCache = null;
47
47
  this.gasBudget = opts.gasBudget ?? (0, format_1.suiToMist)(process.env.SUI_GAS_BUDGET, '0.05');
48
- this.reconcileAfterTx = opts.reconcileAfterTx ?? true;
49
48
  this.onBroadcastResult = opts.onBroadcastResult;
50
49
  this.tradeCoinTypesProvider = opts.tradeCoinTypes;
51
50
  this.tradePoolsProvider = opts.tradePools;
@@ -143,6 +142,7 @@ class CentralExecutor {
143
142
  }
144
143
  }
145
144
  async rebalanceWalletFunds() {
145
+ void this.getValidDuringExpiration().catch(() => { });
146
146
  for (const [addr, kp] of this.tradeWallets) {
147
147
  try {
148
148
  await this.rebalanceOne(addr, kp);
@@ -153,13 +153,10 @@ class CentralExecutor {
153
153
  }
154
154
  }
155
155
  async rebalanceOne(addr, kp) {
156
- if (this.fundModeFor() === 'balance')
157
- return this.rebalanceBalanceMode(addr, kp);
158
- return this.rebalanceObjectMode(addr, kp);
156
+ return this.rebalanceBalanceMode(addr, kp);
159
157
  }
160
158
  async rebalanceBalanceMode(addr, kp) {
161
159
  const SUI = constants_1.SUI_TOKEN_ADDRESS.LONG;
162
- const budget = this.gasBudget;
163
160
  let tradeTypes = [];
164
161
  if (this.tradeCoinTypesProvider) {
165
162
  try {
@@ -169,132 +166,93 @@ class CentralExecutor {
169
166
  (0, dist_1.log_warn)(`[executor] 读交易币种清单失败,本 tick 只归集 SUI: ${e.message}`);
170
167
  }
171
168
  }
172
- for (const t of [SUI, ...new Set(tradeTypes.filter(t => t !== SUI))]) {
169
+ const types = [SUI, ...new Set(tradeTypes.filter(t => t !== SUI))];
170
+ const withObjects = [];
171
+ for (const t of types) {
172
+ try {
173
+ const b = await this.core.getBalance({ owner: addr, coinType: t });
174
+ if (BigInt(b.balance.balance ?? '0') > BigInt(b.balance.addressBalance ?? '0'))
175
+ withObjects.push(t);
176
+ }
177
+ catch (e) {
178
+ withObjects.push(t);
179
+ }
180
+ }
181
+ if (withObjects.length === 0)
182
+ return;
183
+ for (const t of withObjects)
173
184
  await this.reconcileCoins(addr, t, true);
185
+ await this.sweepObjectsToBalance(addr, kp, withObjects, this.gasBudget);
186
+ }
187
+ async sweepObjectsToBalance(addr, kp, coinTypes, budget) {
188
+ const SUI = constants_1.SUI_TOKEN_ADDRESS.LONG;
189
+ const plan = [];
190
+ for (const t of [...new Set(coinTypes)]) {
174
191
  if (this.cache.hasInflight(addr, t))
175
192
  continue;
176
- if (this.cache.snapshot(addr, t).coins.length === 0)
193
+ const coins = this.cache.snapshot(addr, t).coins;
194
+ if (coins.length === 0)
177
195
  continue;
178
- if (await this.sweepObjectsToBalance(addr, kp, t, budget))
179
- return;
196
+ const tag = this.nextTag('sweep');
197
+ if (!this.cache.reserve(addr, t, coins.map(c => c.objectId), tag))
198
+ continue;
199
+ plan.push({ coinType: t, coins, tag });
180
200
  }
181
- }
182
- async rebalanceObjectMode(addr, kp) {
183
- const SUI = constants_1.SUI_TOKEN_ADDRESS.LONG;
184
- const balanceMin = (0, format_1.suiToMist)(process.env.SUI_GAS_BALANCE_MIN, '0.5');
185
- const balanceTarget = (0, format_1.suiToMist)(process.env.SUI_GAS_BALANCE_TARGET, '1');
186
- const chunk = (0, format_1.suiToMist)(process.env.SUI_REDEEM_MIN_CHUNK, '0.1');
187
- const budget = this.gasBudget;
188
- const coinTarget = Number(process.env.SUI_INPUT_COIN_TARGET || 3);
189
- const coinMax = Number(process.env.SUI_INPUT_COIN_MAX || 5);
190
- await this.reconcileCoins(addr, SUI, true);
191
- if (this.cache.hasInflight(addr, SUI))
201
+ if (plan.length === 0)
192
202
  return;
193
- const b = await this.core.getBalance({ owner: addr, coinType: '0x2::sui::SUI' });
194
- const ab = BigInt(b.balance.addressBalance ?? '0');
195
- const coins = this.cache.snapshot(addr, SUI).coins.slice()
196
- .sort((x, y) => (BigInt(y.balance) > BigInt(x.balance) ? 1 : BigInt(y.balance) < BigInt(x.balance) ? -1 : 0));
197
- if (ab < balanceMin) {
198
- const need = balanceTarget - ab;
199
- const src = coins.find(c => BigInt(c.balance) >= need + budget);
200
- if (!src) {
201
- (0, dist_1.log_warn)(`[executor] ${addr.slice(0, 10)}… SUI address-balance ${ab} < gasMin ${balanceMin} 且无足额 SUI 对象充值 —— 请注资`);
203
+ try {
204
+ let suiAb = 0n;
205
+ try {
206
+ const sb = await this.core.getBalance({ owner: addr, coinType: '0x2::sui::SUI' });
207
+ suiAb = BigInt(sb.balance.addressBalance ?? '0');
208
+ }
209
+ catch { }
210
+ const balanceGas = suiAb >= budget;
211
+ const suiPlan = plan.find(p => p.coinType === SUI);
212
+ if (!balanceGas && !suiPlan) {
213
+ (0, dist_1.log_warn)(`[executor] sweep 跳过:SUI balance ${suiAb} < gas ${budget} 且无 SUI 对象付 gas(请注资)`);
202
214
  return;
203
215
  }
204
216
  const tx = new transactions_1.Transaction();
205
217
  tx.setSender(addr);
206
- const [dep] = tx.splitCoins(tx.gas, [tx.pure.u64(need)]);
207
- tx.moveCall({ target: '0x2::coin::send_funds', typeArguments: ['0x2::sui::SUI'], arguments: [dep, tx.pure.address(addr)] });
208
- tx.setGasPayment([{ objectId: src.objectId, version: src.version, digest: src.digest }]);
209
- await this.execMaintenance(tx, addr, kp, budget, `gas 储备充值 ${need} → balance`);
210
- return;
211
- }
212
- if (ab > balanceTarget + chunk) {
213
- const excess = ab - balanceTarget;
214
- const rawClient = this.core.rawClient;
215
- if (!rawClient)
216
- return;
217
- const tx = new transactions_1.Transaction();
218
- tx.setSender(addr);
219
- tx.transferObjects([(0, transactions_1.coinWithBalance)({ type: '0x2::sui::SUI', balance: excess, useGasCoin: false })], tx.pure.address(addr));
220
- tx.setGasPayment([]);
221
- tx.setExpiration(await this.getValidDuringExpiration());
222
- await this.execMaintenance(tx, addr, kp, budget, `redeem 超额 ${excess} → coin`, rawClient);
223
- return;
224
- }
225
- let tradeTypes = [];
226
- if (this.tradeCoinTypesProvider) {
227
- try {
228
- tradeTypes = (await this.tradeCoinTypesProvider()).map(t => (0, format_1.normalizeSuiTokenAddress)(t));
218
+ if (balanceGas) {
219
+ tx.setGasPayment([]);
220
+ tx.setExpiration(await this.getValidDuringExpiration());
221
+ }
222
+ else {
223
+ tx.setGasPayment(suiPlan.coins.map(c => ({ objectId: c.objectId, version: c.version, digest: c.digest })));
224
+ const suiTotal = suiPlan.coins.reduce((s, c) => s + BigInt(c.balance), 0n);
225
+ if (suiTotal > budget) {
226
+ const [dep] = tx.splitCoins(tx.gas, [tx.pure.u64(suiTotal - budget)]);
227
+ tx.moveCall({ target: '0x2::coin::send_funds', typeArguments: [SUI], arguments: [dep, tx.pure.address(addr)] });
228
+ }
229
229
  }
230
- catch (e) {
231
- (0, dist_1.log_warn)(`[executor] 读交易币种清单失败,本 tick 只维护 SUI: ${e.message}`);
230
+ for (const p of plan) {
231
+ if (!balanceGas && p.coinType === SUI)
232
+ continue;
233
+ const primary = tx.object(transactions_1.Inputs.ObjectRef(p.coins[0]));
234
+ if (p.coins.length > 1)
235
+ tx.mergeCoins(primary, p.coins.slice(1).map(c => tx.object(transactions_1.Inputs.ObjectRef(c))));
236
+ tx.moveCall({ target: '0x2::coin::send_funds', typeArguments: [p.coinType], arguments: [primary, tx.pure.address(addr)] });
232
237
  }
238
+ tx.setGasBudget(budget);
239
+ tx.setGasPrice(await this.getGasPrice());
240
+ const bytes = await tx.build();
241
+ const { signature } = await kp.signTransaction(bytes);
242
+ const resp = await this.core.executeTransaction({ transaction: bytes, signatures: [signature], include: { effects: true, objectTypes: true, balanceChanges: true } });
243
+ const st = resp.Transaction?.effects ? (0, effects_1.coreTxStatus)(resp.Transaction.effects) : { success: false, error: 'no effects' };
244
+ const syms = plan.map(p => `${p.coins.length}×${p.coinType.split('::').pop()}`).join(', ');
245
+ (0, dist_1.log_info)(`[executor] sweep 归集 → balance ${st.success ? 'OK' : 'FAIL'} [${syms}] digest=${resp.Transaction?.digest}${st.error ? ' err=' + st.error : ''}`);
233
246
  }
234
- for (const t of [SUI, ...new Set(tradeTypes.filter(t => t !== SUI))]) {
235
- if (t !== SUI)
236
- await this.reconcileCoins(addr, t, true);
237
- if (this.cache.hasInflight(addr, t))
238
- continue;
239
- let minSplitT;
240
- try {
241
- minSplitT = await this.minSplitFor(t);
242
- }
243
- catch (e) {
244
- (0, dist_1.log_warn)(`[executor] 读 ${t} decimals 失败,本 tick 跳过该币种维护: ${e.message}`);
245
- continue;
246
- }
247
- if (await this.maintainCoinObjects(addr, kp, t, { coinTarget, coinMax, budget, minSplit: minSplitT }))
248
- return;
247
+ catch (e) {
248
+ (0, dist_1.log_error)(`[executor] sweep 归集失败`, e);
249
249
  }
250
- }
251
- async sweepObjectsToBalance(addr, kp, coinType, budget) {
252
- const coins = this.cache.snapshot(addr, coinType).coins;
253
- if (coins.length === 0)
254
- return false;
255
- const SUI = constants_1.SUI_TOKEN_ADDRESS.LONG;
256
- const tag = this.nextTag('sweep');
257
- if (!this.cache.reserve(addr, coinType, coins.map(c => c.objectId), tag))
258
- return false;
259
- const tx = new transactions_1.Transaction();
260
- tx.setSender(addr);
261
- let suiAb = 0n;
262
- try {
263
- const sb = await this.core.getBalance({ owner: addr, coinType: '0x2::sui::SUI' });
264
- suiAb = BigInt(sb.balance.addressBalance ?? '0');
265
- }
266
- catch { }
267
- if (coinType === SUI && suiAb < budget) {
268
- const total = coins.reduce((s, c) => s + BigInt(c.balance), 0n);
269
- if (total <= budget) {
270
- this.cache.abort(tag, false);
271
- return false;
250
+ finally {
251
+ for (const p of plan) {
252
+ this.cache.abort(p.tag, true);
253
+ await this.reconcileCoins(addr, p.coinType);
272
254
  }
273
- tx.setGasPayment(coins.map(c => ({ objectId: c.objectId, version: c.version, digest: c.digest })));
274
- const [dep] = tx.splitCoins(tx.gas, [tx.pure.u64(total - budget)]);
275
- tx.moveCall({ target: '0x2::coin::send_funds', typeArguments: [SUI], arguments: [dep, tx.pure.address(addr)] });
276
- }
277
- else {
278
- const primary = tx.object(transactions_1.Inputs.ObjectRef(coins[0]));
279
- if (coins.length > 1)
280
- tx.mergeCoins(primary, coins.slice(1).map(c => tx.object(transactions_1.Inputs.ObjectRef(c))));
281
- tx.moveCall({ target: '0x2::coin::send_funds', typeArguments: [coinType], arguments: [primary, tx.pure.address(addr)] });
282
- tx.setGasPayment([]);
283
- tx.setExpiration(await this.getValidDuringExpiration());
284
- }
285
- await this.execMaintenance(tx, addr, kp, budget, `归集 ${coins.length} 个 ${coinType.split('::').pop()} 对象 → balance`, undefined, coinType, tag);
286
- return true;
287
- }
288
- async selectWalletWithBalance(inType, amount) {
289
- let best = null;
290
- for (const w of this.tradeWallets.keys()) {
291
- const objs = this.cache.snapshot(w, inType).total;
292
- const b = await this.core.getBalance({ owner: w, coinType: inType });
293
- const ab = BigInt(b.balance.addressBalance ?? '0');
294
- if (objs + ab >= amount && (!best || objs + ab > best.sum))
295
- best = { wallet: w, addressBalance: ab, sum: objs + ab };
296
255
  }
297
- return best ? { wallet: best.wallet, addressBalance: best.addressBalance } : null;
298
256
  }
299
257
  async getCoinDecimalsCached(coinType) {
300
258
  let d = this.decimalsCache.get(coinType);
@@ -324,73 +282,6 @@ class CentralExecutor {
324
282
  const frac = (v % base).toString().padStart(d, '0').replace(/0+$/, '');
325
283
  return `${neg ? '-' : ''}${intStr}${frac ? '.' + frac : ''} ${sym}`;
326
284
  }
327
- walletBalancesLabel(coinType) {
328
- return [...this.tradeWallets.keys()].map(w => {
329
- const { total, count } = this.cache.snapshot(w, coinType);
330
- return `${w.slice(0, 8)}…=${this.fmtAmount(coinType, total)}(${count}枚)`;
331
- }).join(', ');
332
- }
333
- async maintainCoinObjects(addr, kp, coinType, opts) {
334
- const short = coinType.split('::').pop();
335
- const zeroMergeMin = Number(process.env.SUI_ZERO_COIN_MERGE_THRESHOLD || 3);
336
- const coins = this.cache.snapshot(addr, coinType).coins;
337
- const valued = coins.filter(c => BigInt(c.balance) > 0n);
338
- const zeros = coins.filter(c => BigInt(c.balance) === 0n);
339
- if (coins.length === 0)
340
- return false;
341
- const total = valued.reduce((s, c) => s + BigInt(c.balance), 0n);
342
- const maxPieces = opts.minSplit > 0n ? total / opts.minSplit : 0n;
343
- const pieces = Math.max(1, Number(maxPieces > BigInt(opts.coinTarget) ? BigInt(opts.coinTarget) : maxPieces));
344
- const needSplit = valued.length > 0 && valued.length < opts.coinTarget && pieces > valued.length;
345
- const needZeroPurge = zeros.length >= zeroMergeMin;
346
- const needDefrag = coins.length > opts.coinMax;
347
- if (!needSplit && !needZeroPurge && !needDefrag)
348
- return false;
349
- const tag = this.nextTag('maint');
350
- if (!this.cache.reserve(addr, coinType, coins.map(c => c.objectId), tag))
351
- return false;
352
- const primaryRef = valued[0] ?? coins[0];
353
- const tx = new transactions_1.Transaction();
354
- tx.setSender(addr);
355
- const primary = tx.object(transactions_1.Inputs.ObjectRef(primaryRef));
356
- const others = coins.filter(c => c.objectId !== primaryRef.objectId);
357
- if (others.length)
358
- tx.mergeCoins(primary, others.map(c => tx.object(transactions_1.Inputs.ObjectRef(c))));
359
- const per = pieces > 1 ? total / BigInt(pieces) : 0n;
360
- if (pieces > 1) {
361
- const splits = tx.splitCoins(primary, Array.from({ length: pieces - 1 }, () => tx.pure.u64(per)));
362
- tx.transferObjects(Array.from({ length: pieces - 1 }, (_, i) => splits[i]), tx.pure.address(addr));
363
- }
364
- tx.setGasPayment([]);
365
- tx.setExpiration(await this.getValidDuringExpiration());
366
- await this.execMaintenance(tx, addr, kp, opts.budget, `整理 ${short}:${valued.length} 值 + ${zeros.length} 壳 → ${pieces} 份均分(每份≈${(total / BigInt(pieces)).toString()})`, undefined, coinType, tag, { primaryId: primaryRef.objectId, perPiece: per, primaryRemainder: total - per * BigInt(pieces - 1), reservedIds: coins.map(c => c.objectId) });
367
- return true;
368
- }
369
- async execMaintenance(tx, addr, kp, budget, label, buildClient, coinType = constants_1.SUI_TOKEN_ADDRESS.LONG, reserveTag, commitPlan) {
370
- let committed = false;
371
- try {
372
- tx.setGasBudget(budget);
373
- tx.setGasPrice(await this.getGasPrice());
374
- const bytes = buildClient ? await tx.build({ client: buildClient }) : await tx.build();
375
- const { signature } = await kp.signTransaction(bytes);
376
- const resp = await this.core.executeTransaction({ transaction: bytes, signatures: [signature], include: { effects: true, objectTypes: true, balanceChanges: true } });
377
- const tr = resp.Transaction;
378
- const { success, error } = (0, effects_1.coreTxStatus)(tr.effects);
379
- (0, dist_1.log_info)(`[executor] rebalance ${label} ${success ? 'OK' : 'FAILED'} wallet=${addr.slice(0, 10)}… digest=${tr.digest}${error ? ' err=' + error : ''}`);
380
- if (success && commitPlan && reserveTag && tr.effects) {
381
- const changes = (0, effects_1.extractMaintCoinChanges)(tr.effects, { wallet: addr, coinType, ...commitPlan });
382
- this.cache.commit(reserveTag, changes);
383
- committed = true;
384
- }
385
- }
386
- finally {
387
- if (!committed) {
388
- if (reserveTag)
389
- this.cache.abort(reserveTag, true);
390
- await this.reconcileCoins(addr, coinType);
391
- }
392
- }
393
- }
394
285
  async getPoolGenerics(poolId) {
395
286
  const hit = this.poolGenericsCache.get(poolId);
396
287
  if (hit !== undefined)
@@ -442,25 +333,39 @@ class CentralExecutor {
442
333
  }
443
334
  return isv;
444
335
  }
445
- async getValidDuringExpiration() {
446
- if (!this.chainIdentifier) {
447
- try {
448
- this.chainIdentifier = (await this.core.getChainIdentifier()).chainIdentifier;
449
- }
450
- catch (e) {
451
- this.chainIdentifier = process.env.SUI_CHAIN_IDENTIFIER || '35834a8a';
452
- (0, dist_1.log_warn)(`[executor] getChainIdentifier 失败(${e.message}),使用兜底 chain id ${this.chainIdentifier}`);
453
- }
454
- }
336
+ async cachedChainId() {
337
+ if (this.chainIdentifier)
338
+ return this.chainIdentifier;
339
+ if (!this.chainIdInflight) {
340
+ this.chainIdInflight = this.core.getChainIdentifier().then(r => r.chainIdentifier)
341
+ .catch(e => {
342
+ const fb = process.env.SUI_CHAIN_IDENTIFIER || '35834a8a';
343
+ (0, dist_1.log_warn)(`[executor] getChainIdentifier 失败(${e.message}),使用兜底 chain id ${fb}`);
344
+ return fb;
345
+ })
346
+ .then(id => { this.chainIdentifier = id; return id; })
347
+ .finally(() => { this.chainIdInflight = undefined; });
348
+ }
349
+ return this.chainIdInflight;
350
+ }
351
+ async cachedEpoch() {
455
352
  const now = Date.now();
456
- if (!this.epochCache || now - this.epochCache.ts > 300_000) {
457
- this.epochCache = { epoch: await this.fetchEpoch(), ts: now };
353
+ if (this.epochCache && now - this.epochCache.ts <= 3_600_000)
354
+ return this.epochCache.epoch;
355
+ if (!this.epochInflight) {
356
+ this.epochInflight = this.fetchEpoch()
357
+ .then(e => { this.epochCache = { epoch: e, ts: Date.now() }; return e; })
358
+ .finally(() => { this.epochInflight = undefined; });
458
359
  }
360
+ return this.epochInflight;
361
+ }
362
+ async getValidDuringExpiration() {
363
+ const [chain, epoch] = await Promise.all([this.cachedChainId(), this.cachedEpoch()]);
459
364
  return { ValidDuring: {
460
- minEpoch: String(this.epochCache.epoch),
461
- maxEpoch: String(this.epochCache.epoch + 1n),
365
+ minEpoch: String(epoch),
366
+ maxEpoch: String(epoch + 1n),
462
367
  minTimestamp: null, maxTimestamp: null,
463
- chain: this.chainIdentifier,
368
+ chain,
464
369
  nonce: Math.floor(Math.random() * 0xFFFFFFFF),
465
370
  } };
466
371
  }
@@ -483,31 +388,9 @@ class CentralExecutor {
483
388
  throw new Error('[executor] getCurrentSystemState 未返回 epoch(ValidDuring 必需)');
484
389
  return BigInt(e);
485
390
  }
486
- fundModeFor(_coinType) {
487
- return (process.env.SUI_FUND_MODE || 'balance').toLowerCase() === 'object' ? 'object' : 'balance';
488
- }
489
391
  nextTag(prefix) { return `${prefix}:${Date.now()}:${++this.seq}`; }
490
392
  inputCoinType(req) { return req.a2b ? req.coinTypeA : req.coinTypeB; }
491
393
  outputCoinType(req) { return req.a2b ? req.coinTypeB : req.coinTypeA; }
492
- chooseTradeWallet(req, coinType, amount) {
493
- if (req.walletAddress) {
494
- if (!this.tradeWallets.has(req.walletAddress))
495
- throw new Error(`未知交易钱包: ${req.walletAddress}`);
496
- return req.walletAddress;
497
- }
498
- let best = '';
499
- let bestTotal = -1n;
500
- for (const w of this.tradeWallets.keys()) {
501
- const { total } = this.cache.snapshot(w, coinType);
502
- if (total >= amount && total > bestTotal) {
503
- best = w;
504
- bestTotal = total;
505
- }
506
- }
507
- if (!best)
508
- throw new Error(`无交易钱包持有 ≥ ${amount} 的 ${coinType}`);
509
- return best;
510
- }
511
394
  async registerShared(tx, objectId, mutable) {
512
395
  const initialSharedVersion = await this.getSharedRefCached(objectId);
513
396
  tx.object(transactions_1.Inputs.SharedObjectRef({ objectId, initialSharedVersion, mutable }));
@@ -517,57 +400,7 @@ class CentralExecutor {
517
400
  req = await this.canonicalizeReq(req);
518
401
  const inType = this.inputCoinType(req);
519
402
  await this.getCoinDecimalsCached(inType).catch(() => { });
520
- if (this.fundModeFor(inType) === 'balance')
521
- return this.submitSwapBalance(req, t0);
522
- let wallet = '';
523
- let inputCoins = [];
524
- let inputTag = '';
525
- let shortfall = 0n;
526
- try {
527
- wallet = this.chooseTradeWallet(req, inType, req.amountIn);
528
- inputTag = this.nextTag('in');
529
- inputCoins = this.cache.acquire(wallet, inType, req.amountIn, inputTag).coins;
530
- }
531
- catch (e) {
532
- (0, dist_1.log_warn)(`[executor] ${this.pairLabel(req)} 对象不足:需 ${this.fmtAmount(inType, req.amountIn)},当前缓存 [${this.walletBalancesLabel(inType)}] → reconcile 重试`);
533
- await Promise.all([...this.tradeWallets.keys()].map(w => this.reconcileCoins(w, inType)));
534
- try {
535
- wallet = this.chooseTradeWallet(req, inType, req.amountIn);
536
- inputTag = this.nextTag('in');
537
- inputCoins = this.cache.acquire(wallet, inType, req.amountIn, inputTag).coins;
538
- }
539
- catch (e2) {
540
- const picked = await this.selectWalletWithBalance(inType, req.amountIn);
541
- if (!picked) {
542
- (0, dist_1.log_error)(`[executor] ${this.pairLabel(req)} 余额不足:需 ${this.fmtAmount(inType, req.amountIn)},对象+address-balance 仍不够 [${this.walletBalancesLabel(inType)}]`, e2);
543
- throw e2;
544
- }
545
- wallet = picked.wallet;
546
- inputTag = this.nextTag('in');
547
- inputCoins = this.cache.acquireAvailable(wallet, inType, req.amountIn, inputTag);
548
- const objSum = inputCoins.reduce((s, c) => s + BigInt(c.balance), 0n);
549
- shortfall = req.amountIn - objSum;
550
- }
551
- }
552
- const tAcquire = Date.now();
553
- const acqTotal = inputCoins.reduce((s, c) => s + BigInt(c.balance), 0n);
554
- const shortStr = shortfall > 0n ? ` + address-balance 补 ${this.fmtAmount(inType, shortfall)}` : '';
555
- (0, dist_1.log_info)(`[executor] coin 已占用 wallet=${wallet.slice(0, 8)}… ${this.pairLabel(req)} 取 ${inputCoins.length} 枚=${this.fmtAmount(inType, acqTotal)}${shortStr}(需 ${this.fmtAmount(inType, req.amountIn)})选钱包+占币 ${tAcquire - t0}ms`);
556
- try {
557
- const { txBytes, tx } = await this.buildSwapTxObject(req, wallet, inputCoins, shortfall);
558
- const tBuild = Date.now();
559
- const { signature: senderSig } = await this.tradeWallets.get(wallet).signTransaction(txBytes);
560
- const digest = await tx.getDigest();
561
- const tSign = Date.now();
562
- void this.broadcastAndCommit(txBytes, [senderSig], digest, wallet, req, inputCoins, inputTag, 0, shortfall > 0n);
563
- (0, dist_1.log_info)(`[executor] swap 已签提交 digest=${digest} dex=${req.dexId} ${this.pairLabel(req)} in=${this.fmtAmount(inType, req.amountIn)} minOut=${this.fmtAmount(this.outputCoinType(req), req.minOut)} cost={build:${tBuild - tAcquire}ms, sign+digest:${tSign - tBuild}ms, total:${tSign - t0}ms}`);
564
- return { digest, submitted: true };
565
- }
566
- catch (e) {
567
- this.cache.abort(inputTag, false);
568
- (0, dist_1.log_error)(`[executor] submitSwap 失败 dex=${req.dexId}`, e);
569
- return { digest: '', submitted: false, error: e.message };
570
- }
403
+ return this.submitSwapBalance(req, t0);
571
404
  }
572
405
  async submitSwapBalance(req, t0) {
573
406
  const inType = this.inputCoinType(req);
@@ -613,12 +446,12 @@ class CentralExecutor {
613
446
  this.onBroadcastResult?.({ digest, success: false, error, receipt: this.toCheckerReceipt(tr, digest) });
614
447
  return;
615
448
  }
616
- if (inputTag)
617
- this.cache.abort(inputTag, true);
618
449
  (0, dist_1.log_info)(`[executor] swap quorum-executed 确认(balance) digest=${digest} broadcast=${Date.now() - tBroadcast}ms`);
619
450
  this.onBroadcastResult?.({ digest, success: true, receipt: this.toCheckerReceipt(tr, digest) });
620
- if (this.reconcileAfterTx)
621
- void this.postTradeRebalance(wallet, inType, outType);
451
+ if (inputTag) {
452
+ this.cache.abort(inputTag, true);
453
+ void this.reconcileCoins(wallet, inType);
454
+ }
622
455
  }
623
456
  catch (e) {
624
457
  if (!inputTag && this.isInsufficientBalanceError(e) && attempt < this.maxRebuildRetries) {
@@ -663,71 +496,6 @@ class CentralExecutor {
663
496
  return null;
664
497
  }
665
498
  }
666
- async broadcastAndCommit(txBytes, signatures, digest, wallet, req, inputCoins, inputTag, attempt = 0, usedAddressBalance = false) {
667
- const inType = this.inputCoinType(req);
668
- const outType = this.outputCoinType(req);
669
- const tBroadcast = Date.now();
670
- try {
671
- const resp = await this.core.executeTransaction({ transaction: txBytes, signatures, include: { effects: true, objectTypes: true, balanceChanges: true } });
672
- const tr = resp.Transaction;
673
- if (!tr?.effects) {
674
- this.cache.abort(inputTag, true);
675
- this.reconcileAfterFailure(wallet, inType, outType);
676
- (0, dist_1.log_error)(`[executor] 响应缺 effects,无法更新 cache digest=${digest}`, new Error('missing effects'));
677
- this.onBroadcastResult?.({ digest, success: false, error: 'missing effects' });
678
- return;
679
- }
680
- const { success, error } = (0, effects_1.coreTxStatus)(tr.effects);
681
- if (!success) {
682
- this.cache.abort(inputTag, true);
683
- this.reconcileAfterFailure(wallet, inType, outType);
684
- (0, dist_1.log_warn)(`[executor] swap 链上失败 digest=${digest} err=${error} broadcast=${Date.now() - tBroadcast}ms`);
685
- this.onBroadcastResult?.({ digest, success: false, error, receipt: this.toCheckerReceipt(tr, digest) });
686
- return;
687
- }
688
- await this.onSuccess(tr, wallet, req, inputCoins, inputTag, usedAddressBalance);
689
- (0, dist_1.log_info)(`[executor] swap quorum-executed 确认 digest=${digest} broadcast=${Date.now() - tBroadcast}ms`);
690
- this.onBroadcastResult?.({ digest, success: true, receipt: this.toCheckerReceipt(tr, digest) });
691
- }
692
- catch (e) {
693
- this.cache.abort(inputTag, true);
694
- if (!usedAddressBalance && this.isRebuildableObjectError(e) && attempt < this.maxRebuildRetries) {
695
- (0, dist_1.log_warn)(`[executor] swap 输入对象版本陈旧,定向刷新 ${inType.split('::').pop()} + 重建重发(第 ${attempt + 1}/${this.maxRebuildRetries} 次)digest=${digest}: ${e.message}`);
696
- await this.reconcileCoins(wallet, inType);
697
- const retry = await this.rebuildSwap(req, wallet);
698
- if (retry) {
699
- void this.broadcastAndCommit(retry.txBytes, retry.signatures, retry.digest, wallet, req, retry.inputCoins, retry.inputTag, attempt + 1);
700
- return;
701
- }
702
- (0, dist_1.log_warn)(`[executor] swap 重建失败(输入不足等),转 reconcile 兜底 digest=${digest}`);
703
- }
704
- this.reconcileAfterFailure(wallet, inType, outType);
705
- (0, dist_1.log_error)(`[executor] broadcast 失败 digest=${digest} broadcast=${Date.now() - tBroadcast}ms attempt=${attempt}`, e);
706
- }
707
- }
708
- isRebuildableObjectError(e) {
709
- const err = e;
710
- const msg = (err?.message || String(e)).toLowerCase();
711
- return msg.includes('unavailable for consumption')
712
- || msg.includes('needs to be rebuilt')
713
- || (err?.code === 'INVALID_ARGUMENT' && msg.includes('object'));
714
- }
715
- async rebuildSwap(req, wallet) {
716
- try {
717
- const inType = this.inputCoinType(req);
718
- const inputTag = this.nextTag('in');
719
- const inputRes = this.cache.acquire(wallet, inType, req.amountIn, inputTag);
720
- const { txBytes, tx } = await this.buildSwapTxObject(req, wallet, inputRes.coins);
721
- const { signature } = await this.tradeWallets.get(wallet).signTransaction(txBytes);
722
- const digest = await tx.getDigest();
723
- (0, dist_1.log_info)(`[executor] swap 重建 digest=${digest} dex=${req.dexId} ${req.a2b ? 'a2b' : 'b2a'} in=${req.amountIn}`);
724
- return { txBytes, signatures: [signature], digest, inputCoins: inputRes.coins, inputTag };
725
- }
726
- catch (e) {
727
- (0, dist_1.log_warn)(`[executor] swap 重建失败 dex=${req.dexId}: ${e.message}`);
728
- return null;
729
- }
730
- }
731
499
  toCheckerReceipt(tr, digest) {
732
500
  const eff = tr.effects;
733
501
  const status = eff?.status?.success
@@ -752,73 +520,16 @@ class CentralExecutor {
752
520
  },
753
521
  };
754
522
  }
755
- reconcileAfterFailure(wallet, inType, outType) {
756
- for (const t of new Set([inType, outType, constants_1.SUI_TOKEN_ADDRESS.LONG]))
757
- void this.reconcileCoins(wallet, t);
758
- }
759
523
  async simulateSwap(req) {
760
524
  req = await this.canonicalizeReq(req);
761
- const inType = this.inputCoinType(req);
762
- const balanceMode = this.fundModeFor() === 'balance';
763
- const wallet = req.walletAddress ?? (balanceMode ? this.tradeWalletAddresses[0] : this.chooseTradeWallet(req, inType, req.amountIn));
764
- let txBytes;
765
- if (balanceMode) {
766
- ({ txBytes } = await this.buildSwapTxBalance(req, wallet));
767
- }
768
- else {
769
- const { coins } = this.cache.snapshot(wallet, inType);
770
- if (!coins.length)
771
- throw new Error(`[executor] simulate 无 ${inType} 输入币(先 reconcileCoins)`);
772
- const picked = [];
773
- let sum = 0n;
774
- for (const c of coins) {
775
- picked.push(c);
776
- sum += BigInt(c.balance);
777
- if (sum >= req.amountIn)
778
- break;
779
- }
780
- if (sum < req.amountIn)
781
- throw new Error(`[executor] simulate 输入不足 ${sum} < ${req.amountIn}`);
782
- ({ txBytes } = await this.buildSwapTxObject(req, wallet, picked));
783
- }
525
+ const wallet = req.walletAddress ?? this.tradeWalletAddresses[0];
526
+ const { txBytes } = await this.buildSwapTxBalance(req, wallet);
784
527
  const res = await this.core.simulateTransaction({ transaction: txBytes, include: { effects: true, objectTypes: true, balanceChanges: true } });
785
528
  if (!res?.Transaction) {
786
529
  throw new Error(`[executor] simulate FailedTransaction: ${JSON.stringify(res?.FailedTransaction ?? res).slice(0, 400)}`);
787
530
  }
788
531
  return res.Transaction;
789
532
  }
790
- async minSplitFor(coinType) {
791
- if (coinType === constants_1.SUI_TOKEN_ADDRESS.LONG) {
792
- return (0, format_1.suiToMist)(process.env.SUI_INPUT_COIN_MIN_SPLIT, '0.1');
793
- }
794
- const d = await this.getCoinDecimalsCached(coinType);
795
- return 10n ** BigInt(d) / 10n;
796
- }
797
- async postTradeRebalance(wallet, inType, outType) {
798
- const kp = this.tradeWallets.get(wallet);
799
- if (!kp)
800
- return;
801
- const coinTarget = Number(process.env.SUI_INPUT_COIN_TARGET || 3);
802
- const coinMax = Number(process.env.SUI_INPUT_COIN_MAX || 5);
803
- const budget = this.gasBudget;
804
- for (const t of new Set([inType, outType, constants_1.SUI_TOKEN_ADDRESS.LONG])) {
805
- try {
806
- await this.reconcileCoins(wallet, t);
807
- if (this.cache.hasInflight(wallet, t))
808
- continue;
809
- if (this.fundModeFor(t) === 'balance') {
810
- if (this.cache.snapshot(wallet, t).coins.length > 0)
811
- await this.sweepObjectsToBalance(wallet, kp, t, budget);
812
- }
813
- else {
814
- await this.maintainCoinObjects(wallet, kp, t, { coinTarget, coinMax, budget, minSplit: await this.minSplitFor(t) });
815
- }
816
- }
817
- catch (e) {
818
- (0, dist_1.log_warn)(`[executor] postTradeRebalance ${t} 失败(交定时 tick 兜底): ${e.message}`);
819
- }
820
- }
821
- }
822
533
  async swapTxShell(req, wallet) {
823
534
  const tx = new transactions_1.Transaction();
824
535
  tx.setSender(wallet);
@@ -835,33 +546,6 @@ class CentralExecutor {
835
546
  const txBytes = online ? await tx.build({ client: this.core.rawClient }) : await tx.build();
836
547
  return { txBytes, tx };
837
548
  }
838
- async buildSwapTxObject(req, wallet, inputCoins, shortfall = 0n) {
839
- const tx = await this.swapTxShell(req, wallet);
840
- const inType = this.inputCoinType(req);
841
- let inCoin;
842
- let usedResolver = false;
843
- if (inputCoins.length === 0) {
844
- inCoin = (0, transactions_1.coinWithBalance)({ type: inType, balance: req.amountIn });
845
- usedResolver = true;
846
- }
847
- else {
848
- const primary = tx.object(transactions_1.Inputs.ObjectRef(inputCoins[0]));
849
- if (inputCoins.length > 1)
850
- tx.mergeCoins(primary, inputCoins.slice(1).map(c => tx.object(transactions_1.Inputs.ObjectRef(c))));
851
- if (shortfall > 0n) {
852
- tx.mergeCoins(primary, [(0, transactions_1.coinWithBalance)({ type: inType, balance: shortfall })]);
853
- usedResolver = true;
854
- }
855
- ;
856
- [inCoin] = tx.splitCoins(primary, [tx.pure.u64(req.amountIn)]);
857
- }
858
- const { outputCoin, leftoverCoins } = (0, swap_1.buildSwapMoveCall)(req.dexId, tx, {
859
- coinTypeA: req.coinTypeA, coinTypeB: req.coinTypeB, poolId: req.poolId,
860
- a2b: req.a2b, byAmountIn: true, amount: req.amountIn, amountLimit: req.minOut, sqrtPriceLimit: req.sqrtPriceLimit,
861
- }, inCoin);
862
- tx.transferObjects([outputCoin, ...leftoverCoins], tx.pure.address(wallet));
863
- return this.finishSwapTx(tx, usedResolver);
864
- }
865
549
  async buildSwapTxBalance(req, wallet, objectCoins = [], shortfall = 0n) {
866
550
  const tx = await this.swapTxShell(req, wallet);
867
551
  const inType = this.inputCoinType(req);
@@ -893,27 +577,6 @@ class CentralExecutor {
893
577
  tx.moveCall({ target: '0x2::coin::send_funds', typeArguments: [inType], arguments: [lo, tx.pure.address(wallet)] });
894
578
  return this.finishSwapTx(tx, false);
895
579
  }
896
- async onSuccess(tr, wallet, req, inputCoins, inputTag, usedAddressBalance = false) {
897
- const inType = this.inputCoinType(req);
898
- const outType = this.outputCoinType(req);
899
- if (usedAddressBalance) {
900
- this.cache.abort(inputTag, true);
901
- await this.reconcileCoins(wallet, inType);
902
- }
903
- else {
904
- const inputTotal = inputCoins.reduce((s, c) => s + BigInt(c.balance), 0n);
905
- const inputChanges = (0, effects_1.extractSwapInputChanges)(tr.effects, {
906
- wallet, inCoinType: inType,
907
- primaryId: inputCoins[0].objectId,
908
- reservedInputIds: inputCoins.map(c => c.objectId),
909
- inputTotal, amountIn: req.amountIn,
910
- });
911
- this.cache.commit(inputTag, inputChanges);
912
- }
913
- if (this.reconcileAfterTx) {
914
- void this.postTradeRebalance(wallet, inType, outType);
915
- }
916
- }
917
580
  get coinCache() { return this.cache; }
918
581
  get tradeWalletAddresses() { return [...this.tradeWallets.keys()]; }
919
582
  get coreClient() { return this.core; }
@@ -8,7 +8,7 @@ class CoinMaintainer {
8
8
  this.executor = executor;
9
9
  this.timer = null;
10
10
  this.running = false;
11
- this.intervalMs = opts.intervalMs ?? (0, format_1.parseDurationMs)(process.env.SUI_COIN_MAINTAIN_INTERVAL_MS, 60000);
11
+ this.intervalMs = opts.intervalMs ?? (0, format_1.parseDurationMs)(process.env.SUI_COIN_MAINTAIN_INTERVAL_MS, 300000);
12
12
  }
13
13
  start() {
14
14
  if (this.timer)
@@ -1,5 +1,5 @@
1
1
  import { CoreClient, SuiClientTypes } from '@mysten/sui/client';
2
- export declare function fetchCurrentEpochViaGraphql(url: string): Promise<bigint>;
2
+ export declare function fetchCurrentEpochViaGraphql(url: string, retries?: number): Promise<bigint>;
3
3
  export interface GrpcCoreOptions {
4
4
  endpoint?: string;
5
5
  token?: string;
@@ -26,13 +26,29 @@ const GRPC_CLIENT_OPTIONS = {
26
26
  'grpc-node.max_session_memory': 64,
27
27
  'grpc.default_compression_algorithm': 2,
28
28
  };
29
- async function fetchCurrentEpochViaGraphql(url) {
30
- const client = new graphql_1.SuiGraphQLClient({ url, network: 'mainnet' });
31
- const res = await client.query({ query: 'query { epoch { epochId } }', variables: {} });
32
- const e = res?.data?.epoch?.epochId;
33
- if (e == null)
34
- throw new Error(`GraphQL epoch 查询无结果: ${JSON.stringify(res?.errors ?? res).slice(0, 200)}`);
35
- return BigInt(e);
29
+ const _gqlEpochClients = new Map();
30
+ async function fetchCurrentEpochViaGraphql(url, retries = 2) {
31
+ let client = _gqlEpochClients.get(url);
32
+ if (!client) {
33
+ client = new graphql_1.SuiGraphQLClient({ url, network: 'mainnet' });
34
+ _gqlEpochClients.set(url, client);
35
+ }
36
+ let lastErr;
37
+ for (let i = 0; i <= retries; i++) {
38
+ try {
39
+ const res = await client.query({ query: 'query { epoch { epochId } }', variables: {} });
40
+ const e = res?.data?.epoch?.epochId;
41
+ if (e == null)
42
+ throw new Error(`GraphQL epoch 查询无结果: ${JSON.stringify(res?.errors ?? res).slice(0, 200)}`);
43
+ return BigInt(e);
44
+ }
45
+ catch (e) {
46
+ lastErr = e;
47
+ if (i < retries)
48
+ await (0, dist_1.sleep)(150 * (i + 1));
49
+ }
50
+ }
51
+ throw lastErr;
36
52
  }
37
53
  function buildGrpcCore(opts = {}) {
38
54
  const endpoint = opts.endpoint ?? (0, dist_1.getCoreEnv)().grpc_endpoint;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clonegod/ttd-sui-common",
3
- "version": "2.0.14",
3
+ "version": "2.0.15",
4
4
  "description": "Sui common library",
5
5
  "license": "UNLICENSED",
6
6
  "main": "dist/index.js",