22a6544373
- 更新 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>
1085 lines
33 KiB
Markdown
1085 lines
33 KiB
Markdown
# 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, // 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`
|
||
|
||
```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
|
||
/**
|
||
* 签名订单 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`
|
||
|
||
**删除整个文件:**
|
||
```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迁移指南编写,如有疑问请参考官方文档或联系技术支持。*
|