@clonegod/ttd-sui-common 2.0.4 → 2.0.6

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;
@@ -53,6 +53,8 @@ export declare abstract class AbstractDexQuote<C extends SuiChainOps = SuiChainO
53
53
  private registerEventHandlers;
54
54
  private assertValidBlockNumber;
55
55
  private isStaleUpdate;
56
+ private appliedSwapWatermark;
57
+ private shouldApplySwapState;
56
58
  private handleBlockUpdateEvent;
57
59
  private calculateQuote;
58
60
  private calculateQuoteForPool;
@@ -12,6 +12,7 @@ class AbstractDexQuote {
12
12
  this.lastPublished = new Map();
13
13
  this.quotePriceVerify = new quote_price_verify_1.QuotePriceVerify();
14
14
  this.MIN_QUOTE_INTERVAL_MS = Math.max(3000, parseInt(process.env.MIN_QUOTE_INTERVAL_MS || '10000', 10) || 10000);
15
+ this.appliedSwapWatermark = new Map();
15
16
  this.appConfig = appConfig;
16
17
  this.chain = chain;
17
18
  }
@@ -58,8 +59,15 @@ class AbstractDexQuote {
58
59
  const last = this.lastPublished.get(poolAddr);
59
60
  return !!last && (block < last.block || (block === last.block && txIndex <= last.txIndex));
60
61
  }
62
+ shouldApplySwapState(poolAddr, block, txIndex) {
63
+ const last = this.appliedSwapWatermark.get(poolAddr);
64
+ if (last && (block < last.block || (block === last.block && txIndex <= last.txIndex)))
65
+ return false;
66
+ this.appliedSwapWatermark.set(poolAddr, { block, txIndex });
67
+ return true;
68
+ }
61
69
  async handleBlockUpdateEvent(eventData) {
62
- const { blockNumber, blockTime } = JSON.parse(eventData);
70
+ const { blockNumber, blockTimestampMs: blockTime } = JSON.parse(eventData);
63
71
  this.assertValidBlockNumber(blockNumber, `${this.dexId} block-event`);
64
72
  for (const poolInfo of this.poolInfoMap.values()) {
65
73
  const poolAddress = poolInfo.pool_address;
@@ -87,7 +95,11 @@ class AbstractDexQuote {
87
95
  return { bundle: null, verifyLog };
88
96
  }
89
97
  this.assertValidBlockNumber(evt.blockNumber, `${this.dexId} ${poolInfo.pool_name} ${eventType}`);
90
- await this.refreshStateFromEvent(poolInfo, evt);
98
+ const skipApply = eventType === 'swap'
99
+ && Number.isInteger(evt.txIndex)
100
+ && !this.shouldApplySwapState(poolInfo.pool_address, evt.blockNumber, evt.txIndex);
101
+ if (!skipApply)
102
+ await this.refreshStateFromEvent(poolInfo, evt);
91
103
  if (Number.isInteger(evt.txIndex) && this.isStaleUpdate(poolInfo.pool_address, evt.blockNumber, evt.txIndex))
92
104
  return { bundle: null, verifyLog };
93
105
  const bundle = await this.calculateQuoteForPool(poolInfo, evt.blockTime, evt.blockNumber, evt);
@@ -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,6 +30,9 @@ export interface CentralExecutorOptions {
30
30
  digest: string;
31
31
  success: boolean;
32
32
  error?: string;
33
+ receipt?: {
34
+ transaction: any;
35
+ };
33
36
  }) => void;
34
37
  }
35
38
  type TxResponse = SuiClientTypes.Transaction<{
@@ -64,6 +67,7 @@ export declare class CentralExecutor {
64
67
  private registerShared;
65
68
  submitSwap(req: SwapExecRequest): Promise<SwapSubmitResult>;
66
69
  private broadcastAndCommit;
70
+ private toCheckerReceipt;
67
71
  private reconcileAfterFailure;
68
72
  simulateSwap(req: SwapExecRequest): Promise<TxResponse>;
69
73
  private buildSwapTx;
@@ -208,16 +208,21 @@ class CentralExecutor {
208
208
  tx.object(transactions_1.Inputs.SharedObjectRef({ objectId, initialSharedVersion, mutable }));
209
209
  }
210
210
  async submitSwap(req) {
211
+ const t0 = Date.now();
211
212
  const inType = this.inputCoinType(req);
212
213
  const wallet = this.chooseTradeWallet(req, inType, req.amountIn);
213
214
  const inputTag = this.nextTag('in');
214
215
  const inputRes = this.cache.acquire(wallet, inType, req.amountIn, inputTag);
216
+ const tAcquire = Date.now();
217
+ (0, dist_1.log_info)(`[executor] coin 已占用 wallet=${wallet.slice(0, 10)}… ${inputRes.coins.length} 个 ${inType.split('::').pop()} (选钱包+占币 ${tAcquire - t0}ms)`);
215
218
  try {
216
219
  const { txBytes, tx } = await this.buildSwapTx(req, wallet, inputRes.coins);
220
+ const tBuild = Date.now();
217
221
  const { signature: senderSig } = await this.tradeWallets.get(wallet).signTransaction(txBytes);
218
222
  const digest = await tx.getDigest();
223
+ const tSign = Date.now();
219
224
  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}`);
225
+ (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
226
  return { digest, submitted: true };
222
227
  }
223
228
  catch (e) {
@@ -227,6 +232,7 @@ class CentralExecutor {
227
232
  }
228
233
  }
229
234
  async broadcastAndCommit(txBytes, signatures, digest, wallet, inType, outType, inputCoins, inputTag) {
235
+ const tBroadcast = Date.now();
230
236
  try {
231
237
  const resp = await this.core.executeTransaction({ transaction: txBytes, signatures, include: { effects: true, objectTypes: true, balanceChanges: true } });
232
238
  const tr = resp.Transaction;
@@ -241,20 +247,44 @@ class CentralExecutor {
241
247
  if (!success) {
242
248
  this.cache.abort(inputTag, true);
243
249
  this.reconcileAfterFailure(wallet, inType, outType);
244
- (0, dist_1.log_warn)(`[executor] swap 链上失败 digest=${digest} err=${error}`);
245
- this.onBroadcastResult?.({ digest, success: false, error });
250
+ (0, dist_1.log_warn)(`[executor] swap 链上失败 digest=${digest} err=${error} broadcast=${Date.now() - tBroadcast}ms`);
251
+ this.onBroadcastResult?.({ digest, success: false, error, receipt: this.toCheckerReceipt(tr, digest) });
246
252
  return;
247
253
  }
248
254
  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 });
255
+ (0, dist_1.log_info)(`[executor] swap quorum-executed 确认 digest=${digest} broadcast=${Date.now() - tBroadcast}ms`);
256
+ this.onBroadcastResult?.({ digest, success: true, receipt: this.toCheckerReceipt(tr, digest) });
251
257
  }
252
258
  catch (e) {
253
259
  this.cache.abort(inputTag, true);
254
260
  this.reconcileAfterFailure(wallet, inType, outType);
255
- (0, dist_1.log_error)(`[executor] broadcast 失败 digest=${digest}`, e);
261
+ (0, dist_1.log_error)(`[executor] broadcast 失败 digest=${digest} broadcast=${Date.now() - tBroadcast}ms`, e);
256
262
  }
257
263
  }
264
+ toCheckerReceipt(tr, digest) {
265
+ const eff = tr.effects;
266
+ const status = eff?.status?.success
267
+ ? { success: true }
268
+ : { success: false, error: { kind: eff?.status?.error?.message ?? JSON.stringify(eff?.status?.error ?? 'unknown') } };
269
+ const g = eff?.gasUsed ?? {};
270
+ return {
271
+ transaction: {
272
+ digest,
273
+ effects: {
274
+ status,
275
+ gas_used: {
276
+ computation_cost: g.computationCost,
277
+ storage_cost: g.storageCost,
278
+ storage_rebate: g.storageRebate,
279
+ non_refundable_storage_fee: g.nonRefundableStorageFee,
280
+ },
281
+ },
282
+ balance_changes: (tr.balanceChanges ?? []).map((b) => ({
283
+ address: b.address, coin_type: b.coinType, amount: b.amount,
284
+ })),
285
+ },
286
+ };
287
+ }
258
288
  reconcileAfterFailure(wallet, inType, outType) {
259
289
  for (const t of new Set([inType, outType, constants_1.SUI_TOKEN_ADDRESS.LONG]))
260
290
  void this.reconcileCoins(wallet, t);
@@ -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;
@@ -19,7 +19,7 @@ class ExecutorWsClient {
19
19
  });
20
20
  this.ws.onMessage((data) => {
21
21
  try {
22
- const env = JSON.parse(typeof data === 'string' ? data : data.toString());
22
+ const env = (typeof data === 'string' ? JSON.parse(data) : data);
23
23
  const p = this.pending.get(env.reqId);
24
24
  if (!p)
25
25
  return;
@@ -177,7 +177,7 @@ class SuiTransactionParser {
177
177
  return Date.now();
178
178
  }
179
179
  extractCheckpoint(receipt) {
180
- return parseInt(receipt.checkpoint || '0');
180
+ return parseInt(receipt.transaction?.checkpoint || receipt.checkpoint || '0');
181
181
  }
182
182
  }
183
183
  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
  }
@@ -1,8 +1,9 @@
1
1
  export interface BlockUpdateEvent {
2
- blockHash: string;
3
2
  blockNumber: number;
4
- blockTime: number;
5
- transactions: TransactionData[];
3
+ blockTimestamp: number;
4
+ blockTimestampMs: number;
5
+ recvBlockTime: number;
6
+ blockHash?: string;
6
7
  }
7
8
  export interface TransactionData {
8
9
  txHash: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clonegod/ttd-sui-common",
3
- "version": "2.0.4",
3
+ "version": "2.0.6",
4
4
  "description": "Sui common library",
5
5
  "license": "UNLICENSED",
6
6
  "main": "dist/index.js",