fix(copytrading): 修复跟单 invalid signature 与 orderbook 不存在

- tokenId: Activity 解析时写入 trade.tokenId=asset,链上解析时写入 tokenId,优先使用避免与 CLOB 不一致
- Neg Risk: 从 Gamma 读取 negRisk,按市场选择 CTF Exchange / Neg Risk Exchange 签约
- 签名: OrderSigningService 支持 exchangeContract 参数,Credentials 路径与 v 提取与 EIP-712 一致
- 校验: 创建订单前校验 signer 与 account.walletAddress 一致
- TradeResponse/EventResponse/MarketResponse 增加 tokenId、negRisk 等字段
- MarketService 增加 getMarketInfoByTokenId、getNegRiskByConditionId

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
WrBug
2026-02-15 00:47:07 +08:00
co-authored by Cursor
parent a136d68e8c
commit 63ce4107c6
7 changed files with 200 additions and 73 deletions
@@ -335,7 +335,8 @@ data class TradeResponse(
val timestamp: String, // ISO 8601 格式字符串或时间戳
val user: String?,
val outcomeIndex: Int? = null, // 结果索引(0=YES, 1=NO
val outcome: String? = null // 结果名称(如 "Up", "Down"
val outcome: String? = null, // 结果名称(如 "Up", "Down"
val tokenId: String? = null // CLOB tokenId(链上解析时从 ERC1155 取得,与 Gamma clobTokenIds 一致,用于下单)
)
/**
@@ -62,19 +62,21 @@ data class GammaEventMarketItem(
/**
* 事件响应(从 MarketResponse.events 解析)
* Gamma API Event 含 negRisk,用于判断是否使用 Neg Risk Exchange 签约
*/
data class EventResponse(
val id: String? = null,
val ticker: String? = null,
val slug: String,
val title: String,
val slug: String? = null,
val title: String? = null,
val category: String? = null,
val active: Boolean? = null,
val closed: Boolean? = null,
val archived: Boolean? = null,
val startDate: String? = null,
val endDate: String? = null,
val createdAt: String? = null
val createdAt: String? = null,
val negRisk: Boolean? = null
)
/**
@@ -106,6 +108,8 @@ data class MarketResponse(
val events: List<EventResponse>? = null, // 事件列表(从 events[0] 获取 slug
// 以下字段可能存在于响应中,但不在标准文档中
val clobTokenIds: String? = null, // CLOB token IDs(可能是 JSON 字符串或数组)
val clob_token_ids: String? = null // 下划线格式(兼容不同 API 版本)
val clob_token_ids: String? = null, // 下划线格式(兼容不同 API 版本)
val negRisk: Boolean? = null, // 事件级 neg risk(部分 API 直接返回在 market
val negRiskOther: Boolean? = null // Market 级 neg risk 标记
)
@@ -8,6 +8,7 @@ import com.wrbug.polymarketbot.entity.Market
import com.wrbug.polymarketbot.repository.MarketRepository
import com.wrbug.polymarketbot.util.RetrofitFactory
import com.wrbug.polymarketbot.util.getEventSlug
import com.wrbug.polymarketbot.util.parseStringArray
import kotlinx.coroutines.runBlocking
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
@@ -207,6 +208,36 @@ class MarketService(
}
}
/**
* 按 tokenId 从 Gamma 解析市场信息(conditionId、outcomeIndex
* 用于链上解析时 Gamma 失败、仅带 tokenId 的交易在 processBuyTrade 中补查市场
*/
suspend fun getMarketInfoByTokenId(tokenId: String): MarketInfoByTokenId? {
if (tokenId.isBlank()) return null
return try {
val gammaApi = retrofitFactory.createGammaApi()
val response = gammaApi.listMarkets(
conditionIds = null,
clobTokenIds = listOf(tokenId),
includeTag = null
)
if (!response.isSuccessful || response.body().isNullOrEmpty()) return null
val market = response.body()!!.first()
val conditionId = market.conditionId ?: return null
val clobTokenIdsRaw = market.clobTokenIds ?: market.clob_token_ids
val clobTokenIds = (clobTokenIdsRaw ?: "").parseStringArray()
val outcomeIndex = clobTokenIds.indexOfFirst { it.equals(tokenId, ignoreCase = true) }.takeIf { it >= 0 }
?: return null
val outcomes = market.outcomes.parseStringArray()
val outcome = if (outcomeIndex < outcomes.size) outcomes[outcomeIndex] else null
saveMarketFromResponse(conditionId, market)
MarketInfoByTokenId(conditionId = conditionId, outcomeIndex = outcomeIndex, outcome = outcome)
} catch (e: Exception) {
logger.warn("按 tokenId 查询市场失败: tokenId=$tokenId, error=${e.message}")
null
}
}
/**
* 清除缓存(用于测试或手动刷新)
*/
@@ -230,5 +261,33 @@ class MarketService(
null
}
}
/**
* 根据 conditionId 查询该市场是否为 Neg Risk(需使用 Neg Risk Exchange 签约)
* 用于跟单下单时选择正确的 exchange 合约,避免 invalid signature
*/
suspend fun getNegRiskByConditionId(conditionId: String): Boolean? {
if (conditionId.isBlank()) return null
return try {
val gammaApi = retrofitFactory.createGammaApi()
val response = gammaApi.listMarkets(conditionIds = listOf(conditionId))
if (!response.isSuccessful || response.body().isNullOrEmpty()) return null
val marketResponse = response.body()!!.first()
val fromEvent = marketResponse.events?.firstOrNull()?.negRisk
val fromMarket = marketResponse.negRisk ?: marketResponse.negRiskOther
fromEvent ?: fromMarket
} catch (e: Exception) {
logger.warn("查询市场 negRisk 失败: conditionId=$conditionId, error=${e.message}")
null
}
}
}
/**
* 按 tokenId 查询 Gamma 得到的市场信息(用于补全 trade.market / outcomeIndex
*/
data class MarketInfoByTokenId(
val conditionId: String,
val outcomeIndex: Int,
val outcome: String? = null
)
@@ -220,10 +220,10 @@ object OnChainWsUtils {
return null
}
// 尝试通过 Gamma API 查询市场信息(通过 tokenId)
// 尝试通过 Gamma API 查询市场信息(通过 tokenId);失败时仍保留链上 tokenId 供后续按 tokenId 补查市场
val marketInfo = fetchMarketByTokenId(asset.toString(), retrofitFactory)
// 创建 TradeResponse
// 创建 TradeResponse:tokenId 始终写入链上解析得到的 asset(与 CLOB 一致),便于 Gamma 失败时在 processBuyTrade 中按 tokenId 再查
return TradeResponse(
id = txHash,
market = marketInfo?.conditionId ?: "",
@@ -233,7 +233,8 @@ object OnChainWsUtils {
timestamp = (timestamp ?: System.currentTimeMillis() / 1000).toString(),
user = walletAddress,
outcomeIndex = marketInfo?.outcomeIndex,
outcome = marketInfo?.outcome
outcome = marketInfo?.outcome,
tokenId = asset.toString()
)
}
@@ -463,6 +463,7 @@ class PolymarketActivityWsService(
// 使用 transactionHash 作为 trade ID,如果没有则生成 fallback ID
val tradeId = payload.transactionHash ?: "${leaderId}_${System.currentTimeMillis()}_${asset.take(10)}"
// asset 即 CLOB 的 tokenId,必须写入 TradeResponse,跟单下单时用此 tokenId 请求订单簿/下单,否则会用 conditionId+outcomeIndex 链上重算,可能得到与 CLOB 不一致的 tokenId
TradeResponse(
id = tradeId,
market = conditionId,
@@ -472,7 +473,8 @@ class PolymarketActivityWsService(
timestamp = timestamp,
user = null, // Activity WS 中不需要
outcomeIndex = outcomeIndex,
outcome = outcome
outcome = outcome,
tokenId = asset
)
} catch (e: Exception) {
logger.error("解析 Activity Trade 失败: ${e.message}", e)
@@ -23,6 +23,14 @@ class OrderSigningService {
private val logger = LoggerFactory.getLogger(OrderSigningService::class.java)
/**
* 根据是否为 Neg Risk 市场返回签约用 exchange 合约地址
* @param negRisk true 时使用 Neg Risk CTF Exchange,否则使用标准 CTF Exchange
*/
fun getExchangeContract(negRisk: Boolean): String {
return if (negRisk) NEG_RISK_EXCHANGE_CONTRACT else EXCHANGE_CONTRACT
}
/**
* 根据钱包类型返回 CLOB 订单签名类型
* @param walletType Magic=邮箱/社交登录, Safe=Web3 钱包
@@ -33,8 +41,10 @@ class OrderSigningService {
return if (walletTypeEnum == com.wrbug.polymarketbot.enums.WalletType.MAGIC) 1 else 2
}
// Polygon 主网合约地址
// Polygon 主网合约地址(标准 CTF Exchange
private val EXCHANGE_CONTRACT = "0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E"
// Neg Risk CTF Exchangeneg risk 市场需用此合约签约,否则服务端返回 invalid signature
private val NEG_RISK_EXCHANGE_CONTRACT = "0xC5d563A36AE78145C45a50134d48A1215220f80a"
private val CHAIN_ID = 137L
// USDC 有 6 位小数
@@ -158,6 +168,7 @@ class OrderSigningService {
* @param nonce nonce(默认 "0"
* @param feeRateBps 费率基点(默认 "0"
* @param expiration 过期时间戳(秒,0 表示永不过期)
* @param exchangeContract 签约用 exchange 合约地址;null 时用标准 CTF Exchangeneg risk 市场需传 Neg Risk Exchange
* @return 签名的订单对象
*/
fun createAndSignOrder(
@@ -170,7 +181,8 @@ class OrderSigningService {
signatureType: Int = 2, // 默认使用 Browser Wallet(与正确订单数据一致)
nonce: String = "0",
feeRateBps: String = "0",
expiration: String = "0"
expiration: String = "0",
exchangeContract: String? = null
): SignedOrderObject {
try {
// 1. 从私钥获取签名地址
@@ -203,10 +215,11 @@ class OrderSigningService {
logger.debug("Salt: $salt, Expiration: $expiration, Nonce: $nonce, FeeRateBPS: $feeRateBps")
logger.debug("Signature Type: $signatureType, Chain ID: $CHAIN_ID")
// 6. 构建订单数据并签名
// 6. 构建订单数据并签名neg risk 市场需用 NEG_RISK_EXCHANGE_CONTRACT
val contract = exchangeContract?.takeIf { it.isNotBlank() } ?: EXCHANGE_CONTRACT
val signature = signOrder(
privateKey = privateKey,
exchangeContract = EXCHANGE_CONTRACT,
exchangeContract = contract,
chainId = CHAIN_ID,
salt = salt,
maker = makerAddressLower,
@@ -268,20 +281,20 @@ class OrderSigningService {
signatureType: Int
): String {
try {
// 1. 私钥创建 BigInteger
// 1. 私钥与密钥对
val cleanPrivateKey = privateKey.removePrefix("0x")
val privateKeyBigInt = BigInteger(cleanPrivateKey, 16)
val ecKeyPair = org.web3j.crypto.ECKeyPair.create(privateKeyBigInt)
// 2. 编码域分隔符
val credentials = Credentials.create(privateKeyBigInt.toString(16))
val ecKeyPair = credentials.ecKeyPair
// 2. 编码域分隔符(verifyingContract 显式小写,与 EIP-712 约定一致)
val domainSeparator = com.wrbug.polymarketbot.util.Eip712Encoder.encodeExchangeDomain(
chainId = chainId,
verifyingContract = exchangeContract
verifyingContract = exchangeContract.lowercase()
)
// 3. 编码订单消息哈希
// signatureType 参数1 = POLY_PROXY (代理钱包), 2 = POLY_GNOSIS_SAFE, 0 = EOA
// 使用传入的 signatureType 参数,而不是硬编码
// signatureType1 = POLY_PROXY (Magic), 2 = POLY_GNOSIS_SAFE (Safe), 0 = EOA
val orderHash = com.wrbug.polymarketbot.util.Eip712Encoder.encodeExchangeOrder(
salt = salt,
maker = maker,
@@ -294,29 +307,25 @@ class OrderSigningService {
nonce = nonce,
feeRateBps = feeRateBps,
side = side,
signatureType = signatureType // 使用传入的参数
signatureType = signatureType
)
// 4. 计算完整结构化数据哈希
// 4. 计算完整 EIP-712 结构化数据哈希
val structuredHash = com.wrbug.polymarketbot.util.Eip712Encoder.hashStructuredData(
domainSeparator = domainSeparator,
messageHash = orderHash
)
// 5. 使用私钥签名
// 5. 使用私钥签名needToHash=false,对 32 字节 hash 直接签名)
val signature = org.web3j.crypto.Sign.signMessage(structuredHash, ecKeyPair, false)
// 6. 组合签名(r + s + v
// 6. 组合 r + s + v
val rHex = org.web3j.utils.Numeric.toHexString(signature.r).removePrefix("0x").padStart(64, '0')
val sHex = org.web3j.utils.Numeric.toHexString(signature.s).removePrefix("0x").padStart(64, '0')
val vBytes = signature.v as ByteArray
val vInt = if (vBytes.isNotEmpty()) {
vBytes[0].toInt() and 0xff
} else {
0
}
val vHex = String.format("%02x", vInt)
val vBytes = signature.v
val vInt = if (vBytes.isNotEmpty()) vBytes[0].toInt() and 0xff else 0
val vHex = "%02x".format(vInt)
return "0x$rHex$sHex$vHex"
} catch (e: Exception) {
logger.error("订单签名失败", e)
@@ -258,19 +258,36 @@ open class CopyOrderTrackingService(
continue
}
// 直接使用outcomeIndex获取tokenId(支持多元市场)
if (trade.outcomeIndex == null) {
logger.warn("交易缺少outcomeIndex,无法确定tokenId: tradeId=${trade.id}, market=${trade.market}")
continue
// 获取 tokenId:优先使用链上解析得到的 tokenId(与 Gamma clobTokenIds 一致),否则用 conditionId+outcomeIndex 链上重算
val tokenId = if (!trade.tokenId.isNullOrBlank()) {
trade.tokenId
} else {
if (trade.outcomeIndex == null) {
logger.warn("交易缺少outcomeIndex且无tokenId,无法确定tokenId: tradeId=${trade.id}, market=${trade.market}")
continue
}
val tokenIdResult = blockchainService.getTokenId(trade.market, trade.outcomeIndex)
if (tokenIdResult.isFailure) {
logger.error("获取tokenId失败: market=${trade.market}, outcomeIndex=${trade.outcomeIndex}, error=${tokenIdResult.exceptionOrNull()?.message}")
continue
}
tokenIdResult.getOrNull() ?: continue
}
// 获取tokenId(直接使用outcomeIndex,不转换为YES/NO
val tokenIdResult = blockchainService.getTokenId(trade.market, trade.outcomeIndex)
if (tokenIdResult.isFailure) {
logger.error("获取tokenId失败: market=${trade.market}, outcomeIndex=${trade.outcomeIndex}, error=${tokenIdResult.exceptionOrNull()?.message}")
// 当链上解析时 Gamma 失败导致 market/outcomeIndex 为空时,按 tokenId 补查市场信息
var effectiveMarketId = trade.market
var effectiveOutcomeIndex = trade.outcomeIndex
if (effectiveMarketId.isBlank() && !trade.tokenId.isNullOrBlank()) {
val infoByToken = marketService.getMarketInfoByTokenId(trade.tokenId)
if (infoByToken != null) {
effectiveMarketId = infoByToken.conditionId
effectiveOutcomeIndex = infoByToken.outcomeIndex
}
}
if (effectiveMarketId.isBlank()) {
logger.warn("无法确定市场(conditionId),跳过: tradeId=${trade.id}, tokenId=${trade.tokenId}")
continue
}
val tokenId = tokenIdResult.getOrNull() ?: continue
// 先计算跟单金额(用于仓位检查)
// 注意:这里先计算金额,即使后续被过滤也会记录
@@ -293,7 +310,7 @@ open class CopyOrderTrackingService(
if (needMarketInfo) {
try {
val market = marketService.getMarket(trade.market)
val market = marketService.getMarket(effectiveMarketId)
marketTitle = market?.title
marketEndDate = market?.endDate
} catch (e: Exception) {
@@ -312,10 +329,10 @@ open class CopyOrderTrackingService(
tokenId,
tradePrice = tradePrice,
copyOrderAmount = copyOrderAmount,
marketId = trade.market,
marketId = effectiveMarketId,
marketTitle = marketTitle,
marketEndDate = marketEndDate,
outcomeIndex = trade.outcomeIndex
outcomeIndex = effectiveOutcomeIndex
)
val orderbook = filterResult.orderbook // 获取订单簿(如果需要)
if (!filterResult.isPassed) {
@@ -325,8 +342,8 @@ open class CopyOrderTrackingService(
notificationScope.launch {
try {
// 获取市场信息(标题和slug
val market = marketService.getMarket(trade.market)
val marketTitle = market?.title ?: trade.market
val market = marketService.getMarket(effectiveMarketId)
val marketTitle = market?.title ?: effectiveMarketId
val marketSlug = market?.slug // 显示用的 slug
// 从过滤结果中提取 filterType
@@ -346,11 +363,11 @@ open class CopyOrderTrackingService(
accountId = copyTrading.accountId,
leaderId = copyTrading.leaderId,
leaderTradeId = trade.id,
marketId = trade.market,
marketId = effectiveMarketId,
marketTitle = marketTitle,
marketSlug = marketSlug,
side = "BUY",
outcomeIndex = trade.outcomeIndex,
outcomeIndex = effectiveOutcomeIndex,
outcome = trade.outcome,
price = trade.price.toSafeBigDecimal(),
size = trade.size.toSafeBigDecimal(),
@@ -376,7 +393,7 @@ open class CopyOrderTrackingService(
telegramNotificationService?.sendOrderFilteredNotification(
marketTitle = marketTitle,
marketId = trade.market,
marketId = effectiveMarketId,
marketSlug = marketSlug,
side = "BUY",
outcome = trade.outcome,
@@ -556,6 +573,11 @@ open class CopyOrderTrackingService(
logger.info("准备创建买入订单: copyTradingId=${copyTrading.id}, tradeId=${trade.id}, leaderPrice=${trade.price}, tolerance=${copyTrading.priceTolerance}, calculatedPrice=$buyPrice, quantity=$finalBuyQuantity, baseFee=$feeRateBps")
// Neg Risk 市场需用 Neg Risk Exchange 签约,否则服务端返回 invalid signature
val negRisk = marketService.getNegRiskByConditionId(effectiveMarketId) == true
val exchangeContract = orderSigningService.getExchangeContract(negRisk)
if (negRisk) logger.debug("市场为 Neg Risk,使用 Neg Risk Exchange 签约: conditionId=$effectiveMarketId")
// 调用API创建订单(带重试机制)
// 重试策略:最多重试 MAX_RETRY_ATTEMPTS 次,每次重试前等待 RETRY_DELAY_MS 毫秒
// 每次重试都会重新生成salt并重新签名,确保签名唯一性
@@ -563,6 +585,8 @@ open class CopyOrderTrackingService(
clobApi = clobApi,
privateKey = decryptedPrivateKey,
makerAddress = account.proxyAddress,
walletAddress = account.walletAddress,
exchangeContract = exchangeContract,
tokenId = tokenId,
side = "BUY",
price = buyPrice.toString(),
@@ -585,8 +609,8 @@ open class CopyOrderTrackingService(
notificationScope.launch {
try {
// 获取市场信息(标题和slug
val market = marketService.getMarket(trade.market)
val marketTitle = market?.title ?: trade.market
val market = marketService.getMarket(effectiveMarketId)
val marketTitle = market?.title ?: effectiveMarketId
val marketSlug = market?.eventSlug // 跳转用的 slug
// 获取当前语言设置(从 LocaleContextHolder
@@ -598,7 +622,7 @@ open class CopyOrderTrackingService(
telegramNotificationService?.sendOrderFailureNotification(
marketTitle = marketTitle,
marketId = trade.market,
marketId = effectiveMarketId,
marketSlug = marketSlug,
side = "BUY",
outcome = null, // 失败时可能没有 outcome
@@ -632,9 +656,9 @@ open class CopyOrderTrackingService(
copyTradingId = copyTrading.id,
accountId = copyTrading.accountId,
leaderId = copyTrading.leaderId,
marketId = trade.market,
side = trade.outcomeIndex.toString(), // 使用outcomeIndex作为side(兼容旧数据)
outcomeIndex = trade.outcomeIndex, // 新增字段
marketId = effectiveMarketId,
side = effectiveOutcomeIndex?.toString() ?: "", // 使用outcomeIndex作为side(兼容旧数据)
outcomeIndex = effectiveOutcomeIndex, // 新增字段
buyOrderId = realOrderId, // 使用真实订单ID
leaderBuyTradeId = trade.id,
leaderBuyQuantity = trade.size.toSafeBigDecimal(), // 存储 Leader 买入数量(用于固定金额模式计算卖出比例)
@@ -904,13 +928,21 @@ open class CopyOrderTrackingService(
finalNeedMatch = BigDecimal.ONE
}
// 4. 获取tokenId(直接使用outcomeIndex,支持多元市场)
val tokenIdResult = blockchainService.getTokenId(leaderSellTrade.market, leaderSellTrade.outcomeIndex)
if (tokenIdResult.isFailure) {
logger.error("获取tokenId失败: market=${leaderSellTrade.market}, outcomeIndex=${leaderSellTrade.outcomeIndex}, error=${tokenIdResult.exceptionOrNull()?.message}")
return
// 4. 获取 tokenId:优先使用链上解析得到的 tokenId,否则用 conditionId+outcomeIndex 链上重算
val tokenId = if (!leaderSellTrade.tokenId.isNullOrBlank()) {
leaderSellTrade.tokenId
} else {
if (leaderSellTrade.outcomeIndex == null) {
logger.error("卖出交易缺少outcomeIndex且无tokenId: market=${leaderSellTrade.market}")
return
}
val tokenIdResult = blockchainService.getTokenId(leaderSellTrade.market, leaderSellTrade.outcomeIndex)
if (tokenIdResult.isFailure) {
logger.error("获取tokenId失败: market=${leaderSellTrade.market}, outcomeIndex=${leaderSellTrade.outcomeIndex}, error=${tokenIdResult.exceptionOrNull()?.message}")
return
}
tokenIdResult.getOrNull() ?: return
}
val tokenId = tokenIdResult.getOrNull() ?: return
// 5. 计算卖出价格(优先使用订单簿 bestBid,失败则使用 Leader 价格,固定按90%计算)
// 注意:需要先计算卖出价格,因为后续创建 matchDetails 需要使用实际卖出价格
@@ -995,7 +1027,12 @@ open class CopyOrderTrackingService(
"0"
}
// 9. 创建并签名卖出订单(按账户钱包类型使用对应 signatureType
// 9. Neg Risk 市场需用 Neg Risk Exchange 签约
val negRiskSell = marketService.getNegRiskByConditionId(leaderSellTrade.market) == true
val exchangeContractSell = orderSigningService.getExchangeContract(negRiskSell)
if (negRiskSell) logger.debug("卖出市场为 Neg Risk,使用 Neg Risk Exchange 签约: conditionId=${leaderSellTrade.market}")
// 10. 创建并签名卖出订单(按账户钱包类型使用对应 signatureType
val signedOrder = try {
orderSigningService.createAndSignOrder(
privateKey = decryptedPrivateKey,
@@ -1007,14 +1044,15 @@ open class CopyOrderTrackingService(
signatureType = orderSigningService.getSignatureTypeForWalletType(account.walletType),
nonce = "0",
feeRateBps = feeRateBps, // 使用动态获取的费率
expiration = "0"
expiration = "0",
exchangeContract = exchangeContractSell
)
} catch (e: Exception) {
logger.error("创建并签名卖出订单失败: copyTradingId=${copyTrading.id}, tradeId=${leaderSellTrade.id}", e)
return
}
// 10. 构建订单请求
// 11. 构建订单请求
// 跟单订单使用 FAK (Fill-And-Kill),允许部分成交,未成交部分立即取消
// 这样可以快速响应 Leader 的交易,避免订单长期挂单导致价格不匹配
val orderRequest = NewOrderRequest(
@@ -1024,7 +1062,7 @@ open class CopyOrderTrackingService(
deferExec = false
)
// 11. 创建带认证的CLOB API客户端(使用解密后的凭证)
// 12. 创建带认证的CLOB API客户端(使用解密后的凭证)
val clobApi = retrofitFactory.createClobApi(
account.apiKey,
apiSecret,
@@ -1032,12 +1070,13 @@ open class CopyOrderTrackingService(
account.walletAddress
)
// 12. 调用API创建卖出订单(带重试机制,重试时会重新生成salt并重新签名)
// 13. 调用API创建卖出订单(带重试机制,重试时会重新生成salt并重新签名)
val createOrderResult = createOrderWithRetry(
clobApi = clobApi,
privateKey = decryptedPrivateKey,
makerAddress = account.proxyAddress,
walletAddress = account.walletAddress,
exchangeContract = exchangeContractSell,
tokenId = tokenId,
side = "SELL",
price = sellPrice.toString(),
@@ -1130,7 +1169,9 @@ open class CopyOrderTrackingService(
*
* @param clobApi CLOB API 客户端
* @param privateKey 私钥(用于签名)
* @param makerAddress 代理钱包地址
* @param makerAddress 代理钱包地址funder
* @param walletAddress 账户 EOA 地址(须与私钥推导的 signer 一致,用于校验及 POLY_ADDRESS
* @param exchangeContract 签约用 exchange 合约(Neg Risk 市场需用 Neg Risk Exchange
* @param tokenId Token ID
* @param side 订单方向(BUY/SELL
* @param price 价格
@@ -1146,6 +1187,8 @@ open class CopyOrderTrackingService(
clobApi: PolymarketClobApi,
privateKey: String,
makerAddress: String,
walletAddress: String,
exchangeContract: String,
tokenId: String,
side: String,
price: String,
@@ -1172,9 +1215,17 @@ open class CopyOrderTrackingService(
signatureType = signatureType,
nonce = "0",
feeRateBps = feeRateBps, // 使用动态获取的费率
expiration = "0"
expiration = "0",
exchangeContract = exchangeContract
)
// 校验 signer 与账户 walletAddress 一致,否则服务端会返回 invalid signaturePOLY_ADDRESS 与 order.signer 需一致)
if (signedOrder.signer.lowercase() != walletAddress.lowercase()) {
val msg = "订单 signer 与账户 walletAddress 不一致,会导致 invalid signature。请确认该账户的私钥与 walletAddress 对应同一 EOA,且 API 密钥由该 EOA 创建。signer=${signedOrder.signer.take(10)}..., walletAddress=${walletAddress.take(10)}..."
logger.error(msg)
return Result.failure(IllegalStateException(msg))
}
// 构建订单请求
// 跟单订单使用 FAK (Fill-And-Kill),允许部分成交,未成交部分立即取消
// 这样可以快速响应 Leader 的交易,避免订单长期挂单导致价格不匹配