@clonegod/ttd-sui-common 2.0.10 → 2.0.12
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)}
|
|
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
|
-
|
|
438
|
-
|
|
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 =
|
|
445
|
-
|
|
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,
|
|
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,
|
|
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
|
|
612
|
-
|
|
613
|
-
|
|
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
|
-
|
|
633
|
-
|
|
634
|
-
wallet,
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
inputTotal,
|
|
638
|
-
|
|
639
|
-
|
|
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
|
|
25
|
-
const
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
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,
|
|
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, '
|
|
210
|
+
this.logCoinBreakdown(res.wallet, ct, '交易后更新');
|
|
187
211
|
}
|
|
188
212
|
abort(txTag, consumed) {
|
|
189
213
|
const res = this.inflight.get(txTag);
|