- 更新 EIP-712 签名结构: domain version "1"→"2", 移除 taker/expiration/nonce/feeRateBps, 新增 timestamp/metadata/builder - 更新合约地址为 V2 (CTF_EXCHANGE, NEG_RISK_EXCHANGE) - CLOB URL 切换到 clob-v2.polymarket.com - 抵押品代币 USDC.e → pUSD (0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB) - 新增 USDC.e → pUSD wrap 功能 (CollateralOnramp 合约) - SignedOrderObject 补充 API payload 必需的 taker/expiration 字段 - NewOrderRequest 补充 deferExec/postOnly 字段 - 前端账户管理新增 pUSD 迁移按钮 - 移除所有 feeRateBps 相关逻辑 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
33 KiB
Polymarket CLOB V2 迁移指南
📋 迁移概述
Polymarket正在升级其整个交易基础设施,包括新的Exchange合约、重写的CLOB后端和新的抵押品代币(pUSD)。本文档详细说明了如何将backend代码从CLOB V1迁移到V2。
迁移截止日期: 2026年4月28日 ~11:00 UTC
预计停机时间: 约1小时
重要提示: V2发布后,V1 SDK将立即停止工作,无向后兼容性。
🔄 主要变更概览
| 变更项 | V1 | V2 |
|---|---|---|
| SDK包名 | @polymarket/clob-client |
@polymarket/clob-client-v2 |
| 构造函数 | 位置参数 | 选项对象 (chainId → chain) |
| 订单字段(移除) | nonce, feeRateBps, taker, expiration |
已移除,不再使用 |
| 订单字段(新增) | - | timestamp(毫秒), metadata(bytes32), builder(bytes32) |
| 费用设置 | 订单中嵌入 (feeRateBps) |
匹配时由协议设定,通过 getClobMarketInfo() 查询 |
| 抵押品代币 | USDC.e | pUSD (Polymarket USD) |
| Builder认证 | HMAC headers | 单个 builderCode 字段 |
| EIP-712版本 | "1" |
"2" |
| Exchange合约地址 | V1地址 | V2地址 (见下文) |
| 订单取消 | 链上 cancel() |
运营商控制的 pauseUser / unpauseUser |
| CLOB URL | clob.polymarket.com |
clob-v2.polymarket.com |
🏗️ 合约地址变更
标准CTF Exchange
// V1 (旧)
private val EXCHANGE_CONTRACT = "0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E"
// V2 (新)
private val EXCHANGE_CONTRACT_V2 = "0xE111180000d2663C0091e4f400237545B87B996B"
Neg Risk CTF Exchange
// V1 (旧)
private val NEG_RISK_EXCHANGE_CONTRACT = "0xC5d563A36AE78145C45a50134d48A1215220f80a"
// V2 (新)
private val NEG_RISK_EXCHANGE_CONTRACT_V2 = "0xe2222d279d744050d28e00520010520000310F59"
📝 详细代码改动
1. EIP-712编码器更新 (Eip712Encoder.kt)
1.1 更新域分隔符编码方法
位置: Eip712Encoder.kt:174-201
直接替换现有方法:
/**
* 编码 ExchangeOrder V2 域分隔符
* Domain: { name: "Polymarket CTF Exchange", version: "2", chainId: chainId, verifyingContract: exchangeContract }
*/
fun encodeExchangeDomain(
chainId: Long,
verifyingContract: String
): ByteArray {
val domainTypeHash = encodeType(
"EIP712Domain",
listOf(
"name" to "string",
"version" to "string",
"chainId" to "uint256",
"verifyingContract" to "address"
)
)
val nameHash = encodeString("Polymarket CTF Exchange")
val versionHash = encodeString("2") // V2:版本从 "1" 改为 "2"
val chainIdBytes = encodeUint256(BigInteger.valueOf(chainId))
val contractBytes = encodeAddress(verifyingContract)
val encoded = ByteArray(32 + 32 + 32 + 32 + 32)
System.arraycopy(domainTypeHash, 0, encoded, 0, 32)
System.arraycopy(nameHash, 0, encoded, 32, 32)
System.arraycopy(versionHash, 0, encoded, 64, 32)
System.arraycopy(chainIdBytes, 0, encoded, 96, 32)
System.arraycopy(contractBytes, 0, encoded, 128, 32)
return keccak256(encoded)
}
1.2 替换订单编码方法
位置: Eip712Encoder.kt:208-280
直接替换现有方法:
/**
* 编码 ExchangeOrder V2 消息哈希
* 参考: CLOB V2 迁移指南
* Order V2: { salt, maker, signer, tokenId, makerAmount, takerAmount, side, signatureType, timestamp, metadata, builder }
*/
fun encodeExchangeOrder(
salt: Long,
maker: String,
signer: String,
tokenId: String,
makerAmount: String,
takerAmount: String,
side: String,
signatureType: Int,
timestamp: String, // V2:订单创建时间(毫秒)
metadata: String, // V2:bytes32 元数据
builder: String // V2:bytes32 builder代码
): ByteArray {
val orderTypeHash = encodeType(
"Order",
listOf(
"salt" to "uint256",
"maker" to "address",
"signer" to "address",
"tokenId" to "uint256",
"makerAmount" to "uint256",
"takerAmount" to "uint256",
"side" to "uint8",
"signatureType" to "uint8",
"timestamp" to "uint256", // V2字段
"metadata" to "bytes32", // V2字段
"builder" to "bytes32" // V2字段
)
)
// 编码订单字段
val saltBytes = encodeUint256(BigInteger.valueOf(salt))
val makerBytes = encodeAddress(maker)
val signerBytes = encodeAddress(signer)
val tokenIdBytes = encodeUint256(BigInteger(tokenId))
val makerAmountBytes = encodeUint256(BigInteger(makerAmount))
val takerAmountBytes = encodeUint256(BigInteger(takerAmount))
// side: BUY = 0, SELL = 1
val sideValue = when (side.uppercase()) {
"BUY" -> 0
"SELL" -> 1
else -> throw IllegalArgumentException("side 必须是 BUY 或 SELL")
}
val sideBytes = encodeUint256(BigInteger.valueOf(sideValue.toLong()))
val signatureTypeBytes = encodeUint256(BigInteger.valueOf(signatureType.toLong()))
// V2 字段编码
val timestampBytes = encodeUint256(BigInteger(timestamp))
val metadataBytes = Numeric.hexStringToByteArray(metadata.removePrefix("0x").padStart(64, '0'))
val builderBytes = Numeric.hexStringToByteArray(builder.removePrefix("0x").padStart(64, '0'))
// 组合所有字段 (typeHash + 11个字段 = 12个slot)
val encoded = ByteArray(32 * 12)
var offset = 0
System.arraycopy(orderTypeHash, 0, encoded, offset, 32); offset += 32
System.arraycopy(saltBytes, 0, encoded, offset, 32); offset += 32
System.arraycopy(makerBytes, 0, encoded, offset, 32); offset += 32
System.arraycopy(signerBytes, 0, encoded, offset, 32); offset += 32
System.arraycopy(tokenIdBytes, 0, encoded, offset, 32); offset += 32
System.arraycopy(makerAmountBytes, 0, encoded, offset, 32); offset += 32
System.arraycopy(takerAmountBytes, 0, encoded, offset, 32); offset += 32
System.arraycopy(sideBytes, 0, encoded, offset, 32); offset += 32
System.arraycopy(signatureTypeBytes, 0, encoded, offset, 32); offset += 32
System.arraycopy(timestampBytes, 0, encoded, offset, 32); offset += 32 // V2 新增
System.arraycopy(metadataBytes, 0, encoded, offset, 32); offset += 32 // V2 新增
System.arraycopy(builderBytes, 0, encoded, offset, 32) // V2 新增
return keccak256(encoded)
}
2. 订单对象结构更新 (PolymarketClobApi.kt)
2.1 创建新的V2订单对象
位置: PolymarketClobApi.kt
/**
* 签名的订单对象 V2
* 参考: https://docs.polymarket.com/v2-migration
*
* V2 移除了以下 V1 字段: taker, expiration, nonce, feeRateBps
* V2 新增了以下字段: timestamp, metadata, builder
*/
data class SignedOrderObject(
val salt: Long, // random salt
val maker: String, // maker address (funder)
val signer: String, // signing address
val tokenId: String, // ERC1155 token ID
val makerAmount: String, // maximum amount maker is willing to spend
val takerAmount: String, // minimum amount taker will pay
val side: String, // "BUY" or "SELL"
val signatureType: Int, // signature type enum index
val timestamp: String, // 订单创建时间戳(毫秒)V2新增
val metadata: String, // bytes32 元数据 V2新增
val builder: String, // bytes32 builder代码 V2新增
val signature: String // hex encoded signature
)
2.2 更新API请求对象
位置: PolymarketClobApi.kt:200-205
直接替换现有类:
/**
* 创建订单请求 (V2)
*/
data class NewOrderRequest(
val order: SignedOrderObject, // V2 signed object
val owner: String, // api key of order owner
val orderType: String // "FOK", "GTC", "GTD", "FAK"
)
3. 订单签名服务更新 (OrderSigningService.kt)
3.1 替换合约地址常量
位置: OrderSigningService.kt:45-47
直接替换现有常量:
// V2 合约地址
private val EXCHANGE_CONTRACT = "0xE111180000d2663C0091e4f400237545B87B996B"
private val NEG_RISK_EXCHANGE_CONTRACT = "0xe2222d279d744050d28e00520010520000310F59"
3.2 简化getExchangeContract方法
位置: OrderSigningService.kt:30-32
直接替换现有方法:
/**
* 根据是否为 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
}
3.3 替换createAndSignOrder方法
位置: OrderSigningService.kt:174-259
直接替换现有方法:
/**
* 创建并签名订单 (V2)
*
* @param privateKey 私钥
* @param makerAddress maker 地址
* @param tokenId token ID
* @param side BUY 或 SELL
* @param price 价格
* @param size 数量
* @param signatureType 签名类型(1: Email/Magic, 2: Browser Wallet)
* @param exchangeContract 签约用 exchange 合约地址;null 时用标准 CTF Exchange
* @param builderCode builder代码(bytes32,可选)
* @param metadata 元数据(bytes32,默认为零)
* @return V2 签名的订单对象
*/
fun createAndSignOrder(
privateKey: String,
makerAddress: String,
tokenId: String,
side: String,
price: String,
size: String,
signatureType: Int = 2,
exchangeContract: String? = null,
builderCode: String? = null,
metadata: String = "0x0000000000000000000000000000000000000000000000000000000000000000"
): SignedOrderObject {
try {
// 1. 从私钥获取签名地址
val cleanPrivateKey = privateKey.removePrefix("0x")
val privateKeyBigInt = BigInteger(cleanPrivateKey, 16)
val credentials = Credentials.create(privateKeyBigInt.toString(16))
val signerAddress = credentials.address.lowercase()
// 2. 计算订单金额(复用现有逻辑)
val amounts = calculateOrderAmounts(side, size, price)
// 3. 生成 salt 和 timestamp(毫秒)
// V2: 使用 timestamp 替代 nonce 保证订单唯一性
val salt = generateSalt()
val timestamp = System.currentTimeMillis().toString()
// 4. 处理 builder 和 metadata
val builder = builderCode?.takeIf { it.isNotBlank() }
?: "0x0000000000000000000000000000000000000000000000000000000000000000"
val metadataClean = if (metadata.isNotBlank()) metadata
else "0x0000000000000000000000000000000000000000000000000000000000000000"
// 5. 确保 maker 地址是小写格式
val makerAddressLower = makerAddress.lowercase()
logger.debug("========== 订单签名前参数 (V2) ==========")
logger.debug("订单方向: $side, 价格: $price, 数量: $size")
logger.debug("Token ID: $tokenId")
logger.debug("Maker: ${makerAddressLower.take(10)}...${makerAddressLower.takeLast(6)}")
logger.debug("Signer: ${signerAddress.take(10)}...${signerAddress.takeLast(6)}")
logger.debug("Amounts - Maker: ${amounts.makerAmount}, Taker: ${amounts.takerAmount}")
logger.debug("Salt: $salt, Timestamp: $timestamp")
logger.debug("Builder: $builder, Metadata: $metadataClean")
logger.debug("Signature Type: $signatureType")
// 6. 使用合约地址
val contract = exchangeContract?.takeIf { it.isNotBlank() } ?: EXCHANGE_CONTRACT
// 7. 构建V2签名
val signature = signOrder(
privateKey = privateKey,
exchangeContract = contract,
chainId = CHAIN_ID,
salt = salt,
maker = makerAddressLower,
signer = signerAddress,
tokenId = tokenId,
makerAmount = amounts.makerAmount,
takerAmount = amounts.takerAmount,
side = side.uppercase(),
signatureType = signatureType,
timestamp = timestamp,
metadata = metadataClean,
builder = builder
)
// 8. 创建V2签名订单对象
// 注意: V2不再包含 taker, expiration, nonce, feeRateBps 字段
return SignedOrderObject(
salt = salt,
maker = makerAddressLower,
signer = signerAddress,
tokenId = tokenId,
makerAmount = amounts.makerAmount,
takerAmount = amounts.takerAmount,
side = side.uppercase(),
signatureType = signatureType,
timestamp = timestamp,
metadata = metadataClean,
builder = builder,
signature = signature
)
} catch (e: Exception) {
logger.error("创建并签名订单失败 (V2)", e)
throw RuntimeException("创建并签名订单失败 (V2): ${e.message}", e)
}
}
3.4 替换signOrder方法
位置: OrderSigningService.kt:266-334
直接替换现有方法:
/**
* 签名订单 V2(EIP-712)
* V2 Order: salt, maker, signer, tokenId, makerAmount, takerAmount, side, signatureType, timestamp, metadata, builder
*/
private fun signOrder(
privateKey: String,
exchangeContract: String,
chainId: Long,
salt: Long,
maker: String,
signer: String,
tokenId: String,
makerAmount: String,
takerAmount: String,
side: String,
signatureType: Int,
timestamp: String,
metadata: String,
builder: String
): String {
try {
// 1. 私钥与密钥对
val cleanPrivateKey = privateKey.removePrefix("0x")
val privateKeyBigInt = BigInteger(cleanPrivateKey, 16)
val credentials = Credentials.create(privateKeyBigInt.toString(16))
val ecKeyPair = credentials.ecKeyPair
// 2. 编码域分隔符(V2:版本为 "2")
val domainSeparator = com.wrbug.polymarketbot.util.Eip712Encoder.encodeExchangeDomain(
chainId = chainId,
verifyingContract = exchangeContract.lowercase()
)
// 3. 编码V2订单消息哈希(11个字段,不含V1的taker/expiration/nonce/feeRateBps)
val orderHash = com.wrbug.polymarketbot.util.Eip712Encoder.encodeExchangeOrder(
salt = salt,
maker = maker,
signer = signer,
tokenId = tokenId,
makerAmount = makerAmount,
takerAmount = takerAmount,
side = side,
signatureType = signatureType,
timestamp = timestamp,
metadata = metadata,
builder = builder
)
// 4. 计算完整 EIP-712 结构化数据哈希
val structuredHash = com.wrbug.polymarketbot.util.Eip712Encoder.hashStructuredData(
domainSeparator = domainSeparator,
messageHash = orderHash
)
// 5. 使用私钥签名
val signature = org.web3j.crypto.Sign.signMessage(structuredHash, ecKeyPair, false)
// 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
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("订单签名失败 (V2)", e)
throw RuntimeException("订单签名失败 (V2): ${e.message}", e)
}
}
4. Builder认证系统清理
4.1 移除BuilderAuthInterceptor
位置: BuilderAuthInterceptor.kt
删除整个文件:
# V2不再需要HMAC builder认证,直接删除此文件
rm BuilderAuthInterceptor.kt
4.2 更新Builder配置
创建新的Builder配置类:
/**
* V2 Builder配置
*/
data class BuilderConfigV2(
val builderCode: String, // bytes32格式的builder代码
val enabled: Boolean = true
)
4.3 更新使用Builder认证的代码
检查并更新所有使用 BuilderAuthInterceptor 的地方:
// V1 (旧)
val builderApi = RetrofitFactory.createRetrofit(
baseUrl = BUILDER_RELAYER_URL,
interceptors = listOf(
BuilderAuthInterceptor(apiKey, secret, passphrase)
)
).create(BuilderRelayerApi::class.java)
// V2 (新) - 移除Builder认证拦截器
val builderApi = RetrofitFactory.createRetrofit(
baseUrl = BUILDER_RELAYER_URL,
interceptors = listOf() // V2不需要额外的认证拦截器
).create(BuilderRelayerApi::class.java)
// builderCode直接在订单中设置
val order = orderSigningService.createAndSignOrder(
// ... 其他参数
builderCode = builderConfig.builderCode
)
5. API端点更新
5.1 移除Builder相关端点
移除以下可能不再需要的端点:
// 删除这些端点(V2不再需要)
// @POST("/auth/builder-api-key")
// suspend fun createBuilderApiKey(): Response<BuilderApiKeyResponse>
// @GET("/auth/builder-api-key")
// suspend fun getBuilderApiKeys(): Response<List<BuilderApiKey>>
5.2 更新费率查询端点(如需要)
检查是否需要更新费率查询:
// V2可能使用新的端点(需要根据实际API文档确认)
@GET("/clob-market-info")
suspend fun getClobMarketInfo(@Query("market") market: String): Response<ClobMarketInfoResponse>
// V2 响应结构
data class ClobMarketInfoResponse(
val feeRate: Int, // 市场费率
val feeExponent: Double, // 费用指数
val builderFeeRate: Int? // Builder费率(如果有)
)
5.3 更新CLOB API基础URL
V2使用新的CLOB URL:
// V1 (旧)
const val CLOB_BASE_URL = "https://clob.polymarket.com"
// V2 (新)
const val CLOB_BASE_URL = "https://clob-v2.polymarket.com"
5.4 更新订单取消方式
V2取消了链上 cancel() 操作,改为运营商控制:
// V1: 链上取消订单
// cancelOrder(orderId) -> DELETE /orders/{orderId}
// V2: 运营商控制的 pauseUser / unpauseUser
// 取消订单的API端点可能保持不变(DELETE /orders/{orderId})
// 但链上取消机制已移除,改为运营商暂停/恢复用户
注意: 需要验证
DELETE /orders/{orderId}和DELETE /orders/batch端点在 V2 中是否仍可用。
6. Nonce追踪逻辑移除
V2 使用 timestamp(毫秒)替代 nonce 保证订单唯一性。需要移除所有 nonce 相关逻辑:
// 移除以下V1代码:
// - nonce 参数传递
// - nonce 追踪/缓存逻辑
// - getNonce() 调用
// V2: 直接使用 timestamp
val timestamp = System.currentTimeMillis().toString()
7. 抵押品代币更新 (USDC.e → pUSD)
V2 使用 pUSD (Polymarket USD) 替代 USDC.e:
// V1 (旧)
const val COLLATERAL_TOKEN = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174" // USDC.e
// V2 (新)
const val COLLATERAL_TOKEN_PUSD = "0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB" // pUSD
// USDC.e 仍保留用于 wrap 操作
const val USDCE_CONTRACT = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174"
// CollateralOnramp (USDC.e → pUSD)
const val COLLATERAL_ONRAMP = "0x93070a847efEf7F70739046A929D47a521F5B8ee"
// 参考: https://docs.polymarket.com/concepts/pusd
7.1 USDC.e → pUSD Wrap 实现
后端已实现自动 wrap 功能:
RelayClientService.createUsdceApproveForWrapTx()— 创建 USDC.e approve 交易RelayClientService.createWrapToPusdTx()— 创建 wrap 交易BlockchainService.wrapUsdcToPusd()— 组合 approve + wrap 为 MultiSend 原子执行BlockchainService.queryUsdceBalance()— 查询 USDC.e 余额
前端已添加迁移按钮:
- 账户管理列表中每个账户的操作栏增加了
SwapOutlined迁移按钮 - 点击后查询 USDC.e 余额,有余额时弹出确认框
- 确认后自动执行 approve + wrap 原子交易
后端 API 端点:
POST /api/accounts/wrap-to-pusd— 执行 wrapPOST /api/accounts/usdce-balance— 查询 USDC.e 余额
8. 迁移期间重要注意事项
迁移当天(2026年4月28日 ~11:00 UTC)的关键变化:
- 所有挂单将被清空。 迁移完成后必须重新下单。
- V1 SDK/客户端将立即停止工作,无向后兼容性。
- 预计约1小时停机时间,交易暂停。
- 通过 Discord、Telegram 和 status.polymarket.com 获取确切维护窗口开始时间。
// 迁移前:停止自动交易策略
tradingService.pauseAllTrading("CLOB V2迁移准备")
// 迁移后:更新配置并重新启动
configService.updateClobBaseUrl(CLOB_BASE_URL_V2)
configService.updateExchangeContracts(V2_CONTRACTS)
tradingService.resumeAllTrading("CLOB V2迁移完成")
// 重新下挂单
orderRecoveryService.replayOpenOrders()
1. 单元测试
1.1 EIP-712编码测试
@Test
fun `test V2 EIP-712 encoding matches expected format`() {
val domain = Eip712Encoder.encodeExchangeDomain(
chainId = 137L,
verifyingContract = "0xE111180000d2663C0091e4f400237545B87B996B"
)
// 验证域分隔符
val expectedDomainHash = "0x..." // 从官方文档或TypeScript SDK获取
assertEquals(expectedDomainHash, Numeric.toHexString(domain))
}
@Test
fun `test V2 order encoding includes new fields`() {
val orderHash = Eip712Encoder.encodeExchangeOrder(
salt = 12345L,
maker = "0x...",
signer = "0x...",
tokenId = "12345",
makerAmount = "1000000",
takerAmount = "2000000",
side = "BUY",
signatureType = 2,
timestamp = "1713398400000",
metadata = "0x0000000000000000000000000000000000000000000000000000000000000000",
builder = "0x0000000000000000000000000000000000000000000000000000000000000000"
)
// 验证订单哈希格式
assertNotNull(orderHash)
assertEquals(32, orderHash.size)
}
1.2 订单签名测试
@Test
fun `test order signing produces valid signature`() {
val testPrivateKey = "0x..." // 测试私钥
val order = orderSigningService.createAndSignOrder(
privateKey = testPrivateKey,
makerAddress = "0x...",
tokenId = "12345",
side = "BUY",
price = "0.5",
size = "10"
)
// 验证订单结构
assertTrue(order.timestamp.isNotBlank())
assertTrue(order.metadata.startsWith("0x"))
assertTrue(order.builder.startsWith("0x"))
assertEquals(132, order.signature.length) // 0x(2) + r(64) + s(64) + v(2)
}
2. 集成测试
2.1 订单创建测试
@Test
@DisplayName("订单创建和提交测试 (V2)")
fun `test create and post order`() {
// 1. 创建订单
val order = orderSigningService.createAndSignOrder(
privateKey = testConfig.privateKey,
makerAddress = testConfig.makerAddress,
tokenId = testTokenId,
side = "BUY",
price = "0.5",
size = "10"
)
// 2. 提交到API
val response = polymarketClobApi.createOrder(
NewOrderRequest(
order = order,
owner = testConfig.apiKey,
orderType = "GTC"
)
)
// 3. 验证响应
assertTrue(response.isSuccessful)
assertNotNull(response.body()?.orderId)
}
2.2 Builder功能测试
@Test
@DisplayName("Builder代码测试 (V2)")
fun `test builder code field`() {
val builderCode = "0x1234...5678" // 有效的builder代码
val order = orderSigningService.createAndSignOrder(
privateKey = testConfig.privateKey,
makerAddress = testConfig.makerAddress,
tokenId = testTokenId,
side = "SELL",
price = "0.6",
size = "5",
builderCode = builderCode
)
// 验证builder字段设置正确
assertEquals(builderCode.lowercase(), order.builder.lowercase())
}
3. 测试市场数据
根据官方文档,使用以下市场进行测试:
// 测试市场数据
val TEST_MARKETS = listOf(
TestMarket(
name = "US / Iran nuclear deal in 2027?",
eventId = "73106",
orderbookTokenId = "102936...7216"
),
TestMarket(
name = "Highest grossing movie in 2026?",
eventId = "79831",
orderbookTokenIds = listOf(
"81662...2777",
"17546...1707",
"28161...2479",
"89576...4694",
"21556...6607",
"51020...2516"
)
)
)
📋 迁移检查清单
阶段1:准备阶段(完成时间:迁移前2周)
- 备份现有代码库
- 创建迁移分支
feature/clob-v2-migration - 设置测试环境和测试账户
- 获取V2测试网络的builder code(如需要)
- 准备测试数据和测试用例
阶段2:代码迁移(完成时间:迁移前1周)
- 更新EIP-712编码器(替换为V2方法,版本号
"1"→"2") - 更新订单对象结构(移除
taker,expiration,nonce,feeRateBps,新增timestamp,metadata,builder) - 更新订单签名服务(替换为V2签名,移除
expiration和nonce参数) - 更新合约地址常量(标准 Exchange 和 Neg Risk Exchange)
- 移除所有 nonce 追踪/缓存逻辑
- 移除Builder认证相关代码(
BuilderAuthInterceptor和 HMAC headers) - 更新API端点方法和CLOB基础URL(
clob-v2.polymarket.com) - 更新抵押品代币余额检查逻辑(USDC.e → pUSD)
- 验证订单取消API端点(
DELETE /orders/{orderId})是否仍可用 - 更新依赖配置(如有需要)
阶段3:测试验证(完成时间:迁移前3天)
- 编写V2单元测试
- 编写V2集成测试
- 在测试环境执行完整测试
- 验证订单签名正确性
- 验证订单提交成功率
- 测试builder code功能
- 压力测试和性能验证
阶段4:部署准备(完成时间:迁移前1天)
- 代码审查
- 更新API文档
- 准备回滚方案
- 通知用户迁移计划
- 准备监控和告警
阶段5:正式迁移(完成时间:2026年4月28日)
- 在迁移窗口开始时停止交易
- 备份所有挂单数据(迁移后所有挂单将被清空)
- 部署更新后的代码到生产环境
- 更新CLOB基础URL到
clob-v2.polymarket.com - 执行冒烟测试
- 重新下单(迁移完成后必须重新提交所有挂单)
- 重新启用交易功能
- 监控系统运行状态
- 验证关键功能正常
阶段6:迁移后验证(完成时间:迁移后1周)
- 持续监控系统性能
- 收集用户反馈
- 修复发现的问题
- 优化V2实现
- 清理临时测试代码
🚨 风险和注意事项
1. 关键风险点
1.1 签名不兼容风险
- 风险: V2签名与V1不兼容,混用会导致订单被拒绝
- 缓解措施:
- 明确区分V1和V2方法
- 添加版本检查和验证
- 在切换时确保所有组件同步更新
1.2 合约地址错误风险
- 风险: 使用V1地址签名V2订单会被拒绝
- 缓解措施:
- 在代码中添加地址版本检查
- 使用配置文件管理合约地址
- 添加地址验证测试
1.3 停机时间风险
- 风险: 迁移期间约1小时的停机
- 缓解措施:
- 提前通知用户
- 暂停自动交易策略
- 准备快速回滚方案
2. 业务逻辑变更
2.1 费用计算变更
- V1: 费用嵌入在订单中(
feeRateBps) - V2: 费用由协议在匹配时决定,公式:
fee = C × feeRate × p × (1 - p) - 关键: Maker 不收取费用,仅 Taker 付费
- 影响: 无法提前知道确切费用,订单中不再设置
feeRateBps - 应对: 更新费用预估逻辑,使用
getClobMarketInfo()获取市场费率
2.2 Builder功能变更
- V1: 使用HMAC headers认证
- V2: 使用订单中的
builderCode字段 - 影响: 需要转换现有的builder配置
- 应对: 联系Polymarket获取builder code,更新配置
2.3 抵押品代币变更
- V1: USDC.e
- V2: pUSD (Polymarket USD)
- 影响: 需要处理代币转换
- 应对: 更新代币余额检查逻辑,支持pUSD
3. 监控和告警
3.1 关键监控指标
// 添加V2特定的监控指标
object ClobV2Metrics {
// 订单创建成功率
val orderCreationSuccess = Counter.build()
.name("clob_v2_order_creation_success_total")
.help("V2订单创建成功次数")
.register()
// 订单提交成功率
val orderSubmissionSuccess = Counter.build()
.name("clob_v2_order_submission_success_total")
.help("V2订单提交成功次数")
.register()
// 签名验证失败率
val signatureValidationFailure = Counter.build()
.name("clob_v2_signature_validation_failure_total")
.help("V2签名验证失败次数")
.register()
// API响应时间
val apiResponseTime = Histogram.build()
.name("clob_v2_api_response_time_seconds")
.help("V2 API响应时间")
.register()
}
3.2 告警配置
# Prometheus告警规则示例
groups:
- name: clob_v2_alerts
rules:
- alert: HighV2OrderFailureRate
expr: rate(clob_v2_order_submission_failure_total[5m]) > 0.1
for: 2m
labels:
severity: critical
annotations:
summary: "V2订单失败率过高"
description: "过去5分钟内V2订单失败率超过10%"
- alert: V2SignatureValidationFailure
expr: rate(clob_v2_signature_validation_failure_total[5m]) > 0.05
for: 1m
labels:
severity: warning
annotations:
summary: "V2签名验证失败"
description: "检查EIP-712编码和签名逻辑"
🔄 回滚方案
1. 回滚触发条件
- V2订单失败率超过20%
- 系统出现严重错误影响交易
- 签名验证大规模失败
- API响应时间超过阈值(如10秒)
2. 回滚步骤
-
立即停止交易
tradingService.pauseAllTrading("执行V2回滚") -
切换回V1代码
git checkout main # 回到迁移前的分支 ./deploy.sh # 重新部署 -
验证V1功能
curl -X POST https://api.example.com/health/check -
恢复交易
tradingService.resumeAllTrading("回滚完成,恢复V1交易")
3. 回滚后验证
- 检查系统日志
- 验证V1订单功能正常
- 检查数据库一致性
- 通知用户回滚完成
- 分析失败原因
📚 参考资源
官方文档
TypeScript SDK参考
合约地址
测试资源
🆘 支持和联系
技术支持
- Discord: https://discord.gg/polymarket
- Telegram: Polymarket官方频道
- Email: support@polymarket.com
问题报告
- GitHub Issues: https://github.com/Polymarket/clob-client-v2/issues
- Bug报告: 在Discord技术支持频道报告
更新通知
- Twitter: @Polymarket
- Blog: https://polymarket.com/blog
- 状态页: https://status.polymarket.com
📝 变更日志
文档版本
- v1.0 (2025-01-20): 初始版本,包含完整的迁移指南
- v1.1 (2025-01-XX): 添加测试市场信息和监控配置示例
重大更新
- 2025-01-20: 创建迁移文档,基于Polymarket官方V2迁移指南
⚠️ 最终检查清单
在执行迁移前,请确认:
- 已仔细阅读官方迁移文档
- 已在测试环境完成所有测试(使用
clob-v2.polymarket.com测试市场) - 已备份生产数据库
- 已备份所有挂单数据(迁移后挂单将被清空,必须重新下单)
- 已移除所有 V1 字段(
taker,expiration,nonce,feeRateBps) - 已移除 nonce 追踪逻辑
- 已移除 Builder HMAC 认证相关代码
- 已更新合约地址和 EIP-712 domain version
- 已通知所有相关用户
- 已准备回滚方案
- 已设置监控和告警
- 团队成员已了解迁移流程
- 已准备迁移期间的客服支持
记住:一旦迁移开始,无法中止。V1将立即停止工作,无向后兼容性。确保所有准备工作完成后再开始!
本文档基于Polymarket官方V2迁移指南编写,如有疑问请参考官方文档或联系技术支持。