@clonegod/ttd-sol-common 2.0.85 → 2.0.86

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.
@@ -51,9 +51,14 @@ class AbstractDexQuote {
51
51
  registerEventHandlers() {
52
52
  this.chain.subscribeNewBlock((raw) => this.handleBlockUpdateEvent(raw));
53
53
  this.chain.subscribePoolEvents(this.dexId, Array.from(this.poolInfoMap.values()), async (evt) => {
54
- const bundle = await this.calculateQuote(evt);
55
- if (bundle)
56
- this.publishQuote(bundle);
54
+ try {
55
+ const bundle = await this.calculateQuote(evt);
56
+ if (bundle)
57
+ this.publishQuote(bundle);
58
+ }
59
+ catch (err) {
60
+ (0, dist_1.log_error)(`[${this.dexId}] v2 事件处理失败 pool=${evt.pool_address}: ${err.message}`, err instanceof Error ? err : new Error(String(err)));
61
+ }
57
62
  }, (sv) => this.handleSwapVerify(sv));
58
63
  }
59
64
  handleSwapVerify(sv) {
@@ -9,7 +9,6 @@ export declare class SolTransactionBuilder {
9
9
  recentBlockheight: number;
10
10
  constructor(appConfig: SolanaTradeAppConfig);
11
11
  init(): Promise<void>;
12
- private warmupSigner;
13
12
  private handleBlockUpdateEvent;
14
13
  private getPreInstructions;
15
14
  private createComputeUnitLimitWithJitoDontFront;
@@ -56,24 +56,6 @@ class SolTransactionBuilder {
56
56
  }
57
57
  async init() {
58
58
  this.appConfig.arb_event_subscriber.subscribe_new_block(dist_1.CHAIN_ID.SOLANA, this.handleBlockUpdateEvent.bind(this));
59
- this.warmupSigner();
60
- }
61
- warmupSigner() {
62
- try {
63
- for (let i = 0; i < 2; i++) {
64
- const tx = new web3_js_1.Transaction();
65
- tx.add(web3_js_1.SystemProgram.transfer({
66
- fromPubkey: this.keypair.publicKey,
67
- toPubkey: this.keypair.publicKey,
68
- lamports: 0,
69
- }));
70
- tx.recentBlockhash = '11111111111111111111111111111111';
71
- tx.feePayer = this.keypair.publicKey;
72
- tx.sign(this.keypair);
73
- }
74
- }
75
- catch {
76
- }
77
59
  }
78
60
  async handleBlockUpdateEvent(eventData) {
79
61
  let blockUpdateEvent = JSON.parse(eventData);
@@ -0,0 +1,2 @@
1
+ import { StandardPoolInfoType } from '@clonegod/ttd-core/dist';
2
+ export declare function assertPoolTokensMatch(poolInfo: StandardPoolInfoType, onChainMintA: string, onChainMintB: string, ctx: string): void;
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.assertPoolTokensMatch = assertPoolTokensMatch;
4
+ function assertPoolTokensMatch(poolInfo, onChainMintA, onChainMintB, ctx) {
5
+ const cfgA = poolInfo.tokenA?.address;
6
+ const cfgB = poolInfo.tokenB?.address;
7
+ const where = `pool=${poolInfo.pool_name}(${poolInfo.pool_address})`;
8
+ if (!cfgA || !cfgB) {
9
+ throw new Error(`[${ctx}] assertPoolTokensMatch 失败:配置缺少 token 地址 A=${cfgA} B=${cfgB},${where}`);
10
+ }
11
+ if (!onChainMintA || !onChainMintB) {
12
+ throw new Error(`[${ctx}] assertPoolTokensMatch 失败:链上 mint 为空 A=${onChainMintA} B=${onChainMintB},${where}`);
13
+ }
14
+ const cfg = new Set([cfgA, cfgB]);
15
+ const matched = cfg.size === 2 && cfg.has(onChainMintA) && cfg.has(onChainMintB);
16
+ if (!matched) {
17
+ throw new Error(`[${ctx}] assertPoolTokensMatch 失败:配置 token 地址与链上 mint 不符(选错地址/配错池)` +
18
+ ` — 配置=(${cfgA} / ${cfgB}) 链上=(${onChainMintA} / ${onChainMintB}),${where}`);
19
+ }
20
+ }
@@ -1 +1,3 @@
1
1
  export * from './trade_direction';
2
+ export * from './assert_pool_tokens';
3
+ export * from './transfer_fee';
@@ -15,3 +15,5 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./trade_direction"), exports);
18
+ __exportStar(require("./assert_pool_tokens"), exports);
19
+ __exportStar(require("./transfer_fee"), exports);
@@ -0,0 +1,5 @@
1
+ import { TransferFeeConfig } from '@solana/spl-token';
2
+ import { Connection, EpochInfo, PublicKey } from '@solana/web3.js';
3
+ import BN from 'bn.js';
4
+ export declare function loadMintTransferFeeConfig(connection: Connection, mint: PublicKey): Promise<TransferFeeConfig | undefined>;
5
+ export declare function computeTransferFee(amount: BN, config: TransferFeeConfig | undefined, epochInfo: EpochInfo): BN;
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.loadMintTransferFeeConfig = loadMintTransferFeeConfig;
7
+ exports.computeTransferFee = computeTransferFee;
8
+ const dist_1 = require("@clonegod/ttd-core/dist");
9
+ const spl_token_1 = require("@solana/spl-token");
10
+ const bn_js_1 = __importDefault(require("bn.js"));
11
+ async function loadMintTransferFeeConfig(connection, mint) {
12
+ try {
13
+ const info = await connection.getAccountInfo(mint);
14
+ if (!info)
15
+ return undefined;
16
+ const parsed = (0, spl_token_1.unpackMint)(mint, info, info.owner);
17
+ return (0, spl_token_1.getTransferFeeConfig)(parsed) ?? undefined;
18
+ }
19
+ catch (err) {
20
+ (0, dist_1.log_debug)(`[transfer_fee] config 解析失败(按无费处理): ${mint.toBase58()}`, err.message);
21
+ return undefined;
22
+ }
23
+ }
24
+ function computeTransferFee(amount, config, epochInfo) {
25
+ if (!config)
26
+ return new bn_js_1.default(0);
27
+ const fee = (0, spl_token_1.calculateEpochFee)(config, BigInt(epochInfo.epoch), BigInt(amount.toString()));
28
+ return new bn_js_1.default(fee.toString());
29
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clonegod/ttd-sol-common",
3
- "version": "2.0.85",
3
+ "version": "2.0.86",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  AppConfig, QuoteDepthOutput, QuoteResultType, StandardPoolInfoType,
3
- log_info, log_warn, on_quote_response, report_quote_candidate,
3
+ log_info, log_warn, log_error, on_quote_response, report_quote_candidate,
4
4
  } from '@clonegod/ttd-core/dist'
5
5
  import { SolanaBlockMetaUpdateEvent, SolanaSwapVerifyEventData } from '../types'
6
6
  import { SolanaChainOps } from './chain_ops'
@@ -176,8 +176,14 @@ export abstract class AbstractDexQuote<C extends SolanaChainOps = SolanaChainOps
176
176
  this.dexId,
177
177
  Array.from(this.poolInfoMap.values()),
178
178
  async (evt: SolPoolEvent) => {
179
- const bundle = await this.calculateQuote(evt)
180
- if (bundle) this.publishQuote(bundle)
179
+ try {
180
+ const bundle = await this.calculateQuote(evt)
181
+ if (bundle) this.publishQuote(bundle)
182
+ } catch (err) {
183
+ // 账户快照解码/计算失败 fail-loud:每次都 log_error + 跳过本拍,绝不拿陈旧状态报价。
184
+ // 落在 handler 边界(非裸抛):一条坏事件不拖垮流式询价进程;v1 block 兜底仍会定期 RPC 刷新报价。
185
+ log_error(`[${this.dexId}] v2 事件处理失败 pool=${evt.pool_address}: ${(err as Error).message}`, err instanceof Error ? err : new Error(String(err)))
186
+ }
181
187
  },
182
188
  // swap-verify 通道(链上真实成交 vault Δ):独立于账户快照,对账缓存报价 → [Verify]
183
189
  (sv: SolanaSwapVerifyEventData) => this.handleSwapVerify(sv),
@@ -264,6 +270,7 @@ export abstract class AbstractDexQuote<C extends SolanaChainOps = SolanaChainOps
264
270
  // 例外:乱序晚到的旧 writeVersion 跳过,防状态回拉(writeVersion 缺省时无条件应用)。
265
271
  const hasWv = Number.isInteger(evt.writeVersion)
266
272
  const skipApply = hasWv && !this.shouldApplyState(evt.pool_address, evt.blockNumber, evt.writeVersion as number)
273
+ // 解码失败直接抛 → 由 v2 事件 handler 边界 log_error + 跳过本拍(不软化、不拿陈旧状态报价)。
267
274
  if (!skipApply) await this.refreshStateFromEvent(poolInfo, evt)
268
275
 
269
276
  // compute 前短路:已陈旧 → 跳过重算+推送(状态已更新)
@@ -83,29 +83,6 @@ export class SolTransactionBuilder {
83
83
  public async init(): Promise<void> {
84
84
  // 订阅区块更新事件
85
85
  this.appConfig.arb_event_subscriber.subscribe_new_block(CHAIN_ID.SOLANA, this.handleBlockUpdateEvent.bind(this));
86
- // 预热签名路径:web3.js 1.98 用 @noble/curves/ed25519,首次 sign 要懒构建 base-point wNAF 表
87
- // + V8 JIT 整条 compileMessage/serialize 路径,冷启动 ~34ms。在 init 跑几笔 dummy sign
88
- // 把这 ~30ms 从「第一笔真实交易的关键路径」挪到进程启动期(实测首笔 34ms→~5ms)。
89
- this.warmupSigner()
90
- }
91
-
92
- /** 进程启动期预热 ed25519 签名 + 序列化热路径,消除第一笔交易的冷启动尖刺 */
93
- private warmupSigner(): void {
94
- try {
95
- for (let i = 0; i < 2; i++) {
96
- const tx = new Transaction()
97
- tx.add(SystemProgram.transfer({
98
- fromPubkey: this.keypair.publicKey,
99
- toPubkey: this.keypair.publicKey,
100
- lamports: 0,
101
- }))
102
- tx.recentBlockhash = '11111111111111111111111111111111' // 仅预热用,不广播
103
- tx.feePayer = this.keypair.publicKey
104
- tx.sign(this.keypair)
105
- }
106
- } catch {
107
- // 预热失败不影响主流程(首笔退化为冷签名)
108
- }
109
86
  }
110
87
 
111
88
  private async handleBlockUpdateEvent(eventData: string): Promise<void> {
@@ -0,0 +1,45 @@
1
+ import { StandardPoolInfoType } from '@clonegod/ttd-core/dist'
2
+
3
+ /**
4
+ * 链上身份 > 配置标签(DEX 报价实现规范原则 3):池子 init 即校验配置 token 地址与链上 mint 一致,
5
+ * 不一致 fail-loud。堵「同名 symbol 选错地址 → 静默哑火」这一类 bug(见 BSC/Base 已落地的
6
+ * assertPoolTokensMatch 不变量 / token_guard 事故)。
7
+ *
8
+ * 用**集合相等**(顺序无关)而非按序比对:各 DEX 询价/深度都靠**地址**解析 baseIsToken0 方向
9
+ * (如 `dir.baseToken.address === poolState.tokenAMint`),不依赖配置 tokenA/tokenB 的排列顺序,
10
+ * 故只要两个配置地址 == 两个链上 mint 即可,按序硬比会对合法池误报。
11
+ *
12
+ * Solana 地址 base58 大小写敏感 → 原值精确比较。原生 SOL 在池子里即 WSOL mint
13
+ * (So111…112),与配置地址一致,无需 EVM 那种 native/wrapped 特殊处理。
14
+ *
15
+ * @param onChainMintA 链上 token0/A(或 base)的 mint 地址(base58 原值)
16
+ * @param onChainMintB 链上 token1/B(或 quote)的 mint 地址
17
+ * @param ctx 协议标识,仅用于报错定位(如 'RAYDIUM_AMM' / 'PUMPSWAP')
18
+ */
19
+ export function assertPoolTokensMatch(
20
+ poolInfo: StandardPoolInfoType,
21
+ onChainMintA: string,
22
+ onChainMintB: string,
23
+ ctx: string,
24
+ ): void {
25
+ const cfgA = poolInfo.tokenA?.address
26
+ const cfgB = poolInfo.tokenB?.address
27
+ const where = `pool=${poolInfo.pool_name}(${poolInfo.pool_address})`
28
+
29
+ if (!cfgA || !cfgB) {
30
+ throw new Error(`[${ctx}] assertPoolTokensMatch 失败:配置缺少 token 地址 A=${cfgA} B=${cfgB},${where}`)
31
+ }
32
+ if (!onChainMintA || !onChainMintB) {
33
+ throw new Error(`[${ctx}] assertPoolTokensMatch 失败:链上 mint 为空 A=${onChainMintA} B=${onChainMintB},${where}`)
34
+ }
35
+
36
+ const cfg = new Set([cfgA, cfgB])
37
+ // size===2 排除配置两 token 同址(退化);两个链上 mint 都在配置集合里 ⟺ 集合相等
38
+ const matched = cfg.size === 2 && cfg.has(onChainMintA) && cfg.has(onChainMintB)
39
+ if (!matched) {
40
+ throw new Error(
41
+ `[${ctx}] assertPoolTokensMatch 失败:配置 token 地址与链上 mint 不符(选错地址/配错池)` +
42
+ ` — 配置=(${cfgA} / ${cfgB}) 链上=(${onChainMintA} / ${onChainMintB}),${where}`,
43
+ )
44
+ }
45
+ }
@@ -1 +1,3 @@
1
1
  export * from './trade_direction'
2
+ export * from './assert_pool_tokens'
3
+ export * from './transfer_fee'
@@ -0,0 +1,49 @@
1
+ import { log_debug } from '@clonegod/ttd-core/dist'
2
+ import { calculateEpochFee, getTransferFeeConfig, TransferFeeConfig, unpackMint } from '@solana/spl-token'
3
+ import { Connection, EpochInfo, PublicKey } from '@solana/web3.js'
4
+ import BN from 'bn.js'
5
+
6
+ /**
7
+ * Token-2022 transfer-fee 通用处理(跨 DEX,放 sol-common 供 quote/trade 共用)。
8
+ *
9
+ * transfer fee 是 token-2022 的**可选**扩展:mint 无扩展 → config=undefined → 费用算 0
10
+ * (对普通 SPL mint 零影响)。任何承载带费 token-2022 池的 DEX 都需要此处理
11
+ * (如 raydium-cpmm / raydium-clmm)。
12
+ *
13
+ * 费率数学走 `@solana/spl-token` 的 `calculateEpochFee`(与链上 token-2022 程序同一套口径,
14
+ * 含 maximumFee 封顶),不引入任何 DEX SDK 依赖。
15
+ */
16
+
17
+ /**
18
+ * 取某 mint 的 token-2022 transfer-fee config(SPL / 无 TransferFee 扩展 → undefined)。
19
+ * 一次性 init 调用,结果由调用方缓存。
20
+ */
21
+ export async function loadMintTransferFeeConfig(
22
+ connection: Connection,
23
+ mint: PublicKey,
24
+ ): Promise<TransferFeeConfig | undefined> {
25
+ try {
26
+ const info = await connection.getAccountInfo(mint)
27
+ if (!info) return undefined
28
+ // 按账户 owner(token program) 解析 mint 扩展;SPL mint → getTransferFeeConfig 返回 null
29
+ const parsed = unpackMint(mint, info, info.owner)
30
+ return getTransferFeeConfig(parsed) ?? undefined
31
+ } catch (err) {
32
+ log_debug(`[transfer_fee] config 解析失败(按无费处理): ${mint.toBase58()}`, (err as Error).message)
33
+ return undefined
34
+ }
35
+ }
36
+
37
+ /**
38
+ * 按当前 epoch 计算「转账总额 amount」应扣的转账费(无 config → 0)。
39
+ * 口径 = 链上 token-2022 转账:从转账总额中扣费(preFeeAmount=amount),含 maximumFee 封顶。
40
+ */
41
+ export function computeTransferFee(
42
+ amount: BN,
43
+ config: TransferFeeConfig | undefined,
44
+ epochInfo: EpochInfo,
45
+ ): BN {
46
+ if (!config) return new BN(0)
47
+ const fee = calculateEpochFee(config, BigInt(epochInfo.epoch), BigInt(amount.toString()))
48
+ return new BN(fee.toString())
49
+ }