@clonegod/ttd-sui-common 2.0.8 → 2.0.9

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.
@@ -35,6 +35,12 @@ export interface CentralExecutorOptions {
35
35
  };
36
36
  }) => void;
37
37
  tradeCoinTypes?: () => Promise<string[]>;
38
+ tradePools?: () => Promise<Array<{
39
+ dexId: DEX_ID;
40
+ poolId: string;
41
+ coinTypeA: string;
42
+ coinTypeB: string;
43
+ }>>;
38
44
  }
39
45
  type TxResponse = SuiClientTypes.Transaction<{
40
46
  effects: true;
@@ -51,8 +57,11 @@ export declare class CentralExecutor {
51
57
  private sharedRefCache;
52
58
  private seq;
53
59
  private readonly tradeCoinTypesProvider?;
60
+ private readonly tradePoolsProvider?;
61
+ private readonly maxRebuildRetries;
54
62
  constructor(core: ExecutorCore, opts?: CentralExecutorOptions);
55
63
  init(): Promise<void>;
64
+ private warmup;
56
65
  reconcileCoins(wallet: string, coinType: string, quietIfUnchanged?: boolean): Promise<void>;
57
66
  rebalanceWalletFunds(): Promise<void>;
58
67
  private rebalanceOne;
@@ -76,6 +85,8 @@ export declare class CentralExecutor {
76
85
  private registerShared;
77
86
  submitSwap(req: SwapExecRequest): Promise<SwapSubmitResult>;
78
87
  private broadcastAndCommit;
88
+ private isRebuildableObjectError;
89
+ private rebuildSwap;
79
90
  private toCheckerReceipt;
80
91
  private reconcileAfterFailure;
81
92
  simulateSwap(req: SwapExecRequest): Promise<TxResponse>;
@@ -38,6 +38,7 @@ class CentralExecutor {
38
38
  this.tradeWallets = new Map();
39
39
  this.sharedRefCache = new Map();
40
40
  this.seq = 0;
41
+ this.maxRebuildRetries = Number(process.env.SUI_SWAP_REBUILD_RETRIES || 2);
41
42
  this.decimalsCache = new Map();
42
43
  this.poolGenericsCache = new Map();
43
44
  this.chainIdentifier = '';
@@ -46,6 +47,7 @@ class CentralExecutor {
46
47
  this.reconcileAfterTx = opts.reconcileAfterTx ?? true;
47
48
  this.onBroadcastResult = opts.onBroadcastResult;
48
49
  this.tradeCoinTypesProvider = opts.tradeCoinTypes;
50
+ this.tradePoolsProvider = opts.tradePools;
49
51
  }
50
52
  async init() {
51
53
  const tradeIds = (process.env.SUI_WALLET_GROUP_IDS || '').split(',').map(s => s.trim()).filter(Boolean);
@@ -59,6 +61,36 @@ class CentralExecutor {
59
61
  await this.reconcileCoins(w, constants_1.SUI_TOKEN_ADDRESS.LONG);
60
62
  }
61
63
  (0, dist_1.log_info)(`[executor] init: trade=${[...this.tradeWallets.keys()].join(',')} (单钱包模式: gas=各自地址余额), gasBudget=${this.gasBudget}`);
64
+ await this.warmup();
65
+ }
66
+ async warmup() {
67
+ try {
68
+ await Promise.all([this.getGasPrice(), this.getValidDuringExpiration()]);
69
+ }
70
+ catch (e) {
71
+ (0, dist_1.log_warn)(`[executor] 预热全局缓存(gas/epoch)失败(首笔将冷启动): ${e.message}`);
72
+ }
73
+ if (!this.tradePoolsProvider)
74
+ return;
75
+ try {
76
+ const pools = await this.tradePoolsProvider();
77
+ let ok = 0;
78
+ await Promise.all(pools.map(async (p) => {
79
+ try {
80
+ await this.canonicalizeReq({ dexId: p.dexId, poolId: p.poolId, coinTypeA: p.coinTypeA, coinTypeB: p.coinTypeB, a2b: true, amountIn: 0n, minOut: 0n, sqrtPriceLimit: 0n });
81
+ await this.getSharedRefCached(p.poolId);
82
+ await this.getSharedRefCached((0, swap_1.configSharedObjectId)(p.dexId));
83
+ ok++;
84
+ }
85
+ catch (e) {
86
+ (0, dist_1.log_warn)(`[executor] 预热池 ${p.poolId.slice(0, 10)}…(${p.dexId}) 失败(首笔冷启动,自愈): ${e.message}`);
87
+ }
88
+ }));
89
+ (0, dist_1.log_info)(`[executor] 预热完成: ${ok}/${pools.length} 池 shared ref + 链上真序 + gas/epoch 全局缓存`);
90
+ }
91
+ catch (e) {
92
+ (0, dist_1.log_warn)(`[executor] 读交易池清单失败,跳过池级预热: ${e.message}`);
93
+ }
62
94
  }
63
95
  async reconcileCoins(wallet, coinType, quietIfUnchanged = false) {
64
96
  try {
@@ -202,17 +234,18 @@ class CentralExecutor {
202
234
  const others = coins.filter(c => c.objectId !== primaryRef.objectId);
203
235
  if (others.length)
204
236
  tx.mergeCoins(primary, others.map(c => tx.object(transactions_1.Inputs.ObjectRef(c))));
237
+ const per = pieces > 1 ? total / BigInt(pieces) : 0n;
205
238
  if (pieces > 1) {
206
- const per = total / BigInt(pieces);
207
239
  const splits = tx.splitCoins(primary, Array.from({ length: pieces - 1 }, () => tx.pure.u64(per)));
208
240
  tx.transferObjects(Array.from({ length: pieces - 1 }, (_, i) => splits[i]), tx.pure.address(addr));
209
241
  }
210
242
  tx.setGasPayment([]);
211
243
  tx.setExpiration(await this.getValidDuringExpiration());
212
- await this.execMaintenance(tx, addr, kp, opts.budget, `整理 ${short}:${valued.length} 值 + ${zeros.length} 壳 → ${pieces} 份均分(每份≈${(total / BigInt(pieces)).toString()})`, undefined, coinType, tag);
244
+ 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) });
213
245
  return true;
214
246
  }
215
- async execMaintenance(tx, addr, kp, budget, label, buildClient, coinType = constants_1.SUI_TOKEN_ADDRESS.LONG, reserveTag) {
247
+ async execMaintenance(tx, addr, kp, budget, label, buildClient, coinType = constants_1.SUI_TOKEN_ADDRESS.LONG, reserveTag, commitPlan) {
248
+ let committed = false;
216
249
  try {
217
250
  tx.setGasBudget(budget);
218
251
  tx.setGasPrice(await this.getGasPrice());
@@ -222,11 +255,18 @@ class CentralExecutor {
222
255
  const tr = resp.Transaction;
223
256
  const { success, error } = (0, effects_1.coreTxStatus)(tr.effects);
224
257
  (0, dist_1.log_info)(`[executor] rebalance ${label} ${success ? 'OK' : 'FAILED'} wallet=${addr.slice(0, 10)}… digest=${tr.digest}${error ? ' err=' + error : ''}`);
258
+ if (success && commitPlan && reserveTag && tr.effects) {
259
+ const changes = (0, effects_1.extractMaintCoinChanges)(tr.effects, { wallet: addr, coinType, ...commitPlan });
260
+ this.cache.commit(reserveTag, changes);
261
+ committed = true;
262
+ }
225
263
  }
226
264
  finally {
227
- if (reserveTag)
228
- this.cache.abort(reserveTag, true);
229
- await this.reconcileCoins(addr, coinType);
265
+ if (!committed) {
266
+ if (reserveTag)
267
+ this.cache.abort(reserveTag, true);
268
+ await this.reconcileCoins(addr, coinType);
269
+ }
230
270
  }
231
271
  }
232
272
  async getPoolGenerics(poolId) {
@@ -358,7 +398,7 @@ class CentralExecutor {
358
398
  const { signature: senderSig } = await this.tradeWallets.get(wallet).signTransaction(txBytes);
359
399
  const digest = await tx.getDigest();
360
400
  const tSign = Date.now();
361
- void this.broadcastAndCommit(txBytes, [senderSig], digest, wallet, inType, this.outputCoinType(req), inputRes.coins, inputTag);
401
+ void this.broadcastAndCommit(txBytes, [senderSig], digest, wallet, req, inputRes.coins, inputTag);
362
402
  (0, dist_1.log_info)(`[executor] swap 已签提交 digest=${digest} dex=${req.dexId} ${req.a2b ? 'a2b' : 'b2a'} in=${req.amountIn} cost={build:${tBuild - tAcquire}ms, sign+digest:${tSign - tBuild}ms, total:${tSign - t0}ms}`);
363
403
  return { digest, submitted: true };
364
404
  }
@@ -368,7 +408,9 @@ class CentralExecutor {
368
408
  return { digest: '', submitted: false, error: e.message };
369
409
  }
370
410
  }
371
- async broadcastAndCommit(txBytes, signatures, digest, wallet, inType, outType, inputCoins, inputTag) {
411
+ async broadcastAndCommit(txBytes, signatures, digest, wallet, req, inputCoins, inputTag, attempt = 0) {
412
+ const inType = this.inputCoinType(req);
413
+ const outType = this.outputCoinType(req);
372
414
  const tBroadcast = Date.now();
373
415
  try {
374
416
  const resp = await this.core.executeTransaction({ transaction: txBytes, signatures, include: { effects: true, objectTypes: true, balanceChanges: true } });
@@ -388,14 +430,47 @@ class CentralExecutor {
388
430
  this.onBroadcastResult?.({ digest, success: false, error, receipt: this.toCheckerReceipt(tr, digest) });
389
431
  return;
390
432
  }
391
- await this.onSuccess(tr, wallet, inType, outType, inputCoins, inputTag);
433
+ await this.onSuccess(tr, wallet, req, inputCoins, inputTag);
392
434
  (0, dist_1.log_info)(`[executor] swap quorum-executed 确认 digest=${digest} broadcast=${Date.now() - tBroadcast}ms`);
393
435
  this.onBroadcastResult?.({ digest, success: true, receipt: this.toCheckerReceipt(tr, digest) });
394
436
  }
395
437
  catch (e) {
396
438
  this.cache.abort(inputTag, true);
439
+ if (this.isRebuildableObjectError(e) && attempt < this.maxRebuildRetries) {
440
+ (0, dist_1.log_warn)(`[executor] swap 输入对象版本陈旧,定向刷新 ${inType.split('::').pop()} + 重建重发(第 ${attempt + 1}/${this.maxRebuildRetries} 次)digest=${digest}: ${e.message}`);
441
+ await this.reconcileCoins(wallet, inType);
442
+ const retry = await this.rebuildSwap(req, wallet);
443
+ if (retry) {
444
+ void this.broadcastAndCommit(retry.txBytes, retry.signatures, retry.digest, wallet, req, retry.inputCoins, retry.inputTag, attempt + 1);
445
+ return;
446
+ }
447
+ (0, dist_1.log_warn)(`[executor] swap 重建失败(输入不足等),转 reconcile 兜底 digest=${digest}`);
448
+ }
397
449
  this.reconcileAfterFailure(wallet, inType, outType);
398
- (0, dist_1.log_error)(`[executor] broadcast 失败 digest=${digest} broadcast=${Date.now() - tBroadcast}ms`, e);
450
+ (0, dist_1.log_error)(`[executor] broadcast 失败 digest=${digest} broadcast=${Date.now() - tBroadcast}ms attempt=${attempt}`, e);
451
+ }
452
+ }
453
+ isRebuildableObjectError(e) {
454
+ const err = e;
455
+ const msg = (err?.message || String(e)).toLowerCase();
456
+ return msg.includes('unavailable for consumption')
457
+ || msg.includes('needs to be rebuilt')
458
+ || (err?.code === 'INVALID_ARGUMENT' && msg.includes('object'));
459
+ }
460
+ async rebuildSwap(req, wallet) {
461
+ try {
462
+ const inType = this.inputCoinType(req);
463
+ const inputTag = this.nextTag('in');
464
+ const inputRes = this.cache.acquire(wallet, inType, req.amountIn, inputTag);
465
+ const { txBytes, tx } = await this.buildSwapTx(req, wallet, inputRes.coins);
466
+ const { signature } = await this.tradeWallets.get(wallet).signTransaction(txBytes);
467
+ const digest = await tx.getDigest();
468
+ (0, dist_1.log_info)(`[executor] swap 重建 digest=${digest} dex=${req.dexId} ${req.a2b ? 'a2b' : 'b2a'} in=${req.amountIn}`);
469
+ return { txBytes, signatures: [signature], digest, inputCoins: inputRes.coins, inputTag };
470
+ }
471
+ catch (e) {
472
+ (0, dist_1.log_warn)(`[executor] swap 重建失败 dex=${req.dexId}: ${e.message}`);
473
+ return null;
399
474
  }
400
475
  }
401
476
  toCheckerReceipt(tr, digest) {
@@ -500,13 +575,17 @@ class CentralExecutor {
500
575
  const txBytes = await tx.build();
501
576
  return { txBytes, tx };
502
577
  }
503
- async onSuccess(tr, wallet, inType, outType, inputCoins, inputTag) {
504
- const objectTypes = await tr.objectTypes;
505
- const coinTypeById = new Map();
506
- for (const c of inputCoins)
507
- coinTypeById.set(c.objectId, inType);
508
- const tradeChanges = (0, effects_1.extractCoinChangesFromCore)(tr.effects, objectTypes, { owners: new Set([wallet]), coinTypeById });
509
- this.cache.commit(inputTag, tradeChanges);
578
+ async onSuccess(tr, wallet, req, inputCoins, inputTag) {
579
+ const inType = this.inputCoinType(req);
580
+ const outType = this.outputCoinType(req);
581
+ const inputTotal = inputCoins.reduce((s, c) => s + BigInt(c.balance), 0n);
582
+ const inputChanges = (0, effects_1.extractSwapInputChanges)(tr.effects, {
583
+ wallet, inCoinType: inType,
584
+ primaryId: inputCoins[0].objectId,
585
+ reservedInputIds: inputCoins.map(c => c.objectId),
586
+ inputTotal, amountIn: req.amountIn,
587
+ });
588
+ this.cache.commit(inputTag, inputChanges);
510
589
  if (this.reconcileAfterTx) {
511
590
  void this.postTradeRebalance(wallet, inType, outType);
512
591
  }
@@ -53,7 +53,10 @@ class InProcessCoinCache {
53
53
  if (r.wallet === wallet && r.coinType === coinType)
54
54
  r.coins.forEach(c => inflightIds.add(c.objectId));
55
55
  }
56
- const kept = freshCoins.filter(c => !inflightIds.has(c.objectId));
56
+ const prev = new Map(this.list(wallet, coinType).map(c => [c.objectId, c]));
57
+ const kept = freshCoins
58
+ .filter(c => !inflightIds.has(c.objectId))
59
+ .map(c => { const old = prev.get(c.objectId); return old && BigInt(old.version) > BigInt(c.version) ? old : c; });
57
60
  this.sortDesc(kept);
58
61
  const sig = (l) => l.map(c => `${c.objectId}@${c.version}:${c.balance}`).join(',');
59
62
  const unchanged = sig(kept) === sig(this.list(wallet, coinType));
@@ -13,4 +13,22 @@ export interface ExtractCoinChangesOptions {
13
13
  coinTypeById?: Map<string, string>;
14
14
  }
15
15
  export declare function extractCoinChangesFromCore(effects: TransactionEffects, objectTypes: Record<string, string>, opts?: ExtractCoinChangesOptions): CoinObjectChange[];
16
+ export interface MaintCoinChangesOptions {
17
+ wallet: string;
18
+ coinType: string;
19
+ primaryId: string;
20
+ perPiece: bigint;
21
+ primaryRemainder: bigint;
22
+ reservedIds: string[];
23
+ }
24
+ export declare function extractMaintCoinChanges(effects: TransactionEffects, o: MaintCoinChangesOptions): CoinObjectChange[];
25
+ export interface SwapInputChangesOptions {
26
+ wallet: string;
27
+ inCoinType: string;
28
+ primaryId: string;
29
+ reservedInputIds: string[];
30
+ inputTotal: bigint;
31
+ amountIn: bigint;
32
+ }
33
+ export declare function extractSwapInputChanges(effects: TransactionEffects, o: SwapInputChangesOptions): CoinObjectChange[];
16
34
  export {};
@@ -4,6 +4,8 @@ exports.parseCoinType = parseCoinType;
4
4
  exports.coreTxStatus = coreTxStatus;
5
5
  exports.coreGasUsed = coreGasUsed;
6
6
  exports.extractCoinChangesFromCore = extractCoinChangesFromCore;
7
+ exports.extractMaintCoinChanges = extractMaintCoinChanges;
8
+ exports.extractSwapInputChanges = extractSwapInputChanges;
7
9
  function parseCoinType(type) {
8
10
  if (!type)
9
11
  return null;
@@ -52,6 +54,53 @@ function extractCoinChangesFromCore(effects, objectTypes, opts = {}) {
52
54
  }
53
55
  return out;
54
56
  }
57
+ function extractMaintCoinChanges(effects, o) {
58
+ const out = [];
59
+ const reserved = new Set(o.reservedIds);
60
+ for (const ch of effects.changedObjects) {
61
+ const kind = changedKind(ch);
62
+ if (!kind)
63
+ continue;
64
+ if (kind === 'deleted') {
65
+ if (reserved.has(ch.objectId))
66
+ out.push({ kind: 'deleted', objectId: ch.objectId, coinType: o.coinType });
67
+ continue;
68
+ }
69
+ if (ownerAddress(ch.outputOwner) !== o.wallet)
70
+ continue;
71
+ if (ch.objectId === o.primaryId) {
72
+ out.push({ kind: 'mutated', objectId: ch.objectId, coinType: o.coinType,
73
+ version: ch.outputVersion ?? undefined, digest: ch.outputDigest ?? undefined, balance: o.primaryRemainder.toString() });
74
+ }
75
+ else if (kind === 'created') {
76
+ out.push({ kind: 'created', objectId: ch.objectId, coinType: o.coinType,
77
+ version: ch.outputVersion ?? undefined, digest: ch.outputDigest ?? undefined, balance: o.perPiece.toString() });
78
+ }
79
+ }
80
+ return out;
81
+ }
82
+ function extractSwapInputChanges(effects, o) {
83
+ const out = [];
84
+ const reserved = new Set(o.reservedInputIds);
85
+ const leftover = o.inputTotal - o.amountIn;
86
+ for (const ch of effects.changedObjects) {
87
+ const kind = changedKind(ch);
88
+ if (!kind)
89
+ continue;
90
+ if (kind === 'deleted') {
91
+ if (reserved.has(ch.objectId))
92
+ out.push({ kind: 'deleted', objectId: ch.objectId, coinType: o.inCoinType });
93
+ continue;
94
+ }
95
+ if (ch.objectId !== o.primaryId)
96
+ continue;
97
+ if (ownerAddress(ch.outputOwner) !== o.wallet)
98
+ continue;
99
+ out.push({ kind: 'mutated', objectId: ch.objectId, coinType: o.inCoinType,
100
+ version: ch.outputVersion ?? undefined, digest: ch.outputDigest ?? undefined, balance: leftover.toString() });
101
+ }
102
+ return out;
103
+ }
55
104
  function changedKind(ch) {
56
105
  if (ch.idOperation === 'Created')
57
106
  return 'created';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clonegod/ttd-sui-common",
3
- "version": "2.0.8",
3
+ "version": "2.0.9",
4
4
  "description": "Sui common library",
5
5
  "license": "UNLICENSED",
6
6
  "main": "dist/index.js",