@clonegod/ttd-sol-common 2.0.66 → 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,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./trade_direction"), exports);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface TradeDirectionResult<T = any> {
|
|
2
|
+
isBuy: boolean;
|
|
3
|
+
inputToken: T;
|
|
4
|
+
outputToken: T;
|
|
5
|
+
baseToken: T;
|
|
6
|
+
quoteToken: T;
|
|
7
|
+
}
|
|
8
|
+
export declare function resolveTradeDirection<T = any>(poolInfo: {
|
|
9
|
+
tokenA: T;
|
|
10
|
+
tokenB: T;
|
|
11
|
+
quote_token: string;
|
|
12
|
+
[key: string]: any;
|
|
13
|
+
}, isBuy: boolean): TradeDirectionResult<T>;
|
|
14
|
+
export declare function calculateStandardPrice(inputAmount: number, outputAmount: number, isBuy: boolean): string;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveTradeDirection = resolveTradeDirection;
|
|
4
|
+
exports.calculateStandardPrice = calculateStandardPrice;
|
|
5
|
+
function resolveTradeDirection(poolInfo, isBuy) {
|
|
6
|
+
const { tokenA, tokenB, quote_token } = poolInfo;
|
|
7
|
+
const quoteToken = [tokenA, tokenB].find(t => t.symbol === quote_token);
|
|
8
|
+
const baseToken = [tokenA, tokenB].find(t => t.symbol !== quote_token);
|
|
9
|
+
if (!quoteToken || !baseToken) {
|
|
10
|
+
throw new Error(`Cannot resolve tokens: tokenA=${tokenA?.symbol}, tokenB=${tokenB?.symbol}, quote_token=${quote_token}`);
|
|
11
|
+
}
|
|
12
|
+
const inputToken = isBuy ? quoteToken : baseToken;
|
|
13
|
+
const outputToken = isBuy ? baseToken : quoteToken;
|
|
14
|
+
return { isBuy, inputToken, outputToken, baseToken, quoteToken };
|
|
15
|
+
}
|
|
16
|
+
function calculateStandardPrice(inputAmount, outputAmount, isBuy) {
|
|
17
|
+
if (isBuy) {
|
|
18
|
+
return (inputAmount / outputAmount).toFixed(18);
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
return (outputAmount / inputAmount).toFixed(18);
|
|
22
|
+
}
|
|
23
|
+
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clonegod/ttd-sol-common",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.68",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
|
-
"types": "
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
7
|
"keywords": [],
|
|
8
8
|
"author": "",
|
|
9
9
|
"license": "ISC",
|
|
@@ -13,13 +13,13 @@
|
|
|
13
13
|
"push": "npm run build && npm publish"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@clonegod/ttd-core": "
|
|
16
|
+
"@clonegod/ttd-core": "3.1.84",
|
|
17
17
|
"@solana/web3.js": "1.91.6",
|
|
18
18
|
"rpc-websockets": "7.10.0",
|
|
19
19
|
"axios": "^1.2.3",
|
|
20
20
|
"bn.js": "^4.12.1",
|
|
21
21
|
"bs58": "^6.0.0",
|
|
22
|
-
"helius-laserstream": "^0.
|
|
22
|
+
"helius-laserstream": "^0.4.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@types/node": "^22.7.9",
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Solana 链通用 Quote 配置基类(对标 BSC 的 BscQuoteAppConfig)。
|
|
3
|
+
*
|
|
4
|
+
* 统一所有 DEX Quote 进程的配置初始化逻辑:
|
|
5
|
+
* - ensureCoreEnv(即便入口忘记 import './appconfig' 也兜底)
|
|
6
|
+
* - 初始化 arb_cache(super.init)+ arb_event_subscriber
|
|
7
|
+
* - 建立 Solana Connection(所有 DEX 共用,commitment 可由 env 覆盖)
|
|
8
|
+
* - env_args 收窄为 SolanaDexEnvArgs
|
|
9
|
+
*
|
|
10
|
+
* 池子加载 / 过滤不在这里做 —— 与 BSC 一致,放到各 DEX 的 entry(quote/index.ts),
|
|
11
|
+
* 直接 arb_cache.get_pair_dex_pool_list([pair],[dex_id]) + filterPools。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { AppConfig, getArbEventSubscriber, getCoreEnv, log_info } from '@clonegod/ttd-core'
|
|
15
|
+
import { Commitment, Connection } from '@solana/web3.js'
|
|
16
|
+
import { COMMITMENT_LEVEL } from '../common/constants'
|
|
17
|
+
import { SolanaDexEnvArgs } from './sol_dex_env_args'
|
|
18
|
+
import { ensureCoreEnv } from './ensure_core_env'
|
|
19
|
+
|
|
20
|
+
export class SolanaQuoteAppConfig extends AppConfig {
|
|
21
|
+
public env_args: SolanaDexEnvArgs
|
|
22
|
+
public connection: Connection
|
|
23
|
+
|
|
24
|
+
constructor() {
|
|
25
|
+
// super 前 ensure coreEnv(AppConfig.constructor 内部读 getCoreEnv())
|
|
26
|
+
ensureCoreEnv()
|
|
27
|
+
super()
|
|
28
|
+
// AppConfig.constructor 已 this.env_args = getCoreEnv();这里只做类型收窄
|
|
29
|
+
this.env_args = getCoreEnv() as SolanaDexEnvArgs
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async init() {
|
|
33
|
+
// super.init() 初始化 arb_cache
|
|
34
|
+
await super.init()
|
|
35
|
+
|
|
36
|
+
// 确保 arb_event_subscriber 已就绪(池事件订阅依赖)
|
|
37
|
+
if (!this.arb_event_subscriber) {
|
|
38
|
+
this.arb_event_subscriber = getArbEventSubscriber(this.arb_cache)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Solana 连接(所有 DEX quote 共用;commitment 默认 PROCESSED,可由 env 覆盖)
|
|
42
|
+
const commitment =
|
|
43
|
+
(process.env.CONNECTION_COMMITMENT_LEVEL as Commitment) || COMMITMENT_LEVEL.PROCESSED
|
|
44
|
+
this.connection = new Connection(this.env_args.rpc_endpoint, { commitment })
|
|
45
|
+
|
|
46
|
+
log_info('SolanaQuoteAppConfig initialized', {
|
|
47
|
+
chain_id: this.env_args.chain_id,
|
|
48
|
+
dex_id: this.env_args.dex_id,
|
|
49
|
+
pair: this.env_args.pair,
|
|
50
|
+
rpc_endpoint: this.env_args.rpc_endpoint,
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// subscribe_config_change 不覆盖 —— 继承 AppConfig 基类版本
|
|
55
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Solana 链通用 Trade 配置基类(对标 BSC 的 BscTradeAppConfig,与 SolanaQuoteAppConfig 对称)。
|
|
3
|
+
*
|
|
4
|
+
* 统一所有 DEX Trade 进程:
|
|
5
|
+
* - ensureCoreEnv + env_args 收窄为 SolanaDexEnvArgs
|
|
6
|
+
* - 建立 Solana Connection(commitment 可由 env 覆盖)
|
|
7
|
+
* - init_trade_runtime:create_trade_runtime + 从 wallet.private_key 还原 keypair(原本每个 DEX 各抄一份,现上提)
|
|
8
|
+
* - subscribe_wallet_raw_txn_event:订阅 stream-trade 推送的链上 tx 结果
|
|
9
|
+
*
|
|
10
|
+
* 各 DEX 的 *TradeAppConfig 直接 `extends SolanaTradeAppConfig` 即可,通常无需再覆盖任何方法。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
AbstractTradeAppConfig,
|
|
15
|
+
getCoreEnv,
|
|
16
|
+
LOCAL_EVENT_NAME,
|
|
17
|
+
log_error,
|
|
18
|
+
log_info,
|
|
19
|
+
sleep,
|
|
20
|
+
WebSocketClient,
|
|
21
|
+
} from '@clonegod/ttd-core/dist'
|
|
22
|
+
import { Commitment, Connection, Keypair } from '@solana/web3.js'
|
|
23
|
+
import bs58 from 'bs58'
|
|
24
|
+
import { COMMITMENT_LEVEL } from '../common/constants'
|
|
25
|
+
import { SolanaDexEnvArgs } from './sol_dex_env_args'
|
|
26
|
+
import { ensureCoreEnv } from './ensure_core_env'
|
|
27
|
+
|
|
28
|
+
export class SolanaTradeAppConfig extends AbstractTradeAppConfig {
|
|
29
|
+
public env_args: SolanaDexEnvArgs
|
|
30
|
+
public connection: Connection
|
|
31
|
+
public keypair: Keypair
|
|
32
|
+
|
|
33
|
+
// WebSocket 客户端实例,用于订阅交易结果
|
|
34
|
+
private ws_client: WebSocketClient | null = null
|
|
35
|
+
|
|
36
|
+
constructor() {
|
|
37
|
+
ensureCoreEnv()
|
|
38
|
+
super()
|
|
39
|
+
this.env_args = getCoreEnv() as SolanaDexEnvArgs
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async init() {
|
|
43
|
+
await super.init()
|
|
44
|
+
|
|
45
|
+
// Solana 连接(所有 DEX trade 共用;commitment 默认 PROCESSED)
|
|
46
|
+
const commitment =
|
|
47
|
+
(process.env.CONNECTION_COMMITMENT_LEVEL as Commitment) || COMMITMENT_LEVEL.PROCESSED
|
|
48
|
+
this.connection = new Connection(this.env_args.rpc_endpoint, { commitment })
|
|
49
|
+
|
|
50
|
+
this.subscribe_wallet_raw_txn_event()
|
|
51
|
+
log_info('SolanaTradeAppConfig init ...')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 初始化交易上下文 + 从 wallet.private_key 还原 keypair。
|
|
56
|
+
* (原本 byreal / meteora / orca / pumpswap / raydium / jupiter 各抄一份,现统一上提)
|
|
57
|
+
*/
|
|
58
|
+
async init_trade_runtime() {
|
|
59
|
+
try {
|
|
60
|
+
const { chain_id, dex_id, group_id, pair } = this.env_args
|
|
61
|
+
|
|
62
|
+
this.trade_runtime = await this.arb_cache.create_trade_runtime(
|
|
63
|
+
chain_id,
|
|
64
|
+
group_id,
|
|
65
|
+
dex_id,
|
|
66
|
+
pair,
|
|
67
|
+
)
|
|
68
|
+
this.trade_runtime.wallet_token_accounts = new Map()
|
|
69
|
+
|
|
70
|
+
// 从 trade_runtime.wallet.private_key 还原 Solana keypair
|
|
71
|
+
const secret_key = new Uint8Array(bs58.decode(this.trade_runtime.wallet.private_key))
|
|
72
|
+
this.keypair = Keypair.fromSecretKey(secret_key)
|
|
73
|
+
} catch (err) {
|
|
74
|
+
log_error(`SolanaTradeAppConfig create_trade_runtime error!`, err)
|
|
75
|
+
await sleep(1000)
|
|
76
|
+
process.exit(0)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 订阅 Wallet 在链上发生的 tx data(stream-trade 经 WS 推送)
|
|
81
|
+
subscribe_wallet_raw_txn_event(): void {
|
|
82
|
+
// 检查是否已经订阅
|
|
83
|
+
if (this.is_already_subscribe_wallet_raw_txn) {
|
|
84
|
+
if (this.ws_client && this.ws_client.isConnected()) {
|
|
85
|
+
return
|
|
86
|
+
} else {
|
|
87
|
+
// 连接已断开,需要重新连接
|
|
88
|
+
log_info('subscribe_wallet_raw_txn_event was subscribed but connection lost, reconnecting...')
|
|
89
|
+
if (this.ws_client) {
|
|
90
|
+
this.ws_client.disconnect()
|
|
91
|
+
this.ws_client = null
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
this.is_already_subscribe_wallet_raw_txn = true
|
|
96
|
+
|
|
97
|
+
const transactionHandler = async (messageStr: any) => {
|
|
98
|
+
let messageObj = messageStr
|
|
99
|
+
if (typeof messageStr === 'string') {
|
|
100
|
+
messageObj = JSON.parse(messageStr)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const tx_id = messageObj.transaction.signature
|
|
104
|
+
log_info(`Received transaction result via WebSocket: ${tx_id}`)
|
|
105
|
+
|
|
106
|
+
this.emit(LOCAL_EVENT_NAME.EVENT_WALLET_TRANSACTION + '#' + tx_id, messageObj)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const ws_port = process.env.STREAM_WS_TRADE_PORT || 10002
|
|
110
|
+
const ws_url = `ws://127.0.0.1:${ws_port}`
|
|
111
|
+
this.ws_client = new WebSocketClient(ws_url)
|
|
112
|
+
this.ws_client.onMessage(transactionHandler)
|
|
113
|
+
this.ws_client.connect()
|
|
114
|
+
|
|
115
|
+
log_info(`subscribe_wallet_raw_txn_event, WebSocket connecting to ${ws_url}`)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 通用 ensureCoreEnv:Solana AppConfig 子类构造器在 super() 之前调用,
|
|
3
|
+
* 确保 ttd-core 的 coreEnv 已被 setCoreEnv —— 即便入口忘记 import './appconfig'。
|
|
4
|
+
*
|
|
5
|
+
* 设计理由(同 BSC ensure_core_env):
|
|
6
|
+
* - AppConfig.constructor() 内部读 getCoreEnv(),没设置就抛错
|
|
7
|
+
* - 入口必须 import './appconfig' 触发 setCoreEnv 是隐式约定,容易漏
|
|
8
|
+
* - 此 helper 在 super 前补一刀;即便已设置(入口正常 import)也无副作用(短路返回)
|
|
9
|
+
*
|
|
10
|
+
* 限制:super() 之前不能访问 this,本 helper 只做无 this 的纯函数调用。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { getCoreEnv, setCoreEnv } from '@clonegod/ttd-core'
|
|
14
|
+
import { SolanaDexEnvArgs } from './sol_dex_env_args'
|
|
15
|
+
|
|
16
|
+
let _ensured = false
|
|
17
|
+
|
|
18
|
+
export function ensureCoreEnv(): void {
|
|
19
|
+
if (_ensured) return
|
|
20
|
+
try {
|
|
21
|
+
getCoreEnv() // 已设置则直接通过
|
|
22
|
+
_ensured = true
|
|
23
|
+
} catch {
|
|
24
|
+
// 未设置:兜底创建一个 Solana DEX 子类的 EnvArgs
|
|
25
|
+
setCoreEnv(new SolanaDexEnvArgs())
|
|
26
|
+
_ensured = true
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { DEX_ID, printEnvConfig } from '@clonegod/ttd-core/dist'
|
|
2
|
+
import { SolanaEnvArgs } from './sol_env_args'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Solana DEX 业务包共享的环境变量 cherry-pick 子类(对标 BSC 的 BscDexEnvArgs)。
|
|
6
|
+
* 供 byreal-clmm / meteora-* / orca-clmm / pumpswap-amm / raydium-* / jupiter-aggr 的
|
|
7
|
+
* quote + trade 进程使用。
|
|
8
|
+
*
|
|
9
|
+
* 每个 DEX 进程都是 Solana 链上 quote 或 trade 的执行者,需要的字段高度一致,
|
|
10
|
+
* 统一在这里 cherry-pick,避免每个包各写一份。
|
|
11
|
+
*
|
|
12
|
+
* 字段全部已在 ttd-core 的 CoreEnvArgs registry 注册(pair / group_id / dex_id /
|
|
13
|
+
* rpc_endpoint / quote_pool_* / wallet_dir 等),此处只做赋值(SSOT 在 registry)。
|
|
14
|
+
*
|
|
15
|
+
* 用法:模块 appconfig/index.ts 里 `export const envArgs = new SolanaDexEnvArgs(); envArgs.print('<dex>')`
|
|
16
|
+
*/
|
|
17
|
+
export class SolanaDexEnvArgs extends SolanaEnvArgs {
|
|
18
|
+
constructor() {
|
|
19
|
+
super()
|
|
20
|
+
const cfg = this._cfg
|
|
21
|
+
|
|
22
|
+
// ─── 进程身份(chain_id / app_name / redis_* 等基础设施已在 SolanaEnvArgs 父类 cherry-pick)───
|
|
23
|
+
this.dex_id = (cfg.dex_id || '').toString().toUpperCase() as DEX_ID // DEX_ID
|
|
24
|
+
|
|
25
|
+
// ─── RPC / gRPC 接入 ───
|
|
26
|
+
this.rpc_endpoint = cfg.rpc_endpoint // RPC_ENDPOINT
|
|
27
|
+
this.ws_endpoint = cfg.ws_endpoint // WS_ENDPOINT
|
|
28
|
+
this.grpc_endpoint = cfg.grpc_endpoint // GRPC_ENDPOINT
|
|
29
|
+
this.grpc_token = cfg.grpc_token // GRPC_TOKEN
|
|
30
|
+
|
|
31
|
+
// ─── 进程绑定(quote / trade 共用)───
|
|
32
|
+
this.pair = cfg.pair ?? '' // PAIR
|
|
33
|
+
this.group_id = cfg.group_id ?? '' // GROUP_ID(trade 必填,quote 可空)
|
|
34
|
+
|
|
35
|
+
// ─── Quote 过滤 ───
|
|
36
|
+
this.quote_pool_address = cfg.quote_pool_address // QUOTE_POOL_ADDRESS
|
|
37
|
+
this.quote_pool_name = cfg.quote_pool_name // QUOTE_POOL_NAME
|
|
38
|
+
this.quote_pool_fee_rate = cfg.quote_pool_fee_rate // QUOTE_POOL_FEE_RATE
|
|
39
|
+
this.quote_amount_usd = cfg.quote_amount_usd // QUOTE_AMOUNT_USD
|
|
40
|
+
|
|
41
|
+
// ─── load_wallet / decrypt 依赖(trade 用)───
|
|
42
|
+
this.wallet_dir = cfg.wallet_dir // WALLET_DIR
|
|
43
|
+
this.encryption_key = cfg.encryption_key ?? '' // ENCRYPTION_KEY
|
|
44
|
+
|
|
45
|
+
this.namespace = cfg.namespace ?? '' // NAMESPACE
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 便捷:模块入口调用一次即完成打印(替代 constructor 里 printEnvConfig) */
|
|
49
|
+
print(moduleName?: string) {
|
|
50
|
+
printEnvConfig(moduleName || this.app_name || 'sol-dex', this)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { CHAIN_ID, EnvArgs, registerEnvVars } from '@clonegod/ttd-core'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Solana 链专有环境变量注册中心 + 类型目录(对标 BSC 的 bsc_env_args.ts)。
|
|
5
|
+
*
|
|
6
|
+
* 约定(同 BSC):
|
|
7
|
+
* 1. `registerEnvVars({...})` 是 SSOT —— env 名 / 类型 / 默认值 / 描述都在这里注册到 ttd-core 的 env_registry。
|
|
8
|
+
* 2. `SolanaEnvArgs extends EnvArgs` 只是**类型目录**(字段 TS 声明)+ 空构造器;
|
|
9
|
+
* 具体 `<Module>EnvArgs` 子类在自己的构造器里从 `this._cfg` cherry-pick 赋值。
|
|
10
|
+
* 3. 基础设施字段(app_name / chain_id / redis_* / config_center_host / rpc_endpoint /
|
|
11
|
+
* token_price_refresh_interval_seconds / trade_analyze_host 等)已在 core 的 CoreEnvArgs 注册,**此处不重复**。
|
|
12
|
+
*
|
|
13
|
+
* ⚠️ 本阶段只注册 **market-data** 用到的字段。quote / trade / stream 各自的 env
|
|
14
|
+
* 在对应 phase 接入时补到这里(按使用方分组),不要提前堆。
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// Solana 原生币 = SOL;链上"wrapped SOL" mint(也是 quote/trade 的 native 锚)
|
|
18
|
+
const WSOL_MINT = 'So11111111111111111111111111111111111111112'
|
|
19
|
+
|
|
20
|
+
registerEnvVars({
|
|
21
|
+
// ══════════════════════════════════════════════════════
|
|
22
|
+
// market-data
|
|
23
|
+
// ══════════════════════════════════════════════════════
|
|
24
|
+
gecko_network: { env: 'GECKO_NETWORK', type: 'string', default: 'solana', desc: 'GeckoTerminal 网络标识' },
|
|
25
|
+
chain_id_num: { env: 'CHAIN_ID_NUM', type: 'number', default: 101, desc: 'Solana mainnet 链 ID 数字(SPL token list chainId)' },
|
|
26
|
+
native_token_symbol: { env: 'NATIVE_TOKEN_SYMBOL', type: 'string', default: 'SOL', desc: '原生代币符号' },
|
|
27
|
+
native_token_address: { env: 'NATIVE_TOKEN_ADDRESS', type: 'string', default: WSOL_MINT, desc: '原生代币地址(wrapped SOL mint)' },
|
|
28
|
+
wrapped_native_address: { env: 'WRAPPED_NATIVE_ADDRESS', type: 'string', default: WSOL_MINT, desc: 'Wrapped SOL mint' },
|
|
29
|
+
sync_pool_interval_ms: { env: 'SYNC_POOL_INTERVAL_MS', type: 'number', default: 900000, desc: '定时池子全量同步间隔(毫秒,默认 15 分钟)' },
|
|
30
|
+
fetch_api_wait_ms: { env: 'FETCH_API_WAIT_MS', type: 'number', default: 1000, desc: '各 DEX/Gecko API 调用间隔(毫秒)' },
|
|
31
|
+
fetch_min_tvl: { env: 'FETCH_MIN_TVL', type: 'number', default: 50000, desc: '最小 TVL(USD)' },
|
|
32
|
+
fetch_min_vol: { env: 'FETCH_MIN_VOL', type: 'number', default: 50000, desc: '最小 24h 成交量(USD)' },
|
|
33
|
+
pool_default_tvl: { env: 'POOL_DEFAULT_TVL', type: 'number', default: 100000, desc: '默认 TVL(无 API 数据来源的池子兜底,如 pumpswap)' },
|
|
34
|
+
fetch_max_page_no: { env: 'FETCH_MAX_PAGE_NO', type: 'number', default: 10, desc: '最大翻页数' },
|
|
35
|
+
token_batch_size: { env: 'TOKEN_BATCH_SIZE', type: 'number', default: 10, desc: 'token 批量查询大小' },
|
|
36
|
+
fetch_on_startup: { env: 'FETCH_ON_STARTUP', type: 'boolean', default: true, desc: '启动时立即执行一次全量同步' },
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Solana 链级 EnvArgs 类型目录。
|
|
41
|
+
*
|
|
42
|
+
* 字段声明 = env_registry 注册项的镜像(供 TS 类型 + IDE 提示)。
|
|
43
|
+
* 空构造器:所有字段由各 <Module>EnvArgs 子类按需 cherry-pick 赋值(SSOT 在 registry)。
|
|
44
|
+
* trade_analyze_host / config_center_host / token_price_refresh_interval_seconds 等基础设施字段在 CoreEnvArgs 声明,此处不重复。
|
|
45
|
+
*/
|
|
46
|
+
export class SolanaEnvArgs extends EnvArgs {
|
|
47
|
+
// ─── market-data ───
|
|
48
|
+
gecko_network: string
|
|
49
|
+
chain_id_num: number
|
|
50
|
+
native_token_symbol: string
|
|
51
|
+
native_token_address: string
|
|
52
|
+
wrapped_native_address: string
|
|
53
|
+
sync_pool_interval_ms: number
|
|
54
|
+
fetch_api_wait_ms: number
|
|
55
|
+
fetch_min_tvl: number
|
|
56
|
+
fetch_min_vol: number
|
|
57
|
+
pool_default_tvl: number
|
|
58
|
+
fetch_max_page_no: number
|
|
59
|
+
token_batch_size: number
|
|
60
|
+
fetch_on_startup: boolean
|
|
61
|
+
|
|
62
|
+
constructor() {
|
|
63
|
+
super()
|
|
64
|
+
const cfg = this._cfg
|
|
65
|
+
|
|
66
|
+
// ─── 基础设施 cherry-pick(所有 sol 服务共享;对标 SuiEnvArgs)───
|
|
67
|
+
// 子类(<Module>EnvArgs)继承即得基础设施,无需重复 cherry-pick;模块专有字段(market-data 段)由各子类自行 pick。
|
|
68
|
+
this.app_name = (cfg.app_name as string || '').toLowerCase() // APP_NAME
|
|
69
|
+
this.chain_id = (cfg.chain_id || CHAIN_ID.SOLANA).toString().toUpperCase() as CHAIN_ID // CHAIN_ID
|
|
70
|
+
this.server_id = cfg.server_id ?? '' // SERVER_ID
|
|
71
|
+
this.redis_host = cfg.redis_host // REDIS_HOST
|
|
72
|
+
this.redis_port = String(cfg.redis_port) // REDIS_PORT
|
|
73
|
+
this.server_ip_list = cfg.server_ip_list ?? '' // SERVER_IP_LIST
|
|
74
|
+
this.ip_exclude_prefix = cfg.ip_exclude_prefix ?? '' // IP_EXCLUDE_PREFIX
|
|
75
|
+
this.config_center_host = cfg.config_center_host // CONFIG_CENTER_HOST
|
|
76
|
+
this.token_price_refresh_interval_seconds = cfg.token_price_refresh_interval_seconds // TOKEN_PRICE_REFRESH_INTERVAL_SECONDS
|
|
77
|
+
this.trade_analyze_host = cfg.trade_analyze_host ?? '' // TRADE_ANALYZE_HOST
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -17,7 +17,7 @@ export const get_token_program_id = (token: StandardTokenInfoType) => {
|
|
|
17
17
|
const wallet_token_accounts_cache = new Map<string, string>()
|
|
18
18
|
|
|
19
19
|
function getWalletTokenAccountCacheKey(wallet_pubkey: string, token_address: string): string {
|
|
20
|
-
return `${wallet_pubkey}_${token_address}
|
|
20
|
+
return `${wallet_pubkey}_${token_address}`
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
function getCachedAtaAddress(
|
|
@@ -68,61 +68,55 @@ export function get_wallet_token_account(wallet_pubkey:string, pool_list:Standar
|
|
|
68
68
|
* 创建Token Account
|
|
69
69
|
*/
|
|
70
70
|
export const create_token_account_if_not_exist = async (connection:Connection, owner:Keypair, token_list: StandardTokenInfoType[]) => {
|
|
71
|
-
|
|
71
|
+
// 用 Promise.all + await:trader 启动时必须等账户全部就绪(且错误能浮现)再开始交易,
|
|
72
|
+
// 旧 forEach(async) 是 fire-and-forget,会在账户没建好时就放行下单。
|
|
73
|
+
await Promise.all(token_list.map(async token => {
|
|
72
74
|
if(['SOL','WSOL','USDC','USDT'].includes(token.symbol?.toUpperCase())) {
|
|
73
75
|
return
|
|
74
76
|
}
|
|
75
|
-
|
|
77
|
+
|
|
76
78
|
try {
|
|
77
79
|
const mint = new PublicKey(token.address)
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
+
// 按 token program 派生 ATA(Token-2022 与 SPL 的 ATA 地址不同!)。
|
|
81
|
+
// 旧代码存在性检查用的 getAssociatedTokenAddress 没传 program → 对 Token-2022 算的是 SPL ATA,
|
|
82
|
+
// 与实际创建/存币的 Token-2022 ATA 不是同一地址 → 检查永远 miss。这里统一用 token program 派生。
|
|
83
|
+
const programId = get_token_program_id(token)
|
|
84
|
+
let ata = await getAssociatedTokenAddress(mint, owner.publicKey, false, programId)
|
|
80
85
|
const account_data = await connection.getAccountInfo(ata)
|
|
81
86
|
if(account_data) {
|
|
82
87
|
log_info(`token account already exist, skip!`, {
|
|
83
88
|
owner: owner.publicKey.toBase58(),
|
|
84
89
|
token,
|
|
85
|
-
ata: ata.toBase58()
|
|
90
|
+
ata: ata.toBase58(),
|
|
91
|
+
program: programId.toBase58(),
|
|
86
92
|
});
|
|
87
93
|
} else {
|
|
88
94
|
log_info(`token account not exist, create new one!`, {
|
|
89
95
|
owner: owner.publicKey.toBase58(),
|
|
90
96
|
token,
|
|
97
|
+
program: programId.toBase58(),
|
|
91
98
|
});
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
)
|
|
104
|
-
} else {
|
|
105
|
-
ata = await createAssociatedTokenAccountIdempotent(
|
|
106
|
-
connection,
|
|
107
|
-
owner,
|
|
108
|
-
mint,
|
|
109
|
-
owner.publicKey,
|
|
110
|
-
{
|
|
111
|
-
commitment: COMMITMENT_LEVEL.CONFIRMED,
|
|
112
|
-
},
|
|
113
|
-
TOKEN_PROGRAM_ID,
|
|
114
|
-
ASSOCIATED_TOKEN_PROGRAM_ID
|
|
115
|
-
)
|
|
116
|
-
}
|
|
99
|
+
ata = await createAssociatedTokenAccountIdempotent(
|
|
100
|
+
connection,
|
|
101
|
+
owner,
|
|
102
|
+
mint,
|
|
103
|
+
owner.publicKey,
|
|
104
|
+
{
|
|
105
|
+
commitment: COMMITMENT_LEVEL.CONFIRMED,
|
|
106
|
+
},
|
|
107
|
+
programId,
|
|
108
|
+
ASSOCIATED_TOKEN_PROGRAM_ID
|
|
109
|
+
)
|
|
117
110
|
log_info(`Create New Token Account`, {
|
|
118
111
|
owner: owner.publicKey.toBase58(),
|
|
119
112
|
token,
|
|
120
|
-
ata: ata.toBase58()
|
|
113
|
+
ata: ata.toBase58(),
|
|
114
|
+
program: programId.toBase58(),
|
|
121
115
|
});
|
|
122
116
|
}
|
|
123
117
|
} catch (err) {
|
|
124
118
|
log_error(`create_token_account_if_not_exist error!`, err)
|
|
125
119
|
}
|
|
126
|
-
})
|
|
120
|
+
}))
|
|
127
121
|
}
|
|
128
122
|
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { log_info, log_warn } from '@clonegod/ttd-core/dist'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* gRPC Provider 注册表(消费侧)—— 从 Redis `${chain}:rpc:providers` 解析 type='grpc' 的端点。
|
|
5
|
+
* 镜像 SUI ttd-sui-common/grpc/grpc_provider_registry.ts,跨链一致。
|
|
6
|
+
*
|
|
7
|
+
* 生产端:config-center RPC Provider CRUD(trade-analyze Config/RPC 页)。
|
|
8
|
+
* record JSON:{ id, grpc_endpoint, auth_token?, enabled, type, default_for_quote?, ... }
|
|
9
|
+
* Solana 约定:grpc_endpoint 存 Helius **laserstream URL**(如 https://laserstream-mainnet-tyo.helius-rpc.com),
|
|
10
|
+
* auth_token 存 Helius **API key**。(SUI 那边 grpc_endpoint 是 host:port,链相关差异)
|
|
11
|
+
*
|
|
12
|
+
* 选取规则:enabled && type==='grpc',default_for_quote 优先,其余按 id 字典序取第一个。
|
|
13
|
+
* 无可用 provider 直接 throw(fail-loud)—— laserstream 端点是唯一数据源,不允许静默缺配。
|
|
14
|
+
*
|
|
15
|
+
* 热切换:watchGrpcProviderChange 订阅 `${chain}:rpc:config:change`,解析结果与当前生效值不同时回调
|
|
16
|
+
* (stream 服务策略:log + process.exit 交 pm2 带新配置拉起)。
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export interface GrpcProviderConfig {
|
|
20
|
+
id: string
|
|
21
|
+
endpoint: string
|
|
22
|
+
token: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function getRpcProvidersKey(chainId: string): string {
|
|
26
|
+
return `${chainId.toLowerCase()}:rpc:providers`
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getRpcConfigChangeChannel(chainId: string): string {
|
|
30
|
+
return `${chainId.toLowerCase()}:rpc:config:change`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 只依赖用到的 redis 能力,避免耦合具体 client 类型 */
|
|
34
|
+
interface RedisLike {
|
|
35
|
+
hgetall(key: string): Promise<Record<string, string> | null>
|
|
36
|
+
subscribe(channel: string, listener: (message: string) => void): Promise<void> | void
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function resolveGrpcProvider(redis: RedisLike, chainId: string, preferId?: string): Promise<GrpcProviderConfig> {
|
|
40
|
+
const key = getRpcProvidersKey(chainId)
|
|
41
|
+
const map = (await redis.hgetall(key)) || {}
|
|
42
|
+
const candidates: Array<{ id: string; grpc_endpoint: string; auth_token?: string; default_for_quote?: boolean }> = []
|
|
43
|
+
for (const [id, json] of Object.entries(map)) {
|
|
44
|
+
try {
|
|
45
|
+
const p = JSON.parse(json)
|
|
46
|
+
if (p.enabled === true && p.type === 'grpc' && p.grpc_endpoint) candidates.push({ ...p, id })
|
|
47
|
+
} catch {
|
|
48
|
+
log_warn(`[grpc-registry] ${key} 字段 ${id} JSON 非法,跳过`)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (candidates.length === 0) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`[grpc-registry] ${key} 无 enabled 的 grpc provider —— 请在 trade-analyze Config/RPC 页新增 type=grpc 的 Provider(Solana: grpc_endpoint 填 Helius laserstream URL,auth_token 填 API key)`,
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
if (preferId) {
|
|
57
|
+
const preferred = candidates.find(c => c.id === preferId)
|
|
58
|
+
if (!preferred) {
|
|
59
|
+
throw new Error(`[grpc-registry] 指定的 grpc provider '${preferId}' 不存在或未 enabled(${key} 可用: ${candidates.map(c => c.id).join(',')})`)
|
|
60
|
+
}
|
|
61
|
+
if (!preferred.auth_token) log_warn(`[grpc-registry] provider ${preferred.id} auth_token 为空,按无鉴权端点连接`)
|
|
62
|
+
log_info(`[grpc-registry] 使用指定 provider: ${preferred.id}`)
|
|
63
|
+
return { id: preferred.id, endpoint: preferred.grpc_endpoint, token: preferred.auth_token || '' }
|
|
64
|
+
}
|
|
65
|
+
candidates.sort((a, b) =>
|
|
66
|
+
(b.default_for_quote === true ? 1 : 0) - (a.default_for_quote === true ? 1 : 0)
|
|
67
|
+
|| a.id.localeCompare(b.id))
|
|
68
|
+
const picked = candidates[0]
|
|
69
|
+
if (candidates.length > 1) {
|
|
70
|
+
log_warn(`[grpc-registry] ${candidates.length} 个 grpc provider 可用,选用 ${picked.id}(default_for_quote 优先)`)
|
|
71
|
+
}
|
|
72
|
+
if (!picked.auth_token) {
|
|
73
|
+
log_warn(`[grpc-registry] provider ${picked.id} auth_token 为空,按无鉴权端点连接`)
|
|
74
|
+
}
|
|
75
|
+
return { id: picked.id, endpoint: picked.grpc_endpoint, token: picked.auth_token || '' }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 订阅 provider 变更,端点配置实际变化时回调。解析失败(如最后一个 grpc provider 被禁用)回调收 null。
|
|
80
|
+
*/
|
|
81
|
+
export function watchGrpcProviderChange(
|
|
82
|
+
redis: RedisLike,
|
|
83
|
+
chainId: string,
|
|
84
|
+
active: GrpcProviderConfig,
|
|
85
|
+
onChange: (next: GrpcProviderConfig | null) => void,
|
|
86
|
+
preferId?: string,
|
|
87
|
+
): void {
|
|
88
|
+
const channel = getRpcConfigChangeChannel(chainId)
|
|
89
|
+
redis.subscribe(channel, (message: string) => {
|
|
90
|
+
void (async () => {
|
|
91
|
+
let next: GrpcProviderConfig | null
|
|
92
|
+
try {
|
|
93
|
+
next = await resolveGrpcProvider(redis, chainId, preferId)
|
|
94
|
+
} catch {
|
|
95
|
+
next = null
|
|
96
|
+
}
|
|
97
|
+
const same = next && next.id === active.id && next.endpoint === active.endpoint && next.token === active.token
|
|
98
|
+
if (same) return
|
|
99
|
+
log_info(`[grpc-registry] provider 配置变化: ${message} → ${next ? `${next.id} ${next.endpoint}` : '<无可用>'}`)
|
|
100
|
+
onChange(next)
|
|
101
|
+
})()
|
|
102
|
+
})
|
|
103
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './grpc_provider_registry'
|