@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.
Files changed (91) hide show
  1. package/dist/appconfig/SolanaQuoteAppConfig.d.ts +9 -0
  2. package/dist/appconfig/SolanaQuoteAppConfig.js +29 -0
  3. package/dist/appconfig/SolanaTradeAppConfig.d.ts +13 -0
  4. package/dist/{config → appconfig}/SolanaTradeAppConfig.js +25 -0
  5. package/dist/appconfig/ensure_core_env.d.ts +1 -0
  6. package/dist/appconfig/ensure_core_env.js +18 -0
  7. package/dist/appconfig/index.d.ts +5 -0
  8. package/dist/appconfig/index.js +21 -0
  9. package/dist/appconfig/sol_dex_env_args.d.ts +5 -0
  10. package/dist/appconfig/sol_dex_env_args.js +29 -0
  11. package/dist/appconfig/sol_env_args.d.ts +17 -0
  12. package/dist/appconfig/sol_env_args.js +37 -0
  13. package/dist/common/get_wallet_token_account.js +13 -16
  14. package/dist/grpc/grpc_provider_registry.d.ts +14 -0
  15. package/dist/grpc/grpc_provider_registry.js +70 -0
  16. package/dist/grpc/index.d.ts +1 -0
  17. package/dist/grpc/index.js +17 -0
  18. package/dist/index.d.ts +3 -0
  19. package/dist/index.js +3 -0
  20. package/dist/quote/abstract_dex_quote.d.ts +68 -0
  21. package/dist/quote/abstract_dex_quote.js +208 -0
  22. package/dist/quote/chain_ops.d.ts +18 -0
  23. package/dist/quote/chain_ops.js +66 -0
  24. package/dist/quote/depth/clmm_depth_calculator.d.ts +42 -0
  25. package/dist/quote/depth/clmm_depth_calculator.js +173 -0
  26. package/dist/quote/depth/index.d.ts +20 -0
  27. package/dist/quote/depth/index.js +100 -0
  28. package/dist/quote/index.d.ts +9 -0
  29. package/dist/quote/index.js +9 -0
  30. package/dist/quote/pool_event.d.ts +20 -0
  31. package/dist/quote/pool_event.js +22 -0
  32. package/dist/quote/pool_subscription_registry.d.ts +4 -0
  33. package/dist/quote/pool_subscription_registry.js +62 -0
  34. package/dist/quote/quote_amount.d.ts +4 -0
  35. package/dist/quote/quote_amount.js +24 -0
  36. package/dist/quote/quote_trace.d.ts +16 -0
  37. package/dist/quote/quote_trace.js +40 -0
  38. package/dist/quote/tick/clmm_tick_math.d.ts +5 -0
  39. package/dist/quote/tick/clmm_tick_math.js +61 -0
  40. package/dist/quote/tick/index.d.ts +1 -0
  41. package/dist/{config → quote/tick}/index.js +1 -1
  42. package/dist/quote/verify/index.d.ts +1 -0
  43. package/dist/quote/verify/index.js +17 -0
  44. package/dist/quote/verify/quote_price_verify.d.ts +30 -0
  45. package/dist/quote/verify/quote_price_verify.js +247 -0
  46. package/dist/trade/index.d.ts +0 -1
  47. package/dist/trade/index.js +0 -1
  48. package/dist/trade/tx_builder.d.ts +1 -1
  49. package/dist/trade/tx_result_parse.js +1 -0
  50. package/dist/types/index.d.ts +9 -1
  51. package/dist/types/index.js +2 -0
  52. package/dist/utils/index.d.ts +1 -0
  53. package/dist/utils/index.js +17 -0
  54. package/dist/utils/trade_direction.d.ts +14 -0
  55. package/dist/utils/trade_direction.js +23 -0
  56. package/package.json +4 -4
  57. package/src/appconfig/SolanaQuoteAppConfig.ts +55 -0
  58. package/src/appconfig/SolanaTradeAppConfig.ts +117 -0
  59. package/src/appconfig/ensure_core_env.ts +28 -0
  60. package/src/appconfig/index.ts +5 -0
  61. package/src/appconfig/sol_dex_env_args.ts +52 -0
  62. package/src/appconfig/sol_env_args.ts +79 -0
  63. package/src/common/get_wallet_token_account.ts +27 -33
  64. package/src/grpc/grpc_provider_registry.ts +103 -0
  65. package/src/grpc/index.ts +1 -0
  66. package/src/index.ts +3 -0
  67. package/src/quote/abstract_dex_quote.ts +337 -0
  68. package/src/quote/chain_ops.ts +91 -0
  69. package/src/quote/depth/clmm_depth_calculator.ts +321 -0
  70. package/src/quote/depth/index.ts +167 -0
  71. package/src/quote/index.ts +9 -0
  72. package/src/quote/pool_event.ts +82 -0
  73. package/src/quote/pool_subscription_registry.ts +81 -0
  74. package/src/quote/quote_amount.ts +37 -0
  75. package/src/quote/quote_trace.ts +56 -0
  76. package/src/quote/tick/clmm_tick_math.ts +77 -0
  77. package/src/quote/tick/index.ts +1 -0
  78. package/src/quote/verify/index.ts +1 -0
  79. package/src/quote/verify/quote_price_verify.ts +508 -0
  80. package/src/trade/index.ts +0 -1
  81. package/src/trade/tx_builder.ts +1 -1
  82. package/src/trade/tx_result_parse.ts +1 -0
  83. package/src/types/index.ts +20 -2
  84. package/src/utils/index.ts +1 -0
  85. package/src/utils/trade_direction.ts +68 -0
  86. package/dist/config/SolanaTradeAppConfig.d.ts +0 -10
  87. package/dist/config/index.d.ts +0 -1
  88. package/dist/trade/SolanaTradeAppConfig.d.ts +0 -8
  89. package/dist/trade/SolanaTradeAppConfig.js +0 -26
  90. package/src/config/SolanaTradeAppConfig.ts +0 -70
  91. 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.67",
3
+ "version": "2.0.68",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
- "types": "types/index.d.ts",
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": "2.1.33",
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.2.7"
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,5 @@
1
+ export * from './sol_env_args'
2
+ export * from './sol_dex_env_args'
3
+ export * from './SolanaQuoteAppConfig'
4
+ export * from './SolanaTradeAppConfig'
5
+ export * from './ensure_core_env'
@@ -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
- token_list.forEach(async token => {
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
- // calculate ATA
79
- let ata = await getAssociatedTokenAddress(new PublicKey(mint), owner.publicKey)
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
- if(token.is_token2022) {
93
- ata = await createAssociatedTokenAccountIdempotent(
94
- connection,
95
- owner,
96
- mint,
97
- owner.publicKey,
98
- {
99
- commitment: COMMITMENT_LEVEL.CONFIRMED,
100
- },
101
- TOKEN_2022_PROGRAM_ID,
102
- ASSOCIATED_TOKEN_PROGRAM_ID
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'
package/src/index.ts CHANGED
@@ -1,5 +1,8 @@
1
+ export * from './appconfig'
2
+ export * from './grpc'
1
3
  export * from './types'
2
4
  export * from './common'
3
5
  export * from './quote'
4
6
  export * from './trade'
7
+ export * from './utils'
5
8