@clonegod/ttd-sui-common 2.0.8 → 2.0.10

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,14 +57,22 @@ 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 dumpAllCoins;
65
+ private warmup;
56
66
  reconcileCoins(wallet: string, coinType: string, quietIfUnchanged?: boolean): Promise<void>;
57
67
  rebalanceWalletFunds(): Promise<void>;
58
68
  private rebalanceOne;
59
69
  private decimalsCache;
60
70
  private objectReader?;
61
71
  private getCoinDecimalsCached;
72
+ private coinSymbol;
73
+ private pairLabel;
74
+ private fmtAmount;
75
+ private walletBalancesLabel;
62
76
  private maintainCoinObjects;
63
77
  private execMaintenance;
64
78
  private poolGenericsCache;
@@ -76,6 +90,8 @@ export declare class CentralExecutor {
76
90
  private registerShared;
77
91
  submitSwap(req: SwapExecRequest): Promise<SwapSubmitResult>;
78
92
  private broadcastAndCommit;
93
+ private isRebuildableObjectError;
94
+ private rebuildSwap;
79
95
  private toCheckerReceipt;
80
96
  private reconcileAfterFailure;
81
97
  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,8 @@ 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;
51
+ this.cache.setAmountFormatter((ct, raw) => this.fmtAmount(ct, raw));
49
52
  }
50
53
  async init() {
51
54
  const tradeIds = (process.env.SUI_WALLET_GROUP_IDS || '').split(',').map(s => s.trim()).filter(Boolean);
@@ -59,6 +62,58 @@ class CentralExecutor {
59
62
  await this.reconcileCoins(w, constants_1.SUI_TOKEN_ADDRESS.LONG);
60
63
  }
61
64
  (0, dist_1.log_info)(`[executor] init: trade=${[...this.tradeWallets.keys()].join(',')} (单钱包模式: gas=各自地址余额), gasBudget=${this.gasBudget}`);
65
+ await this.warmup();
66
+ await this.dumpAllCoins();
67
+ }
68
+ async dumpAllCoins() {
69
+ if (!this.tradeCoinTypesProvider)
70
+ return;
71
+ try {
72
+ const types = [...new Set((await this.tradeCoinTypesProvider()).map(t => (0, format_1.normalizeSuiTokenAddress)(t)))];
73
+ await Promise.all(types.map(t => this.getCoinDecimalsCached(t).catch(() => { })));
74
+ for (const w of this.tradeWallets.keys()) {
75
+ for (const t of types) {
76
+ if (t === constants_1.SUI_TOKEN_ADDRESS.LONG) {
77
+ this.cache.logCoinBreakdown(w, t, '启动一览');
78
+ continue;
79
+ }
80
+ await this.reconcileCoins(w, t);
81
+ }
82
+ }
83
+ (0, dist_1.log_info)(`[executor] 启动 coin 一览完成: ${types.length} 币种 × ${this.tradeWallets.size} 钱包`);
84
+ }
85
+ catch (e) {
86
+ (0, dist_1.log_warn)(`[executor] 启动 coin 一览失败(首笔将冷 reconcile): ${e.message}`);
87
+ }
88
+ }
89
+ async warmup() {
90
+ try {
91
+ await Promise.all([this.getGasPrice(), this.getValidDuringExpiration()]);
92
+ }
93
+ catch (e) {
94
+ (0, dist_1.log_warn)(`[executor] 预热全局缓存(gas/epoch)失败(首笔将冷启动): ${e.message}`);
95
+ }
96
+ if (!this.tradePoolsProvider)
97
+ return;
98
+ try {
99
+ const pools = await this.tradePoolsProvider();
100
+ let ok = 0;
101
+ await Promise.all(pools.map(async (p) => {
102
+ try {
103
+ await this.canonicalizeReq({ dexId: p.dexId, poolId: p.poolId, coinTypeA: p.coinTypeA, coinTypeB: p.coinTypeB, a2b: true, amountIn: 0n, minOut: 0n, sqrtPriceLimit: 0n });
104
+ await this.getSharedRefCached(p.poolId);
105
+ await this.getSharedRefCached((0, swap_1.configSharedObjectId)(p.dexId));
106
+ ok++;
107
+ }
108
+ catch (e) {
109
+ (0, dist_1.log_warn)(`[executor] 预热池 ${p.poolId.slice(0, 10)}…(${p.dexId}) 失败(首笔冷启动,自愈): ${e.message}`);
110
+ }
111
+ }));
112
+ (0, dist_1.log_info)(`[executor] 预热完成: ${ok}/${pools.length} 池 shared ref + 链上真序 + gas/epoch 全局缓存`);
113
+ }
114
+ catch (e) {
115
+ (0, dist_1.log_warn)(`[executor] 读交易池清单失败,跳过池级预热: ${e.message}`);
116
+ }
62
117
  }
63
118
  async reconcileCoins(wallet, coinType, quietIfUnchanged = false) {
64
119
  try {
@@ -176,6 +231,26 @@ class CentralExecutor {
176
231
  }
177
232
  return d;
178
233
  }
234
+ coinSymbol(coinType) { return coinType.split('::').pop() || coinType; }
235
+ pairLabel(req) {
236
+ return `${this.coinSymbol(this.inputCoinType(req))}→${this.coinSymbol(this.outputCoinType(req))}`;
237
+ }
238
+ fmtAmount(coinType, raw) {
239
+ const sym = this.coinSymbol(coinType);
240
+ const d = this.decimalsCache.get(coinType);
241
+ if (d == null)
242
+ return `${raw.toString()}(raw) ${sym}`;
243
+ const neg = raw < 0n, v = neg ? -raw : raw, base = 10n ** BigInt(d);
244
+ const intStr = (v / base).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
245
+ const frac = (v % base).toString().padStart(d, '0').replace(/0+$/, '');
246
+ return `${neg ? '-' : ''}${intStr}${frac ? '.' + frac : ''} ${sym}`;
247
+ }
248
+ walletBalancesLabel(coinType) {
249
+ return [...this.tradeWallets.keys()].map(w => {
250
+ const { total, count } = this.cache.snapshot(w, coinType);
251
+ return `${w.slice(0, 8)}…=${this.fmtAmount(coinType, total)}(${count}枚)`;
252
+ }).join(', ');
253
+ }
179
254
  async maintainCoinObjects(addr, kp, coinType, opts) {
180
255
  const short = coinType.split('::').pop();
181
256
  const zeroMergeMin = Number(process.env.SUI_ZERO_COIN_MERGE_THRESHOLD || 3);
@@ -202,17 +277,18 @@ class CentralExecutor {
202
277
  const others = coins.filter(c => c.objectId !== primaryRef.objectId);
203
278
  if (others.length)
204
279
  tx.mergeCoins(primary, others.map(c => tx.object(transactions_1.Inputs.ObjectRef(c))));
280
+ const per = pieces > 1 ? total / BigInt(pieces) : 0n;
205
281
  if (pieces > 1) {
206
- const per = total / BigInt(pieces);
207
282
  const splits = tx.splitCoins(primary, Array.from({ length: pieces - 1 }, () => tx.pure.u64(per)));
208
283
  tx.transferObjects(Array.from({ length: pieces - 1 }, (_, i) => splits[i]), tx.pure.address(addr));
209
284
  }
210
285
  tx.setGasPayment([]);
211
286
  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);
287
+ 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
288
  return true;
214
289
  }
215
- async execMaintenance(tx, addr, kp, budget, label, buildClient, coinType = constants_1.SUI_TOKEN_ADDRESS.LONG, reserveTag) {
290
+ async execMaintenance(tx, addr, kp, budget, label, buildClient, coinType = constants_1.SUI_TOKEN_ADDRESS.LONG, reserveTag, commitPlan) {
291
+ let committed = false;
216
292
  try {
217
293
  tx.setGasBudget(budget);
218
294
  tx.setGasPrice(await this.getGasPrice());
@@ -222,11 +298,18 @@ class CentralExecutor {
222
298
  const tr = resp.Transaction;
223
299
  const { success, error } = (0, effects_1.coreTxStatus)(tr.effects);
224
300
  (0, dist_1.log_info)(`[executor] rebalance ${label} ${success ? 'OK' : 'FAILED'} wallet=${addr.slice(0, 10)}… digest=${tr.digest}${error ? ' err=' + error : ''}`);
301
+ if (success && commitPlan && reserveTag && tr.effects) {
302
+ const changes = (0, effects_1.extractMaintCoinChanges)(tr.effects, { wallet: addr, coinType, ...commitPlan });
303
+ this.cache.commit(reserveTag, changes);
304
+ committed = true;
305
+ }
225
306
  }
226
307
  finally {
227
- if (reserveTag)
228
- this.cache.abort(reserveTag, true);
229
- await this.reconcileCoins(addr, coinType);
308
+ if (!committed) {
309
+ if (reserveTag)
310
+ this.cache.abort(reserveTag, true);
311
+ await this.reconcileCoins(addr, coinType);
312
+ }
230
313
  }
231
314
  }
232
315
  async getPoolGenerics(poolId) {
@@ -339,27 +422,35 @@ class CentralExecutor {
339
422
  const t0 = Date.now();
340
423
  req = await this.canonicalizeReq(req);
341
424
  const inType = this.inputCoinType(req);
425
+ await this.getCoinDecimalsCached(inType).catch(() => { });
342
426
  let wallet;
343
427
  try {
344
428
  wallet = this.chooseTradeWallet(req, inType, req.amountIn);
345
429
  }
346
430
  catch (e) {
347
- (0, dist_1.log_warn)(`[executor] ${inType} 选钱包失败(${e.message}),按需 reconcile 后重试`);
431
+ (0, dist_1.log_warn)(`[executor] ${this.pairLabel(req)} 选钱包失败:需 ${this.fmtAmount(inType, req.amountIn)},当前各钱包缓存 [${this.walletBalancesLabel(inType)}] → 按需 reconcile 重试`);
348
432
  await Promise.all([...this.tradeWallets.keys()].map(w => this.reconcileCoins(w, inType)));
349
- wallet = this.chooseTradeWallet(req, inType, req.amountIn);
433
+ try {
434
+ wallet = this.chooseTradeWallet(req, inType, req.amountIn);
435
+ }
436
+ catch (e2) {
437
+ (0, dist_1.log_error)(`[executor] ${this.pairLabel(req)} 余额不足:需 ${this.fmtAmount(inType, req.amountIn)},reconcile 后各钱包缓存仍 [${this.walletBalancesLabel(inType)}]`, e2);
438
+ throw e2;
439
+ }
350
440
  }
351
441
  const inputTag = this.nextTag('in');
352
442
  const inputRes = this.cache.acquire(wallet, inType, req.amountIn, inputTag);
353
443
  const tAcquire = Date.now();
354
- (0, dist_1.log_info)(`[executor] coin 已占用 wallet=${wallet.slice(0, 10)}… ${inputRes.coins.length} ${inType.split('::').pop()} (选钱包+占币 ${tAcquire - t0}ms)`);
444
+ const acqTotal = inputRes.coins.reduce((s, c) => s + BigInt(c.balance), 0n);
445
+ (0, dist_1.log_info)(`[executor] coin 已占用 wallet=${wallet.slice(0, 8)}… ${this.pairLabel(req)} 取 ${inputRes.coins.length} 枚=${this.fmtAmount(inType, acqTotal)}(需 ${this.fmtAmount(inType, req.amountIn)})选钱包+占币 ${tAcquire - t0}ms`);
355
446
  try {
356
447
  const { txBytes, tx } = await this.buildSwapTx(req, wallet, inputRes.coins);
357
448
  const tBuild = Date.now();
358
449
  const { signature: senderSig } = await this.tradeWallets.get(wallet).signTransaction(txBytes);
359
450
  const digest = await tx.getDigest();
360
451
  const tSign = Date.now();
361
- void this.broadcastAndCommit(txBytes, [senderSig], digest, wallet, inType, this.outputCoinType(req), inputRes.coins, inputTag);
362
- (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}`);
452
+ void this.broadcastAndCommit(txBytes, [senderSig], digest, wallet, req, inputRes.coins, inputTag);
453
+ (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}`);
363
454
  return { digest, submitted: true };
364
455
  }
365
456
  catch (e) {
@@ -368,7 +459,9 @@ class CentralExecutor {
368
459
  return { digest: '', submitted: false, error: e.message };
369
460
  }
370
461
  }
371
- async broadcastAndCommit(txBytes, signatures, digest, wallet, inType, outType, inputCoins, inputTag) {
462
+ async broadcastAndCommit(txBytes, signatures, digest, wallet, req, inputCoins, inputTag, attempt = 0) {
463
+ const inType = this.inputCoinType(req);
464
+ const outType = this.outputCoinType(req);
372
465
  const tBroadcast = Date.now();
373
466
  try {
374
467
  const resp = await this.core.executeTransaction({ transaction: txBytes, signatures, include: { effects: true, objectTypes: true, balanceChanges: true } });
@@ -388,14 +481,47 @@ class CentralExecutor {
388
481
  this.onBroadcastResult?.({ digest, success: false, error, receipt: this.toCheckerReceipt(tr, digest) });
389
482
  return;
390
483
  }
391
- await this.onSuccess(tr, wallet, inType, outType, inputCoins, inputTag);
484
+ await this.onSuccess(tr, wallet, req, inputCoins, inputTag);
392
485
  (0, dist_1.log_info)(`[executor] swap quorum-executed 确认 digest=${digest} broadcast=${Date.now() - tBroadcast}ms`);
393
486
  this.onBroadcastResult?.({ digest, success: true, receipt: this.toCheckerReceipt(tr, digest) });
394
487
  }
395
488
  catch (e) {
396
489
  this.cache.abort(inputTag, true);
490
+ if (this.isRebuildableObjectError(e) && attempt < this.maxRebuildRetries) {
491
+ (0, dist_1.log_warn)(`[executor] swap 输入对象版本陈旧,定向刷新 ${inType.split('::').pop()} + 重建重发(第 ${attempt + 1}/${this.maxRebuildRetries} 次)digest=${digest}: ${e.message}`);
492
+ await this.reconcileCoins(wallet, inType);
493
+ const retry = await this.rebuildSwap(req, wallet);
494
+ if (retry) {
495
+ void this.broadcastAndCommit(retry.txBytes, retry.signatures, retry.digest, wallet, req, retry.inputCoins, retry.inputTag, attempt + 1);
496
+ return;
497
+ }
498
+ (0, dist_1.log_warn)(`[executor] swap 重建失败(输入不足等),转 reconcile 兜底 digest=${digest}`);
499
+ }
397
500
  this.reconcileAfterFailure(wallet, inType, outType);
398
- (0, dist_1.log_error)(`[executor] broadcast 失败 digest=${digest} broadcast=${Date.now() - tBroadcast}ms`, e);
501
+ (0, dist_1.log_error)(`[executor] broadcast 失败 digest=${digest} broadcast=${Date.now() - tBroadcast}ms attempt=${attempt}`, e);
502
+ }
503
+ }
504
+ isRebuildableObjectError(e) {
505
+ const err = e;
506
+ const msg = (err?.message || String(e)).toLowerCase();
507
+ return msg.includes('unavailable for consumption')
508
+ || msg.includes('needs to be rebuilt')
509
+ || (err?.code === 'INVALID_ARGUMENT' && msg.includes('object'));
510
+ }
511
+ async rebuildSwap(req, wallet) {
512
+ try {
513
+ const inType = this.inputCoinType(req);
514
+ const inputTag = this.nextTag('in');
515
+ const inputRes = this.cache.acquire(wallet, inType, req.amountIn, inputTag);
516
+ const { txBytes, tx } = await this.buildSwapTx(req, wallet, inputRes.coins);
517
+ const { signature } = await this.tradeWallets.get(wallet).signTransaction(txBytes);
518
+ const digest = await tx.getDigest();
519
+ (0, dist_1.log_info)(`[executor] swap 重建 digest=${digest} dex=${req.dexId} ${req.a2b ? 'a2b' : 'b2a'} in=${req.amountIn}`);
520
+ return { txBytes, signatures: [signature], digest, inputCoins: inputRes.coins, inputTag };
521
+ }
522
+ catch (e) {
523
+ (0, dist_1.log_warn)(`[executor] swap 重建失败 dex=${req.dexId}: ${e.message}`);
524
+ return null;
399
525
  }
400
526
  }
401
527
  toCheckerReceipt(tr, digest) {
@@ -500,13 +626,17 @@ class CentralExecutor {
500
626
  const txBytes = await tx.build();
501
627
  return { txBytes, tx };
502
628
  }
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);
629
+ async onSuccess(tr, wallet, req, inputCoins, inputTag) {
630
+ const inType = this.inputCoinType(req);
631
+ const outType = this.outputCoinType(req);
632
+ const inputTotal = inputCoins.reduce((s, c) => s + BigInt(c.balance), 0n);
633
+ const inputChanges = (0, effects_1.extractSwapInputChanges)(tr.effects, {
634
+ wallet, inCoinType: inType,
635
+ primaryId: inputCoins[0].objectId,
636
+ reservedInputIds: inputCoins.map(c => c.objectId),
637
+ inputTotal, amountIn: req.amountIn,
638
+ });
639
+ this.cache.commit(inputTag, inputChanges);
510
640
  if (this.reconcileAfterTx) {
511
641
  void this.postTradeRebalance(wallet, inType, outType);
512
642
  }
@@ -2,10 +2,16 @@ import { CoinRef, CoinReservation, CoinObjectChange } from '../coin/types';
2
2
  export declare class InProcessCoinCache {
3
3
  private available;
4
4
  private inflight;
5
+ private committedAt;
6
+ private commitKey;
5
7
  private epochs;
6
8
  private epochKey;
7
9
  private bumpEpoch;
8
10
  mutationEpoch(wallet: string, coinType: string): number;
11
+ private amountFmt?;
12
+ setAmountFormatter(fn: (coinType: string, raw: bigint) => string): void;
13
+ private fmt;
14
+ logCoinBreakdown(wallet: string, coinType: string, reason: string, quiet?: boolean): void;
9
15
  private walletMap;
10
16
  private list;
11
17
  private sortDesc;
@@ -6,8 +6,10 @@ class InProcessCoinCache {
6
6
  constructor() {
7
7
  this.available = new Map();
8
8
  this.inflight = new Map();
9
+ this.committedAt = new Map();
9
10
  this.epochs = new Map();
10
11
  }
12
+ commitKey(wallet, coinType, objectId) { return `${wallet}|${coinType}|${objectId}`; }
11
13
  epochKey(wallet, coinType) { return `${wallet}|${coinType}`; }
12
14
  bumpEpoch(wallet, coinType) {
13
15
  const k = this.epochKey(wallet, coinType);
@@ -16,6 +18,30 @@ class InProcessCoinCache {
16
18
  mutationEpoch(wallet, coinType) {
17
19
  return this.epochs.get(this.epochKey(wallet, coinType)) || 0;
18
20
  }
21
+ setAmountFormatter(fn) { this.amountFmt = fn; }
22
+ fmt(coinType, raw) { return this.amountFmt ? this.amountFmt(coinType, raw) : `${raw.toString()}(raw)`; }
23
+ logCoinBreakdown(wallet, coinType, reason, quiet = false) {
24
+ const l = this.list(wallet, coinType);
25
+ const total = l.reduce((s, c) => s + BigInt(c.balance), 0n);
26
+ const symbol = coinType.split('::').pop();
27
+ const detail = {
28
+ reason, wallet, symbol, coinType,
29
+ count: l.length,
30
+ total: this.fmt(coinType, total),
31
+ total_raw: total.toString(),
32
+ objects: l.map(c => ({
33
+ objectId: c.objectId,
34
+ version: c.version,
35
+ balance: this.fmt(coinType, BigInt(c.balance)),
36
+ balance_raw: c.balance,
37
+ })),
38
+ };
39
+ const header = `[coin-cache] ${reason} ${symbol} ${l.length}枚 total=${this.fmt(coinType, total)}`;
40
+ if (quiet)
41
+ (0, dist_1.log_debug)(header, detail);
42
+ else
43
+ (0, dist_1.log_info)(header, detail);
44
+ }
19
45
  walletMap(wallet) {
20
46
  let m = this.available.get(wallet);
21
47
  if (!m) {
@@ -53,16 +79,33 @@ class InProcessCoinCache {
53
79
  if (r.wallet === wallet && r.coinType === coinType)
54
80
  r.coins.forEach(c => inflightIds.add(c.objectId));
55
81
  }
56
- const kept = freshCoins.filter(c => !inflightIds.has(c.objectId));
82
+ const prev = new Map(this.list(wallet, coinType).map(c => [c.objectId, c]));
83
+ const freshIds = new Set(freshCoins.map(c => c.objectId));
84
+ const kept = freshCoins
85
+ .filter(c => !inflightIds.has(c.objectId))
86
+ .map(c => { const old = prev.get(c.objectId); return old && BigInt(old.version) > BigInt(c.version) ? old : c; });
87
+ const keptIds = new Set(kept.map(c => c.objectId));
88
+ const now = Date.now();
89
+ const graceMs = Number(process.env.SUI_COIN_RECONCILE_GRACE_MS || 10000);
90
+ for (const [id, c] of prev) {
91
+ if (keptIds.has(id) || inflightIds.has(id) || freshIds.has(id))
92
+ continue;
93
+ const ts = this.committedAt.get(this.commitKey(wallet, coinType, id));
94
+ if (ts != null && now - ts < graceMs) {
95
+ kept.push(c);
96
+ keptIds.add(id);
97
+ }
98
+ }
99
+ for (const id of freshIds)
100
+ this.committedAt.delete(this.commitKey(wallet, coinType, id));
101
+ for (const id of prev.keys())
102
+ if (!keptIds.has(id))
103
+ this.committedAt.delete(this.commitKey(wallet, coinType, id));
57
104
  this.sortDesc(kept);
58
105
  const sig = (l) => l.map(c => `${c.objectId}@${c.version}:${c.balance}`).join(',');
59
106
  const unchanged = sig(kept) === sig(this.list(wallet, coinType));
60
107
  this.walletMap(wallet).set(coinType, kept);
61
- const msg = `[coin-cache] reconcile ${wallet} ${coinType}: ${kept.length} avail (${inflightIds.size} inflight kept)`;
62
- if (unchanged && quietIfUnchanged)
63
- (0, dist_1.log_debug)(msg);
64
- else
65
- (0, dist_1.log_info)(msg);
108
+ this.logCoinBreakdown(wallet, coinType, `reconcile(${inflightIds.size}在途)`, unchanged && quietIfUnchanged);
66
109
  return true;
67
110
  }
68
111
  acquire(wallet, coinType, amount, txTag, maxCoins = 8) {
@@ -123,6 +166,7 @@ class InProcessCoinCache {
123
166
  const wallet = res.wallet;
124
167
  if (ch.kind === 'deleted') {
125
168
  this.removeEverywhere(wallet, ch.coinType, ch.objectId);
169
+ this.committedAt.delete(this.commitKey(wallet, ch.coinType, ch.objectId));
126
170
  continue;
127
171
  }
128
172
  if (ch.version == null || ch.digest == null) {
@@ -136,7 +180,10 @@ class InProcessCoinCache {
136
180
  const l = this.list(wallet, ch.coinType);
137
181
  l.push(ref);
138
182
  this.sortDesc(l);
183
+ this.committedAt.set(this.commitKey(wallet, ch.coinType, ch.objectId), Date.now());
139
184
  }
185
+ for (const ct of new Set(changes.map(c => c.coinType)))
186
+ this.logCoinBreakdown(res.wallet, ct, 'commit后');
140
187
  }
141
188
  abort(txTag, consumed) {
142
189
  const res = this.inflight.get(txTag);
@@ -147,6 +194,8 @@ class InProcessCoinCache {
147
194
  this.inflight.delete(txTag);
148
195
  this.bumpEpoch(res.wallet, res.coinType);
149
196
  if (consumed) {
197
+ for (const c of res.coins)
198
+ this.committedAt.delete(this.commitKey(res.wallet, res.coinType, c.objectId));
150
199
  (0, dist_1.log_warn)(`[coin-cache] abort consumed txTag=${txTag}(${res.coins.length} coin 交 reconcile 兜底)`);
151
200
  return;
152
201
  }
@@ -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.10",
4
4
  "description": "Sui common library",
5
5
  "license": "UNLICENSED",
6
6
  "main": "dist/index.js",