@clonegod/ttd-sui-common 2.0.5 → 2.0.7

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.
@@ -64,9 +64,11 @@ class GrpcConnection {
64
64
  'grpc.max_reconnect_backoff_ms': 30000,
65
65
  'grpc.enable_retries': 1,
66
66
  'grpc.retry_buffer_size': 256 * 1024,
67
+ 'grpc-node.flow_control_window': 8 * 1024 * 1024,
68
+ 'grpc-node.max_session_memory': 64,
69
+ 'grpc.default_compression_algorithm': 2,
67
70
  'grpc.http2.max_frame_size': 16384,
68
71
  'grpc.http2.max_header_list_size': 16384,
69
- 'grpc.http2.initial_window_size': 65535,
70
72
  }; }
71
73
  getChannel() {
72
74
  if (!this.channel) {
@@ -9,6 +9,6 @@ interface RedisLike {
9
9
  hgetall(key: string): Promise<Record<string, string> | null>;
10
10
  subscribe(channel: string, listener: (message: string) => void): Promise<void> | void;
11
11
  }
12
- export declare function resolveGrpcProvider(redis: RedisLike, chainId: string): Promise<GrpcProviderConfig>;
13
- export declare function watchGrpcProviderChange(redis: RedisLike, chainId: string, active: GrpcProviderConfig, onChange: (next: GrpcProviderConfig | null) => void): void;
12
+ export declare function resolveGrpcProvider(redis: RedisLike, chainId: string, preferId?: string): Promise<GrpcProviderConfig>;
13
+ export declare function watchGrpcProviderChange(redis: RedisLike, chainId: string, active: GrpcProviderConfig, onChange: (next: GrpcProviderConfig | null) => void, preferId?: string): void;
14
14
  export {};
@@ -11,7 +11,7 @@ function getRpcProvidersKey(chainId) {
11
11
  function getRpcConfigChangeChannel(chainId) {
12
12
  return `${chainId.toLowerCase()}:rpc:config:change`;
13
13
  }
14
- async function resolveGrpcProvider(redis, chainId) {
14
+ async function resolveGrpcProvider(redis, chainId, preferId) {
15
15
  const key = getRpcProvidersKey(chainId);
16
16
  const map = (await redis.hgetall(key)) || {};
17
17
  const candidates = [];
@@ -28,6 +28,16 @@ async function resolveGrpcProvider(redis, chainId) {
28
28
  if (candidates.length === 0) {
29
29
  throw new Error(`[grpc-registry] ${key} 无 enabled 的 grpc provider —— 请在 trade-analyze Config/RPC 页新增 type=grpc 的 Provider(grpc_endpoint 填 host:port,auth_token 填 x-token)`);
30
30
  }
31
+ if (preferId) {
32
+ const preferred = candidates.find(c => c.id === preferId);
33
+ if (!preferred) {
34
+ throw new Error(`[grpc-registry] 指定的 grpc provider '${preferId}' 不存在或未 enabled(${key} 可用: ${candidates.map(c => c.id).join(',')})`);
35
+ }
36
+ if (!preferred.auth_token)
37
+ (0, dist_1.log_warn)(`[grpc-registry] provider ${preferred.id} auth_token 为空,按无鉴权端点连接`);
38
+ (0, dist_1.log_info)(`[grpc-registry] 使用指定 provider: ${preferred.id}`);
39
+ return { id: preferred.id, endpoint: preferred.grpc_endpoint, token: preferred.auth_token || '' };
40
+ }
31
41
  candidates.sort((a, b) => (b.default_for_quote === true ? 1 : 0) - (a.default_for_quote === true ? 1 : 0)
32
42
  || a.id.localeCompare(b.id));
33
43
  const picked = candidates[0];
@@ -39,13 +49,13 @@ async function resolveGrpcProvider(redis, chainId) {
39
49
  }
40
50
  return { id: picked.id, endpoint: picked.grpc_endpoint, token: picked.auth_token || '' };
41
51
  }
42
- function watchGrpcProviderChange(redis, chainId, active, onChange) {
52
+ function watchGrpcProviderChange(redis, chainId, active, onChange, preferId) {
43
53
  const channel = getRpcConfigChangeChannel(chainId);
44
54
  redis.subscribe(channel, (message) => {
45
55
  void (async () => {
46
56
  let next;
47
57
  try {
48
- next = await resolveGrpcProvider(redis, chainId);
58
+ next = await resolveGrpcProvider(redis, chainId, preferId);
49
59
  }
50
60
  catch {
51
61
  next = null;
@@ -173,6 +173,9 @@ class AbstractDexQuote {
173
173
  streamTime: result.streamTimestamp, quoteStartTime: result.quoteStartTime, blockNumber: result.blockNumber,
174
174
  quotes: [result.askQuote, result.bidQuote], txid: result.txid, source: result.source, depth: result.depth,
175
175
  });
176
+ if (isV2) {
177
+ (0, dist_1.log_info)(`[QUOTE OK v2] ${result.poolInfo.pool_name} ask=${result.askQuote.price} bid=${result.bidQuote.price} blk=${result.blockNumber} tx=${result.txIndex} ${result.txid}`);
178
+ }
176
179
  }
177
180
  result.trace?.set('published', shouldPush);
178
181
  result.trace?.mark('publish');
@@ -0,0 +1,28 @@
1
+ export interface DepthTick {
2
+ index: number;
3
+ liquidityNet: bigint;
4
+ sqrtPriceX64: bigint;
5
+ }
6
+ export interface ClmmDepthInput {
7
+ sqrtPriceX64: bigint;
8
+ currentTick: number;
9
+ liquidity: bigint;
10
+ ticks: DepthTick[];
11
+ zeroForOne: boolean;
12
+ targetBps: number;
13
+ inputDecimals: number;
14
+ outputDecimals: number;
15
+ }
16
+ export interface DepthResult {
17
+ amountInWei: bigint;
18
+ amountIn: number;
19
+ amountOutWei: bigint;
20
+ amountOut: number;
21
+ currentTick: number;
22
+ targetTick: number;
23
+ targetSqrtPriceX64: bigint;
24
+ }
25
+ export declare function calculateClmmDepth(input: ClmmDepthInput): DepthResult;
26
+ export declare function priceFromSqrtX64(sqrtPriceX64: bigint, baseDecimals: number, quoteDecimals: number, baseIsToken0: boolean): number;
27
+ export declare function computeTargetSqrtX64(currentSqrtPriceX64: bigint, bps: number, zeroForOne: boolean): bigint;
28
+ export declare function bigIntSqrt(n: bigint): bigint;
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.calculateClmmDepth = calculateClmmDepth;
4
+ exports.priceFromSqrtX64 = priceFromSqrtX64;
5
+ exports.computeTargetSqrtX64 = computeTargetSqrtX64;
6
+ exports.bigIntSqrt = bigIntSqrt;
7
+ const Q64 = 1n << 64n;
8
+ function calculateClmmDepth(input) {
9
+ const { currentTick, zeroForOne, targetBps, inputDecimals, outputDecimals } = input;
10
+ const currentSqrtPriceX64 = input.sqrtPriceX64;
11
+ let liquidity = input.liquidity;
12
+ if (liquidity <= 0n) {
13
+ return { amountInWei: 0n, amountIn: 0, amountOutWei: 0n, amountOut: 0, currentTick, targetTick: currentTick, targetSqrtPriceX64: currentSqrtPriceX64 };
14
+ }
15
+ const targetSqrtPriceX64 = computeTargetSqrtX64(currentSqrtPriceX64, targetBps, zeroForOne);
16
+ const ticksToTraverse = getTicksBetween(input.ticks, currentTick, zeroForOne);
17
+ let totalInput = 0n;
18
+ let totalOutput = 0n;
19
+ let sqrtPriceCursor = currentSqrtPriceX64;
20
+ for (const tick of ticksToTraverse) {
21
+ const sqrtPriceAtTick = tick.sqrtPriceX64;
22
+ if (zeroForOne && sqrtPriceAtTick <= targetSqrtPriceX64)
23
+ break;
24
+ if (!zeroForOne && sqrtPriceAtTick >= targetSqrtPriceX64)
25
+ break;
26
+ if (liquidity > 0n) {
27
+ totalInput += calcInputForRange(sqrtPriceCursor, sqrtPriceAtTick, liquidity, zeroForOne);
28
+ totalOutput += calcOutputForRange(sqrtPriceCursor, sqrtPriceAtTick, liquidity, zeroForOne);
29
+ }
30
+ let liquidityNet = tick.liquidityNet;
31
+ if (zeroForOne)
32
+ liquidityNet = -liquidityNet;
33
+ liquidity = liquidity + liquidityNet;
34
+ if (liquidity < 0n)
35
+ liquidity = 0n;
36
+ sqrtPriceCursor = sqrtPriceAtTick;
37
+ }
38
+ if (liquidity > 0n && sqrtPriceCursor !== targetSqrtPriceX64) {
39
+ totalInput += calcInputForRange(sqrtPriceCursor, targetSqrtPriceX64, liquidity, zeroForOne);
40
+ totalOutput += calcOutputForRange(sqrtPriceCursor, targetSqrtPriceX64, liquidity, zeroForOne);
41
+ }
42
+ const amountIn = Number(totalInput) / Math.pow(10, inputDecimals);
43
+ const amountOut = Number(totalOutput) / Math.pow(10, outputDecimals);
44
+ const sqrtPriceFloat = Number(targetSqrtPriceX64) / Number(Q64);
45
+ const targetTick = Math.floor(Math.log(sqrtPriceFloat * sqrtPriceFloat) / Math.log(1.0001));
46
+ return { amountInWei: totalInput, amountIn, amountOutWei: totalOutput, amountOut, currentTick, targetTick, targetSqrtPriceX64 };
47
+ }
48
+ function priceFromSqrtX64(sqrtPriceX64, baseDecimals, quoteDecimals, baseIsToken0) {
49
+ const sp = Number(sqrtPriceX64) / Number(Q64);
50
+ const priceToken1PerToken0 = sp * sp;
51
+ if (baseIsToken0) {
52
+ return priceToken1PerToken0 * Math.pow(10, baseDecimals - quoteDecimals);
53
+ }
54
+ else {
55
+ return (1 / priceToken1PerToken0) * Math.pow(10, baseDecimals - quoteDecimals);
56
+ }
57
+ }
58
+ function computeTargetSqrtX64(currentSqrtPriceX64, bps, zeroForOne) {
59
+ const PRECISION = 10n ** 18n;
60
+ const bpsBigInt = BigInt(bps);
61
+ const factor = zeroForOne ? 10000n - bpsBigInt : 10000n + bpsBigInt;
62
+ const sqrtFactor = bigIntSqrt(factor * PRECISION);
63
+ const sqrtBase = bigIntSqrt(10000n * PRECISION);
64
+ return currentSqrtPriceX64 * sqrtFactor / sqrtBase;
65
+ }
66
+ function bigIntSqrt(n) {
67
+ if (n < 0n)
68
+ throw new Error('sqrt of negative');
69
+ if (n === 0n)
70
+ return 0n;
71
+ if (n <= 3n)
72
+ return 1n;
73
+ let x = n;
74
+ let y = (x + 1n) / 2n;
75
+ while (y < x) {
76
+ x = y;
77
+ y = (x + n / x) / 2n;
78
+ }
79
+ return x;
80
+ }
81
+ function getTicksBetween(ticks, currentTick, zeroForOne) {
82
+ if (zeroForOne) {
83
+ return ticks.filter(t => t.index <= currentTick).sort((a, b) => b.index - a.index);
84
+ }
85
+ else {
86
+ return ticks.filter(t => t.index > currentTick).sort((a, b) => a.index - b.index);
87
+ }
88
+ }
89
+ function calcInputForRange(sqrtPriceA, sqrtPriceB, liquidity, zeroForOne) {
90
+ const lower = sqrtPriceA < sqrtPriceB ? sqrtPriceA : sqrtPriceB;
91
+ const upper = sqrtPriceA < sqrtPriceB ? sqrtPriceB : sqrtPriceA;
92
+ const diff = upper - lower;
93
+ if (diff === 0n || liquidity === 0n)
94
+ return 0n;
95
+ if (zeroForOne) {
96
+ return liquidity * diff * Q64 / (lower * upper);
97
+ }
98
+ else {
99
+ return liquidity * diff / Q64;
100
+ }
101
+ }
102
+ function calcOutputForRange(sqrtPriceA, sqrtPriceB, liquidity, zeroForOne) {
103
+ const lower = sqrtPriceA < sqrtPriceB ? sqrtPriceA : sqrtPriceB;
104
+ const upper = sqrtPriceA < sqrtPriceB ? sqrtPriceB : sqrtPriceA;
105
+ const diff = upper - lower;
106
+ if (diff === 0n || liquidity === 0n)
107
+ return 0n;
108
+ if (zeroForOne) {
109
+ return liquidity * diff / Q64;
110
+ }
111
+ else {
112
+ return liquidity * diff * Q64 / (lower * upper);
113
+ }
114
+ }
@@ -0,0 +1,24 @@
1
+ import { QuoteDepthOutput } from '@clonegod/ttd-core';
2
+ import { DepthTick } from './clmm_depth_calculator';
3
+ export { calculateClmmDepth, priceFromSqrtX64, computeTargetSqrtX64, bigIntSqrt } from './clmm_depth_calculator';
4
+ export type { DepthTick, ClmmDepthInput, DepthResult } from './clmm_depth_calculator';
5
+ export interface BuildClmmDepthInput {
6
+ poolInfo: {
7
+ pool_name: string;
8
+ tokenA: any;
9
+ tokenB: any;
10
+ quote_token: string;
11
+ };
12
+ poolAddress: string;
13
+ poolState: {
14
+ currentSqrtPriceX64: bigint;
15
+ currentTick: number;
16
+ liquidity: bigint;
17
+ baseIsToken0: boolean;
18
+ };
19
+ ticks: DepthTick[];
20
+ basePriceUsd: number;
21
+ quotePriceUsd: number;
22
+ feeRateBps: number;
23
+ }
24
+ export declare function buildClmmDepth(input: BuildClmmDepthInput): QuoteDepthOutput | undefined;
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.bigIntSqrt = exports.computeTargetSqrtX64 = exports.priceFromSqrtX64 = exports.calculateClmmDepth = void 0;
4
+ exports.buildClmmDepth = buildClmmDepth;
5
+ const ttd_core_1 = require("@clonegod/ttd-core");
6
+ const ttd_core_2 = require("@clonegod/ttd-core");
7
+ const trade_direction_1 = require("../../utils/trade_direction");
8
+ const clmm_depth_calculator_1 = require("./clmm_depth_calculator");
9
+ var clmm_depth_calculator_2 = require("./clmm_depth_calculator");
10
+ Object.defineProperty(exports, "calculateClmmDepth", { enumerable: true, get: function () { return clmm_depth_calculator_2.calculateClmmDepth; } });
11
+ Object.defineProperty(exports, "priceFromSqrtX64", { enumerable: true, get: function () { return clmm_depth_calculator_2.priceFromSqrtX64; } });
12
+ Object.defineProperty(exports, "computeTargetSqrtX64", { enumerable: true, get: function () { return clmm_depth_calculator_2.computeTargetSqrtX64; } });
13
+ Object.defineProperty(exports, "bigIntSqrt", { enumerable: true, get: function () { return clmm_depth_calculator_2.bigIntSqrt; } });
14
+ let _depthPctLogged = false;
15
+ function logDepthLevelsOnce() {
16
+ if (_depthPctLogged)
17
+ return;
18
+ _depthPctLogged = true;
19
+ (0, ttd_core_1.log_info)(`[Depth] pctLevels=${JSON.stringify((0, ttd_core_2.getDepthPricePctLevels)())}, default=${ttd_core_2.DEFAULT_TIER_PCT}`);
20
+ }
21
+ function grossUpFee(amountInNet, feeRateBps) {
22
+ if (feeRateBps <= 0 || amountInNet <= 0)
23
+ return { amountInGross: amountInNet, feeAmount: 0 };
24
+ const feeRatio = feeRateBps / 10000;
25
+ const amountInGross = amountInNet / (1 - feeRatio);
26
+ return { amountInGross, feeAmount: amountInGross - amountInNet };
27
+ }
28
+ function assembleEntry(tiers) {
29
+ const def = tiers.find(t => t.pct === ttd_core_2.DEFAULT_TIER_PCT) ?? tiers[0];
30
+ return {
31
+ price: def.price, amount: def.amount, amount_in: def.amount_in, amount_in_usd: def.amount_in_usd,
32
+ fee: def.fee, fee_usd: def.fee_usd, tick_move: def.tick_move, tiers,
33
+ };
34
+ }
35
+ function buildClmmDepth(input) {
36
+ logDepthLevelsOnce();
37
+ const pctLevels = (0, ttd_core_2.getDepthPricePctLevels)();
38
+ if (pctLevels.length === 0)
39
+ return undefined;
40
+ const { poolInfo, poolState, ticks, basePriceUsd, quotePriceUsd, feeRateBps } = input;
41
+ if (poolState.liquidity <= 0n)
42
+ return undefined;
43
+ if (feeRateBps == null || feeRateBps < 0) {
44
+ (0, ttd_core_1.log_debug)(`[Depth] ${poolInfo.pool_name} CLMM: invalid feeRateBps=${feeRateBps}, skip`, '');
45
+ return undefined;
46
+ }
47
+ try {
48
+ const dir = (0, trade_direction_1.resolveTradeDirection)(poolInfo, true);
49
+ const baseToken = dir.baseToken;
50
+ const quoteToken = dir.quoteToken;
51
+ const baseIsToken0 = poolState.baseIsToken0;
52
+ const askZeroForOne = !baseIsToken0;
53
+ const bidZeroForOne = baseIsToken0;
54
+ const midPrice = (0, clmm_depth_calculator_1.priceFromSqrtX64)(poolState.currentSqrtPriceX64, baseToken.decimals, quoteToken.decimals, baseIsToken0);
55
+ const askTiers = [];
56
+ const bidTiers = [];
57
+ for (const pct of pctLevels) {
58
+ const bps = Math.round(pct * 100);
59
+ const askResult = (0, clmm_depth_calculator_1.calculateClmmDepth)({
60
+ sqrtPriceX64: poolState.currentSqrtPriceX64, currentTick: poolState.currentTick,
61
+ liquidity: poolState.liquidity, ticks, zeroForOne: askZeroForOne, targetBps: bps,
62
+ inputDecimals: quoteToken.decimals, outputDecimals: baseToken.decimals,
63
+ });
64
+ const bidResult = (0, clmm_depth_calculator_1.calculateClmmDepth)({
65
+ sqrtPriceX64: poolState.currentSqrtPriceX64, currentTick: poolState.currentTick,
66
+ liquidity: poolState.liquidity, ticks, zeroForOne: bidZeroForOne, targetBps: bps,
67
+ inputDecimals: baseToken.decimals, outputDecimals: quoteToken.decimals,
68
+ });
69
+ const askTargetPrice = (0, clmm_depth_calculator_1.priceFromSqrtX64)(askResult.targetSqrtPriceX64, baseToken.decimals, quoteToken.decimals, baseIsToken0);
70
+ const bidTargetPrice = (0, clmm_depth_calculator_1.priceFromSqrtX64)(bidResult.targetSqrtPriceX64, baseToken.decimals, quoteToken.decimals, baseIsToken0);
71
+ const askGross = grossUpFee(askResult.amountIn, feeRateBps);
72
+ const bidGross = grossUpFee(bidResult.amountIn, feeRateBps);
73
+ const askTickMove = `${askResult.targetTick} <- ${askResult.currentTick}`;
74
+ const bidTickMove = `${bidResult.currentTick} -> ${bidResult.targetTick}`;
75
+ const askEffPrice = askResult.amountOut > 0 ? askGross.amountInGross / askResult.amountOut : askTargetPrice;
76
+ const bidEffPrice = bidGross.amountInGross > 0 ? bidResult.amountOut / bidGross.amountInGross : bidTargetPrice;
77
+ askTiers.push({
78
+ pct, price: askEffPrice, amount: askResult.amountOut,
79
+ amount_in: askGross.amountInGross, amount_in_usd: askGross.amountInGross * quotePriceUsd,
80
+ fee: askGross.feeAmount, fee_usd: askGross.feeAmount * quotePriceUsd, tick_move: askTickMove,
81
+ });
82
+ bidTiers.push({
83
+ pct, price: bidEffPrice, amount: bidResult.amountOut,
84
+ amount_in: bidGross.amountInGross, amount_in_usd: bidGross.amountInGross * basePriceUsd,
85
+ fee: bidGross.feeAmount, fee_usd: bidGross.feeAmount * basePriceUsd, tick_move: bidTickMove,
86
+ });
87
+ }
88
+ return {
89
+ mid_price: midPrice,
90
+ fee_rate_bps: feeRateBps,
91
+ ask: assembleEntry(askTiers),
92
+ bid: assembleEntry(bidTiers),
93
+ };
94
+ }
95
+ catch (error) {
96
+ (0, ttd_core_1.log_debug)(`[Depth] ${poolInfo.pool_name} CLMM depth failed: ${error.message}`, '');
97
+ return undefined;
98
+ }
99
+ }
@@ -5,4 +5,5 @@ export * from './quote_trace';
5
5
  export * from './quote_amount';
6
6
  export * from './verify';
7
7
  export * from './tick';
8
+ export * from './depth';
8
9
  export * from './abstract_dex_quote';
@@ -21,4 +21,5 @@ __exportStar(require("./quote_trace"), exports);
21
21
  __exportStar(require("./quote_amount"), exports);
22
22
  __exportStar(require("./verify"), exports);
23
23
  __exportStar(require("./tick"), exports);
24
+ __exportStar(require("./depth"), exports);
24
25
  __exportStar(require("./abstract_dex_quote"), exports);
@@ -46,7 +46,7 @@ class AbstractSuiDexTrade extends dist_1.AbastrcatTrade {
46
46
  try {
47
47
  const msg = JSON.parse(message);
48
48
  if (msg.digest)
49
- this.appConfig.emit(`SUI_TX_RESULT_${msg.digest}`, msg.digest);
49
+ this.appConfig.emit(`SUI_TX_RESULT_${msg.digest}`, msg.receipt ?? msg.digest);
50
50
  }
51
51
  catch (e) {
52
52
  (0, dist_1.log_warn)(`[trade] bad tx result msg`, message);
@@ -10,5 +10,6 @@ export declare class TransactionResultChecker extends AbstractTransactionResultC
10
10
  private isValidTransactionResult;
11
11
  check_tx_result_interval(): Promise<void>;
12
12
  on_subscibe_transaction(): void;
13
+ private ensureCheckpoint;
13
14
  private processTransactionResult;
14
15
  }
@@ -59,6 +59,7 @@ class TransactionResultChecker extends trade_1.AbstractTransactionResultCheck {
59
59
  this.event_emitter.once(`SUI_TX_RESULT_${this.txid}`, async (response) => {
60
60
  (0, dist_1.log_info)(`receive tx result notification, txid=${this.txid}`);
61
61
  if (response.transaction) {
62
+ await this.ensureCheckpoint(response);
62
63
  this.processTransactionResult(response, 'grpc');
63
64
  return;
64
65
  }
@@ -86,6 +87,25 @@ class TransactionResultChecker extends trade_1.AbstractTransactionResultCheck {
86
87
  }
87
88
  });
88
89
  }
90
+ async ensureCheckpoint(receipt) {
91
+ if (receipt.transaction?.checkpoint)
92
+ return;
93
+ for (let i = 0; i < 10; i++) {
94
+ try {
95
+ const r = await this.leadgerService.getTransaction(this.txid, ['checkpoint', 'timestamp']);
96
+ const t = r?.transaction;
97
+ if (t?.checkpoint) {
98
+ receipt.transaction.checkpoint = t.checkpoint;
99
+ if (t.timestamp)
100
+ receipt.transaction.timestamp = t.timestamp;
101
+ return;
102
+ }
103
+ }
104
+ catch { }
105
+ await (0, dist_1.sleep)(200);
106
+ }
107
+ (0, dist_1.log_warn)(`ensureCheckpoint: 补查超窗仍无 checkpoint 号, txid=${this.txid}(block_number 将为 0)`);
108
+ }
89
109
  async processTransactionResult(txReceipt, source) {
90
110
  if (this.trade_result_already_processed) {
91
111
  (0, dist_1.log_warn)(`trade_result_already_processed=${this.trade_result_already_processed}, ignore trade result fetch by ${source} check!`);
@@ -30,7 +30,11 @@ export interface CentralExecutorOptions {
30
30
  digest: string;
31
31
  success: boolean;
32
32
  error?: string;
33
+ receipt?: {
34
+ transaction: any;
35
+ };
33
36
  }) => void;
37
+ tradeCoinTypes?: () => Promise<string[]>;
34
38
  }
35
39
  type TxResponse = SuiClientTypes.Transaction<{
36
40
  effects: true;
@@ -46,12 +50,20 @@ export declare class CentralExecutor {
46
50
  private tradeWallets;
47
51
  private sharedRefCache;
48
52
  private seq;
53
+ private readonly tradeCoinTypesProvider?;
49
54
  constructor(core: ExecutorCore, opts?: CentralExecutorOptions);
50
55
  init(): Promise<void>;
51
- reconcileCoins(wallet: string, coinType: string): Promise<void>;
56
+ reconcileCoins(wallet: string, coinType: string, quietIfUnchanged?: boolean): Promise<void>;
52
57
  rebalanceWalletFunds(): Promise<void>;
53
58
  private rebalanceOne;
59
+ private decimalsCache;
60
+ private objectReader?;
61
+ private getCoinDecimalsCached;
62
+ private maintainCoinObjects;
54
63
  private execMaintenance;
64
+ private poolGenericsCache;
65
+ private getPoolGenerics;
66
+ private canonicalizeReq;
55
67
  private getSharedRefCached;
56
68
  private chainIdentifier;
57
69
  private epochCache;
@@ -64,8 +76,11 @@ export declare class CentralExecutor {
64
76
  private registerShared;
65
77
  submitSwap(req: SwapExecRequest): Promise<SwapSubmitResult>;
66
78
  private broadcastAndCommit;
79
+ private toCheckerReceipt;
67
80
  private reconcileAfterFailure;
68
81
  simulateSwap(req: SwapExecRequest): Promise<TxResponse>;
82
+ private minSplitFor;
83
+ private postTradeRebalance;
69
84
  private buildSwapTx;
70
85
  private onSuccess;
71
86
  get coinCache(): InProcessCoinCache;
@@ -2,12 +2,35 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.CentralExecutor = void 0;
4
4
  const dist_1 = require("@clonegod/ttd-core/dist");
5
+ const grpc_connection_1 = require("../../grpc/grpc-connection");
6
+ const sui_object_reader_1 = require("../../grpc/sui_object_reader");
7
+ const format_1 = require("../../utils/format");
5
8
  const ed25519_1 = require("@mysten/sui/keypairs/ed25519");
6
9
  const transactions_1 = require("@mysten/sui/transactions");
7
10
  const coin_cache_1 = require("./coin_cache");
8
11
  const effects_1 = require("./effects");
9
12
  const swap_1 = require("../swap");
10
13
  const constants_1 = require("../../constants");
14
+ function splitTopLevelGenerics(s) {
15
+ const out = [];
16
+ let depth = 0, cur = '';
17
+ for (const ch of s) {
18
+ if (ch === '<')
19
+ depth++;
20
+ else if (ch === '>')
21
+ depth--;
22
+ if (ch === ',' && depth === 0) {
23
+ out.push(cur.trim());
24
+ cur = '';
25
+ }
26
+ else {
27
+ cur += ch;
28
+ }
29
+ }
30
+ if (cur.trim())
31
+ out.push(cur.trim());
32
+ return out;
33
+ }
11
34
  class CentralExecutor {
12
35
  constructor(core, opts = {}) {
13
36
  this.core = core;
@@ -15,11 +38,14 @@ class CentralExecutor {
15
38
  this.tradeWallets = new Map();
16
39
  this.sharedRefCache = new Map();
17
40
  this.seq = 0;
41
+ this.decimalsCache = new Map();
42
+ this.poolGenericsCache = new Map();
18
43
  this.chainIdentifier = '';
19
44
  this.epochCache = null;
20
45
  this.gasBudget = opts.gasBudget ?? BigInt(process.env.SUI_GAS_BUDGET || '50000000');
21
46
  this.reconcileAfterTx = opts.reconcileAfterTx ?? true;
22
47
  this.onBroadcastResult = opts.onBroadcastResult;
48
+ this.tradeCoinTypesProvider = opts.tradeCoinTypes;
23
49
  }
24
50
  async init() {
25
51
  const tradeIds = (process.env.SUI_WALLET_GROUP_IDS || '').split(',').map(s => s.trim()).filter(Boolean);
@@ -34,18 +60,22 @@ class CentralExecutor {
34
60
  }
35
61
  (0, dist_1.log_info)(`[executor] init: trade=${[...this.tradeWallets.keys()].join(',')} (单钱包模式: gas=各自地址余额), gasBudget=${this.gasBudget}`);
36
62
  }
37
- async reconcileCoins(wallet, coinType) {
63
+ async reconcileCoins(wallet, coinType, quietIfUnchanged = false) {
38
64
  try {
39
- const refs = [];
40
- let cursor = undefined;
41
- do {
42
- const page = await this.core.listCoins({ owner: wallet, coinType, cursor });
43
- for (const c of page.objects) {
44
- refs.push({ objectId: c.objectId, version: c.version, digest: c.digest, balance: c.balance });
45
- }
46
- cursor = page.hasNextPage ? page.cursor : null;
47
- } while (cursor);
48
- this.cache.reconcile(wallet, coinType, refs);
65
+ for (let attempt = 0; attempt < 2; attempt++) {
66
+ const epoch = this.cache.mutationEpoch(wallet, coinType);
67
+ const refs = [];
68
+ let cursor = undefined;
69
+ do {
70
+ const page = await this.core.listCoins({ owner: wallet, coinType, cursor });
71
+ for (const c of page.objects) {
72
+ refs.push({ objectId: c.objectId, version: c.version, digest: c.digest, balance: c.balance });
73
+ }
74
+ cursor = page.hasNextPage ? page.cursor : null;
75
+ } while (cursor);
76
+ if (this.cache.reconcile(wallet, coinType, refs, epoch, quietIfUnchanged))
77
+ return;
78
+ }
49
79
  }
50
80
  catch (e) {
51
81
  (0, dist_1.log_error)(`[executor] reconcileCoins 失败 ${wallet} ${coinType}`, e);
@@ -66,11 +96,10 @@ class CentralExecutor {
66
96
  const balanceMin = BigInt(process.env.SUI_GAS_BALANCE_MIN || '500000000');
67
97
  const balanceTarget = BigInt(process.env.SUI_GAS_BALANCE_TARGET || (balanceMin * 2n).toString());
68
98
  const coinTarget = Number(process.env.SUI_INPUT_COIN_TARGET || 3);
69
- const coinMax = Number(process.env.SUI_INPUT_COIN_MAX || 8);
70
- const minSplit = BigInt(process.env.SUI_INPUT_COIN_MIN_SPLIT || '100000000');
99
+ const coinMax = Number(process.env.SUI_INPUT_COIN_MAX || 5);
71
100
  const chunk = BigInt(process.env.SUI_REDEEM_MIN_CHUNK || '100000000');
72
101
  const budget = BigInt(process.env.SUI_MAINTAIN_GAS_BUDGET || '20000000');
73
- await this.reconcileCoins(addr, SUI);
102
+ await this.reconcileCoins(addr, SUI, true);
74
103
  if (this.cache.hasInflight(addr, SUI))
75
104
  return;
76
105
  const b = await this.core.getBalance({ owner: addr, coinType: '0x2::sui::SUI' });
@@ -87,8 +116,7 @@ class CentralExecutor {
87
116
  const tx = new transactions_1.Transaction();
88
117
  tx.setSender(addr);
89
118
  const [dep] = tx.splitCoins(tx.gas, [tx.pure.u64(need)]);
90
- const bal = tx.moveCall({ target: '0x2::coin::into_balance', typeArguments: ['0x2::sui::SUI'], arguments: [dep] });
91
- tx.moveCall({ target: '0x2::balance::send_funds', typeArguments: ['0x2::sui::SUI'], arguments: [bal, tx.pure.address(addr)] });
119
+ tx.moveCall({ target: '0x2::coin::send_funds', typeArguments: ['0x2::sui::SUI'], arguments: [dep, tx.pure.address(addr)] });
92
120
  tx.setGasPayment([{ objectId: src.objectId, version: src.version, digest: src.digest }]);
93
121
  await this.execMaintenance(tx, addr, kp, budget, `deposit ${need} → balance`);
94
122
  return;
@@ -106,42 +134,136 @@ class CentralExecutor {
106
134
  await this.execMaintenance(tx, addr, kp, budget, `redeem 超额 ${excess} → coin`, rawClient);
107
135
  return;
108
136
  }
109
- if (coins.length > 0 && coins.length < coinTarget && BigInt(coins[0].balance) >= minSplit * 2n) {
110
- const big = coins[0];
111
- const half = BigInt(big.balance) / 2n;
112
- const tx = new transactions_1.Transaction();
113
- tx.setSender(addr);
114
- const c = tx.object(transactions_1.Inputs.ObjectRef(big));
115
- const [piece] = tx.splitCoins(c, [tx.pure.u64(half)]);
116
- tx.transferObjects([piece], tx.pure.address(addr));
117
- tx.setGasPayment([]);
118
- tx.setExpiration(await this.getValidDuringExpiration());
119
- await this.execMaintenance(tx, addr, kp, budget, `split ${big.objectId.slice(0, 10)}… 对半(${coins.length}→${coins.length + 1} 个)`);
120
- return;
137
+ let tradeTypes = [];
138
+ if (this.tradeCoinTypesProvider) {
139
+ try {
140
+ tradeTypes = (await this.tradeCoinTypesProvider()).map(t => (0, format_1.normalizeSuiTokenAddress)(t));
141
+ }
142
+ catch (e) {
143
+ (0, dist_1.log_warn)(`[executor] 读交易币种清单失败,本 tick 只维护 SUI: ${e.message}`);
144
+ }
121
145
  }
122
- if (coins.length > coinMax) {
123
- const primary = coins[0];
124
- const toMerge = coins.slice(coinTarget);
125
- const tx = new transactions_1.Transaction();
126
- tx.setSender(addr);
127
- const primaryArg = tx.object(transactions_1.Inputs.ObjectRef(primary));
128
- tx.mergeCoins(primaryArg, toMerge.map(c => tx.object(transactions_1.Inputs.ObjectRef(c))));
129
- tx.setGasPayment([]);
130
- tx.setExpiration(await this.getValidDuringExpiration());
131
- await this.execMaintenance(tx, addr, kp, budget, `merge ${toMerge.length} 个碎片(${coins.length}→${coinTarget + 1} 个)`);
132
- return;
146
+ const types = [SUI, ...new Set(tradeTypes.filter(t => t !== SUI))];
147
+ for (const t of types) {
148
+ if (t !== SUI)
149
+ await this.reconcileCoins(addr, t, true);
150
+ if (this.cache.hasInflight(addr, t))
151
+ continue;
152
+ let minSplitT;
153
+ try {
154
+ minSplitT = await this.minSplitFor(t);
155
+ }
156
+ catch (e) {
157
+ (0, dist_1.log_warn)(`[executor] 读 ${t} decimals 失败,本 tick 跳过该币种维护: ${e.message}`);
158
+ continue;
159
+ }
160
+ const acted = await this.maintainCoinObjects(addr, kp, t, { coinTarget, coinMax, budget, minSplit: minSplitT });
161
+ if (acted)
162
+ return;
133
163
  }
134
164
  }
135
- async execMaintenance(tx, addr, kp, budget, label, buildClient) {
136
- tx.setGasBudget(budget);
137
- tx.setGasPrice(await this.getGasPrice());
138
- const bytes = buildClient ? await tx.build({ client: buildClient }) : await tx.build();
139
- const { signature } = await kp.signTransaction(bytes);
140
- const resp = await this.core.executeTransaction({ transaction: bytes, signatures: [signature], include: { effects: true, objectTypes: true, balanceChanges: true } });
141
- const tr = resp.Transaction;
142
- const { success, error } = (0, effects_1.coreTxStatus)(tr.effects);
143
- (0, dist_1.log_info)(`[executor] rebalance ${label} ${success ? 'OK' : 'FAILED'} wallet=${addr.slice(0, 10)}… digest=${tr.digest}${error ? ' err=' + error : ''}`);
144
- await this.reconcileCoins(addr, constants_1.SUI_TOKEN_ADDRESS.LONG);
165
+ async getCoinDecimalsCached(coinType) {
166
+ let d = this.decimalsCache.get(coinType);
167
+ if (d == null) {
168
+ if (!this.objectReader) {
169
+ const { grpc_endpoint, grpc_token } = (0, dist_1.getCoreEnv)();
170
+ if (!grpc_endpoint)
171
+ throw new Error('GRPC_ENDPOINT 未配置(读 CoinMetadata decimals 需要)');
172
+ this.objectReader = new sui_object_reader_1.SuiObjectReader(grpc_connection_1.GrpcConnection.getInstance(grpc_endpoint, grpc_token));
173
+ }
174
+ d = await this.objectReader.getCoinDecimals(coinType);
175
+ this.decimalsCache.set(coinType, d);
176
+ }
177
+ return d;
178
+ }
179
+ async maintainCoinObjects(addr, kp, coinType, opts) {
180
+ const short = coinType.split('::').pop();
181
+ const zeroMergeMin = Number(process.env.SUI_ZERO_COIN_MERGE_THRESHOLD || 3);
182
+ const coins = this.cache.snapshot(addr, coinType).coins;
183
+ const valued = coins.filter(c => BigInt(c.balance) > 0n);
184
+ const zeros = coins.filter(c => BigInt(c.balance) === 0n);
185
+ if (coins.length === 0)
186
+ return false;
187
+ const total = valued.reduce((s, c) => s + BigInt(c.balance), 0n);
188
+ const maxPieces = opts.minSplit > 0n ? total / opts.minSplit : 0n;
189
+ const pieces = Math.max(1, Number(maxPieces > BigInt(opts.coinTarget) ? BigInt(opts.coinTarget) : maxPieces));
190
+ const needSplit = valued.length > 0 && valued.length < opts.coinTarget && pieces > valued.length;
191
+ const needZeroPurge = zeros.length >= zeroMergeMin;
192
+ const needDefrag = coins.length > opts.coinMax;
193
+ if (!needSplit && !needZeroPurge && !needDefrag)
194
+ return false;
195
+ const tag = this.nextTag('maint');
196
+ if (!this.cache.reserve(addr, coinType, coins.map(c => c.objectId), tag))
197
+ return false;
198
+ const primaryRef = valued[0] ?? coins[0];
199
+ const tx = new transactions_1.Transaction();
200
+ tx.setSender(addr);
201
+ const primary = tx.object(transactions_1.Inputs.ObjectRef(primaryRef));
202
+ const others = coins.filter(c => c.objectId !== primaryRef.objectId);
203
+ if (others.length)
204
+ tx.mergeCoins(primary, others.map(c => tx.object(transactions_1.Inputs.ObjectRef(c))));
205
+ if (pieces > 1) {
206
+ const per = total / BigInt(pieces);
207
+ const splits = tx.splitCoins(primary, Array.from({ length: pieces - 1 }, () => tx.pure.u64(per)));
208
+ tx.transferObjects(Array.from({ length: pieces - 1 }, (_, i) => splits[i]), tx.pure.address(addr));
209
+ }
210
+ tx.setGasPayment([]);
211
+ 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);
213
+ return true;
214
+ }
215
+ async execMaintenance(tx, addr, kp, budget, label, buildClient, coinType = constants_1.SUI_TOKEN_ADDRESS.LONG, reserveTag) {
216
+ try {
217
+ tx.setGasBudget(budget);
218
+ tx.setGasPrice(await this.getGasPrice());
219
+ const bytes = buildClient ? await tx.build({ client: buildClient }) : await tx.build();
220
+ const { signature } = await kp.signTransaction(bytes);
221
+ const resp = await this.core.executeTransaction({ transaction: bytes, signatures: [signature], include: { effects: true, objectTypes: true, balanceChanges: true } });
222
+ const tr = resp.Transaction;
223
+ const { success, error } = (0, effects_1.coreTxStatus)(tr.effects);
224
+ (0, dist_1.log_info)(`[executor] rebalance ${label} ${success ? 'OK' : 'FAILED'} wallet=${addr.slice(0, 10)}… digest=${tr.digest}${error ? ' err=' + error : ''}`);
225
+ }
226
+ finally {
227
+ if (reserveTag)
228
+ this.cache.abort(reserveTag, true);
229
+ await this.reconcileCoins(addr, coinType);
230
+ }
231
+ }
232
+ async getPoolGenerics(poolId) {
233
+ const hit = this.poolGenericsCache.get(poolId);
234
+ if (hit !== undefined)
235
+ return hit;
236
+ const res = await this.core.getObjects({ objectIds: [poolId] });
237
+ const obj = res.objects[0];
238
+ if (obj instanceof Error)
239
+ throw new Error(`[executor] getObjects(${poolId}) 失败: ${obj.message}`);
240
+ const t = obj.type ?? obj.objectType;
241
+ let out = null;
242
+ const m = t?.match(/<(.+)>$/);
243
+ if (m) {
244
+ const parts = splitTopLevelGenerics(m[1]);
245
+ if (parts.length === 2) {
246
+ out = { typeA: (0, format_1.normalizeSuiTokenAddress)(parts[0]), typeB: (0, format_1.normalizeSuiTokenAddress)(parts[1]) };
247
+ }
248
+ }
249
+ this.poolGenericsCache.set(poolId, out);
250
+ return out;
251
+ }
252
+ async canonicalizeReq(req) {
253
+ const g = await this.getPoolGenerics(req.poolId);
254
+ if (!g)
255
+ return req;
256
+ const a = (0, format_1.normalizeSuiTokenAddress)(req.coinTypeA);
257
+ const b = (0, format_1.normalizeSuiTokenAddress)(req.coinTypeB);
258
+ if (a === g.typeA && b === g.typeB)
259
+ return req;
260
+ if (a === g.typeB && b === g.typeA) {
261
+ if (req.sqrtPriceLimit !== 0n)
262
+ throw new Error(`[executor] pool ${req.poolId} token 序翻转时不支持显式 sqrtPriceLimit`);
263
+ (0, dist_1.log_info)(`[executor] pool ${req.poolId.slice(0, 10)}… 配置 token 序与链上相反,已翻转(a2b ${req.a2b}→${!req.a2b})`);
264
+ return { ...req, coinTypeA: b, coinTypeB: a, a2b: !req.a2b };
265
+ }
266
+ throw new Error(`[executor] pool token 与链上不符: 链上 <${g.typeA}, ${g.typeB}> vs 请求 <${a}, ${b}>(配置可能指错池子)`);
145
267
  }
146
268
  async getSharedRefCached(objectId) {
147
269
  let isv = this.sharedRefCache.get(objectId);
@@ -160,7 +282,13 @@ class CentralExecutor {
160
282
  }
161
283
  async getValidDuringExpiration() {
162
284
  if (!this.chainIdentifier) {
163
- this.chainIdentifier = (await this.core.getChainIdentifier()).chainIdentifier;
285
+ try {
286
+ this.chainIdentifier = (await this.core.getChainIdentifier()).chainIdentifier;
287
+ }
288
+ catch (e) {
289
+ this.chainIdentifier = process.env.SUI_CHAIN_IDENTIFIER || '35834a8a';
290
+ (0, dist_1.log_warn)(`[executor] getChainIdentifier 失败(${e.message}),使用兜底 chain id ${this.chainIdentifier}`);
291
+ }
164
292
  }
165
293
  const now = Date.now();
166
294
  if (!this.epochCache || now - this.epochCache.ts > 300_000) {
@@ -208,16 +336,30 @@ class CentralExecutor {
208
336
  tx.object(transactions_1.Inputs.SharedObjectRef({ objectId, initialSharedVersion, mutable }));
209
337
  }
210
338
  async submitSwap(req) {
339
+ const t0 = Date.now();
340
+ req = await this.canonicalizeReq(req);
211
341
  const inType = this.inputCoinType(req);
212
- const wallet = this.chooseTradeWallet(req, inType, req.amountIn);
342
+ let wallet;
343
+ try {
344
+ wallet = this.chooseTradeWallet(req, inType, req.amountIn);
345
+ }
346
+ catch (e) {
347
+ (0, dist_1.log_warn)(`[executor] ${inType} 选钱包失败(${e.message}),按需 reconcile 后重试`);
348
+ await Promise.all([...this.tradeWallets.keys()].map(w => this.reconcileCoins(w, inType)));
349
+ wallet = this.chooseTradeWallet(req, inType, req.amountIn);
350
+ }
213
351
  const inputTag = this.nextTag('in');
214
352
  const inputRes = this.cache.acquire(wallet, inType, req.amountIn, inputTag);
353
+ const tAcquire = Date.now();
354
+ (0, dist_1.log_info)(`[executor] coin 已占用 wallet=${wallet.slice(0, 10)}… ${inputRes.coins.length} 个 ${inType.split('::').pop()} (选钱包+占币 ${tAcquire - t0}ms)`);
215
355
  try {
216
356
  const { txBytes, tx } = await this.buildSwapTx(req, wallet, inputRes.coins);
357
+ const tBuild = Date.now();
217
358
  const { signature: senderSig } = await this.tradeWallets.get(wallet).signTransaction(txBytes);
218
359
  const digest = await tx.getDigest();
360
+ const tSign = Date.now();
219
361
  void this.broadcastAndCommit(txBytes, [senderSig], digest, wallet, inType, this.outputCoinType(req), inputRes.coins, inputTag);
220
- (0, dist_1.log_info)(`[executor] swap 已签提交 digest=${digest} dex=${req.dexId} ${req.a2b ? 'a2b' : 'b2a'} in=${req.amountIn}`);
362
+ (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}`);
221
363
  return { digest, submitted: true };
222
364
  }
223
365
  catch (e) {
@@ -227,6 +369,7 @@ class CentralExecutor {
227
369
  }
228
370
  }
229
371
  async broadcastAndCommit(txBytes, signatures, digest, wallet, inType, outType, inputCoins, inputTag) {
372
+ const tBroadcast = Date.now();
230
373
  try {
231
374
  const resp = await this.core.executeTransaction({ transaction: txBytes, signatures, include: { effects: true, objectTypes: true, balanceChanges: true } });
232
375
  const tr = resp.Transaction;
@@ -241,25 +384,50 @@ class CentralExecutor {
241
384
  if (!success) {
242
385
  this.cache.abort(inputTag, true);
243
386
  this.reconcileAfterFailure(wallet, inType, outType);
244
- (0, dist_1.log_warn)(`[executor] swap 链上失败 digest=${digest} err=${error}`);
245
- this.onBroadcastResult?.({ digest, success: false, error });
387
+ (0, dist_1.log_warn)(`[executor] swap 链上失败 digest=${digest} err=${error} broadcast=${Date.now() - tBroadcast}ms`);
388
+ this.onBroadcastResult?.({ digest, success: false, error, receipt: this.toCheckerReceipt(tr, digest) });
246
389
  return;
247
390
  }
248
391
  await this.onSuccess(tr, wallet, inType, outType, inputCoins, inputTag);
249
- (0, dist_1.log_info)(`[executor] swap quorum-executed 确认 digest=${digest}`);
250
- this.onBroadcastResult?.({ digest, success: true });
392
+ (0, dist_1.log_info)(`[executor] swap quorum-executed 确认 digest=${digest} broadcast=${Date.now() - tBroadcast}ms`);
393
+ this.onBroadcastResult?.({ digest, success: true, receipt: this.toCheckerReceipt(tr, digest) });
251
394
  }
252
395
  catch (e) {
253
396
  this.cache.abort(inputTag, true);
254
397
  this.reconcileAfterFailure(wallet, inType, outType);
255
- (0, dist_1.log_error)(`[executor] broadcast 失败 digest=${digest}`, e);
398
+ (0, dist_1.log_error)(`[executor] broadcast 失败 digest=${digest} broadcast=${Date.now() - tBroadcast}ms`, e);
256
399
  }
257
400
  }
401
+ toCheckerReceipt(tr, digest) {
402
+ const eff = tr.effects;
403
+ const status = eff?.status?.success
404
+ ? { success: true }
405
+ : { success: false, error: { kind: eff?.status?.error?.message ?? JSON.stringify(eff?.status?.error ?? 'unknown') } };
406
+ const g = eff?.gasUsed ?? {};
407
+ return {
408
+ transaction: {
409
+ digest,
410
+ effects: {
411
+ status,
412
+ gas_used: {
413
+ computation_cost: g.computationCost,
414
+ storage_cost: g.storageCost,
415
+ storage_rebate: g.storageRebate,
416
+ non_refundable_storage_fee: g.nonRefundableStorageFee,
417
+ },
418
+ },
419
+ balance_changes: (tr.balanceChanges ?? []).map((b) => ({
420
+ address: b.address, coin_type: b.coinType, amount: b.amount,
421
+ })),
422
+ },
423
+ };
424
+ }
258
425
  reconcileAfterFailure(wallet, inType, outType) {
259
426
  for (const t of new Set([inType, outType, constants_1.SUI_TOKEN_ADDRESS.LONG]))
260
427
  void this.reconcileCoins(wallet, t);
261
428
  }
262
429
  async simulateSwap(req) {
430
+ req = await this.canonicalizeReq(req);
263
431
  const inType = this.inputCoinType(req);
264
432
  const wallet = req.walletAddress ?? this.chooseTradeWallet(req, inType, req.amountIn);
265
433
  const { coins } = this.cache.snapshot(wallet, inType);
@@ -282,6 +450,32 @@ class CentralExecutor {
282
450
  }
283
451
  return res.Transaction;
284
452
  }
453
+ async minSplitFor(coinType) {
454
+ if (coinType === constants_1.SUI_TOKEN_ADDRESS.LONG) {
455
+ return BigInt(process.env.SUI_INPUT_COIN_MIN_SPLIT || '100000000');
456
+ }
457
+ const d = await this.getCoinDecimalsCached(coinType);
458
+ return 10n ** BigInt(d) / 10n;
459
+ }
460
+ async postTradeRebalance(wallet, inType, outType) {
461
+ const kp = this.tradeWallets.get(wallet);
462
+ if (!kp)
463
+ return;
464
+ const coinTarget = Number(process.env.SUI_INPUT_COIN_TARGET || 3);
465
+ const coinMax = Number(process.env.SUI_INPUT_COIN_MAX || 5);
466
+ const budget = BigInt(process.env.SUI_MAINTAIN_GAS_BUDGET || '20000000');
467
+ for (const t of new Set([inType, outType, constants_1.SUI_TOKEN_ADDRESS.LONG])) {
468
+ try {
469
+ await this.reconcileCoins(wallet, t);
470
+ if (this.cache.hasInflight(wallet, t))
471
+ continue;
472
+ await this.maintainCoinObjects(wallet, kp, t, { coinTarget, coinMax, budget, minSplit: await this.minSplitFor(t) });
473
+ }
474
+ catch (e) {
475
+ (0, dist_1.log_warn)(`[executor] postTradeRebalance ${t} 失败(交 30s tick 兜底): ${e.message}`);
476
+ }
477
+ }
478
+ }
285
479
  async buildSwapTx(req, wallet, inputCoins) {
286
480
  const tx = new transactions_1.Transaction();
287
481
  tx.setSender(wallet);
@@ -314,8 +508,7 @@ class CentralExecutor {
314
508
  const tradeChanges = (0, effects_1.extractCoinChangesFromCore)(tr.effects, objectTypes, { owners: new Set([wallet]), coinTypeById });
315
509
  this.cache.commit(inputTag, tradeChanges);
316
510
  if (this.reconcileAfterTx) {
317
- for (const t of new Set([inType, outType, constants_1.SUI_TOKEN_ADDRESS.LONG]))
318
- void this.reconcileCoins(wallet, t);
511
+ void this.postTradeRebalance(wallet, inType, outType);
319
512
  }
320
513
  }
321
514
  get coinCache() { return this.cache; }
@@ -2,16 +2,22 @@ import { CoinRef, CoinReservation, CoinObjectChange } from '../coin/types';
2
2
  export declare class InProcessCoinCache {
3
3
  private available;
4
4
  private inflight;
5
+ private epochs;
6
+ private epochKey;
7
+ private bumpEpoch;
8
+ mutationEpoch(wallet: string, coinType: string): number;
5
9
  private walletMap;
6
10
  private list;
7
11
  private sortDesc;
8
12
  seed(wallet: string, coinType: string, coins: CoinRef[]): void;
9
- reconcile(wallet: string, coinType: string, freshCoins: CoinRef[]): void;
13
+ reconcile(wallet: string, coinType: string, freshCoins: CoinRef[], epochAtStart?: number, quietIfUnchanged?: boolean): boolean;
10
14
  acquire(wallet: string, coinType: string, amount: bigint, txTag: string, maxCoins?: number): CoinReservation;
15
+ reserve(wallet: string, coinType: string, objectIds: string[], txTag: string): CoinRef[] | null;
11
16
  hasInflight(wallet: string, coinType: string): boolean;
12
17
  commit(txTag: string, changes: CoinObjectChange[]): void;
13
18
  abort(txTag: string, consumed: boolean): void;
14
19
  private removeEverywhere;
20
+ coinTypes(wallet: string): string[];
15
21
  snapshot(wallet: string, coinType: string): {
16
22
  count: number;
17
23
  total: bigint;
@@ -6,6 +6,15 @@ class InProcessCoinCache {
6
6
  constructor() {
7
7
  this.available = new Map();
8
8
  this.inflight = new Map();
9
+ this.epochs = new Map();
10
+ }
11
+ epochKey(wallet, coinType) { return `${wallet}|${coinType}`; }
12
+ bumpEpoch(wallet, coinType) {
13
+ const k = this.epochKey(wallet, coinType);
14
+ this.epochs.set(k, (this.epochs.get(k) || 0) + 1);
15
+ }
16
+ mutationEpoch(wallet, coinType) {
17
+ return this.epochs.get(this.epochKey(wallet, coinType)) || 0;
9
18
  }
10
19
  walletMap(wallet) {
11
20
  let m = this.available.get(wallet);
@@ -31,9 +40,14 @@ class InProcessCoinCache {
31
40
  const l = coins.slice();
32
41
  this.sortDesc(l);
33
42
  this.walletMap(wallet).set(coinType, l);
43
+ this.bumpEpoch(wallet, coinType);
34
44
  (0, dist_1.log_info)(`[coin-cache] seed ${wallet} ${coinType}: ${l.length} coins`);
35
45
  }
36
- reconcile(wallet, coinType, freshCoins) {
46
+ reconcile(wallet, coinType, freshCoins, epochAtStart, quietIfUnchanged = false) {
47
+ if (epochAtStart != null && this.mutationEpoch(wallet, coinType) !== epochAtStart) {
48
+ (0, dist_1.log_info)(`[coin-cache] reconcile ${wallet} ${coinType}: 快照在途期间缓存已变更,丢弃(防旧覆盖新)`);
49
+ return false;
50
+ }
37
51
  const inflightIds = new Set();
38
52
  for (const r of this.inflight.values()) {
39
53
  if (r.wallet === wallet && r.coinType === coinType)
@@ -41,8 +55,15 @@ class InProcessCoinCache {
41
55
  }
42
56
  const kept = freshCoins.filter(c => !inflightIds.has(c.objectId));
43
57
  this.sortDesc(kept);
58
+ const sig = (l) => l.map(c => `${c.objectId}@${c.version}:${c.balance}`).join(',');
59
+ const unchanged = sig(kept) === sig(this.list(wallet, coinType));
44
60
  this.walletMap(wallet).set(coinType, kept);
45
- (0, dist_1.log_info)(`[coin-cache] reconcile ${wallet} ${coinType}: ${kept.length} avail (${inflightIds.size} inflight kept)`);
61
+ const msg = `[coin-cache] reconcile ${wallet} ${coinType}: ${kept.length} avail (${inflightIds.size} inflight kept)`;
62
+ if (unchanged && quietIfUnchanged)
63
+ (0, dist_1.log_debug)(msg);
64
+ else
65
+ (0, dist_1.log_info)(msg);
66
+ return true;
46
67
  }
47
68
  acquire(wallet, coinType, amount, txTag, maxCoins = 8) {
48
69
  if (this.inflight.has(txTag))
@@ -65,8 +86,22 @@ class InProcessCoinCache {
65
86
  const pickedIds = new Set(picked.map(c => c.objectId));
66
87
  this.walletMap(wallet).set(coinType, l.filter(c => !pickedIds.has(c.objectId)));
67
88
  this.inflight.set(txTag, { wallet, coinType, coins: picked, gas: false });
89
+ this.bumpEpoch(wallet, coinType);
68
90
  return { txTag, coinType, coins: picked };
69
91
  }
92
+ reserve(wallet, coinType, objectIds, txTag) {
93
+ if (this.inflight.has(txTag))
94
+ throw new Error(`[coin-cache] txTag 重复: ${txTag}`);
95
+ const l = this.list(wallet, coinType);
96
+ const ids = new Set(objectIds);
97
+ const picked = l.filter(c => ids.has(c.objectId));
98
+ if (picked.length !== ids.size)
99
+ return null;
100
+ this.walletMap(wallet).set(coinType, l.filter(c => !ids.has(c.objectId)));
101
+ this.inflight.set(txTag, { wallet, coinType, coins: picked, gas: false });
102
+ this.bumpEpoch(wallet, coinType);
103
+ return picked;
104
+ }
70
105
  hasInflight(wallet, coinType) {
71
106
  for (const r of this.inflight.values()) {
72
107
  if (r.wallet === wallet && r.coinType === coinType)
@@ -81,6 +116,9 @@ class InProcessCoinCache {
81
116
  return;
82
117
  }
83
118
  this.inflight.delete(txTag);
119
+ this.bumpEpoch(res.wallet, res.coinType);
120
+ for (const ch of changes)
121
+ this.bumpEpoch(res.wallet, ch.coinType);
84
122
  for (const ch of changes) {
85
123
  const wallet = res.wallet;
86
124
  if (ch.kind === 'deleted') {
@@ -107,6 +145,7 @@ class InProcessCoinCache {
107
145
  return;
108
146
  }
109
147
  this.inflight.delete(txTag);
148
+ this.bumpEpoch(res.wallet, res.coinType);
110
149
  if (consumed) {
111
150
  (0, dist_1.log_warn)(`[coin-cache] abort consumed txTag=${txTag}(${res.coins.length} coin 交 reconcile 兜底)`);
112
151
  return;
@@ -123,6 +162,9 @@ class InProcessCoinCache {
123
162
  if (idx >= 0)
124
163
  l.splice(idx, 1);
125
164
  }
165
+ coinTypes(wallet) {
166
+ return [...this.walletMap(wallet).keys()];
167
+ }
126
168
  snapshot(wallet, coinType) {
127
169
  const l = this.list(wallet, coinType);
128
170
  const total = l.reduce((s, c) => s + BigInt(c.balance), 0n);
@@ -21,6 +21,9 @@ const GRPC_CLIENT_OPTIONS = {
21
21
  'grpc.max_send_message_length': 8 * 1024 * 1024,
22
22
  'grpc.initial_reconnect_backoff_ms': 1000,
23
23
  'grpc.max_reconnect_backoff_ms': 30000,
24
+ 'grpc-node.flow_control_window': 8 * 1024 * 1024,
25
+ 'grpc-node.max_session_memory': 64,
26
+ 'grpc.default_compression_algorithm': 2,
24
27
  };
25
28
  function buildGrpcCore(opts = {}) {
26
29
  const endpoint = opts.endpoint ?? (0, dist_1.getCoreEnv)().grpc_endpoint;
@@ -39,10 +39,18 @@ class SuiTransactionParser {
39
39
  let tokenBChangeAmount = new decimal_js_1.default(0);
40
40
  const balanceChanges = tx_receipt.transaction?.balance_changes || [];
41
41
  (0, dist_1.log_info)('balance_changes', { txid, balance_changes: balanceChanges });
42
+ const g = tx_receipt.transaction?.effects?.gas_used;
43
+ const gasNetMist = g
44
+ ? new decimal_js_1.default(g.computation_cost || 0).plus(g.storage_cost || 0).minus(g.storage_rebate || 0)
45
+ : new decimal_js_1.default(0);
46
+ const SUI_LONG = (0, index_1.normalizeSuiTokenAddress)('0x2::sui::SUI');
42
47
  for (const change of balanceChanges) {
43
- const amount = new decimal_js_1.default(change.amount);
48
+ let amount = new decimal_js_1.default(change.amount);
44
49
  const coinType = change.coin_type;
45
50
  const normalizedCoinType = (0, index_1.normalizeSuiTokenAddress)(coinType);
51
+ if (normalizedCoinType === SUI_LONG) {
52
+ amount = amount.plus(gasNetMist);
53
+ }
46
54
  const poolTokenAAddress = (0, index_1.normalizeSuiTokenAddress)(pool_info.tokenA.address);
47
55
  const poolTokenBAddress = (0, index_1.normalizeSuiTokenAddress)(pool_info.tokenB.address);
48
56
  if (normalizedCoinType === poolTokenAAddress) {
@@ -177,7 +185,7 @@ class SuiTransactionParser {
177
185
  return Date.now();
178
186
  }
179
187
  extractCheckpoint(receipt) {
180
- return parseInt(receipt.checkpoint || '0');
188
+ return parseInt(receipt.transaction?.checkpoint || receipt.checkpoint || '0');
181
189
  }
182
190
  }
183
191
  exports.SuiTransactionParser = SuiTransactionParser;
@@ -4,4 +4,7 @@ export interface SuiTxResultMsg {
4
4
  success?: boolean;
5
5
  error?: string;
6
6
  source?: 'executor' | 'stream-trade';
7
+ receipt?: {
8
+ transaction: any;
9
+ };
7
10
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clonegod/ttd-sui-common",
3
- "version": "2.0.5",
3
+ "version": "2.0.7",
4
4
  "description": "Sui common library",
5
5
  "license": "UNLICENSED",
6
6
  "main": "dist/index.js",