diff --git a/backend/TEST_ORDER_PUSH.md b/backend/TEST_ORDER_PUSH.md index 2753376..be4edd7 100644 --- a/backend/TEST_ORDER_PUSH.md +++ b/backend/TEST_ORDER_PUSH.md @@ -130,7 +130,7 @@ export PROXY_PORT=8888 - 代理配置错误 **排查步骤**: -1. 检查 `polymarket.rtds.ws-url` 配置是否正确 +1. 检查 Polymarket RTDS WebSocket URL(现在使用代码常量 `PolymarketConstants.RTDS_WS_URL`) 2. 检查网络连接 3. 查看详细错误日志 diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/config/RetrofitConfig.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/config/RetrofitConfig.kt index 514964d..f944c32 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/config/RetrofitConfig.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/config/RetrofitConfig.kt @@ -2,8 +2,8 @@ package com.wrbug.polymarketbot.config import com.google.gson.Gson import com.wrbug.polymarketbot.api.PolymarketClobApi +import com.wrbug.polymarketbot.constants.PolymarketConstants import com.wrbug.polymarketbot.util.createClient -import org.springframework.beans.factory.annotation.Value import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import retrofit2.Retrofit @@ -23,9 +23,6 @@ class RetrofitConfig( private val gson: Gson ) { - @Value("\${polymarket.clob.base-url}") - private lateinit var clobBaseUrl: String - /** * 创建 CLOB API 客户端 * 用于跟单系统的订单操作和交易查询 @@ -38,7 +35,7 @@ class RetrofitConfig( val okHttpClient = createClient().build() return Retrofit.Builder() - .baseUrl(clobBaseUrl) + .baseUrl(PolymarketConstants.CLOB_BASE_URL) .client(okHttpClient) .addConverterFactory(GsonConverterFactory.create(gson)) .build() diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/constants/PolymarketConstants.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/constants/PolymarketConstants.kt new file mode 100644 index 0000000..3df493c --- /dev/null +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/constants/PolymarketConstants.kt @@ -0,0 +1,48 @@ +package com.wrbug.polymarketbot.constants + +/** + * Polymarket API 常量 + * 集中管理所有 Polymarket API 的 URL 配置 + */ +object PolymarketConstants { + + /** + * Polymarket CLOB API 基础 URL + */ + const val CLOB_BASE_URL = "https://clob.polymarket.com" + + /** + * Polymarket RTDS WebSocket URL + * 用于订单推送服务 + */ + const val RTDS_WS_URL = "wss://ws-subscriptions-clob.polymarket.com" + + /** + * Polymarket User Channel WebSocket URL + * 用于跟单服务(订阅 Leader 交易) + */ + const val USER_WS_URL = "wss://ws-live-data.polymarket.com" + + /** + * Polymarket Activity WebSocket URL + * 用于 Activity 全局交易流监听 + */ + const val ACTIVITY_WS_URL = "wss://ws-live-data.polymarket.com" + + /** + * Polymarket Data API 基础 URL + */ + const val DATA_API_BASE_URL = "https://data-api.polymarket.com" + + /** + * Polymarket Gamma API 基础 URL + */ + const val GAMMA_BASE_URL = "https://gamma-api.polymarket.com" + + /** + * Builder Relayer API URL + * 用于 Gasless 交易 + */ + const val BUILDER_RELAYER_URL = "https://relayer-v2.polymarket.com/" +} + diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/common/BlockchainService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/common/BlockchainService.kt index b94750a..e0d669d 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/common/BlockchainService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/common/BlockchainService.kt @@ -7,11 +7,11 @@ import com.wrbug.polymarketbot.api.JsonRpcResponse import com.wrbug.polymarketbot.api.PolymarketDataApi import com.wrbug.polymarketbot.api.PositionResponse import com.wrbug.polymarketbot.api.ValueResponse +import com.wrbug.polymarketbot.constants.PolymarketConstants import com.wrbug.polymarketbot.util.EthereumUtils import com.wrbug.polymarketbot.util.RetrofitFactory import com.wrbug.polymarketbot.util.createClient import org.slf4j.LoggerFactory -import org.springframework.beans.factory.annotation.Value import com.wrbug.polymarketbot.service.system.RelayClientService import com.wrbug.polymarketbot.service.system.RpcNodeService import org.springframework.stereotype.Service @@ -26,8 +26,6 @@ import java.math.BigInteger */ @Service class BlockchainService( - @Value("\${polymarket.data-api.base-url:https://data-api.polymarket.com}") - private val dataApiBaseUrl: String, private val retrofitFactory: RetrofitFactory, private val relayClientService: RelayClientService, private val rpcNodeService: RpcNodeService, @@ -61,10 +59,10 @@ class BlockchainService( private val computeProxyAddressFunctionSignature = "computeProxyAddress(address)" private val dataApi: PolymarketDataApi by lazy { - val baseUrl = if (dataApiBaseUrl.endsWith("/")) { - dataApiBaseUrl.dropLast(1) + val baseUrl = if (PolymarketConstants.DATA_API_BASE_URL.endsWith("/")) { + PolymarketConstants.DATA_API_BASE_URL.dropLast(1) } else { - dataApiBaseUrl + PolymarketConstants.DATA_API_BASE_URL } val okHttpClient = createClient() .followRedirects(true) diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/common/PolymarketApiKeyService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/common/PolymarketApiKeyService.kt index 37ff3ee..44ac94b 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/common/PolymarketApiKeyService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/common/PolymarketApiKeyService.kt @@ -3,12 +3,12 @@ package com.wrbug.polymarketbot.service.common import com.google.gson.Gson import com.wrbug.polymarketbot.api.ApiKeyResponse import com.wrbug.polymarketbot.api.PolymarketClobApi +import com.wrbug.polymarketbot.constants.PolymarketConstants import com.wrbug.polymarketbot.util.PolymarketL1AuthInterceptor import com.wrbug.polymarketbot.util.RetrofitFactory import com.wrbug.polymarketbot.util.createClient import kotlinx.coroutines.runBlocking import org.slf4j.LoggerFactory -import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Service import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory @@ -19,8 +19,6 @@ import retrofit2.converter.gson.GsonConverterFactory */ @Service class PolymarketApiKeyService( - @Value("\${polymarket.clob.base-url}") - private val clobBaseUrl: String, private val gson: Gson ) { @@ -224,7 +222,7 @@ class PolymarketApiKeyService( .build() return Retrofit.Builder() - .baseUrl(clobBaseUrl) + .baseUrl(PolymarketConstants.CLOB_BASE_URL) .client(okHttpClient) .addConverterFactory(GsonConverterFactory.create(gson)) .build() @@ -238,7 +236,7 @@ class PolymarketApiKeyService( val okHttpClient = createClient().build() return Retrofit.Builder() - .baseUrl(clobBaseUrl) + .baseUrl(PolymarketConstants.CLOB_BASE_URL) .client(okHttpClient) .addConverterFactory(GsonConverterFactory.create(gson)) .build() diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/monitor/CopyTradingWebSocketService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/monitor/CopyTradingWebSocketService.kt index 825be9f..23b2974 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/monitor/CopyTradingWebSocketService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/monitor/CopyTradingWebSocketService.kt @@ -9,8 +9,8 @@ import com.wrbug.polymarketbot.repository.CopyTradingTemplateRepository import com.wrbug.polymarketbot.websocket.PolymarketWebSocketClient import jakarta.annotation.PreDestroy import kotlinx.coroutines.* +import com.wrbug.polymarketbot.constants.PolymarketConstants import org.slf4j.LoggerFactory -import org.springframework.beans.factory.annotation.Value import com.wrbug.polymarketbot.service.copytrading.statistics.CopyOrderTrackingService import org.springframework.stereotype.Service import java.util.concurrent.ConcurrentHashMap @@ -28,8 +28,7 @@ class CopyTradingWebSocketService( private val logger = LoggerFactory.getLogger(CopyTradingWebSocketService::class.java) - @Value("\${polymarket.websocket.url:wss://ws-live-data.polymarket.com}") - private var websocketUrl: String = "wss://ws-live-data.polymarket.com" + private val websocketUrl: String = PolymarketConstants.USER_WS_URL private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob()) // 存储每个Leader的WebSocket客户端:leaderId -> WebSocketClient diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/monitor/UnifiedOnChainWsService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/monitor/UnifiedOnChainWsService.kt index 94d45f2..4442018 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/monitor/UnifiedOnChainWsService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/monitor/UnifiedOnChainWsService.kt @@ -129,12 +129,20 @@ class UnifiedOnChainWsService( } addressConnections.clear() } - + + /** + * 获取连接状态 + * @return Map + */ + fun getConnectionStatuses(): Map { + return addressConnections.mapValues { (_, connection) -> connection.isConnected() } + } + @PostConstruct fun init() { logger.info("统一链上 WebSocket 服务已初始化 (独立连接模式)") } - + @PreDestroy fun destroy() { stop() @@ -211,6 +219,10 @@ class UnifiedOnChainWsService( return subscriptions.isEmpty() } + fun isConnected(): Boolean { + return isConnected + } + private suspend fun startConnectionLoop() { while (scope.isActive) { try { diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/orders/OrderPushService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/orders/OrderPushService.kt index de1d03b..6ca9e1c 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/orders/OrderPushService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/orders/OrderPushService.kt @@ -18,6 +18,7 @@ import com.wrbug.polymarketbot.util.CryptoUtils import com.wrbug.polymarketbot.repository.CopyOrderTrackingRepository import com.wrbug.polymarketbot.repository.CopyTradingRepository import com.wrbug.polymarketbot.repository.LeaderRepository +import com.wrbug.polymarketbot.constants.PolymarketConstants import com.wrbug.polymarketbot.service.common.MarketService import org.springframework.stereotype.Service import java.util.concurrent.ConcurrentHashMap @@ -41,8 +42,7 @@ class OrderPushService( private val logger = LoggerFactory.getLogger(OrderPushService::class.java) - @Value("\${polymarket.rtds.ws-url}") - private lateinit var polymarketWsUrl: String + private val polymarketWsUrl: String = PolymarketConstants.RTDS_WS_URL // 存储账户 ID 和对应的 WebSocket 连接 private val accountConnections = ConcurrentHashMap() diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/system/ApiHealthCheckService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/system/ApiHealthCheckService.kt index 094c0bf..83d3389 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/system/ApiHealthCheckService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/system/ApiHealthCheckService.kt @@ -1,5 +1,6 @@ package com.wrbug.polymarketbot.service.system +import com.wrbug.polymarketbot.constants.PolymarketConstants import com.wrbug.polymarketbot.dto.ApiHealthCheckDto import com.wrbug.polymarketbot.dto.ApiHealthCheckResponse import com.wrbug.polymarketbot.util.createClient @@ -11,9 +12,9 @@ import org.slf4j.LoggerFactory import org.springframework.beans.BeansException import org.springframework.context.ApplicationContext import org.springframework.context.ApplicationContextAware -import org.springframework.beans.factory.annotation.Value import com.wrbug.polymarketbot.service.copytrading.orders.OrderPushService -import com.wrbug.polymarketbot.service.copytrading.monitor.CopyTradingWebSocketService +import com.wrbug.polymarketbot.service.copytrading.monitor.PolymarketActivityWsService +import com.wrbug.polymarketbot.service.copytrading.monitor.UnifiedOnChainWsService import org.springframework.stereotype.Service import java.util.concurrent.TimeUnit @@ -22,16 +23,6 @@ import java.util.concurrent.TimeUnit */ @Service class ApiHealthCheckService( - @Value("\${polymarket.clob.base-url}") - private val clobBaseUrl: String, - @Value("\${polymarket.data-api.base-url}") - private val dataApiBaseUrl: String, - @Value("\${polymarket.gamma.base-url}") - private val gammaBaseUrl: String, - @Value("\${polymarket.rtds.ws-url}") - private val polymarketWsUrl: String, - @Value("\${polymarket.builder.relayer-url:}") - private val builderRelayerUrl: String, private val rpcNodeService: RpcNodeService ) : ApplicationContextAware { @@ -52,17 +43,6 @@ class ApiHealthCheckService( } } - /** - * 获取跟单 WebSocket 服务(通过 ApplicationContext 避免循环依赖) - */ - private fun getCopyTradingWebSocketService(): CopyTradingWebSocketService? { - return try { - applicationContext?.getBean(CopyTradingWebSocketService::class.java) - } catch (e: BeansException) { - null - } - } - /** * 获取 RelayClientService(通过 ApplicationContext 避免循环依赖) */ @@ -74,6 +54,28 @@ class ApiHealthCheckService( } } + /** + * 获取 PolymarketActivityWsService(通过 ApplicationContext 避免循环依赖) + */ + private fun getPolymarketActivityWsService(): PolymarketActivityWsService? { + return try { + applicationContext?.getBean(PolymarketActivityWsService::class.java) + } catch (e: BeansException) { + null + } + } + + /** + * 获取 UnifiedOnChainWsService(通过 ApplicationContext 避免循环依赖) + */ + private fun getUnifiedOnChainWsService(): UnifiedOnChainWsService? { + return try { + applicationContext?.getBean(UnifiedOnChainWsService::class.java) + } catch (e: BeansException) { + null + } + } + private val logger = LoggerFactory.getLogger(ApiHealthCheckService::class.java) /** @@ -89,7 +91,9 @@ class ApiHealthCheckService( async { checkDataApi() }, async { checkGammaApi() }, async { checkPolygonRpc() }, - async { checkPolymarketWebSocket() }, + async { checkPolymarketRtdsWebSocket() }, + async { checkPolymarketActivityWebSocket() }, + async { checkUnifiedOnChainWebSocket() }, async { checkBuilderRelayerApi() }, async { checkGitHubApi() } ) @@ -106,7 +110,7 @@ class ApiHealthCheckService( * 检查 Polymarket CLOB API */ private suspend fun checkClobApi(): ApiHealthCheckDto = withContext(Dispatchers.IO) { - val url = "$clobBaseUrl/" + val url = "${PolymarketConstants.CLOB_BASE_URL}/" checkApi("Polymarket CLOB API", url) } @@ -114,7 +118,7 @@ class ApiHealthCheckService( * 检查 Polymarket Data API */ private suspend fun checkDataApi(): ApiHealthCheckDto = withContext(Dispatchers.IO) { - val url = "$dataApiBaseUrl/" + val url = "${PolymarketConstants.DATA_API_BASE_URL}/" checkApi("Polymarket Data API", url) } @@ -131,7 +135,7 @@ class ApiHealthCheckService( .build() // 使用 /markets 接口检查(不传参数,返回空列表或少量市场数据) - val url = "$gammaBaseUrl/markets" + val url = "${PolymarketConstants.GAMMA_BASE_URL}/markets" val request = Request.Builder() .url(url) .get() @@ -176,7 +180,7 @@ class ApiHealthCheckService( logger.warn("检查 Polymarket Gamma API 失败", e) ApiHealthCheckDto( name = "Polymarket Gamma API", - url = "$gammaBaseUrl/markets", + url = "${PolymarketConstants.GAMMA_BASE_URL}/markets", status = "error", message = e.message ?: "连接失败" ) @@ -194,69 +198,143 @@ class ApiHealthCheckService( } /** - * 检查 Polymarket WebSocket 连接状态 - * 不显示延时,只显示连接状态 + * 检查 Polymarket RTDS WebSocket 连接状态 + * 用于订单推送服务 */ - private suspend fun checkPolymarketWebSocket(): ApiHealthCheckDto = withContext(Dispatchers.Default) { + private suspend fun checkPolymarketRtdsWebSocket(): ApiHealthCheckDto = withContext(Dispatchers.Default) { try { - // 检查订单推送服务的连接状态 val orderPushService = getOrderPushService() - val orderPushStatuses = orderPushService?.getConnectionStatuses() ?: emptyMap() - val orderPushConnected = orderPushStatuses.values.any { it } - val orderPushTotal = orderPushStatuses.size - val orderPushConnectedCount = orderPushStatuses.values.count { it } + val statuses = orderPushService?.getConnectionStatuses() ?: emptyMap() + val total = statuses.size + val connected = statuses.values.count { it } - // 检查跟单 WebSocket 服务的连接状态 - val copyTradingWebSocketService = getCopyTradingWebSocketService() - val copyTradingStatuses = copyTradingWebSocketService?.getConnectionStatuses() ?: emptyMap() - val copyTradingConnected = copyTradingStatuses.values.any { it } - val copyTradingTotal = copyTradingStatuses.size - val copyTradingConnectedCount = copyTradingStatuses.values.count { it } - - // 计算总体状态 - val totalConnections = orderPushTotal + copyTradingTotal - val connectedConnections = orderPushConnectedCount + copyTradingConnectedCount - - val url = polymarketWsUrl - val hasAnyConnection = orderPushConnected || copyTradingConnected - - if (totalConnections == 0) { - // 没有配置任何 WebSocket 连接 + if (total == 0) { ApiHealthCheckDto( - name = "Polymarket WebSocket", - url = url, + name = "Polymarket RTDS WebSocket", + url = PolymarketConstants.RTDS_WS_URL, status = "skipped", - message = "未配置 WebSocket 连接" + message = "未配置账户连接" ) - } else if (hasAnyConnection) { - // 至少有一个连接是活跃的 - val message = if (connectedConnections == totalConnections) { - "所有连接正常 ($connectedConnections/$totalConnections)" + } else if (connected > 0) { + val message = if (connected == total) { + "所有账户连接正常 ($connected/$total)" } else { - "部分连接正常 ($connectedConnections/$totalConnections)" + "部分账户连接正常 ($connected/$total)" } ApiHealthCheckDto( - name = "Polymarket WebSocket", - url = url, + name = "Polymarket RTDS WebSocket", + url = PolymarketConstants.RTDS_WS_URL, status = "success", message = message - // 不设置 responseTime,WebSocket 不显示延时 ) } else { - // 所有连接都断开 ApiHealthCheckDto( - name = "Polymarket WebSocket", - url = url, + name = "Polymarket RTDS WebSocket", + url = PolymarketConstants.RTDS_WS_URL, status = "error", - message = "所有连接断开 ($connectedConnections/$totalConnections)" - // 不设置 responseTime,WebSocket 不显示延时 + message = "所有账户连接断开 (0/$total)" ) } } catch (e: Exception) { - logger.warn("检查 Polymarket WebSocket 状态失败", e) + logger.warn("检查 Polymarket RTDS WebSocket 状态失败", e) ApiHealthCheckDto( - name = "Polymarket WebSocket", - url = polymarketWsUrl, + name = "Polymarket RTDS WebSocket", + url = PolymarketConstants.RTDS_WS_URL, + status = "error", + message = "检查失败:${e.message}" + ) + } + } + + /** + * 检查 Polymarket Activity WebSocket 连接状态 + * 用于 Activity 全局交易流监听 + */ + private suspend fun checkPolymarketActivityWebSocket(): ApiHealthCheckDto = withContext(Dispatchers.Default) { + try { + val activityWsService = getPolymarketActivityWsService() + val isConnected = activityWsService?.isConnected() ?: false + + if (isConnected) { + ApiHealthCheckDto( + name = "Polymarket Activity WebSocket", + url = PolymarketConstants.ACTIVITY_WS_URL, + status = "success", + message = "连接正常" + ) + } else { + ApiHealthCheckDto( + name = "Polymarket Activity WebSocket", + url = PolymarketConstants.ACTIVITY_WS_URL, + status = "error", + message = "连接断开" + ) + } + } catch (e: Exception) { + logger.warn("检查 Polymarket Activity WebSocket 状态失败", e) + ApiHealthCheckDto( + name = "Polymarket Activity WebSocket", + url = PolymarketConstants.ACTIVITY_WS_URL, + status = "error", + message = "检查失败:${e.message}" + ) + } + } + + /** + * 检查统一链上 WebSocket 连接状态 + * 用于监听链上事件 + */ + private suspend fun checkUnifiedOnChainWebSocket(): ApiHealthCheckDto = withContext(Dispatchers.Default) { + try { + val unifiedOnChainWsService = getUnifiedOnChainWsService() + + if (unifiedOnChainWsService == null) { + return@withContext ApiHealthCheckDto( + name = "链上 WebSocket", + url = rpcNodeService.getWsUrl(), + status = "error", + message = "服务未初始化" + ) + } + + // 检查连接状态 + val statuses = unifiedOnChainWsService.getConnectionStatuses() + val total = statuses.size + val connected = statuses.values.count { it } + + if (total == 0) { + ApiHealthCheckDto( + name = "链上 WebSocket", + url = rpcNodeService.getWsUrl(), + status = "skipped", + message = "未配置地址监听" + ) + } else if (connected > 0) { + val message = if (connected == total) { + "所有地址连接正常 ($connected/$total)" + } else { + "部分地址连接正常 ($connected/$total)" + } + ApiHealthCheckDto( + name = "链上 WebSocket", + url = rpcNodeService.getWsUrl(), + status = "success", + message = message + ) + } else { + ApiHealthCheckDto( + name = "链上 WebSocket", + url = rpcNodeService.getWsUrl(), + status = "error", + message = "所有地址连接断开 (0/$total)" + ) + } + } catch (e: Exception) { + logger.warn("检查链上 WebSocket 状态失败", e) + ApiHealthCheckDto( + name = "链上 WebSocket", + url = rpcNodeService.getWsUrl(), status = "error", message = "检查失败:${e.message}" ) @@ -390,19 +468,10 @@ class ApiHealthCheckService( private suspend fun checkBuilderRelayerApi(): ApiHealthCheckDto = withContext(Dispatchers.IO) { val relayClientService = getRelayClientService() - if (builderRelayerUrl.isBlank()) { - return@withContext ApiHealthCheckDto( - name = "Builder Relayer API", - url = "未配置", - status = "skipped", - message = "未配置 Builder Relayer URL" - ) - } - if (relayClientService == null) { return@withContext ApiHealthCheckDto( name = "Builder Relayer API", - url = builderRelayerUrl, + url = PolymarketConstants.BUILDER_RELAYER_URL, status = "error", message = "服务未初始化" ) @@ -411,7 +480,7 @@ class ApiHealthCheckService( if (!relayClientService.isBuilderApiKeyConfigured()) { return@withContext ApiHealthCheckDto( name = "Builder Relayer API", - url = builderRelayerUrl, + url = PolymarketConstants.BUILDER_RELAYER_URL, status = "skipped", message = "Builder API Key 未配置" ) @@ -423,7 +492,7 @@ class ApiHealthCheckService( onSuccess = { responseTime -> ApiHealthCheckDto( name = "Builder Relayer API", - url = builderRelayerUrl, + url = PolymarketConstants.BUILDER_RELAYER_URL, status = "success", message = "连接成功", responseTime = responseTime @@ -432,7 +501,7 @@ class ApiHealthCheckService( onFailure = { e -> ApiHealthCheckDto( name = "Builder Relayer API", - url = builderRelayerUrl, + url = PolymarketConstants.BUILDER_RELAYER_URL, status = "error", message = e.message ?: "连接失败" ) @@ -442,7 +511,7 @@ class ApiHealthCheckService( logger.warn("检查 Builder Relayer API 失败", e) ApiHealthCheckDto( name = "Builder Relayer API", - url = builderRelayerUrl, + url = PolymarketConstants.BUILDER_RELAYER_URL, status = "error", message = e.message ?: "连接失败" ) diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/system/RelayClientService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/system/RelayClientService.kt index 898a9cd..698533d 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/system/RelayClientService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/system/RelayClientService.kt @@ -3,11 +3,11 @@ package com.wrbug.polymarketbot.service.system import com.wrbug.polymarketbot.api.BuilderRelayerApi import com.wrbug.polymarketbot.api.EthereumRpcApi import com.wrbug.polymarketbot.api.JsonRpcRequest +import com.wrbug.polymarketbot.constants.PolymarketConstants import com.wrbug.polymarketbot.util.EthereumUtils import com.wrbug.polymarketbot.util.RetrofitFactory import com.wrbug.polymarketbot.util.createClient import org.slf4j.LoggerFactory -import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Service import java.math.BigInteger @@ -24,8 +24,6 @@ import java.math.BigInteger */ @Service class RelayClientService( - @Value("\${polymarket.builder.relayer-url:}") - private val builderRelayerUrl: String, private val retrofitFactory: RetrofitFactory, private val systemConfigService: SystemConfigService, private val rpcNodeService: RpcNodeService @@ -54,10 +52,10 @@ class RelayClientService( val builderApiKey = systemConfigService.getBuilderApiKey() val builderSecret = systemConfigService.getBuilderSecret() val builderPassphrase = systemConfigService.getBuilderPassphrase() - + if (isBuilderRelayerEnabled(builderApiKey, builderSecret, builderPassphrase)) { return retrofitFactory.createBuilderRelayerApi( - relayerUrl = builderRelayerUrl, + relayerUrl = PolymarketConstants.BUILDER_RELAYER_URL, apiKey = builderApiKey!!, secret = builderSecret!!, passphrase = builderPassphrase!! @@ -74,19 +72,19 @@ class RelayClientService( builderSecret: String?, builderPassphrase: String? ): Boolean { - return builderRelayerUrl.isNotBlank() && + return PolymarketConstants.BUILDER_RELAYER_URL.isNotBlank() && builderApiKey != null && builderApiKey.isNotBlank() && builderSecret != null && builderSecret.isNotBlank() && builderPassphrase != null && builderPassphrase.isNotBlank() } - + /** * 检查 Builder API Key 是否已配置 */ fun isBuilderApiKeyConfigured(): Boolean { return systemConfigService.isBuilderApiKeyConfigured() } - + /** * 检查 Builder Relayer API 健康状态(用于 API 健康检查) */ @@ -95,24 +93,24 @@ class RelayClientService( val builderApiKey = systemConfigService.getBuilderApiKey() val builderSecret = systemConfigService.getBuilderSecret() val builderPassphrase = systemConfigService.getBuilderPassphrase() - + if (builderApiKey == null || builderSecret == null || builderPassphrase == null) { return Result.failure(IllegalStateException("Builder API Key 未配置")) } - + val relayerApi = retrofitFactory.createBuilderRelayerApi( - relayerUrl = builderRelayerUrl, + relayerUrl = PolymarketConstants.BUILDER_RELAYER_URL, apiKey = builderApiKey, secret = builderSecret, passphrase = builderPassphrase ) - + // 使用一个测试地址来检查 API 是否可用(使用一个已知的地址,如零地址) val testAddress = "0x0000000000000000000000000000000000000000" val startTime = System.currentTimeMillis() val response = relayerApi.getDeployed(testAddress) val responseTime = System.currentTimeMillis() - startTime - + if (response.isSuccessful) { Result.success(responseTime) } else { @@ -228,11 +226,18 @@ class RelayClientService( val builderApiKey = systemConfigService.getBuilderApiKey() val builderSecret = systemConfigService.getBuilderSecret() val builderPassphrase = systemConfigService.getBuilderPassphrase() - + // 优先使用 Builder Relayer(Gasless) if (isBuilderRelayerEnabled(builderApiKey, builderSecret, builderPassphrase)) { logger.info("使用 Builder Relayer 执行 Gasless 交易") - return executeViaBuilderRelayer(privateKey, proxyAddress, safeTx, builderApiKey!!, builderSecret!!, builderPassphrase!!) + return executeViaBuilderRelayer( + privateKey, + proxyAddress, + safeTx, + builderApiKey!!, + builderSecret!!, + builderPassphrase!! + ) } // 回退到手动发送交易(需要用户支付 gas) @@ -256,9 +261,8 @@ class RelayClientService( builderSecret: String, builderPassphrase: String ): Result { - val rpcApi = polygonRpcApi val relayerApi = retrofitFactory.createBuilderRelayerApi( - relayerUrl = builderRelayerUrl, + relayerUrl = PolymarketConstants.BUILDER_RELAYER_URL, apiKey = builderApiKey, secret = builderSecret, passphrase = builderPassphrase @@ -320,19 +324,19 @@ class RelayClientService( val messageWithPrefix = ByteArray(prefix.size + safeTxStructuredHash.size) System.arraycopy(prefix, 0, messageWithPrefix, 0, prefix.size) System.arraycopy(safeTxStructuredHash, 0, messageWithPrefix, prefix.size, safeTxStructuredHash.size) - + // 对带前缀的消息进行 keccak256 哈希 val keccak256 = org.bouncycastle.crypto.digests.KeccakDigest(256) keccak256.update(messageWithPrefix, 0, messageWithPrefix.size) val hashWithPrefix = ByteArray(keccak256.digestSize) keccak256.doFinal(hashWithPrefix, 0) - + val ecKeyPair = org.web3j.crypto.ECKeyPair.create(privateKeyBigInt) val safeSignature = org.web3j.crypto.Sign.signMessage(hashWithPrefix, ecKeyPair, false) // 打包签名(参考 builder-relayer-client/src/utils/index.ts 的 splitAndPackSig) val packedSignature = splitAndPackSig(safeSignature) - + // 调试日志(地址已遮蔽) logger.debug("=== Builder Relayer 签名调试 ===") logger.debug("Safe: ${proxyAddress.take(10)}..., From: ${fromAddress.take(10)}..., Nonce: $proxyNonce") @@ -358,7 +362,7 @@ class RelayClientService( ), metadata = "Redeem positions via Builder Relayer" ) - + logger.debug("Request: type=${request.type}, dataLen=${request.data.length}, sigLen=${request.signature.length}, nonce=${request.nonce}") // 调用 Builder Relayer API(认证头通过拦截器添加) @@ -372,7 +376,7 @@ class RelayClientService( val relayerResponse = response.body()!! val txHash = relayerResponse.transactionHash ?: relayerResponse.hash - ?: return Result.failure(Exception("Builder Relayer 返回的交易哈希为空")) + ?: return Result.failure(Exception("Builder Relayer 返回的交易哈希为空")) logger.info("Builder Relayer 执行成功: transactionID=${relayerResponse.transactionID}, txHash=$txHash") return Result.success(txHash) @@ -381,14 +385,14 @@ class RelayClientService( /** * 打包签名(参考 builder-relayer-client/src/utils/index.ts 的 splitAndPackSig) * 将签名打包成 Gnosis Safe 接受的格式:encodePacked(["uint256", "uint256", "uint8"], [r, s, v]) - * + * * TypeScript 实现流程: * 1. 从签名字符串中提取 v(最后 2 个字符) * 2. 调整 v 值(0,1 -> +31; 27,28 -> +4) * 3. 修改签名字符串(替换最后 2 个字符) * 4. 从修改后的签名字符串中提取 r, s, v(作为十进制字符串) * 5. 使用 encodePacked 打包:uint256(BigInt(r)) + uint256(BigInt(s)) + uint8(parseInt(v)) - * + * * 关键:encodePacked 会将 BigInt 编码为 32 字节(64 个十六进制字符),uint8 编码为 1 字节(2 个十六进制字符) */ private fun splitAndPackSig(signature: org.web3j.crypto.Sign.SignatureData): String { @@ -403,37 +407,37 @@ class RelayClientService( } val originalVHex = String.format("%02x", originalV) val sigString = "0x$rHex$sHex$originalVHex" // 130 个十六进制字符(65 字节) - + // 2. 从签名字符串中提取 v(最后 2 个字符,作为十六进制) val sigV = sigString.substring(sigString.length - 2).toInt(16) - + // 3. 调整 v 值(参考 TypeScript 实现) val adjustedV = when (sigV) { 0, 1 -> sigV + 31 27, 28 -> sigV + 4 else -> throw IllegalArgumentException("Invalid signature v value: $sigV") } - + // 4. 修改签名字符串(替换最后 2 个字符) val modifiedSigString = sigString.substring(0, sigString.length - 2) + String.format("%02x", adjustedV) - + // 5. 从修改后的签名字符串中提取 r, s, v(作为十六进制字符串) // modifiedSigString 格式:0x + r(64) + s(64) + v(2) = 132 个字符 val rHexStr = modifiedSigString.substring(2, 66) // 64 个字符(十六进制) val sHexStr = modifiedSigString.substring(66, 130) // 64 个字符(十六进制) val vHexStr = modifiedSigString.substring(130, 132) // 2 个字符(十六进制) - + // 6. 转换为 BigInteger 和 Int(模拟 TypeScript 的 BigInt 和 parseInt) val rBigInt = BigInteger(rHexStr, 16) val sBigInt = BigInteger(sHexStr, 16) val vInt = vHexStr.toInt(16) - + // 7. 使用 encodePacked 打包:uint256(r) + uint256(s) + uint8(v) // encodePacked 会将 BigInt 编码为 32 字节(64 个十六进制字符),uint8 编码为 1 字节(2 个十六进制字符) val rEncoded = EthereumUtils.encodeUint256(rBigInt) // 64 个十六进制字符 val sEncoded = EthereumUtils.encodeUint256(sBigInt) // 64 个十六进制字符 val vEncoded = String.format("%02x", vInt) // 2 个十六进制字符 - + return "0x$rEncoded$sEncoded$vEncoded" } @@ -504,13 +508,13 @@ class RelayClientService( val messageWithPrefix = ByteArray(prefix.size + safeTxStructuredHash.size) System.arraycopy(prefix, 0, messageWithPrefix, 0, prefix.size) System.arraycopy(safeTxStructuredHash, 0, messageWithPrefix, prefix.size, safeTxStructuredHash.size) - + // 对带前缀的消息进行 keccak256 哈希 val keccak256 = org.bouncycastle.crypto.digests.KeccakDigest(256) keccak256.update(messageWithPrefix, 0, messageWithPrefix.size) val hashWithPrefix = ByteArray(keccak256.digestSize) keccak256.doFinal(hashWithPrefix, 0) - + val ecKeyPair = org.web3j.crypto.ECKeyPair.create(privateKeyBigInt) val safeSignature = org.web3j.crypto.Sign.signMessage(hashWithPrefix, ecKeyPair, false) @@ -572,7 +576,8 @@ class RelayClientService( redeemCallData: String, safeSignatureHex: String ): String { - val execFunctionSelector = EthereumUtils.getFunctionSelector("execTransaction(address,uint256,bytes,uint8,uint256,uint256,uint256,address,address,bytes)") + val execFunctionSelector = + EthereumUtils.getFunctionSelector("execTransaction(address,uint256,bytes,uint8,uint256,uint256,uint256,address,address,bytes)") val encodedTo = EthereumUtils.encodeAddress(safeTx.to) val encodedValue = EthereumUtils.encodeUint256(BigInteger.ZERO) @@ -600,20 +605,20 @@ class RelayClientService( val encodedSignatures = safeSignatureHex return "0x" + execFunctionSelector.removePrefix("0x") + - encodedTo + - encodedValue + - encodedDataOffset + - encodedDataLength + - encodedData + - encodedOperation + - encodedSafeTxGas + - encodedBaseGas + - encodedGasPrice + - encodedGasToken + - encodedRefundReceiver + - encodedSignaturesOffset + - encodedSignaturesLength + - encodedSignatures + encodedTo + + encodedValue + + encodedDataOffset + + encodedDataLength + + encodedData + + encodedOperation + + encodedSafeTxGas + + encodedBaseGas + + encodedGasPrice + + encodedGasToken + + encodedRefundReceiver + + encodedSignaturesOffset + + encodedSignaturesLength + + encodedSignatures } /** diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/util/RetrofitFactory.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/util/RetrofitFactory.kt index f7d5041..2adb5b2 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/util/RetrofitFactory.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/util/RetrofitFactory.kt @@ -7,6 +7,7 @@ import com.wrbug.polymarketbot.api.GitHubApi import com.wrbug.polymarketbot.api.PolymarketClobApi import com.wrbug.polymarketbot.api.PolymarketDataApi import com.wrbug.polymarketbot.api.PolymarketGammaApi +import com.wrbug.polymarketbot.constants.PolymarketConstants import okhttp3.HttpUrl import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.Interceptor @@ -18,7 +19,6 @@ import okhttp3.Response import okio.Buffer import java.util.concurrent.TimeUnit import org.slf4j.LoggerFactory -import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Component import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory @@ -34,10 +34,6 @@ import jakarta.annotation.PreDestroy */ @Component class RetrofitFactory( - @Value("\${polymarket.clob.base-url}") - private val clobBaseUrl: String, - @Value("\${polymarket.gamma.base-url}") - private val gammaBaseUrl: String, private val gson: Gson ) { @@ -58,10 +54,10 @@ class RetrofitFactory( // 缓存 Gamma API 客户端(单例) private val gammaApi: PolymarketGammaApi by lazy { - val baseUrl = if (gammaBaseUrl.endsWith("/")) { - gammaBaseUrl.dropLast(1) + val baseUrl = if (PolymarketConstants.GAMMA_BASE_URL.endsWith("/")) { + PolymarketConstants.GAMMA_BASE_URL.dropLast(1) } else { - gammaBaseUrl + PolymarketConstants.GAMMA_BASE_URL } Retrofit.Builder() @@ -74,7 +70,7 @@ class RetrofitFactory( // 缓存 Data API 客户端(单例) private val dataApi: PolymarketDataApi by lazy { - val baseUrl = "https://data-api.polymarket.com" + val baseUrl = PolymarketConstants.DATA_API_BASE_URL Retrofit.Builder() .baseUrl("$baseUrl/") @@ -113,7 +109,7 @@ class RetrofitFactory( // 缓存不带认证的 CLOB API 客户端(单例) private val clobApiWithoutAuth: PolymarketClobApi by lazy { Retrofit.Builder() - .baseUrl(clobBaseUrl) + .baseUrl(PolymarketConstants.CLOB_BASE_URL) .client(sharedOkHttpClient) .addConverterFactory(GsonConverterFactory.create(gson)) .build() @@ -158,7 +154,7 @@ class RetrofitFactory( .build() Retrofit.Builder() - .baseUrl(clobBaseUrl) + .baseUrl(PolymarketConstants.CLOB_BASE_URL) .client(okHttpClient) .addConverterFactory(GsonConverterFactory.create(gson)) .build() diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/websocket/PolymarketWebSocketHandler.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/websocket/PolymarketWebSocketHandler.kt index 48284f3..a0522e2 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/websocket/PolymarketWebSocketHandler.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/websocket/PolymarketWebSocketHandler.kt @@ -1,7 +1,7 @@ package com.wrbug.polymarketbot.websocket +import com.wrbug.polymarketbot.constants.PolymarketConstants import org.slf4j.LoggerFactory -import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Component import org.springframework.web.socket.* import java.util.concurrent.ConcurrentHashMap @@ -15,8 +15,7 @@ class PolymarketWebSocketHandler : WebSocketHandler { private val logger = LoggerFactory.getLogger(PolymarketWebSocketHandler::class.java) - @Value("\${polymarket.rtds.ws-url}") - private lateinit var polymarketWsUrl: String + private val polymarketWsUrl: String = PolymarketConstants.RTDS_WS_URL // 存储客户端会话和对应的 Polymarket 连接的映射 private val clientSessions = ConcurrentHashMap() diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 7d012a0..891555d 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -35,18 +35,14 @@ logging.level.com.wrbug.polymarketbot=${LOG_LEVEL_APP:INFO} logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} - %msg%n # Polymarket API 配置 -polymarket.clob.base-url=https://clob.polymarket.com -polymarket.rtds.ws-url=wss://ws-subscriptions-clob.polymarket.com -polymarket.websocket.url=wss://ws-live-data.polymarket.com -polymarket.websocket.activity.url=${POLYMARKET_WEBSOCKET_ACTIVITY_URL:wss://ws-live-data.polymarket.com} -polymarket.data-api.base-url=https://data-api.polymarket.com -polymarket.gamma.base-url=https://gamma-api.polymarket.com +# 注意:Polymarket API URL 现在使用代码常量(PolymarketConstants),不再从配置文件读取 +# 如需修改,请修改 com.wrbug.polymarketbot.constants.PolymarketConstants 类 # Builder Relayer 配置(用于 Gasless 交易) # 从 polymarket.com/settings?tab=builder 获取 Builder API 凭证 # Builder API Key、Secret、Passphrase 现在通过系统设置页面配置,存储在数据库中 # 如果未配置,将使用手动发送交易的方式(需要用户支付 gas) -polymarket.builder.relayer-url=${POLYMARKET_BUILDER_RELAYER_URL:https://relayer-v2.polymarket.com/} +# 注意:Builder Relayer URL 现在使用代码常量(PolymarketConstants.BUILDER_RELAYER_URL),不再从配置文件读取 # 跟单轮询配置 # 轮询间隔(毫秒),默认2秒 diff --git a/docs/zh/copy-trading-dual-monitoring-plan.md b/docs/zh/copy-trading-dual-monitoring-plan.md index 0e9b9f1..9e0d0b1 100644 --- a/docs/zh/copy-trading-dual-monitoring-plan.md +++ b/docs/zh/copy-trading-dual-monitoring-plan.md @@ -399,8 +399,8 @@ CopyOrderTrackingService.processTrade( ### 5.1 application.properties ```properties -# Polymarket WebSocket -polymarket.websocket.url=wss://ws-live-data.polymarket.com +# 注意:Polymarket API URL 现在使用代码常量(PolymarketConstants),不再从配置文件读取 +# 如需修改,请修改 com.wrbug.polymarketbot.constants.PolymarketConstants 类 # 监听策略 copy.trading.monitor.strategy=dual