@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.
- package/dist/trade/executor/central_executor.d.ts +16 -0
- package/dist/trade/executor/central_executor.js +151 -21
- package/dist/trade/executor/coin_cache.d.ts +6 -0
- package/dist/trade/executor/coin_cache.js +55 -6
- package/dist/trade/executor/effects.d.ts +18 -0
- package/dist/trade/executor/effects.js +49 -0
- package/package.json +1 -1
|
@@ -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 (
|
|
228
|
-
|
|
229
|
-
|
|
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}
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
362
|
-
(0, dist_1.log_info)(`[executor] swap 已签提交 digest=${digest} dex=${req.dexId} ${req
|
|
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,
|
|
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,
|
|
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,
|
|
504
|
-
const
|
|
505
|
-
const
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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
|
|
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
|
-
|
|
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';
|