Files
PolyHermes/backend/CLOB_V2_MIGRATION.md
WrBug 22a6544373 feat: CLOB V1 → V2 完整迁移
- 更新 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>
2026-04-20 22:24:50 +08:00

1085 lines
33 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
```kotlin
// V1 (旧)
private val EXCHANGE_CONTRACT = "0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E"
// V2 (新)
private val EXCHANGE_CONTRACT_V2 = "0xE111180000d2663C0091e4f400237545B87B996B"
```
### Neg Risk CTF Exchange
```kotlin
// 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`
**直接替换现有方法:**
```kotlin
/**
* 编码 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`
**直接替换现有方法:**
```kotlin
/**
* 编码 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, // V2bytes32 元数据
builder: String // V2bytes32 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`
```kotlin
/**
* 签名的订单对象 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`
**直接替换现有类:**
```kotlin
/**
* 创建订单请求 (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`
**直接替换现有常量:**
```kotlin
// V2 合约地址
private val EXCHANGE_CONTRACT = "0xE111180000d2663C0091e4f400237545B87B996B"
private val NEG_RISK_EXCHANGE_CONTRACT = "0xe2222d279d744050d28e00520010520000310F59"
```
#### 3.2 简化getExchangeContract方法
**位置:** `OrderSigningService.kt:30-32`
**直接替换现有方法:**
```kotlin
/**
* 根据是否为 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`
**直接替换现有方法:**
```kotlin
/**
* 创建并签名订单 (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`
**直接替换现有方法:**
```kotlin
/**
* 签名订单 V2EIP-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`
**删除整个文件:**
```bash
# V2不再需要HMAC builder认证,直接删除此文件
rm BuilderAuthInterceptor.kt
```
#### 4.2 更新Builder配置
**创建新的Builder配置类:**
```kotlin
/**
* V2 Builder配置
*/
data class BuilderConfigV2(
val builderCode: String, // bytes32格式的builder代码
val enabled: Boolean = true
)
```
#### 4.3 更新使用Builder认证的代码
**检查并更新所有使用 `BuilderAuthInterceptor` 的地方:**
```kotlin
// 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相关端点
**移除以下可能不再需要的端点:**
```kotlin
// 删除这些端点(V2不再需要)
// @POST("/auth/builder-api-key")
// suspend fun createBuilderApiKey(): Response<BuilderApiKeyResponse>
// @GET("/auth/builder-api-key")
// suspend fun getBuilderApiKeys(): Response<List<BuilderApiKey>>
```
#### 5.2 更新费率查询端点(如需要)
**检查是否需要更新费率查询:**
```kotlin
// 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**
```kotlin
// 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()` 操作,改为运营商控制:**
```kotlin
// 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 相关逻辑:
```kotlin
// 移除以下V1代码:
// - nonce 参数传递
// - nonce 追踪/缓存逻辑
// - getNonce() 调用
// V2: 直接使用 timestamp
val timestamp = System.currentTimeMillis().toString()
```
---
### 7. 抵押品代币更新 (USDC.e → pUSD)
V2 使用 pUSD (Polymarket USD) 替代 USDC.e
```kotlin
// 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` — 执行 wrap
- `POST /api/accounts/usdce-balance` — 查询 USDC.e 余额
---
### 8. 迁移期间重要注意事项
**迁移当天(2026年4月28日 ~11:00 UTC)的关键变化:**
- **所有挂单将被清空。** 迁移完成后必须重新下单。
- V1 SDK/客户端将立即停止工作,无向后兼容性。
- 预计约1小时停机时间,交易暂停。
- 通过 Discord、Telegram 和 status.polymarket.com 获取确切维护窗口开始时间。
```kotlin
// 迁移前:停止自动交易策略
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编码测试
```kotlin
@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 订单签名测试
```kotlin
@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 订单创建测试
```kotlin
@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功能测试
```kotlin
@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. 测试市场数据
根据官方文档,使用以下市场进行测试:
```kotlin
// 测试市场数据
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 关键监控指标
```kotlin
// 添加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 告警配置
```yaml
# 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. 回滚步骤
1. **立即停止交易**
```kotlin
tradingService.pauseAllTrading("执行V2回滚")
```
2. **切换回V1代码**
```bash
git checkout main # 回到迁移前的分支
./deploy.sh # 重新部署
```
3. **验证V1功能**
```bash
curl -X POST https://api.example.com/health/check
```
4. **恢复交易**
```kotlin
tradingService.resumeAllTrading("回滚完成,恢复V1交易")
```
### 3. 回滚后验证
- [ ] 检查系统日志
- [ ] 验证V1订单功能正常
- [ ] 检查数据库一致性
- [ ] 通知用户回滚完成
- [ ] 分析失败原因
---
## 📚 参考资源
### 官方文档
- [Polymarket CLOB V2迁移指南](https://docs.polymarket.com/v2-migration)
- [CLOB API文档](https://docs.polymarket.com/developers/CLOB)
- [EIP-712标准](https://eips.ethereum.org/EIPS/eip-712)
### TypeScript SDK参考
- [clob-client-v2 GitHub](https://github.com/Polymarket/clob-client-v2)
- [V2订单类型定义](https://github.com/Polymarket/clob-client-v2/blob/main/src/types/ordersV2.ts)
### 合约地址
- [V2合约地址列表](https://docs.polymarket.com/contracts)
- [pUSD合约信息](https://docs.polymarket.com/pusd)
### 测试资源
- [测试市场列表](https://docs.polymarket.com/v2-migration#test-markets)
- [Polymarket Discord](https://discord.gg/polymarket)
- [状态页面](https://status.polymarket.com)
---
## 🆘 支持和联系
### 技术支持
- **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迁移指南编写,如有疑问请参考官方文档或联系技术支持。*