@clonegod/ttd-sui-common 2.0.9 → 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.
@@ -61,6 +61,7 @@ export declare class CentralExecutor {
61
61
  private readonly maxRebuildRetries;
62
62
  constructor(core: ExecutorCore, opts?: CentralExecutorOptions);
63
63
  init(): Promise<void>;
64
+ private dumpAllCoins;
64
65
  private warmup;
65
66
  reconcileCoins(wallet: string, coinType: string, quietIfUnchanged?: boolean): Promise<void>;
66
67
  rebalanceWalletFunds(): Promise<void>;
@@ -68,6 +69,10 @@ export declare class CentralExecutor {
68
69
  private decimalsCache;
69
70
  private objectReader?;
70
71
  private getCoinDecimalsCached;
72
+ private coinSymbol;
73
+ private pairLabel;
74
+ private fmtAmount;
75
+ private walletBalancesLabel;
71
76
  private maintainCoinObjects;
72
77
  private execMaintenance;
73
78
  private poolGenericsCache;
@@ -48,6 +48,7 @@ class CentralExecutor {
48
48
  this.onBroadcastResult = opts.onBroadcastResult;
49
49
  this.tradeCoinTypesProvider = opts.tradeCoinTypes;
50
50
  this.tradePoolsProvider = opts.tradePools;
51
+ this.cache.setAmountFormatter((ct, raw) => this.fmtAmount(ct, raw));
51
52
  }
52
53
  async init() {
53
54
  const tradeIds = (process.env.SUI_WALLET_GROUP_IDS || '').split(',').map(s => s.trim()).filter(Boolean);
@@ -62,6 +63,28 @@ class CentralExecutor {
62
63
  }
63
64
  (0, dist_1.log_info)(`[executor] init: trade=${[...this.tradeWallets.keys()].join(',')} (单钱包模式: gas=各自地址余额), gasBudget=${this.gasBudget}`);
64
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
+ }
65
88
  }
66
89
  async warmup() {
67
90
  try {
@@ -208,6 +231,26 @@ class CentralExecutor {
208
231
  }
209
232
  return d;
210
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
+ }
211
254
  async maintainCoinObjects(addr, kp, coinType, opts) {
212
255
  const short = coinType.split('::').pop();
213
256
  const zeroMergeMin = Number(process.env.SUI_ZERO_COIN_MERGE_THRESHOLD || 3);
@@ -379,19 +422,27 @@ class CentralExecutor {
379
422
  const t0 = Date.now();
380
423
  req = await this.canonicalizeReq(req);
381
424
  const inType = this.inputCoinType(req);
425
+ await this.getCoinDecimalsCached(inType).catch(() => { });
382
426
  let wallet;
383
427
  try {
384
428
  wallet = this.chooseTradeWallet(req, inType, req.amountIn);
385
429
  }
386
430
  catch (e) {
387
- (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 重试`);
388
432
  await Promise.all([...this.tradeWallets.keys()].map(w => this.reconcileCoins(w, inType)));
389
- 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
+ }
390
440
  }
391
441
  const inputTag = this.nextTag('in');
392
442
  const inputRes = this.cache.acquire(wallet, inType, req.amountIn, inputTag);
393
443
  const tAcquire = Date.now();
394
- (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`);
395
446
  try {
396
447
  const { txBytes, tx } = await this.buildSwapTx(req, wallet, inputRes.coins);
397
448
  const tBuild = Date.now();
@@ -399,7 +450,7 @@ class CentralExecutor {
399
450
  const digest = await tx.getDigest();
400
451
  const tSign = Date.now();
401
452
  void this.broadcastAndCommit(txBytes, [senderSig], digest, wallet, req, inputRes.coins, inputTag);
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}`);
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}`);
403
454
  return { digest, submitted: true };
404
455
  }
405
456
  catch (e) {
@@ -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) {
@@ -54,18 +80,32 @@ class InProcessCoinCache {
54
80
  r.coins.forEach(c => inflightIds.add(c.objectId));
55
81
  }
56
82
  const prev = new Map(this.list(wallet, coinType).map(c => [c.objectId, c]));
83
+ const freshIds = new Set(freshCoins.map(c => c.objectId));
57
84
  const kept = freshCoins
58
85
  .filter(c => !inflightIds.has(c.objectId))
59
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));
60
104
  this.sortDesc(kept);
61
105
  const sig = (l) => l.map(c => `${c.objectId}@${c.version}:${c.balance}`).join(',');
62
106
  const unchanged = sig(kept) === sig(this.list(wallet, coinType));
63
107
  this.walletMap(wallet).set(coinType, kept);
64
- const msg = `[coin-cache] reconcile ${wallet} ${coinType}: ${kept.length} avail (${inflightIds.size} inflight kept)`;
65
- if (unchanged && quietIfUnchanged)
66
- (0, dist_1.log_debug)(msg);
67
- else
68
- (0, dist_1.log_info)(msg);
108
+ this.logCoinBreakdown(wallet, coinType, `reconcile(${inflightIds.size}在途)`, unchanged && quietIfUnchanged);
69
109
  return true;
70
110
  }
71
111
  acquire(wallet, coinType, amount, txTag, maxCoins = 8) {
@@ -126,6 +166,7 @@ class InProcessCoinCache {
126
166
  const wallet = res.wallet;
127
167
  if (ch.kind === 'deleted') {
128
168
  this.removeEverywhere(wallet, ch.coinType, ch.objectId);
169
+ this.committedAt.delete(this.commitKey(wallet, ch.coinType, ch.objectId));
129
170
  continue;
130
171
  }
131
172
  if (ch.version == null || ch.digest == null) {
@@ -139,7 +180,10 @@ class InProcessCoinCache {
139
180
  const l = this.list(wallet, ch.coinType);
140
181
  l.push(ref);
141
182
  this.sortDesc(l);
183
+ this.committedAt.set(this.commitKey(wallet, ch.coinType, ch.objectId), Date.now());
142
184
  }
185
+ for (const ct of new Set(changes.map(c => c.coinType)))
186
+ this.logCoinBreakdown(res.wallet, ct, 'commit后');
143
187
  }
144
188
  abort(txTag, consumed) {
145
189
  const res = this.inflight.get(txTag);
@@ -150,6 +194,8 @@ class InProcessCoinCache {
150
194
  this.inflight.delete(txTag);
151
195
  this.bumpEpoch(res.wallet, res.coinType);
152
196
  if (consumed) {
197
+ for (const c of res.coins)
198
+ this.committedAt.delete(this.commitKey(res.wallet, res.coinType, c.objectId));
153
199
  (0, dist_1.log_warn)(`[coin-cache] abort consumed txTag=${txTag}(${res.coins.length} coin 交 reconcile 兜底)`);
154
200
  return;
155
201
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clonegod/ttd-sui-common",
3
- "version": "2.0.9",
3
+ "version": "2.0.10",
4
4
  "description": "Sui common library",
5
5
  "license": "UNLICENSED",
6
6
  "main": "dist/index.js",