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