diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/constants/PolymarketConstants.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/constants/PolymarketConstants.kt index 4748b58..11d90b2 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/constants/PolymarketConstants.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/constants/PolymarketConstants.kt @@ -9,7 +9,7 @@ object PolymarketConstants { /** * Polymarket CLOB API 基础 URL */ - const val CLOB_BASE_URL = "https://clob-v2.polymarket.com" + const val CLOB_BASE_URL = "https://clob.polymarket.com" /** * Polymarket RTDS WebSocket URL 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 7351006..2068f9d 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 @@ -907,19 +907,41 @@ class BlockchainService( val unlimitedAllowance = BigInteger.valueOf(2).pow(256).minus(BigInteger.ONE) val approveTx = relayClientService.createUsdceApproveForWrapTx(unlimitedAllowance) val wrapTx = relayClientService.createWrapToPusdTx(proxyAddress, wrapAmountWei) - - val safeTx = relayClientService.createMultiSendTx(listOf(approveTx, wrapTx)) - val executeResult = relayClientService.execute(privateKey, proxyAddress, safeTx, walletType) - executeResult.fold( - onSuccess = { txHash -> - logger.info("USDC.e → pUSD wrap 成功: txHash=$txHash") - Result.success(txHash) - }, - onFailure = { e -> - logger.error("USDC.e → pUSD wrap 失败: ${e.message}", e) - Result.failure(e) + if (walletType == WalletType.MAGIC) { + // MAGIC 账户走 PROXY 时,不使用 Safe MultiSend(delegatecall); + // 改为顺序执行两笔 CALL,避免内层 delegatecall 回滚导致“外层成功但业务失败”。 + val approveResult = relayClientService.execute(privateKey, proxyAddress, approveTx, walletType) + val approveHash = approveResult.getOrElse { + logger.error("USDC.e approve 失败: ${it.message}", it) + return Result.failure(it) } - ) + logger.info("USDC.e approve 成功: txHash=$approveHash") + + val wrapResult = relayClientService.execute(privateKey, proxyAddress, wrapTx, walletType) + wrapResult.fold( + onSuccess = { txHash -> + logger.info("USDC.e → pUSD wrap 成功: txHash=$txHash") + Result.success(txHash) + }, + onFailure = { e -> + logger.error("USDC.e → pUSD wrap 失败: ${e.message}", e) + Result.failure(e) + } + ) + } else { + val safeTx = relayClientService.createMultiSendTx(listOf(approveTx, wrapTx)) + val executeResult = relayClientService.execute(privateKey, proxyAddress, safeTx, walletType) + executeResult.fold( + onSuccess = { txHash -> + logger.info("USDC.e → pUSD wrap 成功: txHash=$txHash") + Result.success(txHash) + }, + onFailure = { e -> + logger.error("USDC.e → pUSD wrap 失败: ${e.message}", e) + Result.failure(e) + } + ) + } } catch (e: Exception) { logger.error("USDC.e → pUSD wrap 异常: ${e.message}", e) Result.failure(e) diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/monitor/OnChainWsUtils.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/monitor/OnChainWsUtils.kt index b50b230..8e83d5f 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/monitor/OnChainWsUtils.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/monitor/OnChainWsUtils.kt @@ -45,7 +45,10 @@ object OnChainWsUtils { } // 合约地址 - const val USDC_CONTRACT = "0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB" + const val PUSD_CONTRACT = "0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB" // V2 pUSD + const val USDCE_CONTRACT = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174" // V1 USDC.e(迁移前仍需识别) + const val USDC_CONTRACT = PUSD_CONTRACT // 默认使用 pUSD + private val COLLATERAL_CONTRACTS = setOf(PUSD_CONTRACT.lowercase(), USDCE_CONTRACT.lowercase()) const val ERC1155_CONTRACT = "0x4d97dcd97ec945f40cf65f87097ace5ea0476045" const val ERC20_TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" const val ERC1155_TRANSFER_SINGLE_TOPIC = "0xc3d58168c5ae7397731d063d5bbf3d657854427343f4c083240f7aacaa2d0f62" @@ -96,8 +99,8 @@ object OnChainWsUtils { val t0 = topics[0].lowercase() val data = log.get("data")?.asString ?: "0x" - // USDC ERC20 Transfer - if (address == USDC_CONTRACT.lowercase() && t0 == ERC20_TRANSFER_TOPIC && topics.size >= 3) { + // USDC/pUSD ERC20 Transfer(同时匹配 USDC.e 和 pUSD,V2 迁移后可移除 USDCE) + if (address in COLLATERAL_CONTRACTS && t0 == ERC20_TRANSFER_TOPIC && topics.size >= 3) { val from = topicToAddress(topics[1]) val to = topicToAddress(topics[2]) val value = hexToBigInt(data) @@ -149,6 +152,42 @@ object OnChainWsUtils { return Pair(erc20, erc1155) } + /** + * 通过 CLOB 交易历史获取 Leader 真实成交价(用于无 USDC 转账的 ERC1155-only 场景) + * 查询该 token 最近的成交记录,匹配 side 方向的第一条作为成交价 + * 返回 usdcRaw = price × sizeRaw + */ + private suspend fun fetchEstimatedUsdcRaw( + tokenId: String, + side: String, + sizeRaw: BigInteger, + retrofitFactory: RetrofitFactory + ): BigInteger? { + return try { + val clobApi = retrofitFactory.createClobApiWithoutAuth() + val response = clobApi.getTrades(asset_id = tokenId) + if (!response.isSuccessful || response.body() == null) { + logger.warn("CLOB 交易历史查询失败: tokenId=$tokenId, code=${response.code()}") + return null + } + val trades = response.body()!!.data + val clobSide = side.lowercase() + val matchedTrade = trades.firstOrNull { it.side.equals(clobSide, ignoreCase = true) } + if (matchedTrade == null) { + logger.warn("CLOB 交易历史中未找到匹配成交: tokenId=$tokenId, side=$clobSide, totalTrades=${trades.size}") + return null + } + val price = matchedTrade.price.toBigDecimal() + val usdcRaw = price.multiply(sizeRaw.toBigDecimal()) + .setScale(0, java.math.RoundingMode.DOWN).toBigInteger() + logger.debug("CLOB 交易历史估算: tokenId=$tokenId, side=$clobSide, tradePrice=${matchedTrade.price}, tradeSize=${matchedTrade.size}, sizeRaw=$sizeRaw, usdcRaw=$usdcRaw") + usdcRaw + } catch (e: Exception) { + logger.warn("获取 CLOB 交易历史失败: tokenId=$tokenId, side=$side, error=${e.message}") + null + } + } + /** * 从 Transfer 日志解析交易信息 */ @@ -205,6 +244,32 @@ object OnChainWsUtils { asset = bestOutId sizeRaw = bestOutVal usdcRaw = usdcIn + } else if (bestInId != null && bestInVal > BigInteger.ZERO && bestOutId == null + && usdcOut == BigInteger.ZERO && usdcIn == BigInteger.ZERO + ) { + // BUY(无 USDC): 只收到 ERC1155 token,无 USDC 流动(CLOB 内部结算等场景) + side = "BUY" + asset = bestInId + sizeRaw = bestInVal + usdcRaw = fetchEstimatedUsdcRaw(bestInId.toString(), "BUY", bestInVal, retrofitFactory) + ?: run { + logger.warn("无法获取估算价格(ERC1155-only BUY): txHash=$txHash, tokenId=$bestInId") + return null + } + logger.debug("ERC1155-only BUY: txHash=$txHash, tokenId=$bestInId, sizeRaw=$sizeRaw, usdcRaw=$usdcRaw") + } else if (bestOutId != null && bestOutVal > BigInteger.ZERO && bestInId == null + && usdcOut == BigInteger.ZERO && usdcIn == BigInteger.ZERO + ) { + // SELL(无 USDC): 只发出 ERC1155 token,无 USDC 流动 + side = "SELL" + asset = bestOutId + sizeRaw = bestOutVal + usdcRaw = fetchEstimatedUsdcRaw(bestOutId.toString(), "SELL", bestOutVal, retrofitFactory) + ?: run { + logger.warn("无法获取估算价格(ERC1155-only SELL): txHash=$txHash, tokenId=$bestOutId") + return null + } + logger.debug("ERC1155-only SELL: txHash=$txHash, tokenId=$bestOutId, sizeRaw=$sizeRaw, usdcRaw=$usdcRaw") } else { // 无法判断交易方向 logger.debug("无法判断交易方向: txHash=$txHash, bestInId=$bestInId, bestInVal=$bestInVal, bestOutId=$bestOutId, bestOutVal=$bestOutVal, usdcOut=$usdcOut, usdcIn=$usdcIn") 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 192cd43..ca5f0e9 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 @@ -57,7 +57,9 @@ class RelayClientService( // Polygon PROXY(Magic)合约地址,参考 builder-relayer-client config private val proxyFactoryAddress = "0xaB45c5A4B0c941a2F231C04C3f49182e1A254052" private val relayHubAddress = "0xD216153c06E857cD7f72665E0aF1d7D82172F494" - private val defaultProxyGasLimit = "10000000" + // PROXY relayCall 内层 gasLimit(签名参数)不能给过大值,否则 RelayHub 会因 gasleft 校验失败回滚。 + private val defaultProxyGasLimit = "2400000" + private val maxProxyGasLimit = BigInteger.valueOf(2400000) // Safe MultiSend 合约地址(Polygon 主网) private val safeMultisendAddress = "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761" @@ -530,7 +532,16 @@ class RelayClientService( // 估算 gas limit(参考 builder-relayer-client builder/proxy.ts getGasLimit) val gasLimit = try { - estimateProxyGasLimit(fromAddress, proxyFactoryAddress, proxyCallData) + val estimatedGasLimit = estimateProxyGasLimit(fromAddress, proxyFactoryAddress, proxyCallData) + val estimatedBigInt = BigInteger(estimatedGasLimit) + if (estimatedBigInt > maxProxyGasLimit) { + logger.warn( + "估算 PROXY gas limit 过大,进行截断: estimated=$estimatedGasLimit, capped=$maxProxyGasLimit" + ) + maxProxyGasLimit.toString() + } else { + estimatedGasLimit + } } catch (e: Exception) { logger.warn("估算 PROXY gas limit 失败,使用默认值: ${e.message}", e) defaultProxyGasLimit