@clonegod/ttd-sui-common 2.0.9 → 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.
|
@@ -61,13 +61,19 @@ 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>;
|
|
67
68
|
private rebalanceOne;
|
|
69
|
+
private selectWalletWithBalance;
|
|
68
70
|
private decimalsCache;
|
|
69
71
|
private objectReader?;
|
|
70
72
|
private getCoinDecimalsCached;
|
|
73
|
+
private coinSymbol;
|
|
74
|
+
private pairLabel;
|
|
75
|
+
private fmtAmount;
|
|
76
|
+
private walletBalancesLabel;
|
|
71
77
|
private maintainCoinObjects;
|
|
72
78
|
private execMaintenance;
|
|
73
79
|
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 {
|
|
@@ -194,6 +217,17 @@ class CentralExecutor {
|
|
|
194
217
|
return;
|
|
195
218
|
}
|
|
196
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
|
+
}
|
|
197
231
|
async getCoinDecimalsCached(coinType) {
|
|
198
232
|
let d = this.decimalsCache.get(coinType);
|
|
199
233
|
if (d == null) {
|
|
@@ -208,6 +242,26 @@ class CentralExecutor {
|
|
|
208
242
|
}
|
|
209
243
|
return d;
|
|
210
244
|
}
|
|
245
|
+
coinSymbol(coinType) { return coinType.split('::').pop() || coinType; }
|
|
246
|
+
pairLabel(req) {
|
|
247
|
+
return `${this.coinSymbol(this.inputCoinType(req))}→${this.coinSymbol(this.outputCoinType(req))}`;
|
|
248
|
+
}
|
|
249
|
+
fmtAmount(coinType, raw) {
|
|
250
|
+
const sym = this.coinSymbol(coinType);
|
|
251
|
+
const d = this.decimalsCache.get(coinType);
|
|
252
|
+
if (d == null)
|
|
253
|
+
return `${raw.toString()}(raw) ${sym}`;
|
|
254
|
+
const neg = raw < 0n, v = neg ? -raw : raw, base = 10n ** BigInt(d);
|
|
255
|
+
const intStr = (v / base).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
256
|
+
const frac = (v % base).toString().padStart(d, '0').replace(/0+$/, '');
|
|
257
|
+
return `${neg ? '-' : ''}${intStr}${frac ? '.' + frac : ''} ${sym}`;
|
|
258
|
+
}
|
|
259
|
+
walletBalancesLabel(coinType) {
|
|
260
|
+
return [...this.tradeWallets.keys()].map(w => {
|
|
261
|
+
const { total, count } = this.cache.snapshot(w, coinType);
|
|
262
|
+
return `${w.slice(0, 8)}…=${this.fmtAmount(coinType, total)}(${count}枚)`;
|
|
263
|
+
}).join(', ');
|
|
264
|
+
}
|
|
211
265
|
async maintainCoinObjects(addr, kp, coinType, opts) {
|
|
212
266
|
const short = coinType.split('::').pop();
|
|
213
267
|
const zeroMergeMin = Number(process.env.SUI_ZERO_COIN_MERGE_THRESHOLD || 3);
|
|
@@ -379,27 +433,49 @@ class CentralExecutor {
|
|
|
379
433
|
const t0 = Date.now();
|
|
380
434
|
req = await this.canonicalizeReq(req);
|
|
381
435
|
const inType = this.inputCoinType(req);
|
|
382
|
-
|
|
436
|
+
await this.getCoinDecimalsCached(inType).catch(() => { });
|
|
437
|
+
let wallet = '';
|
|
438
|
+
let inputCoins = [];
|
|
439
|
+
let inputTag = '';
|
|
440
|
+
let shortfall = 0n;
|
|
383
441
|
try {
|
|
384
442
|
wallet = this.chooseTradeWallet(req, inType, req.amountIn);
|
|
443
|
+
inputTag = this.nextTag('in');
|
|
444
|
+
inputCoins = this.cache.acquire(wallet, inType, req.amountIn, inputTag).coins;
|
|
385
445
|
}
|
|
386
446
|
catch (e) {
|
|
387
|
-
(0, dist_1.log_warn)(`[executor] ${inType}
|
|
447
|
+
(0, dist_1.log_warn)(`[executor] ${this.pairLabel(req)} 对象不足:需 ${this.fmtAmount(inType, req.amountIn)},当前缓存 [${this.walletBalancesLabel(inType)}] → reconcile 重试`);
|
|
388
448
|
await Promise.all([...this.tradeWallets.keys()].map(w => this.reconcileCoins(w, inType)));
|
|
389
|
-
|
|
449
|
+
try {
|
|
450
|
+
wallet = this.chooseTradeWallet(req, inType, req.amountIn);
|
|
451
|
+
inputTag = this.nextTag('in');
|
|
452
|
+
inputCoins = this.cache.acquire(wallet, inType, req.amountIn, inputTag).coins;
|
|
453
|
+
}
|
|
454
|
+
catch (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;
|
|
465
|
+
}
|
|
390
466
|
}
|
|
391
|
-
const inputTag = this.nextTag('in');
|
|
392
|
-
const inputRes = this.cache.acquire(wallet, inType, req.amountIn, inputTag);
|
|
393
467
|
const tAcquire = Date.now();
|
|
394
|
-
|
|
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`);
|
|
395
471
|
try {
|
|
396
|
-
const { txBytes, tx } = await this.buildSwapTx(req, wallet,
|
|
472
|
+
const { txBytes, tx } = await this.buildSwapTx(req, wallet, inputCoins, shortfall);
|
|
397
473
|
const tBuild = Date.now();
|
|
398
474
|
const { signature: senderSig } = await this.tradeWallets.get(wallet).signTransaction(txBytes);
|
|
399
475
|
const digest = await tx.getDigest();
|
|
400
476
|
const tSign = Date.now();
|
|
401
|
-
void this.broadcastAndCommit(txBytes, [senderSig], digest, wallet, req,
|
|
402
|
-
(0, dist_1.log_info)(`[executor] swap 已签提交 digest=${digest} dex=${req.dexId} ${req
|
|
477
|
+
void this.broadcastAndCommit(txBytes, [senderSig], digest, wallet, req, inputCoins, inputTag, 0, shortfall > 0n);
|
|
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}`);
|
|
403
479
|
return { digest, submitted: true };
|
|
404
480
|
}
|
|
405
481
|
catch (e) {
|
|
@@ -408,7 +484,7 @@ class CentralExecutor {
|
|
|
408
484
|
return { digest: '', submitted: false, error: e.message };
|
|
409
485
|
}
|
|
410
486
|
}
|
|
411
|
-
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) {
|
|
412
488
|
const inType = this.inputCoinType(req);
|
|
413
489
|
const outType = this.outputCoinType(req);
|
|
414
490
|
const tBroadcast = Date.now();
|
|
@@ -430,13 +506,13 @@ class CentralExecutor {
|
|
|
430
506
|
this.onBroadcastResult?.({ digest, success: false, error, receipt: this.toCheckerReceipt(tr, digest) });
|
|
431
507
|
return;
|
|
432
508
|
}
|
|
433
|
-
await this.onSuccess(tr, wallet, req, inputCoins, inputTag);
|
|
509
|
+
await this.onSuccess(tr, wallet, req, inputCoins, inputTag, usedAddressBalance);
|
|
434
510
|
(0, dist_1.log_info)(`[executor] swap quorum-executed 确认 digest=${digest} broadcast=${Date.now() - tBroadcast}ms`);
|
|
435
511
|
this.onBroadcastResult?.({ digest, success: true, receipt: this.toCheckerReceipt(tr, digest) });
|
|
436
512
|
}
|
|
437
513
|
catch (e) {
|
|
438
514
|
this.cache.abort(inputTag, true);
|
|
439
|
-
if (this.isRebuildableObjectError(e) && attempt < this.maxRebuildRetries) {
|
|
515
|
+
if (!usedAddressBalance && this.isRebuildableObjectError(e) && attempt < this.maxRebuildRetries) {
|
|
440
516
|
(0, dist_1.log_warn)(`[executor] swap 输入对象版本陈旧,定向刷新 ${inType.split('::').pop()} + 重建重发(第 ${attempt + 1}/${this.maxRebuildRetries} 次)digest=${digest}: ${e.message}`);
|
|
441
517
|
await this.reconcileCoins(wallet, inType);
|
|
442
518
|
const retry = await this.rebuildSwap(req, wallet);
|
|
@@ -551,17 +627,26 @@ class CentralExecutor {
|
|
|
551
627
|
}
|
|
552
628
|
}
|
|
553
629
|
}
|
|
554
|
-
async buildSwapTx(req, wallet, inputCoins) {
|
|
630
|
+
async buildSwapTx(req, wallet, inputCoins, shortfall = 0n) {
|
|
555
631
|
const tx = new transactions_1.Transaction();
|
|
556
632
|
tx.setSender(wallet);
|
|
557
633
|
await this.registerShared(tx, req.poolId, true);
|
|
558
634
|
await this.registerShared(tx, (0, swap_1.configSharedObjectId)(req.dexId), true);
|
|
559
635
|
tx.object(transactions_1.Inputs.SharedObjectRef({ objectId: swap_1.SUI_CLOCK_ID, initialSharedVersion: '1', mutable: false }));
|
|
560
|
-
const
|
|
561
|
-
|
|
562
|
-
|
|
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)]);
|
|
563
649
|
}
|
|
564
|
-
const [inCoin] = tx.splitCoins(primary, [tx.pure.u64(req.amountIn)]);
|
|
565
650
|
const { outputCoin, leftoverCoins } = (0, swap_1.buildSwapMoveCall)(req.dexId, tx, {
|
|
566
651
|
coinTypeA: req.coinTypeA, coinTypeB: req.coinTypeB, poolId: req.poolId,
|
|
567
652
|
a2b: req.a2b, byAmountIn: true, amount: req.amountIn,
|
|
@@ -572,20 +657,26 @@ class CentralExecutor {
|
|
|
572
657
|
tx.setExpiration(await this.getValidDuringExpiration());
|
|
573
658
|
tx.setGasBudget(this.gasBudget);
|
|
574
659
|
tx.setGasPrice(await this.getGasPrice());
|
|
575
|
-
const txBytes = await tx.build();
|
|
660
|
+
const txBytes = usedBalance ? await tx.build({ client: this.core.rawClient }) : await tx.build();
|
|
576
661
|
return { txBytes, tx };
|
|
577
662
|
}
|
|
578
|
-
async onSuccess(tr, wallet, req, inputCoins, inputTag) {
|
|
663
|
+
async onSuccess(tr, wallet, req, inputCoins, inputTag, usedAddressBalance = false) {
|
|
579
664
|
const inType = this.inputCoinType(req);
|
|
580
665
|
const outType = this.outputCoinType(req);
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
wallet,
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
inputTotal,
|
|
587
|
-
|
|
588
|
-
|
|
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
|
+
}
|
|
589
680
|
if (this.reconcileAfterTx) {
|
|
590
681
|
void this.postTradeRebalance(wallet, inType, outType);
|
|
591
682
|
}
|
|
@@ -2,16 +2,23 @@ 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;
|
|
12
18
|
seed(wallet: string, coinType: string, coins: CoinRef[]): void;
|
|
13
19
|
reconcile(wallet: string, coinType: string, freshCoins: CoinRef[], epochAtStart?: number, quietIfUnchanged?: boolean): boolean;
|
|
14
20
|
acquire(wallet: string, coinType: string, amount: bigint, txTag: string, maxCoins?: number): CoinReservation;
|
|
21
|
+
acquireAvailable(wallet: string, coinType: string, amount: bigint, txTag: string): CoinRef[];
|
|
15
22
|
reserve(wallet: string, coinType: string, objectIds: string[], txTag: string): CoinRef[] | null;
|
|
16
23
|
hasInflight(wallet: string, coinType: string): boolean;
|
|
17
24
|
commit(txTag: string, changes: CoinObjectChange[]): void;
|
|
@@ -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,36 @@ 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 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
|
+
}
|
|
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
|
+
}));
|
|
38
|
+
const detail = {
|
|
39
|
+
reason, wallet, symbol, coinType,
|
|
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),
|
|
43
|
+
};
|
|
44
|
+
const useStr = inUse.length ? `, ${inUse.length} 个正在交易中` : '';
|
|
45
|
+
const header = `[coin-cache] ${reason} ${symbol}: 共 ${this.fmt(coinType, grand)}(可用 ${avail.length} 个${useStr})`;
|
|
46
|
+
if (quiet)
|
|
47
|
+
(0, dist_1.log_debug)(header, detail);
|
|
48
|
+
else
|
|
49
|
+
(0, dist_1.log_info)(header, detail);
|
|
50
|
+
}
|
|
19
51
|
walletMap(wallet) {
|
|
20
52
|
let m = this.available.get(wallet);
|
|
21
53
|
if (!m) {
|
|
@@ -54,18 +86,32 @@ class InProcessCoinCache {
|
|
|
54
86
|
r.coins.forEach(c => inflightIds.add(c.objectId));
|
|
55
87
|
}
|
|
56
88
|
const prev = new Map(this.list(wallet, coinType).map(c => [c.objectId, c]));
|
|
89
|
+
const freshIds = new Set(freshCoins.map(c => c.objectId));
|
|
57
90
|
const kept = freshCoins
|
|
58
91
|
.filter(c => !inflightIds.has(c.objectId))
|
|
59
92
|
.map(c => { const old = prev.get(c.objectId); return old && BigInt(old.version) > BigInt(c.version) ? old : c; });
|
|
93
|
+
const keptIds = new Set(kept.map(c => c.objectId));
|
|
94
|
+
const now = Date.now();
|
|
95
|
+
const graceMs = Number(process.env.SUI_COIN_RECONCILE_GRACE_MS || 10000);
|
|
96
|
+
for (const [id, c] of prev) {
|
|
97
|
+
if (keptIds.has(id) || inflightIds.has(id) || freshIds.has(id))
|
|
98
|
+
continue;
|
|
99
|
+
const ts = this.committedAt.get(this.commitKey(wallet, coinType, id));
|
|
100
|
+
if (ts != null && now - ts < graceMs) {
|
|
101
|
+
kept.push(c);
|
|
102
|
+
keptIds.add(id);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
for (const id of freshIds)
|
|
106
|
+
this.committedAt.delete(this.commitKey(wallet, coinType, id));
|
|
107
|
+
for (const id of prev.keys())
|
|
108
|
+
if (!keptIds.has(id))
|
|
109
|
+
this.committedAt.delete(this.commitKey(wallet, coinType, id));
|
|
60
110
|
this.sortDesc(kept);
|
|
61
111
|
const sig = (l) => l.map(c => `${c.objectId}@${c.version}:${c.balance}`).join(',');
|
|
62
112
|
const unchanged = sig(kept) === sig(this.list(wallet, coinType));
|
|
63
113
|
this.walletMap(wallet).set(coinType, kept);
|
|
64
|
-
|
|
65
|
-
if (unchanged && quietIfUnchanged)
|
|
66
|
-
(0, dist_1.log_debug)(msg);
|
|
67
|
-
else
|
|
68
|
-
(0, dist_1.log_info)(msg);
|
|
114
|
+
this.logCoinBreakdown(wallet, coinType, '链上对账', unchanged && quietIfUnchanged);
|
|
69
115
|
return true;
|
|
70
116
|
}
|
|
71
117
|
acquire(wallet, coinType, amount, txTag, maxCoins = 8) {
|
|
@@ -92,6 +138,24 @@ class InProcessCoinCache {
|
|
|
92
138
|
this.bumpEpoch(wallet, coinType);
|
|
93
139
|
return { txTag, coinType, coins: picked };
|
|
94
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
|
+
}
|
|
95
159
|
reserve(wallet, coinType, objectIds, txTag) {
|
|
96
160
|
if (this.inflight.has(txTag))
|
|
97
161
|
throw new Error(`[coin-cache] txTag 重复: ${txTag}`);
|
|
@@ -126,6 +190,7 @@ class InProcessCoinCache {
|
|
|
126
190
|
const wallet = res.wallet;
|
|
127
191
|
if (ch.kind === 'deleted') {
|
|
128
192
|
this.removeEverywhere(wallet, ch.coinType, ch.objectId);
|
|
193
|
+
this.committedAt.delete(this.commitKey(wallet, ch.coinType, ch.objectId));
|
|
129
194
|
continue;
|
|
130
195
|
}
|
|
131
196
|
if (ch.version == null || ch.digest == null) {
|
|
@@ -139,7 +204,10 @@ class InProcessCoinCache {
|
|
|
139
204
|
const l = this.list(wallet, ch.coinType);
|
|
140
205
|
l.push(ref);
|
|
141
206
|
this.sortDesc(l);
|
|
207
|
+
this.committedAt.set(this.commitKey(wallet, ch.coinType, ch.objectId), Date.now());
|
|
142
208
|
}
|
|
209
|
+
for (const ct of new Set(changes.map(c => c.coinType)))
|
|
210
|
+
this.logCoinBreakdown(res.wallet, ct, '交易后更新');
|
|
143
211
|
}
|
|
144
212
|
abort(txTag, consumed) {
|
|
145
213
|
const res = this.inflight.get(txTag);
|
|
@@ -150,6 +218,8 @@ class InProcessCoinCache {
|
|
|
150
218
|
this.inflight.delete(txTag);
|
|
151
219
|
this.bumpEpoch(res.wallet, res.coinType);
|
|
152
220
|
if (consumed) {
|
|
221
|
+
for (const c of res.coins)
|
|
222
|
+
this.committedAt.delete(this.commitKey(res.wallet, res.coinType, c.objectId));
|
|
153
223
|
(0, dist_1.log_warn)(`[coin-cache] abort consumed txTag=${txTag}(${res.coins.length} coin 交 reconcile 兜底)`);
|
|
154
224
|
return;
|
|
155
225
|
}
|