@clonegod/ttd-sui-common 2.0.10 → 2.0.11

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.
@@ -66,6 +66,7 @@ export declare class CentralExecutor {
66
66
  reconcileCoins(wallet: string, coinType: string, quietIfUnchanged?: boolean): Promise<void>;
67
67
  rebalanceWalletFunds(): Promise<void>;
68
68
  private rebalanceOne;
69
+ private selectWalletWithBalance;
69
70
  private decimalsCache;
70
71
  private objectReader?;
71
72
  private getCoinDecimalsCached;
@@ -217,6 +217,17 @@ class CentralExecutor {
217
217
  return;
218
218
  }
219
219
  }
220
+ async selectWalletWithBalance(inType, amount) {
221
+ let best = null;
222
+ for (const w of this.tradeWallets.keys()) {
223
+ const objs = this.cache.snapshot(w, inType).total;
224
+ const b = await this.core.getBalance({ owner: w, coinType: inType });
225
+ const ab = BigInt(b.balance.addressBalance ?? '0');
226
+ if (objs + ab >= amount && (!best || objs + ab > best.sum))
227
+ best = { wallet: w, addressBalance: ab, sum: objs + ab };
228
+ }
229
+ return best ? { wallet: best.wallet, addressBalance: best.addressBalance } : null;
230
+ }
220
231
  async getCoinDecimalsCached(coinType) {
221
232
  let d = this.decimalsCache.get(coinType);
222
233
  if (d == null) {
@@ -423,33 +434,47 @@ class CentralExecutor {
423
434
  req = await this.canonicalizeReq(req);
424
435
  const inType = this.inputCoinType(req);
425
436
  await this.getCoinDecimalsCached(inType).catch(() => { });
426
- let wallet;
437
+ let wallet = '';
438
+ let inputCoins = [];
439
+ let inputTag = '';
440
+ let shortfall = 0n;
427
441
  try {
428
442
  wallet = this.chooseTradeWallet(req, inType, req.amountIn);
443
+ inputTag = this.nextTag('in');
444
+ inputCoins = this.cache.acquire(wallet, inType, req.amountIn, inputTag).coins;
429
445
  }
430
446
  catch (e) {
431
- (0, dist_1.log_warn)(`[executor] ${this.pairLabel(req)} 选钱包失败:需 ${this.fmtAmount(inType, req.amountIn)},当前各钱包缓存 [${this.walletBalancesLabel(inType)}] → 按需 reconcile 重试`);
447
+ (0, dist_1.log_warn)(`[executor] ${this.pairLabel(req)} 对象不足:需 ${this.fmtAmount(inType, req.amountIn)},当前缓存 [${this.walletBalancesLabel(inType)}] → reconcile 重试`);
432
448
  await Promise.all([...this.tradeWallets.keys()].map(w => this.reconcileCoins(w, inType)));
433
449
  try {
434
450
  wallet = this.chooseTradeWallet(req, inType, req.amountIn);
451
+ inputTag = this.nextTag('in');
452
+ inputCoins = this.cache.acquire(wallet, inType, req.amountIn, inputTag).coins;
435
453
  }
436
454
  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;
455
+ const picked = await this.selectWalletWithBalance(inType, req.amountIn);
456
+ if (!picked) {
457
+ (0, dist_1.log_error)(`[executor] ${this.pairLabel(req)} 余额不足:需 ${this.fmtAmount(inType, req.amountIn)},对象+address-balance 仍不够 [${this.walletBalancesLabel(inType)}]`, e2);
458
+ throw e2;
459
+ }
460
+ wallet = picked.wallet;
461
+ inputTag = this.nextTag('in');
462
+ inputCoins = this.cache.acquireAvailable(wallet, inType, req.amountIn, inputTag);
463
+ const objSum = inputCoins.reduce((s, c) => s + BigInt(c.balance), 0n);
464
+ shortfall = req.amountIn - objSum;
439
465
  }
440
466
  }
441
- const inputTag = this.nextTag('in');
442
- const inputRes = this.cache.acquire(wallet, inType, req.amountIn, inputTag);
443
467
  const tAcquire = Date.now();
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`);
468
+ const acqTotal = inputCoins.reduce((s, c) => s + BigInt(c.balance), 0n);
469
+ const shortStr = shortfall > 0n ? ` + address-balance ${this.fmtAmount(inType, shortfall)}` : '';
470
+ (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`);
446
471
  try {
447
- const { txBytes, tx } = await this.buildSwapTx(req, wallet, inputRes.coins);
472
+ const { txBytes, tx } = await this.buildSwapTx(req, wallet, inputCoins, shortfall);
448
473
  const tBuild = Date.now();
449
474
  const { signature: senderSig } = await this.tradeWallets.get(wallet).signTransaction(txBytes);
450
475
  const digest = await tx.getDigest();
451
476
  const tSign = Date.now();
452
- void this.broadcastAndCommit(txBytes, [senderSig], digest, wallet, req, inputRes.coins, inputTag);
477
+ void this.broadcastAndCommit(txBytes, [senderSig], digest, wallet, req, inputCoins, inputTag, 0, shortfall > 0n);
453
478
  (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}`);
454
479
  return { digest, submitted: true };
455
480
  }
@@ -459,7 +484,7 @@ class CentralExecutor {
459
484
  return { digest: '', submitted: false, error: e.message };
460
485
  }
461
486
  }
462
- async broadcastAndCommit(txBytes, signatures, digest, wallet, req, inputCoins, inputTag, attempt = 0) {
487
+ async broadcastAndCommit(txBytes, signatures, digest, wallet, req, inputCoins, inputTag, attempt = 0, usedAddressBalance = false) {
463
488
  const inType = this.inputCoinType(req);
464
489
  const outType = this.outputCoinType(req);
465
490
  const tBroadcast = Date.now();
@@ -481,13 +506,13 @@ class CentralExecutor {
481
506
  this.onBroadcastResult?.({ digest, success: false, error, receipt: this.toCheckerReceipt(tr, digest) });
482
507
  return;
483
508
  }
484
- await this.onSuccess(tr, wallet, req, inputCoins, inputTag);
509
+ await this.onSuccess(tr, wallet, req, inputCoins, inputTag, usedAddressBalance);
485
510
  (0, dist_1.log_info)(`[executor] swap quorum-executed 确认 digest=${digest} broadcast=${Date.now() - tBroadcast}ms`);
486
511
  this.onBroadcastResult?.({ digest, success: true, receipt: this.toCheckerReceipt(tr, digest) });
487
512
  }
488
513
  catch (e) {
489
514
  this.cache.abort(inputTag, true);
490
- if (this.isRebuildableObjectError(e) && attempt < this.maxRebuildRetries) {
515
+ if (!usedAddressBalance && this.isRebuildableObjectError(e) && attempt < this.maxRebuildRetries) {
491
516
  (0, dist_1.log_warn)(`[executor] swap 输入对象版本陈旧,定向刷新 ${inType.split('::').pop()} + 重建重发(第 ${attempt + 1}/${this.maxRebuildRetries} 次)digest=${digest}: ${e.message}`);
492
517
  await this.reconcileCoins(wallet, inType);
493
518
  const retry = await this.rebuildSwap(req, wallet);
@@ -602,17 +627,26 @@ class CentralExecutor {
602
627
  }
603
628
  }
604
629
  }
605
- async buildSwapTx(req, wallet, inputCoins) {
630
+ async buildSwapTx(req, wallet, inputCoins, shortfall = 0n) {
606
631
  const tx = new transactions_1.Transaction();
607
632
  tx.setSender(wallet);
608
633
  await this.registerShared(tx, req.poolId, true);
609
634
  await this.registerShared(tx, (0, swap_1.configSharedObjectId)(req.dexId), true);
610
635
  tx.object(transactions_1.Inputs.SharedObjectRef({ objectId: swap_1.SUI_CLOCK_ID, initialSharedVersion: '1', mutable: false }));
611
- const primary = tx.object(transactions_1.Inputs.ObjectRef(inputCoins[0]));
612
- if (inputCoins.length > 1) {
613
- tx.mergeCoins(primary, inputCoins.slice(1).map(c => tx.object(transactions_1.Inputs.ObjectRef(c))));
636
+ const inType = this.inputCoinType(req);
637
+ const usedBalance = shortfall > 0n || inputCoins.length === 0;
638
+ let inCoin;
639
+ if (inputCoins.length === 0) {
640
+ inCoin = (0, transactions_1.coinWithBalance)({ type: inType, balance: req.amountIn });
641
+ }
642
+ else {
643
+ const primary = tx.object(transactions_1.Inputs.ObjectRef(inputCoins[0]));
644
+ if (inputCoins.length > 1)
645
+ tx.mergeCoins(primary, inputCoins.slice(1).map(c => tx.object(transactions_1.Inputs.ObjectRef(c))));
646
+ if (shortfall > 0n)
647
+ tx.mergeCoins(primary, [(0, transactions_1.coinWithBalance)({ type: inType, balance: shortfall })]);
648
+ [inCoin] = tx.splitCoins(primary, [tx.pure.u64(req.amountIn)]);
614
649
  }
615
- const [inCoin] = tx.splitCoins(primary, [tx.pure.u64(req.amountIn)]);
616
650
  const { outputCoin, leftoverCoins } = (0, swap_1.buildSwapMoveCall)(req.dexId, tx, {
617
651
  coinTypeA: req.coinTypeA, coinTypeB: req.coinTypeB, poolId: req.poolId,
618
652
  a2b: req.a2b, byAmountIn: true, amount: req.amountIn,
@@ -623,20 +657,26 @@ class CentralExecutor {
623
657
  tx.setExpiration(await this.getValidDuringExpiration());
624
658
  tx.setGasBudget(this.gasBudget);
625
659
  tx.setGasPrice(await this.getGasPrice());
626
- const txBytes = await tx.build();
660
+ const txBytes = usedBalance ? await tx.build({ client: this.core.rawClient }) : await tx.build();
627
661
  return { txBytes, tx };
628
662
  }
629
- async onSuccess(tr, wallet, req, inputCoins, inputTag) {
663
+ async onSuccess(tr, wallet, req, inputCoins, inputTag, usedAddressBalance = false) {
630
664
  const inType = this.inputCoinType(req);
631
665
  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);
666
+ if (usedAddressBalance) {
667
+ this.cache.abort(inputTag, true);
668
+ await this.reconcileCoins(wallet, inType);
669
+ }
670
+ else {
671
+ const inputTotal = inputCoins.reduce((s, c) => s + BigInt(c.balance), 0n);
672
+ const inputChanges = (0, effects_1.extractSwapInputChanges)(tr.effects, {
673
+ wallet, inCoinType: inType,
674
+ primaryId: inputCoins[0].objectId,
675
+ reservedInputIds: inputCoins.map(c => c.objectId),
676
+ inputTotal, amountIn: req.amountIn,
677
+ });
678
+ this.cache.commit(inputTag, inputChanges);
679
+ }
640
680
  if (this.reconcileAfterTx) {
641
681
  void this.postTradeRebalance(wallet, inType, outType);
642
682
  }
@@ -18,6 +18,7 @@ export declare class InProcessCoinCache {
18
18
  seed(wallet: string, coinType: string, coins: CoinRef[]): void;
19
19
  reconcile(wallet: string, coinType: string, freshCoins: CoinRef[], epochAtStart?: number, quietIfUnchanged?: boolean): boolean;
20
20
  acquire(wallet: string, coinType: string, amount: bigint, txTag: string, maxCoins?: number): CoinReservation;
21
+ acquireAvailable(wallet: string, coinType: string, amount: bigint, txTag: string): CoinRef[];
21
22
  reserve(wallet: string, coinType: string, objectIds: string[], txTag: string): CoinRef[] | null;
22
23
  hasInflight(wallet: string, coinType: string): boolean;
23
24
  commit(txTag: string, changes: CoinObjectChange[]): void;
@@ -21,22 +21,28 @@ class InProcessCoinCache {
21
21
  setAmountFormatter(fn) { this.amountFmt = fn; }
22
22
  fmt(coinType, raw) { return this.amountFmt ? this.amountFmt(coinType, raw) : `${raw.toString()}(raw)`; }
23
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);
24
+ const avail = this.list(wallet, coinType);
25
+ const inUse = [];
26
+ for (const r of this.inflight.values()) {
27
+ if (r.wallet === wallet && r.coinType === coinType)
28
+ inUse.push(...r.coins);
29
+ }
26
30
  const symbol = coinType.split('::').pop();
31
+ const sum = (cs) => cs.reduce((s, c) => s + BigInt(c.balance), 0n);
32
+ const availT = sum(avail), useT = sum(inUse), grand = availT + useT;
33
+ const mapObj = (cs) => cs.map(c => ({
34
+ objectId: c.objectId,
35
+ version: c.version,
36
+ balance: this.fmt(coinType, BigInt(c.balance)),
37
+ }));
27
38
  const detail = {
28
39
  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
- })),
40
+ total: this.fmt(coinType, grand),
41
+ available_count: avail.length, available_total: this.fmt(coinType, availT), available: mapObj(avail),
42
+ in_use_count: inUse.length, in_use_total: this.fmt(coinType, useT), in_use: mapObj(inUse),
38
43
  };
39
- const header = `[coin-cache] ${reason} ${symbol} ${l.length} total=${this.fmt(coinType, total)}`;
44
+ const useStr = inUse.length ? `, ${inUse.length} 个正在交易中` : '';
45
+ const header = `[coin-cache] ${reason} ${symbol}: 共 ${this.fmt(coinType, grand)}(可用 ${avail.length} 个${useStr})`;
40
46
  if (quiet)
41
47
  (0, dist_1.log_debug)(header, detail);
42
48
  else
@@ -105,7 +111,7 @@ class InProcessCoinCache {
105
111
  const sig = (l) => l.map(c => `${c.objectId}@${c.version}:${c.balance}`).join(',');
106
112
  const unchanged = sig(kept) === sig(this.list(wallet, coinType));
107
113
  this.walletMap(wallet).set(coinType, kept);
108
- this.logCoinBreakdown(wallet, coinType, `reconcile(${inflightIds.size}在途)`, unchanged && quietIfUnchanged);
114
+ this.logCoinBreakdown(wallet, coinType, '链上对账', unchanged && quietIfUnchanged);
109
115
  return true;
110
116
  }
111
117
  acquire(wallet, coinType, amount, txTag, maxCoins = 8) {
@@ -132,6 +138,24 @@ class InProcessCoinCache {
132
138
  this.bumpEpoch(wallet, coinType);
133
139
  return { txTag, coinType, coins: picked };
134
140
  }
141
+ acquireAvailable(wallet, coinType, amount, txTag) {
142
+ if (this.inflight.has(txTag))
143
+ throw new Error(`[coin-cache] txTag 重复: ${txTag}`);
144
+ const l = this.list(wallet, coinType);
145
+ const picked = [];
146
+ let sum = 0n;
147
+ for (const c of l) {
148
+ if (sum >= amount)
149
+ break;
150
+ picked.push(c);
151
+ sum += BigInt(c.balance);
152
+ }
153
+ const ids = new Set(picked.map(c => c.objectId));
154
+ this.walletMap(wallet).set(coinType, l.filter(c => !ids.has(c.objectId)));
155
+ this.inflight.set(txTag, { wallet, coinType, coins: picked, gas: false });
156
+ this.bumpEpoch(wallet, coinType);
157
+ return picked;
158
+ }
135
159
  reserve(wallet, coinType, objectIds, txTag) {
136
160
  if (this.inflight.has(txTag))
137
161
  throw new Error(`[coin-cache] txTag 重复: ${txTag}`);
@@ -183,7 +207,7 @@ class InProcessCoinCache {
183
207
  this.committedAt.set(this.commitKey(wallet, ch.coinType, ch.objectId), Date.now());
184
208
  }
185
209
  for (const ct of new Set(changes.map(c => c.coinType)))
186
- this.logCoinBreakdown(res.wallet, ct, 'commit后');
210
+ this.logCoinBreakdown(res.wallet, ct, '交易后更新');
187
211
  }
188
212
  abort(txTag, consumed) {
189
213
  const res = this.inflight.get(txTag);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clonegod/ttd-sui-common",
3
- "version": "2.0.10",
3
+ "version": "2.0.11",
4
4
  "description": "Sui common library",
5
5
  "license": "UNLICENSED",
6
6
  "main": "dist/index.js",