@clonegod/ttd-sol-common 2.0.51 → 2.0.53
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/common/get_wallet_token_account.d.ts +1 -1
- package/dist/common/get_wallet_token_account.js +19 -5
- package/dist/trade/send/jito.js +2 -3
- package/dist/trade/tx_result_check.js +3 -4
- package/dist/trade/tx_result_parse.js +13 -4
- package/docs/clmm.md +108 -0
- package/package.json +2 -2
- package/src/common/get_wallet_token_account.ts +42 -4
- package/src/common/index.ts +1 -1
- package/tsconfig.json +3 -4
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { StandardPoolInfoType, StandardTokenInfoType } from "@clonegod/ttd-core";
|
|
2
2
|
import { Connection, Keypair, PublicKey } from "@solana/web3.js";
|
|
3
3
|
export declare const get_token_program_id: (token: StandardTokenInfoType) => PublicKey;
|
|
4
|
-
export declare function get_wallet_token_account(wallet_pubkey: string, pool_list: StandardPoolInfoType[]):
|
|
4
|
+
export declare function get_wallet_token_account(wallet_pubkey: string, pool_list: StandardPoolInfoType[]): Map<string, string>;
|
|
5
5
|
export declare const create_token_account_if_not_exist: (connection: Connection, owner: Keypair, token_list: StandardTokenInfoType[]) => Promise<void>;
|
|
@@ -14,18 +14,32 @@ const get_token_program_id = (token) => {
|
|
|
14
14
|
return programId;
|
|
15
15
|
};
|
|
16
16
|
exports.get_token_program_id = get_token_program_id;
|
|
17
|
-
|
|
17
|
+
const wallet_token_accounts_cache = new Map();
|
|
18
|
+
function getWalletTokenAccountCacheKey(wallet_pubkey, token_address) {
|
|
19
|
+
return `${wallet_pubkey}_${token_address}}`;
|
|
20
|
+
}
|
|
21
|
+
function getCachedAtaAddress(wallet_pubkey, token_address, programId) {
|
|
22
|
+
const cacheKey = getWalletTokenAccountCacheKey(wallet_pubkey, token_address);
|
|
23
|
+
if (wallet_token_accounts_cache.has(cacheKey)) {
|
|
24
|
+
return wallet_token_accounts_cache.get(cacheKey);
|
|
25
|
+
}
|
|
26
|
+
const ataAddress = (0, spl_token_1.getAssociatedTokenAddressSync)(new web3_js_1.PublicKey(token_address), new web3_js_1.PublicKey(wallet_pubkey), false, programId).toBase58();
|
|
27
|
+
wallet_token_accounts_cache.set(cacheKey, ataAddress);
|
|
28
|
+
return ataAddress;
|
|
29
|
+
}
|
|
30
|
+
function get_wallet_token_account(wallet_pubkey, pool_list) {
|
|
18
31
|
let account_map = new Map();
|
|
19
32
|
for (let { tokenA, tokenB } of pool_list) {
|
|
20
|
-
|
|
21
|
-
|
|
33
|
+
const programIdA = (0, exports.get_token_program_id)(tokenA);
|
|
34
|
+
const programIdB = (0, exports.get_token_program_id)(tokenB);
|
|
35
|
+
account_map.set(tokenA.symbol, getCachedAtaAddress(wallet_pubkey, tokenA.address, programIdA));
|
|
36
|
+
account_map.set(tokenB.symbol, getCachedAtaAddress(wallet_pubkey, tokenB.address, programIdB));
|
|
22
37
|
}
|
|
23
38
|
return account_map;
|
|
24
39
|
}
|
|
25
40
|
const create_token_account_if_not_exist = async (connection, owner, token_list) => {
|
|
26
41
|
token_list.forEach(async (token) => {
|
|
27
|
-
|
|
28
|
-
if (['SOL', 'WSOL', 'USDC', 'USDT'].includes((_a = token.symbol) === null || _a === void 0 ? void 0 : _a.toUpperCase())) {
|
|
42
|
+
if (['SOL', 'WSOL', 'USDC', 'USDT'].includes(token.symbol?.toUpperCase())) {
|
|
29
43
|
return;
|
|
30
44
|
}
|
|
31
45
|
try {
|
package/dist/trade/send/jito.js
CHANGED
|
@@ -77,7 +77,6 @@ const getJitoTipAccount = () => {
|
|
|
77
77
|
exports.getJitoTipAccount = getJitoTipAccount;
|
|
78
78
|
JitoUtils.init();
|
|
79
79
|
const sendBundleWithJito = async (mainTxBase64, tipTxBase64) => {
|
|
80
|
-
var _a, _b;
|
|
81
80
|
let url = process.env.JITO_SEND_BUNDLE_URL || 'https://tokyo.mainnet.block-engine.jito.wtf/api/v1/bundles';
|
|
82
81
|
const client = (0, http_client_1.getHttpClient)(url);
|
|
83
82
|
const requestData = {
|
|
@@ -99,8 +98,8 @@ const sendBundleWithJito = async (mainTxBase64, tipTxBase64) => {
|
|
|
99
98
|
}
|
|
100
99
|
catch (error) {
|
|
101
100
|
if (error instanceof axios_1.AxiosError) {
|
|
102
|
-
const status =
|
|
103
|
-
const statusText =
|
|
101
|
+
const status = error.response?.status;
|
|
102
|
+
const statusText = error.response?.statusText;
|
|
104
103
|
(0, dist_1.log_warn)(`[sendBundleWithJito] Request failed: ${status} - ${statusText}`);
|
|
105
104
|
}
|
|
106
105
|
else {
|
|
@@ -36,7 +36,6 @@ class TransactionResultChecker extends dist_1.AbstractTransactionResultCheck {
|
|
|
36
36
|
}
|
|
37
37
|
on_subscibe_transaction() {
|
|
38
38
|
this.event_emitter.once(dist_1.LOCAL_EVENT_NAME.EVENT_WALLET_TRANSACTION + '#' + this.txid, (message) => {
|
|
39
|
-
var _a, _b, _c;
|
|
40
39
|
try {
|
|
41
40
|
this.trade_result_already_processed = true;
|
|
42
41
|
let messageObj = message;
|
|
@@ -60,9 +59,9 @@ class TransactionResultChecker extends dist_1.AbstractTransactionResultCheck {
|
|
|
60
59
|
result = messageObj.params.result;
|
|
61
60
|
_txid = result.signature;
|
|
62
61
|
slot = result.slot;
|
|
63
|
-
transactionData =
|
|
64
|
-
meta =
|
|
65
|
-
version =
|
|
62
|
+
transactionData = result.transaction?.transaction;
|
|
63
|
+
meta = result.transaction?.meta;
|
|
64
|
+
version = result.transaction?.version !== undefined ? result.transaction.version : 0;
|
|
66
65
|
}
|
|
67
66
|
else {
|
|
68
67
|
(0, dist_1.log_error)('Invalid message format: unrecognized structure', new Error(JSON.stringify(messageObj)));
|
|
@@ -66,16 +66,25 @@ class TransactionResultParser {
|
|
|
66
66
|
block_number: slot,
|
|
67
67
|
txid,
|
|
68
68
|
pool_address,
|
|
69
|
-
tokenA:
|
|
70
|
-
|
|
69
|
+
tokenA: {
|
|
70
|
+
...tokenA,
|
|
71
|
+
pre_bal: tokenA_PreBalance,
|
|
72
|
+
post_bal: tokenA_PostBalance,
|
|
73
|
+
change: tokenAChange
|
|
74
|
+
},
|
|
75
|
+
tokenB: {
|
|
76
|
+
...tokenB,
|
|
77
|
+
pre_bal: tokenB_PreBalance,
|
|
78
|
+
post_bal: tokenB_PostBalance,
|
|
79
|
+
change: tokenBChange
|
|
80
|
+
},
|
|
71
81
|
tx_price: price,
|
|
72
82
|
gas_fee
|
|
73
83
|
};
|
|
74
84
|
return tradeResult;
|
|
75
85
|
}
|
|
76
86
|
getTokenBalance(owner, mint, tokenBalances) {
|
|
77
|
-
|
|
78
|
-
const balance = (_b = (_a = tokenBalances === null || tokenBalances === void 0 ? void 0 : tokenBalances.find((e) => e.owner === owner && e.mint == mint)) === null || _a === void 0 ? void 0 : _a.uiTokenAmount) === null || _b === void 0 ? void 0 : _b.uiAmount;
|
|
87
|
+
const balance = tokenBalances?.find((e) => e.owner === owner && e.mint == mint)?.uiTokenAmount?.uiAmount;
|
|
79
88
|
if (typeof balance !== 'undefined' && balance !== null) {
|
|
80
89
|
return balance;
|
|
81
90
|
}
|
package/docs/clmm.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
我想让你教会我完整分析CLMM池子的技术:
|
|
2
|
+
1、有个叫bitmap的数据,是关于池子已经初始化的tick信息?
|
|
3
|
+
2、bitmap -> tickArray -> currentTickIndex -> currentTickArray -> neiberhood TickArray
|
|
4
|
+
3、比如我通过监听链上池子account change,可以知道当前池子的current tickIndex, liqudity, sqrtprice。目前我只会根据3个数据来处略的做询价(比如100U的单子的ask,bid价格,为后面做交易参考)。跟你聊了之后,我想做到更好。
|
|
5
|
+
4、另外,池子的一些基本信息,我可以在启动时初始化。池子的费率,池子的tickspace。
|
|
6
|
+
5、我想能更加详细的在本地使用公式来计算出池子的报价:当前静态价格、按某个size询价得到的ask,bid价格(比如我固定按USDT计价。 SOL/USDT 的池子,ask : SOL->USDC, bid: USDC->SOL)
|
|
7
|
+
6、需要有两种方式:简化计算,精确计算(这个依赖于本地能获取到更多的tick信息,我希望能看到每一个计算步骤,丛当前tick->tick移动->跨tickArray的流程日志)。
|
|
8
|
+
7、我还希望能知道池子深度,也就是当前tick附近可交易买卖的USDC,SOL的数量。这个表达可能有问题,应该是当前tick状态下,比如在千分之15价格范围可买卖的USDC和SOL的个数。类似CEX ORDER BOOK上的数据那样。有价格,有深度。
|
|
9
|
+
你能教会上上面的那些技术吗?
|
|
10
|
+
|
|
11
|
+
-----------------------------
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
## sqrtPrice
|
|
15
|
+
|
|
16
|
+
同一个 tick 内,sqrtPrice 可以连续变化(每笔小单都产生新 sqrtPrice),所以单纯通过 tick 获取 sqrtPrice 确实有问题 ——它只能给你边界值,不是当前精确值。这就是为什么池子状态总是存 sqrtPriceX64 而非只存 tick。
|
|
17
|
+
|
|
18
|
+
tick 的作用:tick 是“价格区间的标签”(discrete anchors)。
|
|
19
|
+
每个 tick 对应一个精确的边界点,Uniswap V3 / Raydium / Orca 约定俗成:tickToSqrtPriceX64(i) 返回的是 tick i 的左边界(下边界),即价格区间的最小 sqrtPrice。
|
|
20
|
+
公式(Orca/Uniswap V3):P(i) = 1.0001^{i}
|
|
21
|
+
|
|
22
|
+
sqrtPriceX64 = √P(i) × 2^{64}(下边界)。上边界 = √P(i + 1) × 2^{64}。
|
|
23
|
+
sqrtPriceX64 的作用:这是当前精确价格(continuous),每笔交易后更新(连续偏移)。它总是在当前 tick 的 [下边界, 上边界) 内。 小单:sqrtPrice 在区间内小移,tick 不变。
|
|
24
|
+
累积小单:sqrtPrice 逐步推进,最终跨边界 → tick 更新。
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
tickToSqrtPriceX64(tick) 只返回 边界(e.g., 下边界),不是当前真实 sqrtPrice。
|
|
28
|
+
如果用这个计算价格,会忽略区间内滑点(e.g., 同一个 tick,价格从边界起点滑到终点,变化 ~0.01% × spacing)。
|
|
29
|
+
|
|
30
|
+
总是优先用池子状态的 sqrtPriceX64 计算当前价格(实时精确)。
|
|
31
|
+
tick 只用于:边界检查、跨 tick 更新 L、初始化 tickArray。
|
|
32
|
+
转换工具:tick → sqrtPrice(边界),sqrtPrice → tick(floor 到当前区间)。
|
|
33
|
+
|
|
34
|
+
当前真实 sqrtPriceX64 总是满足:
|
|
35
|
+
tickToSqrtPriceX64(currentTick) ≤ sqrtPriceX64 < tickToSqrtPriceX64(currentTick + tickSpacing)
|
|
36
|
+
|
|
37
|
+
-----------------------------
|
|
38
|
+
pool config: tickspace, feerate, bitmap
|
|
39
|
+
bitmap: 标记的是哪些 TickArray 已初始化(有至少一个 tick 被使用),而不是标记单个 tick。
|
|
40
|
+
TickArray: 一组离散的Tick,不一定全初始化。核心参数:tick spacing(相邻tick之间的间距。 相邻tick之间的价差,指的是相邻tick的下边界sqrtprice的变化比率)
|
|
41
|
+
TickIndex: 当前tick的索引,通过它可定位处于tick属于哪个TickArray。由于tick是边界,因此通过tickToSqrtPrice实际上计算出的是 [sqrtPrice_下边界,sqrtPrice_上边界]的价格区间。
|
|
42
|
+
|
|
43
|
+
PoolState:
|
|
44
|
+
currentTickIndex
|
|
45
|
+
liqudity
|
|
46
|
+
sqrtPriceX64
|
|
47
|
+
|
|
48
|
+
-----------------------------
|
|
49
|
+
在SOL/USDC这个池子进行“询价”:
|
|
50
|
+
-- 询价,实际上就是模拟做一笔交易,预测'未来'进行一笔差不多大小的交易的可成交价。
|
|
51
|
+
|
|
52
|
+
1、初始化
|
|
53
|
+
- 初始化池子的配置信息:
|
|
54
|
+
tokenA, tokenB, feeRate
|
|
55
|
+
bitmap (一般在poolData中,不用单独订阅)
|
|
56
|
+
- 初始化池子的状态信息(随时会变):
|
|
57
|
+
tickArray
|
|
58
|
+
* 订阅当前tick前后相邻3个Tick Array的账户数据(根据currentTick动态调整被监控的TickArray账户列表)
|
|
59
|
+
|
|
60
|
+
2、动态更新
|
|
61
|
+
- 监听池子的状态变换,可获得池子的最新状态
|
|
62
|
+
current_tick_index
|
|
63
|
+
liqudity
|
|
64
|
+
sqrtPrice
|
|
65
|
+
|
|
66
|
+
- 监听池子的事件
|
|
67
|
+
* solana 似乎没有基于事件监听的机制?比如监听池子的swap,burn,mint事件
|
|
68
|
+
监听池子的交易,丛交易中解析是否存在burn,mint,如果有,则需要本地刷新bitmap,tickArray
|
|
69
|
+
|
|
70
|
+
3、模拟交易量
|
|
71
|
+
- 输入100USDC
|
|
72
|
+
- 池子费率0.05%
|
|
73
|
+
- 扣除交易费用 100 * 0.05%,剩下的作为真正的inputAmount
|
|
74
|
+
- currentTick => currentTickOfTickArray
|
|
75
|
+
- neiberhoodTickArray => 相邻tickArray通过订阅
|
|
76
|
+
- 逐tick吃流动性,跨tick吃,跨tickArray吃
|
|
77
|
+
- inputAmount/outputAmount => usdc per SOL (用USDC计价)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
-------------------
|
|
81
|
+
# CLMM 核心事实(2025版)
|
|
82
|
+
|
|
83
|
+
1. sqrtPriceX64 是当前**真实价格**(连续),每笔交易都变
|
|
84
|
+
- 公式:price = (sqrtPriceX64 / 2^64)² → token1 ÷ token0
|
|
85
|
+
- 必须用这个算当前价格,不能用 tickToSqrtPriceX64
|
|
86
|
+
|
|
87
|
+
2. tickIndex 是**当前价格所在的价格区间标签**(离散)
|
|
88
|
+
- tickToSqrtPriceX64(i) 返回的是 tick i 的**左边界**
|
|
89
|
+
- 真实关系:tickToSqrtPriceX64(currentTick) ≤ sqrtPriceX64 < tickToSqrtPriceX64(currentTick + spacing)
|
|
90
|
+
|
|
91
|
+
3. 小单不会跨 tick,但 sqrtPrice 会连续下滑/上涨 → 价格实时滑点
|
|
92
|
+
累积小单最终会跨 tick → 更新 liquidity
|
|
93
|
+
|
|
94
|
+
4. TickArray 是存储结构:
|
|
95
|
+
- Raydium / Orca:每个 account 存 88 个 tick slot
|
|
96
|
+
- bitmap(Raydium)标记哪些 TickArray 已初始化(不是单个 tick)
|
|
97
|
+
|
|
98
|
+
5. 生产级实时监控方案:
|
|
99
|
+
- 订阅 whirlpool account → 实时 currentTick/liquidity/sqrtPrice
|
|
100
|
+
- 维护当前 ±5 个 TickArray(通过 bitmap 或 PDA 计算)
|
|
101
|
+
- 每笔询价都用最新 sqrtPrice + 当前 liquidity + 预加载的 tick 数据模拟
|
|
102
|
+
|
|
103
|
+
6. 询价 = 模拟 swap:
|
|
104
|
+
输入 amountIn → 扣费 → 逐 tick 吃(跨 tick 更新 L,跨 array 加载下一个)→ 输出 amountOut + 最终价格
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clonegod/ttd-sol-common",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.53",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "types/index.d.ts",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"push": "npm run build && npm publish"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@clonegod/ttd-core": "2.1.
|
|
16
|
+
"@clonegod/ttd-core": "2.1.8",
|
|
17
17
|
"@solana/web3.js": "1.91.6",
|
|
18
18
|
"rpc-websockets": "7.10.0",
|
|
19
19
|
"axios": "^1.2.3",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { StandardPoolInfoType, StandardTokenInfoType } from "@clonegod/ttd-core";
|
|
2
2
|
import { log_info, log_error } from "@clonegod/ttd-core/dist";
|
|
3
3
|
import { COMMITMENT_LEVEL } from "./constants";
|
|
4
|
-
import { ASSOCIATED_TOKEN_PROGRAM_ID, createAssociatedTokenAccountIdempotent, getAssociatedTokenAddress, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from "@solana/spl-token";
|
|
4
|
+
import { ASSOCIATED_TOKEN_PROGRAM_ID, createAssociatedTokenAccountIdempotent, getAssociatedTokenAddress, getAssociatedTokenAddressSync, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from "@solana/spl-token";
|
|
5
5
|
import { Connection, Keypair, PublicKey } from "@solana/web3.js";
|
|
6
6
|
|
|
7
7
|
|
|
@@ -13,13 +13,51 @@ export const get_token_program_id = (token: StandardTokenInfoType) => {
|
|
|
13
13
|
return programId
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
// 缓存 ATA 地址:key = `${wallet_pubkey}_${token_address}`
|
|
17
|
+
const wallet_token_accounts_cache = new Map<string, string>()
|
|
18
|
+
|
|
19
|
+
function getWalletTokenAccountCacheKey(wallet_pubkey: string, token_address: string): string {
|
|
20
|
+
return `${wallet_pubkey}_${token_address}}`
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getCachedAtaAddress(
|
|
24
|
+
wallet_pubkey: string,
|
|
25
|
+
token_address: string,
|
|
26
|
+
programId: PublicKey
|
|
27
|
+
): string {
|
|
28
|
+
const cacheKey = getWalletTokenAccountCacheKey(wallet_pubkey, token_address)
|
|
29
|
+
|
|
30
|
+
if (wallet_token_accounts_cache.has(cacheKey)) {
|
|
31
|
+
return wallet_token_accounts_cache.get(cacheKey)!
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const ataAddress = getAssociatedTokenAddressSync(
|
|
35
|
+
new PublicKey(token_address),
|
|
36
|
+
new PublicKey(wallet_pubkey),
|
|
37
|
+
false,
|
|
38
|
+
programId
|
|
39
|
+
).toBase58()
|
|
40
|
+
|
|
41
|
+
wallet_token_accounts_cache.set(cacheKey, ataAddress)
|
|
42
|
+
return ataAddress
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function get_wallet_token_account(wallet_pubkey:string, pool_list:StandardPoolInfoType[]
|
|
17
46
|
) {
|
|
18
47
|
let account_map = new Map<string,string>()
|
|
19
48
|
|
|
20
49
|
for(let {tokenA, tokenB} of pool_list) {
|
|
21
|
-
|
|
22
|
-
|
|
50
|
+
const programIdA = get_token_program_id(tokenA)
|
|
51
|
+
const programIdB = get_token_program_id(tokenB)
|
|
52
|
+
|
|
53
|
+
account_map.set(
|
|
54
|
+
tokenA.symbol,
|
|
55
|
+
getCachedAtaAddress(wallet_pubkey, tokenA.address, programIdA)
|
|
56
|
+
)
|
|
57
|
+
account_map.set(
|
|
58
|
+
tokenB.symbol,
|
|
59
|
+
getCachedAtaAddress(wallet_pubkey, tokenB.address, programIdB)
|
|
60
|
+
)
|
|
23
61
|
}
|
|
24
62
|
return account_map
|
|
25
63
|
}
|
package/src/common/index.ts
CHANGED
package/tsconfig.json
CHANGED
|
@@ -5,11 +5,10 @@
|
|
|
5
5
|
"./types",
|
|
6
6
|
],
|
|
7
7
|
"lib": [
|
|
8
|
-
"
|
|
9
|
-
"dom"
|
|
8
|
+
"es2020"
|
|
10
9
|
],
|
|
11
10
|
"module": "CommonJS",
|
|
12
|
-
"target": "
|
|
11
|
+
"target": "es2020",
|
|
13
12
|
"rootDirs": [
|
|
14
13
|
"src"
|
|
15
14
|
],
|
|
@@ -20,6 +19,6 @@
|
|
|
20
19
|
"resolveJsonModule": true,
|
|
21
20
|
"removeComments": true
|
|
22
21
|
},
|
|
23
|
-
"include": ["src/**/*"],
|
|
22
|
+
"include": ["src/**/*", "docs/clmm_local_calc.ts", "../ttd-sol-orca-clmm/src/quote/clmm_engine.ts"],
|
|
24
23
|
"exclude": ["dist"]
|
|
25
24
|
}
|