refactor: 将 Polymarket API URL 配置改为代码常量

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