@clonegod/ttd-sol-common 2.0.67 → 2.0.68
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/appconfig/SolanaQuoteAppConfig.d.ts +9 -0
- package/dist/appconfig/SolanaQuoteAppConfig.js +29 -0
- package/dist/appconfig/SolanaTradeAppConfig.d.ts +13 -0
- package/dist/{config → appconfig}/SolanaTradeAppConfig.js +25 -0
- package/dist/appconfig/ensure_core_env.d.ts +1 -0
- package/dist/appconfig/ensure_core_env.js +18 -0
- package/dist/appconfig/index.d.ts +5 -0
- package/dist/appconfig/index.js +21 -0
- package/dist/appconfig/sol_dex_env_args.d.ts +5 -0
- package/dist/appconfig/sol_dex_env_args.js +29 -0
- package/dist/appconfig/sol_env_args.d.ts +17 -0
- package/dist/appconfig/sol_env_args.js +37 -0
- package/dist/common/get_wallet_token_account.js +13 -16
- package/dist/grpc/grpc_provider_registry.d.ts +14 -0
- package/dist/grpc/grpc_provider_registry.js +70 -0
- package/dist/grpc/index.d.ts +1 -0
- package/dist/grpc/index.js +17 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/quote/abstract_dex_quote.d.ts +68 -0
- package/dist/quote/abstract_dex_quote.js +208 -0
- package/dist/quote/chain_ops.d.ts +18 -0
- package/dist/quote/chain_ops.js +66 -0
- package/dist/quote/depth/clmm_depth_calculator.d.ts +42 -0
- package/dist/quote/depth/clmm_depth_calculator.js +173 -0
- package/dist/quote/depth/index.d.ts +20 -0
- package/dist/quote/depth/index.js +100 -0
- package/dist/quote/index.d.ts +9 -0
- package/dist/quote/index.js +9 -0
- package/dist/quote/pool_event.d.ts +20 -0
- package/dist/quote/pool_event.js +22 -0
- package/dist/quote/pool_subscription_registry.d.ts +4 -0
- package/dist/quote/pool_subscription_registry.js +62 -0
- package/dist/quote/quote_amount.d.ts +4 -0
- package/dist/quote/quote_amount.js +24 -0
- package/dist/quote/quote_trace.d.ts +16 -0
- package/dist/quote/quote_trace.js +40 -0
- package/dist/quote/tick/clmm_tick_math.d.ts +5 -0
- package/dist/quote/tick/clmm_tick_math.js +61 -0
- package/dist/quote/tick/index.d.ts +1 -0
- package/dist/{config → quote/tick}/index.js +1 -1
- package/dist/quote/verify/index.d.ts +1 -0
- package/dist/quote/verify/index.js +17 -0
- package/dist/quote/verify/quote_price_verify.d.ts +30 -0
- package/dist/quote/verify/quote_price_verify.js +247 -0
- package/dist/trade/index.d.ts +0 -1
- package/dist/trade/index.js +0 -1
- package/dist/trade/tx_builder.d.ts +1 -1
- package/dist/trade/tx_result_parse.js +1 -0
- package/dist/types/index.d.ts +9 -1
- package/dist/types/index.js +2 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +17 -0
- package/dist/utils/trade_direction.d.ts +14 -0
- package/dist/utils/trade_direction.js +23 -0
- package/package.json +4 -4
- package/src/appconfig/SolanaQuoteAppConfig.ts +55 -0
- package/src/appconfig/SolanaTradeAppConfig.ts +117 -0
- package/src/appconfig/ensure_core_env.ts +28 -0
- package/src/appconfig/index.ts +5 -0
- package/src/appconfig/sol_dex_env_args.ts +52 -0
- package/src/appconfig/sol_env_args.ts +79 -0
- package/src/common/get_wallet_token_account.ts +27 -33
- package/src/grpc/grpc_provider_registry.ts +103 -0
- package/src/grpc/index.ts +1 -0
- package/src/index.ts +3 -0
- package/src/quote/abstract_dex_quote.ts +337 -0
- package/src/quote/chain_ops.ts +91 -0
- package/src/quote/depth/clmm_depth_calculator.ts +321 -0
- package/src/quote/depth/index.ts +167 -0
- package/src/quote/index.ts +9 -0
- package/src/quote/pool_event.ts +82 -0
- package/src/quote/pool_subscription_registry.ts +81 -0
- package/src/quote/quote_amount.ts +37 -0
- package/src/quote/quote_trace.ts +56 -0
- package/src/quote/tick/clmm_tick_math.ts +77 -0
- package/src/quote/tick/index.ts +1 -0
- package/src/quote/verify/index.ts +1 -0
- package/src/quote/verify/quote_price_verify.ts +508 -0
- package/src/trade/index.ts +0 -1
- package/src/trade/tx_builder.ts +1 -1
- package/src/trade/tx_result_parse.ts +1 -0
- package/src/types/index.ts +20 -2
- package/src/utils/index.ts +1 -0
- package/src/utils/trade_direction.ts +68 -0
- package/dist/config/SolanaTradeAppConfig.d.ts +0 -10
- package/dist/config/index.d.ts +0 -1
- package/dist/trade/SolanaTradeAppConfig.d.ts +0 -8
- package/dist/trade/SolanaTradeAppConfig.js +0 -26
- package/src/config/SolanaTradeAppConfig.ts +0 -70
- package/src/config/index.ts +0 -2
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLMM 深度计算器(SUI X64 版)
|
|
3
|
+
*
|
|
4
|
+
* port 自 BSC `ttd-bsc-common/src/quote/depth/clmm_depth_calculator.ts`,差异(有理由的偏离):
|
|
5
|
+
* · **sqrtPrice 表示 Q96 → Q64**:SUI CLMM(Cetus/Bluefin/Magma/Momentum,integer-mate V3 家族)用 X64
|
|
6
|
+
* 定点(sqrtPriceX64 = sqrt(price) × 2^64),故常量 Q96 改 Q64,amount0/1 公式同构换底。
|
|
7
|
+
* · **不内建 TickMath**:各 DEX 的 tick→sqrtPrice 由其 SDK 提供(Cetus TickMath.tickIndexToSqrtPriceX64 等),
|
|
8
|
+
* 调用方把已算好的 sqrtPriceX64 随 tick 传入 → 本模块零 SDK 依赖、零 TickMath 移植。
|
|
9
|
+
*
|
|
10
|
+
* 给定价格偏移(bps),计算把现价推到 mid × (1 ± bps/10000) 范围内可成交的 token 数量。
|
|
11
|
+
* 复用与 swap 引擎相同的 CLMM 数学,但终止条件不同:到达目标 sqrtPrice 即停(非 amountIn 用完)。
|
|
12
|
+
*
|
|
13
|
+
* 重要:深度(output amount)与 fee 无关 —— fee 只影响需要多少 input,不影响给定价格范围能产出多少 output。
|
|
14
|
+
* fee gross-up 在 buildClmmDepth 层做(与 BSC 一致)。
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const Q64 = 1n << 64n;
|
|
18
|
+
|
|
19
|
+
// ========== 深度计算核心 ==========
|
|
20
|
+
|
|
21
|
+
/** 已初始化 tick(调用方从 SuiClmmTickCache 物化,sqrtPriceX64 由 DEX SDK TickMath 算)。 */
|
|
22
|
+
export interface DepthTick {
|
|
23
|
+
index: number;
|
|
24
|
+
/** 有符号 net liquidity(正=position 下界,负=上界) */
|
|
25
|
+
liquidityNet: bigint;
|
|
26
|
+
/** 该 tick 的 sqrtPriceX64(Q64.64) */
|
|
27
|
+
sqrtPriceX64: bigint;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ClmmDepthInput {
|
|
31
|
+
/** 当前 sqrtPriceX64(Q64.64) */
|
|
32
|
+
sqrtPriceX64: bigint;
|
|
33
|
+
currentTick: number;
|
|
34
|
+
/** 当前 active liquidity */
|
|
35
|
+
liquidity: bigint;
|
|
36
|
+
/** 全量已初始化 tick(含 sqrtPriceX64,无需预排序) */
|
|
37
|
+
ticks: DepthTick[];
|
|
38
|
+
/** swap 方向(aToB / zeroForOne,由 resolveTradeDirection 决定) */
|
|
39
|
+
zeroForOne: boolean;
|
|
40
|
+
/** 目标价格偏移(如 10, 20) */
|
|
41
|
+
targetBps: number;
|
|
42
|
+
inputDecimals: number;
|
|
43
|
+
outputDecimals: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface DepthResult {
|
|
47
|
+
amountInWei: bigint;
|
|
48
|
+
amountIn: number;
|
|
49
|
+
amountOutWei: bigint;
|
|
50
|
+
amountOut: number;
|
|
51
|
+
currentTick: number;
|
|
52
|
+
targetTick: number;
|
|
53
|
+
/** 目标 sqrtPriceX64(Q64.64,用于精确反算 targetPrice) */
|
|
54
|
+
targetSqrtPriceX64: bigint;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 计算 CLMM 池子在给定价格偏移范围内的深度。
|
|
59
|
+
* @returns 在 targetBps 价格范围内可获得的 output token 数量(不含 fee)
|
|
60
|
+
*/
|
|
61
|
+
export function calculateClmmDepth(input: ClmmDepthInput): DepthResult {
|
|
62
|
+
const { currentTick, zeroForOne, targetBps, inputDecimals, outputDecimals } = input;
|
|
63
|
+
|
|
64
|
+
const currentSqrtPriceX64 = input.sqrtPriceX64;
|
|
65
|
+
let liquidity = input.liquidity;
|
|
66
|
+
|
|
67
|
+
if (liquidity <= 0n) {
|
|
68
|
+
return { amountInWei: 0n, amountIn: 0, amountOutWei: 0n, amountOut: 0, currentTick, targetTick: currentTick, targetSqrtPriceX64: currentSqrtPriceX64 };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 1. 目标 sqrtPriceX64
|
|
72
|
+
const targetSqrtPriceX64 = computeTargetSqrtX64(currentSqrtPriceX64, targetBps, zeroForOne);
|
|
73
|
+
|
|
74
|
+
// 2. 现价 → 目标价之间的已初始化 tick(按方向排序)
|
|
75
|
+
const ticksToTraverse = getTicksBetween(input.ticks, currentTick, zeroForOne);
|
|
76
|
+
|
|
77
|
+
// 3. 逐段累加输入量和输出量
|
|
78
|
+
let totalInput = 0n;
|
|
79
|
+
let totalOutput = 0n;
|
|
80
|
+
let sqrtPriceCursor = currentSqrtPriceX64;
|
|
81
|
+
|
|
82
|
+
for (const tick of ticksToTraverse) {
|
|
83
|
+
const sqrtPriceAtTick = tick.sqrtPriceX64;
|
|
84
|
+
|
|
85
|
+
// 已越过目标价 → 停(本段在步骤 4 收尾)
|
|
86
|
+
if (zeroForOne && sqrtPriceAtTick <= targetSqrtPriceX64) break;
|
|
87
|
+
if (!zeroForOne && sqrtPriceAtTick >= targetSqrtPriceX64) break;
|
|
88
|
+
|
|
89
|
+
if (liquidity > 0n) {
|
|
90
|
+
totalInput += calcInputForRange(sqrtPriceCursor, sqrtPriceAtTick, liquidity, zeroForOne);
|
|
91
|
+
totalOutput += calcOutputForRange(sqrtPriceCursor, sqrtPriceAtTick, liquidity, zeroForOne);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 跨越 tick:有符号 net 直接加减(zeroForOne 价格下行 -net,反向 +net)
|
|
95
|
+
let liquidityNet = tick.liquidityNet;
|
|
96
|
+
if (zeroForOne) liquidityNet = -liquidityNet;
|
|
97
|
+
liquidity = liquidity + liquidityNet;
|
|
98
|
+
if (liquidity < 0n) liquidity = 0n;
|
|
99
|
+
|
|
100
|
+
sqrtPriceCursor = sqrtPriceAtTick;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 4. 末段:最后一个 tick → 目标价
|
|
104
|
+
if (liquidity > 0n && sqrtPriceCursor !== targetSqrtPriceX64) {
|
|
105
|
+
totalInput += calcInputForRange(sqrtPriceCursor, targetSqrtPriceX64, liquidity, zeroForOne);
|
|
106
|
+
totalOutput += calcOutputForRange(sqrtPriceCursor, targetSqrtPriceX64, liquidity, zeroForOne);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const amountIn = Number(totalInput) / Math.pow(10, inputDecimals);
|
|
110
|
+
const amountOut = Number(totalOutput) / Math.pow(10, outputDecimals);
|
|
111
|
+
|
|
112
|
+
// 5. 从 targetSqrtPriceX64 反推 targetTick(仅 tick_move 展示用)
|
|
113
|
+
// tick ≈ 2 · log(sqrtPriceX64 / 2^64) / log(1.0001)
|
|
114
|
+
const sqrtPriceFloat = Number(targetSqrtPriceX64) / Number(Q64);
|
|
115
|
+
const targetTick = Math.floor(Math.log(sqrtPriceFloat * sqrtPriceFloat) / Math.log(1.0001));
|
|
116
|
+
|
|
117
|
+
return { amountInWei: totalInput, amountIn, amountOutWei: totalOutput, amountOut, currentTick, targetTick, targetSqrtPriceX64 };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 从 sqrtPriceX64 + decimals 反算价格(quote per base,UI 单位)。
|
|
122
|
+
*
|
|
123
|
+
* sqrtPriceX64 = sqrt(rawToken1 / rawToken0) × 2^64
|
|
124
|
+
* priceToken1PerToken0(raw) = (sqrtPriceX64 / 2^64)^2
|
|
125
|
+
* UI price = quote/base = (raw 比值) × 10^(baseDecimals - quoteDecimals)
|
|
126
|
+
* base 为 token1 时取倒数。
|
|
127
|
+
*/
|
|
128
|
+
export function priceFromSqrtX64(
|
|
129
|
+
sqrtPriceX64: bigint,
|
|
130
|
+
baseDecimals: number,
|
|
131
|
+
quoteDecimals: number,
|
|
132
|
+
baseIsToken0: boolean,
|
|
133
|
+
): number {
|
|
134
|
+
const sp = Number(sqrtPriceX64) / Number(Q64);
|
|
135
|
+
const priceToken1PerToken0 = sp * sp;
|
|
136
|
+
if (baseIsToken0) {
|
|
137
|
+
return priceToken1PerToken0 * Math.pow(10, baseDecimals - quoteDecimals);
|
|
138
|
+
} else {
|
|
139
|
+
return (1 / priceToken1PerToken0) * Math.pow(10, baseDecimals - quoteDecimals);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ========== 内部工具 ==========
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* 计算目标 sqrtPriceX64:
|
|
147
|
+
* price_new = price × (1 ± bps/10000) → sqrtPrice_new = sqrtPrice × sqrt(1 ± bps/10000)
|
|
148
|
+
* 用高精度整数 sqrt(与表示无关,故 X64/X96 通用)。
|
|
149
|
+
*/
|
|
150
|
+
export function computeTargetSqrtX64(currentSqrtPriceX64: bigint, bps: number, zeroForOne: boolean): bigint {
|
|
151
|
+
const PRECISION = 10n ** 18n;
|
|
152
|
+
const bpsBigInt = BigInt(bps);
|
|
153
|
+
// zeroForOne 价格下降 → 10000 - bps;反向 → 10000 + bps
|
|
154
|
+
const factor = zeroForOne ? 10000n - bpsBigInt : 10000n + bpsBigInt;
|
|
155
|
+
const sqrtFactor = bigIntSqrt(factor * PRECISION);
|
|
156
|
+
const sqrtBase = bigIntSqrt(10000n * PRECISION);
|
|
157
|
+
return currentSqrtPriceX64 * sqrtFactor / sqrtBase;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** 整数平方根(Newton 法)。 */
|
|
161
|
+
export function bigIntSqrt(n: bigint): bigint {
|
|
162
|
+
if (n < 0n) throw new Error('sqrt of negative');
|
|
163
|
+
if (n === 0n) return 0n;
|
|
164
|
+
if (n <= 3n) return 1n;
|
|
165
|
+
let x = n;
|
|
166
|
+
let y = (x + 1n) / 2n;
|
|
167
|
+
while (y < x) {
|
|
168
|
+
x = y;
|
|
169
|
+
y = (x + n / x) / 2n;
|
|
170
|
+
}
|
|
171
|
+
return x;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** 现价 tick 与目标价之间的已初始化 tick(按 swap 方向排序)。 */
|
|
175
|
+
function getTicksBetween(ticks: DepthTick[], currentTick: number, zeroForOne: boolean): DepthTick[] {
|
|
176
|
+
if (zeroForOne) {
|
|
177
|
+
// 价格下降:tick ≤ currentTick,降序(从现价往下走)
|
|
178
|
+
return ticks.filter(t => t.index <= currentTick).sort((a, b) => b.index - a.index);
|
|
179
|
+
} else {
|
|
180
|
+
// 价格上升:tick > currentTick,升序
|
|
181
|
+
return ticks.filter(t => t.index > currentTick).sort((a, b) => a.index - b.index);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ========== amountIn 驱动的跨 tick swap(SDK-free,供 quoteV2 真跨 tick)==========
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* 给定一段流动性内,把 amountIn 全部吃进去后到达的 nextSqrtPriceX64(canonical Uniswap V3,纯 bigint)。
|
|
189
|
+
* zeroForOne=true(输入 token0,价格下行):
|
|
190
|
+
* sqrtNext = L·sqrt·Q64 / (L·Q64 + amountIn·sqrt)
|
|
191
|
+
* zeroForOne=false(输入 token1,价格上行):
|
|
192
|
+
* sqrtNext = sqrt + amountIn·Q64 / L
|
|
193
|
+
* 与 byreal/raydium SqrtPriceMath.getNextSqrtPriceX64FromInput 同公式(此处用 amountIn 已扣 fee 的净额)。
|
|
194
|
+
*/
|
|
195
|
+
function nextSqrtFromAmountIn(sqrtPriceX64: bigint, liquidity: bigint, amountIn: bigint, zeroForOne: boolean): bigint {
|
|
196
|
+
if (amountIn <= 0n || liquidity <= 0n) return sqrtPriceX64;
|
|
197
|
+
if (zeroForOne) {
|
|
198
|
+
const numerator = liquidity * sqrtPriceX64 * Q64;
|
|
199
|
+
const denominator = liquidity * Q64 + amountIn * sqrtPriceX64;
|
|
200
|
+
return numerator / denominator;
|
|
201
|
+
} else {
|
|
202
|
+
return sqrtPriceX64 + (amountIn * Q64) / liquidity;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export interface SwapExactInInput {
|
|
207
|
+
sqrtPriceX64: bigint;
|
|
208
|
+
currentTick: number;
|
|
209
|
+
liquidity: bigint;
|
|
210
|
+
ticks: DepthTick[];
|
|
211
|
+
zeroForOne: boolean;
|
|
212
|
+
/** 净输入量(已扣 fee 的链上整数 wei) */
|
|
213
|
+
amountInNetWei: bigint;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export interface SwapExactInResult {
|
|
217
|
+
amountInConsumedWei: bigint;
|
|
218
|
+
amountOutWei: bigint;
|
|
219
|
+
endSqrtPriceX64: bigint;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* amountIn 驱动的跨 tick swap:把 amountInNetWei 顺方向吃进,逐 tick 累加 output(SDK-free,与深度同一套 X64 数学)。
|
|
224
|
+
* 终止条件:amountIn 用完(区别于 calculateClmmDepth 的"到达目标价")。流动性耗尽(无更多 tick)则到此为止。
|
|
225
|
+
*
|
|
226
|
+
* 与链上 swap 引擎同源:calcInputForRange/calcOutputForRange 段内累加 + 有符号 liquidityNet 跨 tick + 末段
|
|
227
|
+
* nextSqrtFromAmountIn 反算 partial。**fee 在调用方扣**(传净额),与 buildClmmDepth gross-up 口径一致。
|
|
228
|
+
*/
|
|
229
|
+
export function swapExactInClmm(input: SwapExactInInput): SwapExactInResult {
|
|
230
|
+
const { currentTick, zeroForOne } = input;
|
|
231
|
+
let liquidity = input.liquidity;
|
|
232
|
+
let sqrtCursor = input.sqrtPriceX64;
|
|
233
|
+
let remaining = input.amountInNetWei;
|
|
234
|
+
let totalOut = 0n;
|
|
235
|
+
let consumed = 0n;
|
|
236
|
+
|
|
237
|
+
if (liquidity <= 0n || remaining <= 0n) {
|
|
238
|
+
return { amountInConsumedWei: 0n, amountOutWei: 0n, endSqrtPriceX64: sqrtCursor };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const ticksToTraverse = getTicksBetween(input.ticks, currentTick, zeroForOne);
|
|
242
|
+
|
|
243
|
+
for (const tick of ticksToTraverse) {
|
|
244
|
+
if (remaining <= 0n) break;
|
|
245
|
+
const sqrtAtTick = tick.sqrtPriceX64;
|
|
246
|
+
|
|
247
|
+
if (liquidity > 0n) {
|
|
248
|
+
// 当前段(cursor → tick 边界)最多能吃多少 input
|
|
249
|
+
const segMaxIn = calcInputForRange(sqrtCursor, sqrtAtTick, liquidity, zeroForOne);
|
|
250
|
+
if (remaining < segMaxIn) {
|
|
251
|
+
// input 在本段内耗尽 → 反算 partial nextSqrt + partial output
|
|
252
|
+
const nextSqrt = nextSqrtFromAmountIn(sqrtCursor, liquidity, remaining, zeroForOne);
|
|
253
|
+
totalOut += calcOutputForRange(sqrtCursor, nextSqrt, liquidity, zeroForOne);
|
|
254
|
+
consumed += remaining;
|
|
255
|
+
remaining = 0n;
|
|
256
|
+
sqrtCursor = nextSqrt;
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
// 吃满本段到 tick 边界
|
|
260
|
+
totalOut += calcOutputForRange(sqrtCursor, sqrtAtTick, liquidity, zeroForOne);
|
|
261
|
+
consumed += segMaxIn;
|
|
262
|
+
remaining -= segMaxIn;
|
|
263
|
+
}
|
|
264
|
+
sqrtCursor = sqrtAtTick;
|
|
265
|
+
|
|
266
|
+
// 跨 tick:有符号 net(zeroForOne 价格下行取负)
|
|
267
|
+
let liquidityNet = tick.liquidityNet;
|
|
268
|
+
if (zeroForOne) liquidityNet = -liquidityNet;
|
|
269
|
+
liquidity = liquidity + liquidityNet;
|
|
270
|
+
if (liquidity < 0n) liquidity = 0n;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// 末段:无更多 tick 但仍有 input 且有流动性 → 在当前流动性内继续吃完剩余(无边界)
|
|
274
|
+
if (remaining > 0n && liquidity > 0n) {
|
|
275
|
+
const nextSqrt = nextSqrtFromAmountIn(sqrtCursor, liquidity, remaining, zeroForOne);
|
|
276
|
+
totalOut += calcOutputForRange(sqrtCursor, nextSqrt, liquidity, zeroForOne);
|
|
277
|
+
consumed += remaining;
|
|
278
|
+
sqrtCursor = nextSqrt;
|
|
279
|
+
remaining = 0n;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return { amountInConsumedWei: consumed, amountOutWei: totalOut, endSqrtPriceX64: sqrtCursor };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* 一段价格区间内的输入 token 数量(不含 fee)。
|
|
287
|
+
* zeroForOne=true → 输入 token0 → amount0 公式
|
|
288
|
+
* zeroForOne=false → 输入 token1 → amount1 公式
|
|
289
|
+
*/
|
|
290
|
+
function calcInputForRange(sqrtPriceA: bigint, sqrtPriceB: bigint, liquidity: bigint, zeroForOne: boolean): bigint {
|
|
291
|
+
const lower = sqrtPriceA < sqrtPriceB ? sqrtPriceA : sqrtPriceB;
|
|
292
|
+
const upper = sqrtPriceA < sqrtPriceB ? sqrtPriceB : sqrtPriceA;
|
|
293
|
+
const diff = upper - lower;
|
|
294
|
+
if (diff === 0n || liquidity === 0n) return 0n;
|
|
295
|
+
if (zeroForOne) {
|
|
296
|
+
// amount0 = L · (upper - lower) · 2^64 / (lower · upper)
|
|
297
|
+
return liquidity * diff * Q64 / (lower * upper);
|
|
298
|
+
} else {
|
|
299
|
+
// amount1 = L · (upper - lower) / 2^64
|
|
300
|
+
return liquidity * diff / Q64;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* 一段价格区间内的输出 token 数量。
|
|
306
|
+
* zeroForOne=true → 输出 token1 → amount1 公式
|
|
307
|
+
* zeroForOne=false → 输出 token0 → amount0 公式
|
|
308
|
+
*/
|
|
309
|
+
function calcOutputForRange(sqrtPriceA: bigint, sqrtPriceB: bigint, liquidity: bigint, zeroForOne: boolean): bigint {
|
|
310
|
+
const lower = sqrtPriceA < sqrtPriceB ? sqrtPriceA : sqrtPriceB;
|
|
311
|
+
const upper = sqrtPriceA < sqrtPriceB ? sqrtPriceB : sqrtPriceA;
|
|
312
|
+
const diff = upper - lower;
|
|
313
|
+
if (diff === 0n || liquidity === 0n) return 0n;
|
|
314
|
+
if (zeroForOne) {
|
|
315
|
+
// 输出 token1: amount1 = L · (upper - lower) / 2^64
|
|
316
|
+
return liquidity * diff / Q64;
|
|
317
|
+
} else {
|
|
318
|
+
// 输出 token0: amount0 = L · (upper - lower) · 2^64 / (lower · upper)
|
|
319
|
+
return liquidity * diff * Q64 / (lower * upper);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 深度计算模块(Solana,tier 版)
|
|
3
|
+
*
|
|
4
|
+
* port 自 SUI `ttd-sui-common/src/quote/depth/index.ts`(同源 BSC),差异(有理由的偏离):
|
|
5
|
+
* · **仅 CLMM**:Solana 三个 CLMM(Raydium/Orca/ByReal)全是 Uniswap V3 风格集中流动性 X64;
|
|
6
|
+
* AMM/DAMM 走各自路径,无统一深度引擎 → 暂只点亮 buildClmmDepth。
|
|
7
|
+
* · **X64**:调用 X64 版 clmm_depth_calculator(见同目录,逐字照搬 SUI)。
|
|
8
|
+
* · **tick + sqrtPriceX64 由调用方物化**:各 DEX 把本地 tickArray cache 的已初始化 tick 配
|
|
9
|
+
* **共享 TickMath**(`../tick/clmm_tick_math` getSqrtPriceX64FromTick)算的 sqrtPriceX64,
|
|
10
|
+
* 组装成 DepthTick[] 传入 —— 三个 CLMM 同源 Uniswap V3,故共享同一份 TickMath。
|
|
11
|
+
* · **方向**:调用方传 baseIsToken0(用链上 mintA/mintB 判定 base 是否为 token0=mintA),
|
|
12
|
+
* ask(买 base,输入 quote)zeroForOne = !baseIsToken0;bid(卖 base,输入 base)zeroForOne = baseIsToken0。
|
|
13
|
+
*
|
|
14
|
+
* 输出语义(与 BSC/SUI 一致):
|
|
15
|
+
* amount = 推到 target price 时获得的 base token(不受 fee 影响)
|
|
16
|
+
* amount_in = 推到 target price 时需投入的 quote token(含费 gross up)
|
|
17
|
+
* fee = amount_in × fee_rate_bps/10000
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { log_debug, log_info, StandardPoolInfoType } from '@clonegod/ttd-core/dist';
|
|
21
|
+
import {
|
|
22
|
+
QuoteTier, QuoteEntry, QuoteDepthOutput,
|
|
23
|
+
getDepthPricePctLevels, DEFAULT_TIER_PCT,
|
|
24
|
+
} from '@clonegod/ttd-core/dist';
|
|
25
|
+
import { resolveTradeDirection } from '../../utils/trade_direction';
|
|
26
|
+
import { DepthTick, calculateClmmDepth, priceFromSqrtX64 } from './clmm_depth_calculator';
|
|
27
|
+
|
|
28
|
+
export { calculateClmmDepth, priceFromSqrtX64, computeTargetSqrtX64, bigIntSqrt, swapExactInClmm } from './clmm_depth_calculator';
|
|
29
|
+
export type { DepthTick, ClmmDepthInput, DepthResult, SwapExactInInput, SwapExactInResult } from './clmm_depth_calculator';
|
|
30
|
+
|
|
31
|
+
// ========== 启动日志一次性输出 ==========
|
|
32
|
+
let _depthPctLogged = false;
|
|
33
|
+
function logDepthLevelsOnce(): void {
|
|
34
|
+
if (_depthPctLogged) return;
|
|
35
|
+
_depthPctLogged = true;
|
|
36
|
+
log_info(`[Depth] pctLevels=${JSON.stringify(getDepthPricePctLevels())}, default=${DEFAULT_TIER_PCT}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ========== Fee gross-up ==========
|
|
40
|
+
/**
|
|
41
|
+
* 池子曲线纯输入 → 用户实付(含费):
|
|
42
|
+
* amountInGross = amountInNet / (1 - feeRate);feeAmount = amountInGross - amountInNet
|
|
43
|
+
*/
|
|
44
|
+
function grossUpFee(amountInNet: number, feeRateBps: number): { amountInGross: number; feeAmount: number } {
|
|
45
|
+
if (feeRateBps <= 0 || amountInNet <= 0) return { amountInGross: amountInNet, feeAmount: 0 };
|
|
46
|
+
const feeRatio = feeRateBps / 10000;
|
|
47
|
+
const amountInGross = amountInNet / (1 - feeRatio);
|
|
48
|
+
return { amountInGross, feeAmount: amountInGross - amountInNet };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ========== 把多档 tier 装配成 QuoteEntry(主档镜像顶层)==========
|
|
52
|
+
function assembleEntry(tiers: QuoteTier[]): QuoteEntry {
|
|
53
|
+
const def = tiers.find(t => t.pct === DEFAULT_TIER_PCT) ?? tiers[0];
|
|
54
|
+
return {
|
|
55
|
+
price: def.price, amount: def.amount, amount_in: def.amount_in, amount_in_usd: def.amount_in_usd,
|
|
56
|
+
fee: def.fee, fee_usd: def.fee_usd, tick_move: def.tick_move, tiers,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ========== CLMM 通用深度构建 ==========
|
|
61
|
+
|
|
62
|
+
export interface BuildClmmDepthInput {
|
|
63
|
+
poolInfo: StandardPoolInfoType;
|
|
64
|
+
poolAddress: string;
|
|
65
|
+
/** 池子动态态(v2 维护的本地 cache 或 v1 现读) */
|
|
66
|
+
poolState: {
|
|
67
|
+
currentSqrtPriceX64: bigint;
|
|
68
|
+
currentTick: number;
|
|
69
|
+
liquidity: bigint;
|
|
70
|
+
/** base token 是否为链上 token0(=mintA);调用方用链上 mint 判定 */
|
|
71
|
+
baseIsToken0: boolean;
|
|
72
|
+
};
|
|
73
|
+
/** 全量已初始化 tick(含 sqrtPriceX64),调用方从本地 tickArray cache + 共享 TickMath 物化 */
|
|
74
|
+
ticks: DepthTick[];
|
|
75
|
+
basePriceUsd: number;
|
|
76
|
+
quotePriceUsd: number;
|
|
77
|
+
/**
|
|
78
|
+
* 池子 effective fee rate (bps),调用方从链上 feeRate 换算(各 DEX 单位不同)。
|
|
79
|
+
* 不接受 fallback 到配置漂移值之外的兜底——错则 verify 跨源 diff 系统性偏移会暴露。
|
|
80
|
+
*/
|
|
81
|
+
feeRateBps: number;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 通用 CLMM 深度构建:多档(默认 5 档)ask/bid 深度,从内存 tick 数据零额外 RPC 计算。
|
|
86
|
+
*/
|
|
87
|
+
export function buildClmmDepth(input: BuildClmmDepthInput): QuoteDepthOutput | undefined {
|
|
88
|
+
logDepthLevelsOnce();
|
|
89
|
+
const pctLevels = getDepthPricePctLevels();
|
|
90
|
+
if (pctLevels.length === 0) return undefined;
|
|
91
|
+
|
|
92
|
+
const { poolInfo, poolState, ticks, basePriceUsd, quotePriceUsd, feeRateBps } = input;
|
|
93
|
+
if (poolState.liquidity <= 0n) return undefined;
|
|
94
|
+
if (feeRateBps == null || feeRateBps < 0) {
|
|
95
|
+
log_debug(`[Depth] ${poolInfo.pool_name} CLMM: invalid feeRateBps=${feeRateBps}, skip`, '');
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const dir = resolveTradeDirection(poolInfo, true);
|
|
101
|
+
const baseToken = dir.baseToken;
|
|
102
|
+
const quoteToken = dir.quoteToken;
|
|
103
|
+
const baseIsToken0 = poolState.baseIsToken0;
|
|
104
|
+
// ask 买 base(输入 quote):quote 为 token0 ⟺ base 为 token1 ⟺ !baseIsToken0
|
|
105
|
+
const askZeroForOne = !baseIsToken0;
|
|
106
|
+
// bid 卖 base(输入 base):base 为 token0 ⟺ baseIsToken0
|
|
107
|
+
const bidZeroForOne = baseIsToken0;
|
|
108
|
+
|
|
109
|
+
const midPrice = priceFromSqrtX64(poolState.currentSqrtPriceX64, baseToken.decimals, quoteToken.decimals, baseIsToken0);
|
|
110
|
+
|
|
111
|
+
const askTiers: QuoteTier[] = [];
|
|
112
|
+
const bidTiers: QuoteTier[] = [];
|
|
113
|
+
|
|
114
|
+
for (const pct of pctLevels) {
|
|
115
|
+
const bps = Math.round(pct * 100); // 0.1% → 10bp
|
|
116
|
+
|
|
117
|
+
const askResult = calculateClmmDepth({
|
|
118
|
+
sqrtPriceX64: poolState.currentSqrtPriceX64, currentTick: poolState.currentTick,
|
|
119
|
+
liquidity: poolState.liquidity, ticks, zeroForOne: askZeroForOne, targetBps: bps,
|
|
120
|
+
inputDecimals: quoteToken.decimals, outputDecimals: baseToken.decimals,
|
|
121
|
+
});
|
|
122
|
+
const bidResult = calculateClmmDepth({
|
|
123
|
+
sqrtPriceX64: poolState.currentSqrtPriceX64, currentTick: poolState.currentTick,
|
|
124
|
+
liquidity: poolState.liquidity, ticks, zeroForOne: bidZeroForOne, targetBps: bps,
|
|
125
|
+
inputDecimals: baseToken.decimals, outputDecimals: quoteToken.decimals,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const askTargetPrice = priceFromSqrtX64(askResult.targetSqrtPriceX64, baseToken.decimals, quoteToken.decimals, baseIsToken0);
|
|
129
|
+
const bidTargetPrice = priceFromSqrtX64(bidResult.targetSqrtPriceX64, baseToken.decimals, quoteToken.decimals, baseIsToken0);
|
|
130
|
+
|
|
131
|
+
// Ask: amount_in 是 quote token(gross up),amount_out 是 base token
|
|
132
|
+
const askGross = grossUpFee(askResult.amountIn, feeRateBps);
|
|
133
|
+
// Bid: amount_in 是 base token(gross up),amount_out 是 quote token
|
|
134
|
+
const bidGross = grossUpFee(bidResult.amountIn, feeRateBps);
|
|
135
|
+
|
|
136
|
+
const askTickMove = `${askResult.targetTick} <- ${askResult.currentTick}`;
|
|
137
|
+
const bidTickMove = `${bidResult.currentTick} -> ${bidResult.targetTick}`;
|
|
138
|
+
|
|
139
|
+
// 【强制·原则2】tier.price = effective average price including fee = amount_in_gross / amount_out
|
|
140
|
+
// 跟链上 swap exec_price 严格同语义;禁止改回 marginal / 不含 fee 形态(G4 事故根因)。
|
|
141
|
+
// 详见 arb-common/docs/20260602-dex-quote-implementation-standard.md 原则2;verify 跨源对账兜底。
|
|
142
|
+
const askEffPrice = askResult.amountOut > 0 ? askGross.amountInGross / askResult.amountOut : askTargetPrice;
|
|
143
|
+
const bidEffPrice = bidGross.amountInGross > 0 ? bidResult.amountOut / bidGross.amountInGross : bidTargetPrice;
|
|
144
|
+
|
|
145
|
+
askTiers.push({
|
|
146
|
+
pct, price: askEffPrice, amount: askResult.amountOut,
|
|
147
|
+
amount_in: askGross.amountInGross, amount_in_usd: askGross.amountInGross * quotePriceUsd,
|
|
148
|
+
fee: askGross.feeAmount, fee_usd: askGross.feeAmount * quotePriceUsd, tick_move: askTickMove,
|
|
149
|
+
});
|
|
150
|
+
bidTiers.push({
|
|
151
|
+
pct, price: bidEffPrice, amount: bidResult.amountOut,
|
|
152
|
+
amount_in: bidGross.amountInGross, amount_in_usd: bidGross.amountInGross * basePriceUsd,
|
|
153
|
+
fee: bidGross.feeAmount, fee_usd: bidGross.feeAmount * basePriceUsd, tick_move: bidTickMove,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
mid_price: midPrice,
|
|
159
|
+
fee_rate_bps: feeRateBps,
|
|
160
|
+
ask: assembleEntry(askTiers),
|
|
161
|
+
bid: assembleEntry(bidTiers),
|
|
162
|
+
};
|
|
163
|
+
} catch (error) {
|
|
164
|
+
log_debug(`[Depth] ${poolInfo.pool_name} CLMM depth failed: ${(error as Error).message}`, '');
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
}
|
package/src/quote/index.ts
CHANGED
|
@@ -1 +1,10 @@
|
|
|
1
1
|
export * from './pricing'
|
|
2
|
+
export * from './pool_subscription_registry'
|
|
3
|
+
export * from './pool_event'
|
|
4
|
+
export * from './chain_ops'
|
|
5
|
+
export * from './abstract_dex_quote'
|
|
6
|
+
export * from './quote_trace'
|
|
7
|
+
export * from './quote_amount'
|
|
8
|
+
export * from './verify'
|
|
9
|
+
export * from './depth'
|
|
10
|
+
export * from './tick'
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { SolanaPoolAccountUpdateEventData } from '../types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SolPoolEvent —— 询价基类消费的归一化池事件(对齐 BSC/SUI 的 pool_event 角色,
|
|
5
|
+
* 但建模 **Solana 的账户快照模型**,与 EVM/SUI 的 swap-event 模型有本质差异)。
|
|
6
|
+
*
|
|
7
|
+
* ── 关键差异(设计文档 §2.1)──
|
|
8
|
+
* EVM/SUI:stream 解析 swap/mint/burn 事件 → 推结构化 {type, amount0/1, txIndex},
|
|
9
|
+
* 消费端靠解析事件维护本地 tick。
|
|
10
|
+
* Solana :tickArray/binArray/vault 本身就是独立链上账户。stream-quote 直接订阅这些账户,
|
|
11
|
+
* 账户一变即推**最新账户原始数据**(base64),消费端解码 = 拿到链上真相(天然一致)。
|
|
12
|
+
* → 没有"swap 事件 + amount",事件本身就是新状态快照。
|
|
13
|
+
*
|
|
14
|
+
* 因此 SolPoolEvent 不带 swap/add/remove 分类、不带 amount0/1:
|
|
15
|
+
* - 每个池账户更新都触发 v2 重算(refreshStateFromEvent 解码账户 → 覆盖本地状态)。
|
|
16
|
+
* - 单调水位线用 Solana 账户的 writeVersion(Geyser write_version,全局单调递增),是 txIndex 的天然对等物。
|
|
17
|
+
* stream-quote 已把 write_version 补进推送 wire(step 2)→ 基类启用 (slot, writeVersion) 去重。
|
|
18
|
+
* AMM 聚合快照取 pool+vaultA+vaultB 三账户的 max writeVersion。
|
|
19
|
+
* (writeVersion 缺省时基类回退"无序号无条件推",对齐 SUI 缺 txIndex 行为,防御性兜底。)
|
|
20
|
+
*
|
|
21
|
+
* verify(swap amount 对账)暂缓:SOL 账户快照不含 swap amount,buildSwapVerify 默认返回 null,
|
|
22
|
+
* 待 stream-quote 解码 swap amount 后再接(设计文档 §2.4 / verify 跨源 cacheQuote 仍生效)。
|
|
23
|
+
*/
|
|
24
|
+
export interface SolPoolEvent {
|
|
25
|
+
pool_address: string
|
|
26
|
+
dex_id: string
|
|
27
|
+
pool_name: string
|
|
28
|
+
/** 触发事件的 slot(= blockNumber,去重/水位线的"块"维度) */
|
|
29
|
+
blockNumber: number
|
|
30
|
+
/** 账户写版本(= txIndex 的 Solana 对等物,单调递增;wire 暂未携带 → 可空) */
|
|
31
|
+
writeVersion?: number
|
|
32
|
+
/** 导致账户变化的交易哈希(txid 透传,无则 'account') */
|
|
33
|
+
txHash: string
|
|
34
|
+
/** 区块时间(毫秒;缺省 0,stream 暂未必带) */
|
|
35
|
+
blockTime: number
|
|
36
|
+
|
|
37
|
+
// ── 账户原始数据(base64);refreshStateFromEvent 按 DEX layout 解码 ──
|
|
38
|
+
/** 池主账户原始数据(CLMM=pool state / DLMM=lbPair / AMM=pool);子账户独立 push 时为空 */
|
|
39
|
+
poolAccountData: string
|
|
40
|
+
/** AMM vault A 原始数据(reserve;仅 AMM 携带) */
|
|
41
|
+
vaultAAccountData?: string
|
|
42
|
+
/** AMM vault B 原始数据(仅 AMM 携带) */
|
|
43
|
+
vaultBAccountData?: string
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 动态子账户更新(CLMM tickArray / DLMM binArray,方案 A 独立 push)。
|
|
47
|
+
* 存在时本事件是"子账户变更"而非"池主账户变更" → refreshStateFromEvent 按 role 解码更新本地分布。
|
|
48
|
+
*/
|
|
49
|
+
subAccount?: {
|
|
50
|
+
role: string // 'tickArray' | 'binArray'
|
|
51
|
+
address: string
|
|
52
|
+
data: string // base64
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** 原始事件(需要额外字段时兜底访问,避免反复扩接口) */
|
|
56
|
+
raw: SolanaPoolAccountUpdateEventData
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 把 stream-quote 推送的 SolanaPoolAccountUpdateEventData 归一化为 SolPoolEvent。
|
|
61
|
+
* 收敛 wire → 基类契约的映射,后续 wire 增字段只改这里一处。
|
|
62
|
+
*/
|
|
63
|
+
export function toSolPoolEvent(data: SolanaPoolAccountUpdateEventData): SolPoolEvent {
|
|
64
|
+
const d = data.data
|
|
65
|
+
return {
|
|
66
|
+
pool_address: data.pool_address,
|
|
67
|
+
dex_id: data.dex_id,
|
|
68
|
+
pool_name: data.pool_name,
|
|
69
|
+
blockNumber: d.slot,
|
|
70
|
+
// Geyser write_version(全局单调);stream-quote 已补进 wire(step 2)→ (slot, writeVersion) 水位线去重
|
|
71
|
+
writeVersion: d.write_version,
|
|
72
|
+
txHash: d.tx_hash || 'account',
|
|
73
|
+
blockTime: data.event_time || 0,
|
|
74
|
+
poolAccountData: d.pool_account_data,
|
|
75
|
+
vaultAAccountData: d.vaultA_account_data,
|
|
76
|
+
vaultBAccountData: d.vaultB_account_data,
|
|
77
|
+
subAccount: d.sub_account
|
|
78
|
+
? { role: d.sub_account.role, address: d.sub_account.account, data: d.sub_account.account_data }
|
|
79
|
+
: undefined,
|
|
80
|
+
raw: data,
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { AppConfig, HttpUtil, log_info, log_warn, SERVICE_PORT, StandardPoolInfoType } from '@clonegod/ttd-core/dist'
|
|
2
|
+
import { resolveGrpcProvider } from '../grpc/grpc_provider_registry'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 按需订阅 —— 生产端(询价进程调用)。对齐 BSC/BASE/SUI:询价进程通过 config-center 声明所需池子,
|
|
6
|
+
* config-center 写 `${chain}:pool:subscriptions` Hash + 广播 `:change`,stream-quote 消费后订阅 Helius。
|
|
7
|
+
*
|
|
8
|
+
* Solana 适配(与 BSC 的差异):
|
|
9
|
+
* - **单 Helius provider,且对齐 SUI 由运维在 analyze Config/RPC 页配(type=grpc, endpoint=laserstream URL, auth_token=API key)**。
|
|
10
|
+
* 本 helper **不自动种 provider**;register 用 `resolveGrpcProvider` 解析出 analyze 配的 provider id(无配 fail-loud)。
|
|
11
|
+
* - **base58 地址大小写敏感**:依赖 config-center `lower()` 已改为 0x 感知(非 0x 原样保留),不破坏地址。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
function configCenterBase(appConfig: AppConfig): string {
|
|
15
|
+
const chain = appConfig.env_args.chain_id // SOLANA
|
|
16
|
+
let host = String(appConfig.env_args.config_center_host || '127.0.0.1').trim()
|
|
17
|
+
// host 可能含端口(127.0.0.1:4000)或不含(127.0.0.1);不含则补 CONFIG_CENTER_HTTP
|
|
18
|
+
if (!host.includes(':')) host = `${host}:${SERVICE_PORT.CONFIG_CENTER_HTTP}`
|
|
19
|
+
return `http://${host}/${chain}/config`
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** 注册一批池子订阅(每池 POST config-center /quote/register),provider_ids 用 analyze 配的 grpc provider。 */
|
|
23
|
+
export async function registerPoolSubscriptions(
|
|
24
|
+
appConfig: AppConfig,
|
|
25
|
+
pools: StandardPoolInfoType[],
|
|
26
|
+
quote_app_id: string,
|
|
27
|
+
): Promise<void> {
|
|
28
|
+
// provider 由运维在 analyze Config/RPC 配(type=grpc);解析其 id 供 register 引用,无配 fail-loud
|
|
29
|
+
const provider = await resolveGrpcProvider(appConfig.arb_cache.redis_cmd, appConfig.env_args.chain_id)
|
|
30
|
+
const provider_ids = [provider.id]
|
|
31
|
+
|
|
32
|
+
const base = configCenterBase(appConfig)
|
|
33
|
+
let ok = 0
|
|
34
|
+
for (const p of pools) {
|
|
35
|
+
try {
|
|
36
|
+
await HttpUtil.post(`${base}/quote/register`, {
|
|
37
|
+
pool_address: p.pool_address,
|
|
38
|
+
provider_ids,
|
|
39
|
+
quote_app_id,
|
|
40
|
+
pair: p.pair,
|
|
41
|
+
dex_id: p.dex_id,
|
|
42
|
+
pool_name: p.pool_name,
|
|
43
|
+
fee_rate: p.fee_rate,
|
|
44
|
+
})
|
|
45
|
+
ok++
|
|
46
|
+
} catch (e: any) {
|
|
47
|
+
log_warn(`[pool/subscriptions] register ${p.pool_address} failed: ${e.message}`)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
log_info(`[pool/subscriptions] registered ${ok}/${pools.length} pools by ${quote_app_id} → provider=${provider.id}`)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 注销本进程注册的所有池子订阅(按 quote_app_id)。 */
|
|
54
|
+
export async function unregisterPoolSubscriptions(appConfig: AppConfig, quote_app_id: string): Promise<void> {
|
|
55
|
+
try {
|
|
56
|
+
await HttpUtil.post(`${configCenterBase(appConfig)}/quote/unregister`, { quote_app_id })
|
|
57
|
+
log_info(`[pool/subscriptions] unregistered all pools by ${quote_app_id}`)
|
|
58
|
+
} catch (e: any) {
|
|
59
|
+
log_warn(`[pool/subscriptions] unregister failed: ${e.message}`)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 询价进程启动后调用一次:确保 helius provider + 注册全部池子 + 装退出时自动 unregister。
|
|
65
|
+
* quote_app_id 取 env_args.app_name(每个 DEX 询价进程唯一)。
|
|
66
|
+
*/
|
|
67
|
+
export async function attachPoolSubscriptionLifecycle(appConfig: AppConfig, pools: StandardPoolInfoType[]): Promise<void> {
|
|
68
|
+
const quote_app_id = String(appConfig.env_args.app_name || '').toLowerCase() || 'sol-quote'
|
|
69
|
+
await registerPoolSubscriptions(appConfig, pools, quote_app_id)
|
|
70
|
+
|
|
71
|
+
let cleaned = false
|
|
72
|
+
const cleanup = async (sig: string) => {
|
|
73
|
+
if (cleaned) return
|
|
74
|
+
cleaned = true
|
|
75
|
+
log_info(`[pool/subscriptions] ${sig} → unregister ${quote_app_id}`)
|
|
76
|
+
await unregisterPoolSubscriptions(appConfig, quote_app_id)
|
|
77
|
+
process.exit(0)
|
|
78
|
+
}
|
|
79
|
+
process.on('SIGINT', () => void cleanup('SIGINT'))
|
|
80
|
+
process.on('SIGTERM', () => void cleanup('SIGTERM'))
|
|
81
|
+
}
|