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>
This commit is contained in:
+2
-1
@@ -112,4 +112,5 @@ __pycache__/
|
||||
clob-client/
|
||||
builder-relayer-client/
|
||||
landing-page/
|
||||
|
||||
clob-client-v2/
|
||||
settings.local.json
|
||||
File diff suppressed because it is too large
Load Diff
@@ -73,7 +73,7 @@ interface PolymarketClobApi {
|
||||
*/
|
||||
@POST("/orders/batch")
|
||||
suspend fun createOrdersBatch(
|
||||
@Body request: CreateOrdersBatchRequest
|
||||
@Body request: List<NewOrderRequest>
|
||||
): Response<List<OrderResponse>>
|
||||
|
||||
/**
|
||||
@@ -174,22 +174,25 @@ interface PolymarketClobApi {
|
||||
// 请求和响应数据类
|
||||
|
||||
/**
|
||||
* 签名的订单对象(根据官方文档)
|
||||
* 参考: https://docs.polymarket.com/developers/CLOB/orders/create-order
|
||||
* V2 签名的订单对象
|
||||
* EIP-712 签名字段: salt, maker, signer, tokenId, makerAmount, takerAmount, side, signatureType, timestamp, metadata, builder
|
||||
* API payload 额外字段: taker, expiration (不在 EIP-712 签名中,但 API 请求需要)
|
||||
* 参考: clob-client-v2/src/types/ordersV2.ts NewOrderV2
|
||||
*/
|
||||
data class SignedOrderObject(
|
||||
val salt: Long, // random salt used to create unique order
|
||||
val maker: String, // maker address (funder)
|
||||
val signer: String, // signing address
|
||||
val taker: String, // taker address (operator)
|
||||
val taker: String, // taker address (zero address for public orders, NOT in EIP-712 signing)
|
||||
val tokenId: String, // ERC1155 token ID of conditional token being traded
|
||||
val makerAmount: String, // maximum amount maker is willing to spend
|
||||
val takerAmount: String, // minimum amount taker will pay the maker in return
|
||||
val expiration: String, // unix expiration timestamp
|
||||
val nonce: String, // maker's exchange nonce of the order is associated
|
||||
val feeRateBps: String, // fee rate basis points as required by the operator
|
||||
val side: String, // buy or sell enum index ("BUY" or "SELL")
|
||||
val signatureType: Int, // signature type enum index
|
||||
val timestamp: String, // order creation time in milliseconds (V2)
|
||||
val expiration: String, // expiration timestamp unix seconds, "0" = no expiration (NOT in EIP-712 signing)
|
||||
val metadata: String, // bytes32 metadata (V2)
|
||||
val builder: String, // bytes32 builder code (V2)
|
||||
val signature: String // hex encoded signature
|
||||
)
|
||||
|
||||
@@ -198,10 +201,11 @@ data class SignedOrderObject(
|
||||
* 参考: https://docs.polymarket.com/developers/CLOB/orders/create-order
|
||||
*/
|
||||
data class NewOrderRequest(
|
||||
val order: SignedOrderObject, // signed object
|
||||
val order: SignedOrderObject, // V2 signed object
|
||||
val owner: String, // api key of order owner
|
||||
val orderType: String, // order type ("FOK", "GTC", "GTD", "FAK")
|
||||
val deferExec: Boolean = false // defer execution flag
|
||||
val deferExec: Boolean = false, // defer execution
|
||||
val postOnly: Boolean = false // post only (maker-only)
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -235,26 +239,6 @@ data class NewOrderResponse(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 旧的订单请求格式(已废弃,保留用于兼容)
|
||||
* @deprecated 使用 NewOrderRequest 代替
|
||||
*/
|
||||
@Deprecated("使用 NewOrderRequest 代替,需要签名的订单对象")
|
||||
data class CreateOrderRequest(
|
||||
val market: String? = null, // condition ID(可选,如果提供tokenId则不需要)
|
||||
val token_id: String? = null, // token ID(可选,如果提供market则不需要)
|
||||
val side: String, // "BUY" or "SELL"
|
||||
val price: String,
|
||||
val size: String,
|
||||
val type: String = "LIMIT",
|
||||
val expiration: Long? = null
|
||||
)
|
||||
|
||||
@Deprecated("使用 NewOrderRequest 代替")
|
||||
data class CreateOrdersBatchRequest(
|
||||
val orders: List<NewOrderRequest>
|
||||
)
|
||||
|
||||
data class CancelOrdersBatchRequest(
|
||||
val orderIds: List<String>
|
||||
)
|
||||
|
||||
@@ -9,7 +9,7 @@ object PolymarketConstants {
|
||||
/**
|
||||
* Polymarket CLOB API 基础 URL
|
||||
*/
|
||||
const val CLOB_BASE_URL = "https://clob.polymarket.com"
|
||||
const val CLOB_BASE_URL = "https://clob-v2.polymarket.com"
|
||||
|
||||
/**
|
||||
* Polymarket RTDS WebSocket URL
|
||||
|
||||
+48
@@ -572,5 +572,53 @@ class AccountController(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 USDC.e wrap 为 pUSD(V2 迁移)
|
||||
*/
|
||||
@PostMapping("/wrap-to-pusd")
|
||||
fun wrapToPusd(@RequestBody request: Map<String, Any>): ResponseEntity<ApiResponse<Map<String, String?>>> {
|
||||
return try {
|
||||
val accountId = (request["accountId"] as? Number)?.toLong()
|
||||
?: return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ACCOUNT_ID_INVALID, messageSource = messageSource))
|
||||
val result = runBlocking { accountService.wrapUsdcToPusd(accountId) }
|
||||
result.fold(
|
||||
onSuccess = { txHash ->
|
||||
ResponseEntity.ok(ApiResponse.success(mapOf("transactionHash" to txHash)))
|
||||
},
|
||||
onFailure = { e ->
|
||||
logger.error("USDC.e → pUSD wrap 失败: ${e.message}", e)
|
||||
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, e.message, messageSource))
|
||||
}
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger.error("USDC.e → pUSD wrap 异常: ${e.message}", e)
|
||||
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, e.message, messageSource))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询 USDC.e 余额(V2 迁移用)
|
||||
*/
|
||||
@PostMapping("/usdce-balance")
|
||||
fun getUsdceBalance(@RequestBody request: Map<String, Any>): ResponseEntity<ApiResponse<Map<String, String>>> {
|
||||
return try {
|
||||
val accountId = (request["accountId"] as? Number)?.toLong()
|
||||
?: return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ACCOUNT_ID_INVALID, messageSource = messageSource))
|
||||
val result = runBlocking { accountService.getUsdceBalance(accountId) }
|
||||
result.fold(
|
||||
onSuccess = { balance ->
|
||||
ResponseEntity.ok(ApiResponse.success(mapOf("balance" to balance.toPlainString())))
|
||||
},
|
||||
onFailure = { e ->
|
||||
logger.error("查询 USDC.e 余额失败: ${e.message}", e)
|
||||
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, e.message, messageSource))
|
||||
}
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger.error("查询 USDC.e 余额异常: ${e.message}", e)
|
||||
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, e.message, messageSource))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
+30
-21
@@ -371,8 +371,8 @@ class AccountService(
|
||||
*/
|
||||
private val setupApprovalSpenders = mapOf(
|
||||
"CTF_CONTRACT" to "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045", // Conditional Tokens
|
||||
"CTF_EXCHANGE" to "0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E", // 普通市场交易所
|
||||
"NEG_RISK_EXCHANGE" to "0xC5d563A36AE78145C45a50134d48A1215220f80a", // 负风险市场交易所
|
||||
"CTF_EXCHANGE" to "0xE111180000d2663C0091e4f400237545B87B996B", // 普通市场交易所
|
||||
"NEG_RISK_EXCHANGE" to "0xe2222d279d744050d28e00520010520000310F59", // 负风险市场交易所
|
||||
"NEG_RISK_ADAPTER" to "0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296" // 负风险适配器(非 WCOL 地址)
|
||||
)
|
||||
|
||||
@@ -1244,22 +1244,9 @@ class AccountService(
|
||||
else -> "GTC"
|
||||
}
|
||||
|
||||
// GTC 和 FOK 订单的 expiration 必须为 "0"
|
||||
// 只有 GTD 订单才需要设置具体的过期时间
|
||||
val expiration = "0"
|
||||
|
||||
// 7. 解密私钥
|
||||
val decryptedPrivateKey = decryptPrivateKey(account)
|
||||
|
||||
// 获取费率(根据 Polymarket Maker Rebates Program 要求)
|
||||
val feeRateResult = clobService.getFeeRate(tokenId)
|
||||
val feeRateBps = if (feeRateResult.isSuccess) {
|
||||
feeRateResult.getOrNull()?.toString() ?: "0"
|
||||
} else {
|
||||
logger.warn("获取费率失败,使用默认值 0: tokenId=$tokenId, error=${feeRateResult.exceptionOrNull()?.message}")
|
||||
"0"
|
||||
}
|
||||
|
||||
// 11. 创建并签名订单(使用计算后的卖出数量,按账户钱包类型使用对应 signatureType)
|
||||
val signedOrder = try {
|
||||
orderSigningService.createAndSignOrder(
|
||||
@@ -1269,10 +1256,7 @@ class AccountService(
|
||||
side = "SELL",
|
||||
price = sellPrice,
|
||||
size = sellQuantity.toPlainString(), // 使用计算后的卖出数量
|
||||
signatureType = orderSigningService.getSignatureTypeForWalletType(account.walletType),
|
||||
nonce = "0",
|
||||
feeRateBps = feeRateBps, // 使用动态获取的费率
|
||||
expiration = expiration
|
||||
signatureType = orderSigningService.getSignatureTypeForWalletType(account.walletType)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger.error("创建并签名订单失败", e)
|
||||
@@ -1284,8 +1268,7 @@ class AccountService(
|
||||
val newOrderRequest = com.wrbug.polymarketbot.api.NewOrderRequest(
|
||||
order = signedOrder,
|
||||
owner = account.apiKey, // API Key
|
||||
orderType = orderType,
|
||||
deferExec = false
|
||||
orderType = orderType
|
||||
)
|
||||
|
||||
// 13. 解密 API 凭证并使用账户的API凭证创建订单
|
||||
@@ -1925,6 +1908,32 @@ class AccountService(
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将账户的 USDC.e wrap 为 pUSD
|
||||
*/
|
||||
suspend fun wrapUsdcToPusd(accountId: Long): Result<String?> {
|
||||
val account = accountRepository.findById(accountId).orElse(null)
|
||||
?: return Result.failure(IllegalArgumentException("账户不存在"))
|
||||
if (account.proxyAddress.isBlank()) {
|
||||
return Result.failure(IllegalStateException("账户代理地址不存在"))
|
||||
}
|
||||
val privateKey = cryptoUtils.decrypt(account.privateKey)
|
||||
val walletType = WalletType.fromStringOrDefault(account.walletType, WalletType.SAFE)
|
||||
return blockchainService.wrapUsdcToPusd(privateKey, account.proxyAddress, walletType)
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询 USDC.e 余额(用于迁移提示)
|
||||
*/
|
||||
suspend fun getUsdceBalance(accountId: Long): Result<BigDecimal> {
|
||||
val account = accountRepository.findById(accountId).orElse(null)
|
||||
?: return Result.failure(IllegalArgumentException("账户不存在"))
|
||||
if (account.proxyAddress.isBlank()) {
|
||||
return Result.failure(IllegalStateException("账户代理地址不存在"))
|
||||
}
|
||||
return blockchainService.queryUsdceBalance(account.proxyAddress)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
+73
-2
@@ -39,8 +39,8 @@ class BlockchainService(
|
||||
|
||||
private val logger = LoggerFactory.getLogger(BlockchainService::class.java)
|
||||
|
||||
// USDC 合约地址(Polygon 主网,Polymarket 使用 Polygon)
|
||||
private val usdcContractAddress = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174"
|
||||
// pUSD 合约地址(Polygon 主网,Polymarket 使用 Polygon)
|
||||
private val usdcContractAddress = "0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB"
|
||||
|
||||
// Polymarket Safe 代理工厂合约地址(Polygon 主网,用于 MetaMask 用户)
|
||||
// 合约地址: 0xaacFeEa03eb1561C4e67d661e40682Bd20E3541b
|
||||
@@ -855,6 +855,77 @@ class BlockchainService(
|
||||
}
|
||||
}
|
||||
|
||||
// USDC.e 合约地址(仅用于 wrap 查询)
|
||||
private val usdceContractAddress = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174"
|
||||
|
||||
/**
|
||||
* 查询 USDC.e 余额(用于 wrap 前检查)
|
||||
*/
|
||||
suspend fun queryUsdceBalance(walletAddress: String): Result<BigDecimal> {
|
||||
return try {
|
||||
val rpcApi = polygonRpcApi
|
||||
val functionSelector = "0x70a08231"
|
||||
val paddedAddress = walletAddress.removePrefix("0x").lowercase().padStart(64, '0')
|
||||
val data = functionSelector + paddedAddress
|
||||
val rpcRequest = JsonRpcRequest(
|
||||
method = "eth_call",
|
||||
params = listOf(mapOf("to" to usdceContractAddress, "data" to data), "latest")
|
||||
)
|
||||
val response = rpcApi.call(rpcRequest)
|
||||
if (!response.isSuccessful || response.body() == null) {
|
||||
return Result.failure(Exception("RPC 请求失败"))
|
||||
}
|
||||
val hexBalance = response.body()!!.result?.asString ?: return Result.failure(Exception("result 为空"))
|
||||
val balanceWei = BigInteger(hexBalance.removePrefix("0x"), 16)
|
||||
Result.success(BigDecimal(balanceWei).divide(BigDecimal("1000000")))
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 USDC.e wrap 为 pUSD
|
||||
* 步骤:1) 检查 USDC.e 余额 → 2) approve CollateralOnramp → 3) wrap
|
||||
*/
|
||||
suspend fun wrapUsdcToPusd(
|
||||
privateKey: String,
|
||||
proxyAddress: String,
|
||||
walletType: WalletType
|
||||
): Result<String?> {
|
||||
return try {
|
||||
val balanceResult = queryUsdceBalance(proxyAddress)
|
||||
val balance = balanceResult.getOrElse {
|
||||
logger.warn("查询 USDC.e 余额失败: ${it.message}")
|
||||
return Result.failure(it)
|
||||
}
|
||||
if (balance <= BigDecimal.ZERO) {
|
||||
return Result.success(null)
|
||||
}
|
||||
val wrapAmountWei = balance.movePointRight(6).toBigInteger()
|
||||
logger.info("开始 wrap USDC.e → pUSD: proxy=${proxyAddress.take(10)}..., amount=$balance")
|
||||
|
||||
val unlimitedAllowance = BigInteger.valueOf(2).pow(256).minus(BigInteger.ONE)
|
||||
val approveTx = relayClientService.createUsdceApproveForWrapTx(unlimitedAllowance)
|
||||
val wrapTx = relayClientService.createWrapToPusdTx(proxyAddress, wrapAmountWei)
|
||||
|
||||
val safeTx = relayClientService.createMultiSendTx(listOf(approveTx, wrapTx))
|
||||
val executeResult = relayClientService.execute(privateKey, proxyAddress, safeTx, walletType)
|
||||
executeResult.fold(
|
||||
onSuccess = { txHash ->
|
||||
logger.info("USDC.e → pUSD wrap 成功: txHash=$txHash")
|
||||
Result.success(txHash)
|
||||
},
|
||||
onFailure = { e ->
|
||||
logger.error("USDC.e → pUSD wrap 失败: ${e.message}", e)
|
||||
Result.failure(e)
|
||||
}
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger.error("USDC.e → pUSD wrap 异常: ${e.message}", e)
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取代理钱包的 nonce(用于构建 Safe 交易)
|
||||
*/
|
||||
|
||||
+1
-9
@@ -223,15 +223,7 @@ class PolymarketClobService(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建订单(已废弃,使用 createSignedOrder 代替)
|
||||
* @deprecated 使用 createSignedOrder 代替,需要签名的订单对象
|
||||
*/
|
||||
@Deprecated("使用 createSignedOrder 代替")
|
||||
suspend fun createOrder(request: CreateOrderRequest): Result<OrderResponse> {
|
||||
return Result.failure(UnsupportedOperationException("已废弃,请使用 createSignedOrder 方法"))
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 创建签名的订单
|
||||
* 注意:此方法需要完整的订单签名逻辑,当前为占位实现
|
||||
|
||||
+1
-1
@@ -45,7 +45,7 @@ object OnChainWsUtils {
|
||||
}
|
||||
|
||||
// 合约地址
|
||||
const val USDC_CONTRACT = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174"
|
||||
const val USDC_CONTRACT = "0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB"
|
||||
const val ERC1155_CONTRACT = "0x4d97dcd97ec945f40cf65f87097ace5ea0476045"
|
||||
const val ERC20_TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
|
||||
const val ERC1155_TRANSFER_SINGLE_TOPIC = "0xc3d58168c5ae7397731d063d5bbf3d657854427343f4c083240f7aacaa2d0f62"
|
||||
|
||||
+44
-64
@@ -41,10 +41,9 @@ class OrderSigningService {
|
||||
return if (walletTypeEnum == com.wrbug.polymarketbot.enums.WalletType.MAGIC) 1 else 2
|
||||
}
|
||||
|
||||
// Polygon 主网合约地址(标准 CTF Exchange)
|
||||
private val EXCHANGE_CONTRACT = "0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E"
|
||||
// Neg Risk CTF Exchange(neg risk 市场需用此合约签约,否则服务端返回 invalid signature)
|
||||
private val NEG_RISK_EXCHANGE_CONTRACT = "0xC5d563A36AE78145C45a50134d48A1215220f80a"
|
||||
// V2 合约地址
|
||||
private val EXCHANGE_CONTRACT = "0xE111180000d2663C0091e4f400237545B87B996B"
|
||||
private val NEG_RISK_EXCHANGE_CONTRACT = "0xe2222d279d744050d28e00520010520000310F59"
|
||||
private val CHAIN_ID = 137L
|
||||
|
||||
// USDC 有 6 位小数
|
||||
@@ -156,8 +155,8 @@ class OrderSigningService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建并签名订单
|
||||
*
|
||||
* 创建并签名订单 (V2)
|
||||
*
|
||||
* @param privateKey 私钥(十六进制字符串)
|
||||
* @param makerAddress maker 地址(funder,通常是 proxyAddress)
|
||||
* @param tokenId token ID
|
||||
@@ -165,9 +164,6 @@ class OrderSigningService {
|
||||
* @param price 价格
|
||||
* @param size 数量
|
||||
* @param signatureType 签名类型(1: Email/Magic, 2: Browser Wallet, 0: EOA)
|
||||
* @param nonce nonce(默认 "0")
|
||||
* @param feeRateBps 费率基点(默认 "0")
|
||||
* @param expiration 过期时间戳(秒,0 表示永不过期)
|
||||
* @param exchangeContract 签约用 exchange 合约地址;null 时用标准 CTF Exchange,neg risk 市场需传 Neg Risk Exchange
|
||||
* @return 签名的订单对象
|
||||
*/
|
||||
@@ -178,10 +174,7 @@ class OrderSigningService {
|
||||
side: String,
|
||||
price: String,
|
||||
size: String,
|
||||
signatureType: Int = 2, // 默认使用 Browser Wallet(与正确订单数据一致)
|
||||
nonce: String = "0",
|
||||
feeRateBps: String = "0",
|
||||
expiration: String = "0",
|
||||
signatureType: Int = 2,
|
||||
exchangeContract: String? = null
|
||||
): SignedOrderObject {
|
||||
try {
|
||||
@@ -189,33 +182,32 @@ class OrderSigningService {
|
||||
val cleanPrivateKey = privateKey.removePrefix("0x")
|
||||
val privateKeyBigInt = BigInteger(cleanPrivateKey, 16)
|
||||
val credentials = Credentials.create(privateKeyBigInt.toString(16))
|
||||
// 统一转换为小写,确保与 EIP-712 编码时使用的地址格式一致
|
||||
// EIP-712 编码时地址会被转换为小写,所以订单对象中的地址也应该是小写
|
||||
val signerAddress = credentials.address.lowercase()
|
||||
|
||||
|
||||
// 2. 计算订单金额
|
||||
val amounts = calculateOrderAmounts(side, size, price)
|
||||
|
||||
// 3. 生成 salt(使用时间戳,毫秒)
|
||||
|
||||
// 3. 生成 salt 和 timestamp(V2: timestamp 替代 nonce 保证唯一性)
|
||||
val salt = generateSalt()
|
||||
|
||||
// 4. taker 地址(默认使用零地址)
|
||||
val taker = "0x0000000000000000000000000000000000000000"
|
||||
|
||||
val timestamp = System.currentTimeMillis().toString()
|
||||
|
||||
// 4. V2 字段默认值
|
||||
val metadata = "0x0000000000000000000000000000000000000000000000000000000000000000"
|
||||
val builder = "0x0000000000000000000000000000000000000000000000000000000000000000"
|
||||
|
||||
// 5. 确保 maker 地址也是小写格式
|
||||
val makerAddressLower = makerAddress.lowercase()
|
||||
|
||||
// 打印签名前的订单参数(DEBUG 级别,避免敏感信息泄露)
|
||||
logger.debug("========== 订单签名前参数 ==========")
|
||||
|
||||
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, Expiration: $expiration, Nonce: $nonce, FeeRateBPS: $feeRateBps")
|
||||
logger.debug("Salt: $salt, Timestamp: $timestamp")
|
||||
logger.debug("Signature Type: $signatureType, Chain ID: $CHAIN_ID")
|
||||
|
||||
// 6. 构建订单数据并签名(neg risk 市场需用 NEG_RISK_EXCHANGE_CONTRACT)
|
||||
|
||||
// 6. 构建订单数据并签名
|
||||
val contract = exchangeContract?.takeIf { it.isNotBlank() } ?: EXCHANGE_CONTRACT
|
||||
val signature = signOrder(
|
||||
privateKey = privateKey,
|
||||
@@ -224,44 +216,41 @@ class OrderSigningService {
|
||||
salt = salt,
|
||||
maker = makerAddressLower,
|
||||
signer = signerAddress,
|
||||
taker = taker,
|
||||
tokenId = tokenId,
|
||||
makerAmount = amounts.makerAmount,
|
||||
takerAmount = amounts.takerAmount,
|
||||
expiration = expiration,
|
||||
nonce = nonce,
|
||||
feeRateBps = feeRateBps,
|
||||
side = side.uppercase(),
|
||||
signatureType = signatureType
|
||||
signatureType = signatureType,
|
||||
timestamp = timestamp,
|
||||
metadata = metadata,
|
||||
builder = builder
|
||||
)
|
||||
|
||||
// 7. 创建签名的订单对象
|
||||
// 注意:所有地址字段都使用小写格式,确保与签名时使用的地址一致
|
||||
|
||||
// 7. 创建 V2 签名订单对象
|
||||
return SignedOrderObject(
|
||||
salt = salt,
|
||||
maker = makerAddressLower,
|
||||
signer = signerAddress,
|
||||
taker = taker,
|
||||
taker = "0x0000000000000000000000000000000000000000",
|
||||
tokenId = tokenId,
|
||||
makerAmount = amounts.makerAmount,
|
||||
takerAmount = amounts.takerAmount,
|
||||
expiration = expiration,
|
||||
nonce = nonce,
|
||||
feeRateBps = feeRateBps,
|
||||
side = side.uppercase(),
|
||||
signatureType = signatureType,
|
||||
timestamp = timestamp,
|
||||
expiration = "0",
|
||||
metadata = metadata,
|
||||
builder = builder,
|
||||
signature = signature
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger.error("创建并签名订单失败", e)
|
||||
throw RuntimeException("创建并签名订单失败: ${e.message}", e)
|
||||
logger.error("创建并签名订单失败 (V2)", e)
|
||||
throw RuntimeException("创建并签名订单失败 (V2): ${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 签名订单(EIP-712)
|
||||
*
|
||||
* 参考: @polymarket/order-utils 的 ExchangeOrderBuilder
|
||||
* 签名订单 V2(EIP-712)
|
||||
*/
|
||||
private fun signOrder(
|
||||
privateKey: String,
|
||||
@@ -270,56 +259,47 @@ class OrderSigningService {
|
||||
salt: Long,
|
||||
maker: String,
|
||||
signer: String,
|
||||
taker: String,
|
||||
tokenId: String,
|
||||
makerAmount: String,
|
||||
takerAmount: String,
|
||||
expiration: String,
|
||||
nonce: String,
|
||||
feeRateBps: String,
|
||||
side: String,
|
||||
signatureType: Int
|
||||
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. 编码域分隔符(verifyingContract 显式小写,与 EIP-712 约定一致)
|
||||
val domainSeparator = com.wrbug.polymarketbot.util.Eip712Encoder.encodeExchangeDomain(
|
||||
chainId = chainId,
|
||||
verifyingContract = exchangeContract.lowercase()
|
||||
)
|
||||
|
||||
// 3. 编码订单消息哈希
|
||||
// signatureType:1 = POLY_PROXY (Magic), 2 = POLY_GNOSIS_SAFE (Safe), 0 = EOA
|
||||
val orderHash = com.wrbug.polymarketbot.util.Eip712Encoder.encodeExchangeOrder(
|
||||
salt = salt,
|
||||
maker = maker,
|
||||
signer = signer,
|
||||
taker = taker,
|
||||
tokenId = tokenId,
|
||||
makerAmount = makerAmount,
|
||||
takerAmount = takerAmount,
|
||||
expiration = expiration,
|
||||
nonce = nonce,
|
||||
feeRateBps = feeRateBps,
|
||||
side = side,
|
||||
signatureType = signatureType
|
||||
signatureType = signatureType,
|
||||
timestamp = timestamp,
|
||||
metadata = metadata,
|
||||
builder = builder
|
||||
)
|
||||
|
||||
// 4. 计算完整 EIP-712 结构化数据哈希
|
||||
val structuredHash = com.wrbug.polymarketbot.util.Eip712Encoder.hashStructuredData(
|
||||
domainSeparator = domainSeparator,
|
||||
messageHash = orderHash
|
||||
)
|
||||
|
||||
// 5. 使用私钥签名(needToHash=false,对 32 字节 hash 直接签名)
|
||||
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
|
||||
@@ -328,8 +308,8 @@ class OrderSigningService {
|
||||
|
||||
return "0x$rHex$sHex$vHex"
|
||||
} catch (e: Exception) {
|
||||
logger.error("订单签名失败", e)
|
||||
throw RuntimeException("订单签名失败: ${e.message}", e)
|
||||
logger.error("订单签名失败 (V2)", e)
|
||||
throw RuntimeException("订单签名失败 (V2): ${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-33
@@ -562,16 +562,7 @@ open class CopyOrderTrackingService(
|
||||
// 解密私钥
|
||||
val decryptedPrivateKey = decryptPrivateKey(account)
|
||||
|
||||
// 获取费率(根据 Polymarket Maker Rebates Program 要求)
|
||||
val feeRateResult = clobService.getFeeRate(tokenId)
|
||||
val feeRateBps = if (feeRateResult.isSuccess) {
|
||||
feeRateResult.getOrNull()?.toString() ?: "0"
|
||||
} else {
|
||||
logger.warn("获取费率失败,使用默认值 0: tokenId=$tokenId, error=${feeRateResult.exceptionOrNull()?.message}")
|
||||
"0"
|
||||
}
|
||||
|
||||
logger.info("准备创建买入订单: copyTradingId=${copyTrading.id}, tradeId=${trade.id}, leaderPrice=${trade.price}, tolerance=${copyTrading.priceTolerance}, calculatedPrice=$buyPrice, quantity=$finalBuyQuantity, baseFee=$feeRateBps")
|
||||
logger.info("准备创建买入订单: copyTradingId=${copyTrading.id}, tradeId=${trade.id}, leaderPrice=${trade.price}, tolerance=${copyTrading.priceTolerance}, calculatedPrice=$buyPrice, quantity=$finalBuyQuantity")
|
||||
|
||||
// Neg Risk 市场需用 Neg Risk Exchange 签约,否则服务端返回 invalid signature
|
||||
val negRisk = marketService.getNegRiskByConditionId(effectiveMarketId) == true
|
||||
@@ -594,7 +585,6 @@ open class CopyOrderTrackingService(
|
||||
owner = account.apiKey,
|
||||
copyTradingId = copyTrading.id!!,
|
||||
tradeId = trade.id,
|
||||
feeRateBps = feeRateBps,
|
||||
signatureType = orderSigningService.getSignatureTypeForWalletType(account.walletType)
|
||||
)
|
||||
|
||||
@@ -1018,15 +1008,6 @@ open class CopyOrderTrackingService(
|
||||
// 8. 解密私钥(在方法开始时解密一次,后续复用)
|
||||
val decryptedPrivateKey = decryptPrivateKey(account)
|
||||
|
||||
// 获取费率(根据 Polymarket Maker Rebates Program 要求)
|
||||
val feeRateResult = clobService.getFeeRate(tokenId)
|
||||
val feeRateBps = if (feeRateResult.isSuccess) {
|
||||
feeRateResult.getOrNull()?.toString() ?: "0"
|
||||
} else {
|
||||
logger.warn("获取费率失败,使用默认值 0: tokenId=$tokenId, error=${feeRateResult.exceptionOrNull()?.message}")
|
||||
"0"
|
||||
}
|
||||
|
||||
// 9. Neg Risk 市场需用 Neg Risk Exchange 签约
|
||||
val negRiskSell = marketService.getNegRiskByConditionId(leaderSellTrade.market) == true
|
||||
val exchangeContractSell = orderSigningService.getExchangeContract(negRiskSell)
|
||||
@@ -1042,9 +1023,6 @@ open class CopyOrderTrackingService(
|
||||
price = sellPrice.toString(),
|
||||
size = totalMatched.toString(),
|
||||
signatureType = orderSigningService.getSignatureTypeForWalletType(account.walletType),
|
||||
nonce = "0",
|
||||
feeRateBps = feeRateBps, // 使用动态获取的费率
|
||||
expiration = "0",
|
||||
exchangeContract = exchangeContractSell
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
@@ -1058,8 +1036,7 @@ open class CopyOrderTrackingService(
|
||||
val orderRequest = NewOrderRequest(
|
||||
order = signedOrder,
|
||||
owner = account.apiKey,
|
||||
orderType = "FAK", // Fill-And-Kill
|
||||
deferExec = false
|
||||
orderType = "FAK" // Fill-And-Kill
|
||||
)
|
||||
|
||||
// 12. 创建带认证的CLOB API客户端(使用解密后的凭证)
|
||||
@@ -1084,7 +1061,6 @@ open class CopyOrderTrackingService(
|
||||
owner = account.apiKey,
|
||||
copyTradingId = copyTrading.id,
|
||||
tradeId = leaderSellTrade.id,
|
||||
feeRateBps = feeRateBps,
|
||||
signatureType = orderSigningService.getSignatureTypeForWalletType(account.walletType)
|
||||
)
|
||||
|
||||
@@ -1179,7 +1155,6 @@ open class CopyOrderTrackingService(
|
||||
* @param owner API Key(用于owner字段)
|
||||
* @param copyTradingId 跟单配置ID(用于日志)
|
||||
* @param tradeId Leader 交易ID(用于日志)
|
||||
* @param feeRateBps 费率基点(从API动态获取)
|
||||
* @param signatureType 签名类型(1=Magic, 2=Safe)
|
||||
* @return 成功返回订单ID,失败返回异常
|
||||
*/
|
||||
@@ -1196,7 +1171,6 @@ open class CopyOrderTrackingService(
|
||||
owner: String,
|
||||
copyTradingId: Long,
|
||||
tradeId: String,
|
||||
feeRateBps: String,
|
||||
signatureType: Int
|
||||
): Result<String> {
|
||||
var lastError: Exception? = null
|
||||
@@ -1213,9 +1187,6 @@ open class CopyOrderTrackingService(
|
||||
price = price,
|
||||
size = size,
|
||||
signatureType = signatureType,
|
||||
nonce = "0",
|
||||
feeRateBps = feeRateBps, // 使用动态获取的费率
|
||||
expiration = "0",
|
||||
exchangeContract = exchangeContract
|
||||
)
|
||||
|
||||
@@ -1232,8 +1203,7 @@ open class CopyOrderTrackingService(
|
||||
val orderRequest = NewOrderRequest(
|
||||
order = signedOrder,
|
||||
owner = owner,
|
||||
orderType = "FAK", // Fill-And-Kill
|
||||
deferExec = false
|
||||
orderType = "FAK" // Fill-And-Kill
|
||||
)
|
||||
|
||||
// 调用 API 创建订单
|
||||
|
||||
+4
-2
@@ -815,9 +815,11 @@ class OrderStatusUpdateService(
|
||||
val actualPrice = orderDetail.price?.toSafeBigDecimal() ?: order.price
|
||||
val actualSize = orderDetail.originalSize?.toSafeBigDecimal() ?: order.quantity
|
||||
val actualOutcome = orderDetail.outcome
|
||||
// 使用交易所订单的实际创建时间(API返回秒级,转为毫秒)
|
||||
val actualCreatedAt = if (orderDetail.createdAt > 0) orderDetail.createdAt * 1000 else order.createdAt
|
||||
|
||||
// 更新订单数据(如果实际数据与临时数据不同)
|
||||
val needUpdate = actualPrice != order.price || actualSize != order.quantity
|
||||
val needUpdate = actualPrice != order.price || actualSize != order.quantity || actualCreatedAt != order.createdAt
|
||||
|
||||
// 先保存更新后的订单,标记 notificationSent = true
|
||||
// 这样可以防止其他并发任务重复发送通知
|
||||
@@ -838,7 +840,7 @@ class OrderStatusUpdateService(
|
||||
status = order.status,
|
||||
notificationSent = true, // 标记为已发送通知
|
||||
source = order.source, // 保留原始订单来源
|
||||
createdAt = order.createdAt,
|
||||
createdAt = actualCreatedAt,
|
||||
updatedAt = System.currentTimeMillis()
|
||||
)
|
||||
|
||||
|
||||
+6
-26
@@ -61,7 +61,6 @@ private data class PeriodContext(
|
||||
val apiSecretDecrypted: String,
|
||||
val apiPassphraseDecrypted: String,
|
||||
val clobApi: PolymarketClobApi,
|
||||
val feeRateByTokenId: Map<String, String>,
|
||||
val signatureType: Int,
|
||||
val tokenIds: List<String>,
|
||||
val marketTitle: String?
|
||||
@@ -152,9 +151,6 @@ class CryptoTailStrategyExecutionService(
|
||||
}
|
||||
|
||||
val clobApi = retrofitFactory.createClobApi(account.apiKey, apiSecret, apiPassphrase, account.walletAddress)
|
||||
val feeRateByTokenId = tokenIds.associate { tokenId ->
|
||||
tokenId to (clobService.getFeeRate(tokenId).getOrNull()?.toString() ?: "0")
|
||||
}
|
||||
val signatureType = orderSigningService.getSignatureTypeForWalletType(account.walletType)
|
||||
|
||||
if (strategy.amountMode.uppercase() != "RATIO" && strategy.amountValue < MIN_ORDER_USDC) return null
|
||||
@@ -167,7 +163,6 @@ class CryptoTailStrategyExecutionService(
|
||||
apiSecretDecrypted = apiSecret,
|
||||
apiPassphraseDecrypted = apiPassphrase,
|
||||
clobApi = clobApi,
|
||||
feeRateByTokenId = feeRateByTokenId,
|
||||
signatureType = signatureType,
|
||||
tokenIds = tokenIds,
|
||||
marketTitle = marketTitle
|
||||
@@ -397,7 +392,6 @@ class CryptoTailStrategyExecutionService(
|
||||
}
|
||||
val priceStr = price.toPlainString()
|
||||
val size = computeSize(amountUsdc, price)
|
||||
val feeRateBps = ctx.feeRateByTokenId[tokenId] ?: "0"
|
||||
val signedOrder = orderSigningService.createAndSignOrder(
|
||||
privateKey = ctx.decryptedPrivateKey,
|
||||
makerAddress = ctx.account.proxyAddress,
|
||||
@@ -405,16 +399,12 @@ class CryptoTailStrategyExecutionService(
|
||||
side = "BUY",
|
||||
price = priceStr,
|
||||
size = size,
|
||||
signatureType = ctx.signatureType,
|
||||
nonce = "0",
|
||||
feeRateBps = feeRateBps,
|
||||
expiration = "0"
|
||||
signatureType = ctx.signatureType
|
||||
)
|
||||
val orderRequest = NewOrderRequest(
|
||||
order = signedOrder,
|
||||
owner = ctx.account.apiKey!!,
|
||||
orderType = "FAK",
|
||||
deferExec = false
|
||||
orderType = "FAK"
|
||||
)
|
||||
submitOrderAndSaveRecord(
|
||||
ctx.clobApi,
|
||||
@@ -609,7 +599,6 @@ class CryptoTailStrategyExecutionService(
|
||||
""
|
||||
}
|
||||
val clobApi = retrofitFactory.createClobApi(account.apiKey, apiSecret, apiPassphrase, account.walletAddress)
|
||||
val feeRateBps = clobService.getFeeRate(tokenId).getOrNull()?.toString() ?: "0"
|
||||
val signatureType = orderSigningService.getSignatureTypeForWalletType(account.walletType)
|
||||
|
||||
val signedOrder = orderSigningService.createAndSignOrder(
|
||||
@@ -619,16 +608,12 @@ class CryptoTailStrategyExecutionService(
|
||||
side = "BUY",
|
||||
price = priceStr,
|
||||
size = size,
|
||||
signatureType = signatureType,
|
||||
nonce = "0",
|
||||
feeRateBps = feeRateBps,
|
||||
expiration = "0"
|
||||
signatureType = signatureType
|
||||
)
|
||||
val orderRequest = NewOrderRequest(
|
||||
order = signedOrder,
|
||||
owner = account.apiKey!!,
|
||||
orderType = "FAK",
|
||||
deferExec = false
|
||||
orderType = "FAK"
|
||||
)
|
||||
submitOrderAndSaveRecord(
|
||||
clobApi,
|
||||
@@ -745,7 +730,6 @@ class CryptoTailStrategyExecutionService(
|
||||
|
||||
val priceStr = priceRounded.toPlainString()
|
||||
val sizeStr = size.toPlainString()
|
||||
val feeRateBps = ctx.feeRateByTokenId[tokenId] ?: "0"
|
||||
|
||||
val signedOrder = orderSigningService.createAndSignOrder(
|
||||
privateKey = ctx.decryptedPrivateKey,
|
||||
@@ -754,17 +738,13 @@ class CryptoTailStrategyExecutionService(
|
||||
side = "BUY",
|
||||
price = priceStr,
|
||||
size = sizeStr,
|
||||
signatureType = ctx.signatureType,
|
||||
nonce = "0",
|
||||
feeRateBps = feeRateBps,
|
||||
expiration = "0"
|
||||
signatureType = ctx.signatureType
|
||||
)
|
||||
|
||||
val orderRequest = NewOrderRequest(
|
||||
order = signedOrder,
|
||||
owner = ctx.account.apiKey!!,
|
||||
orderType = "FAK",
|
||||
deferExec = false
|
||||
orderType = "FAK"
|
||||
)
|
||||
|
||||
val orderResult = submitOrderForManualOrder(
|
||||
|
||||
+43
-2
@@ -39,8 +39,14 @@ class RelayClientService(
|
||||
// ConditionalTokens 合约地址
|
||||
private val conditionalTokensAddress = "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045"
|
||||
|
||||
// USDC.e 合约地址(普通市场抵押品)
|
||||
private val usdcContractAddress = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174"
|
||||
// pUSD 合约地址(普通市场抵押品)
|
||||
private val usdcContractAddress = "0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB"
|
||||
|
||||
// USDC.e 合约地址(仅用于 wrap 到 pUSD)
|
||||
private val usdceContractAddress = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174"
|
||||
|
||||
// CollateralOnramp 合约地址(USDC.e → pUSD)
|
||||
private val collateralOnrampAddress = "0x93070a847efEf7F70739046A929D47a521F5B8ee"
|
||||
|
||||
// Neg Risk 市场使用的 WrappedCollateral 合约地址(Polygon,neg-risk-ctf-adapter)
|
||||
private val negRiskWrappedCollateralAddress = "0x3A3BD7bb9528E159577F7C2e685CC81A765002E2"
|
||||
@@ -323,6 +329,41 @@ class RelayClientService(
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 USDC.e approve 交易(用于 wrap 到 pUSD)
|
||||
* 授权 CollateralOnramp 合约花费用户的 USDC.e
|
||||
*/
|
||||
fun createUsdceApproveForWrapTx(amount: BigInteger): SafeTransaction {
|
||||
val functionSelector = EthereumUtils.getFunctionSelector("approve(address,uint256)")
|
||||
val encodedSpender = EthereumUtils.encodeAddress(collateralOnrampAddress)
|
||||
val encodedAmount = EthereumUtils.encodeUint256(amount)
|
||||
val callData = "0x" + functionSelector.removePrefix("0x") + encodedSpender + encodedAmount
|
||||
return SafeTransaction(
|
||||
to = usdceContractAddress,
|
||||
operation = 0,
|
||||
data = callData,
|
||||
value = "0"
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 USDC.e → pUSD wrap 交易
|
||||
* CollateralOnramp.wrap(address _asset, address _to, uint256 _amount)
|
||||
*/
|
||||
fun createWrapToPusdTx(recipientAddress: String, amount: BigInteger): SafeTransaction {
|
||||
val functionSelector = EthereumUtils.getFunctionSelector("wrap(address,address,uint256)")
|
||||
val asset = EthereumUtils.encodeAddress(usdceContractAddress)
|
||||
val to = EthereumUtils.encodeAddress(recipientAddress)
|
||||
val amt = EthereumUtils.encodeUint256(amount)
|
||||
val callData = "0x" + functionSelector.removePrefix("0x") + asset + to + amt
|
||||
return SafeTransaction(
|
||||
to = collateralOnrampAddress,
|
||||
operation = 0,
|
||||
data = callData,
|
||||
value = "0"
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 MultiSend 交易(合并多个 SafeTransaction 为一笔交易)
|
||||
* 参考 TypeScript: builder-relayer-client/src/encode/safe.ts createSafeMultisendTransaction
|
||||
|
||||
@@ -167,9 +167,8 @@ object Eip712Encoder {
|
||||
}
|
||||
|
||||
/**
|
||||
* 编码 ExchangeOrder 域分隔符
|
||||
* 参考: @polymarket/order-utils 的 ExchangeOrderBuilder
|
||||
* Domain: { name: "Polymarket CTF Exchange", version: "1", chainId: chainId, verifyingContract: exchangeContract }
|
||||
* 编码 ExchangeOrder V2 域分隔符
|
||||
* Domain: { name: "Polymarket CTF Exchange", version: "2", chainId: chainId, verifyingContract: exchangeContract }
|
||||
*/
|
||||
fun encodeExchangeDomain(
|
||||
chainId: Long,
|
||||
@@ -184,9 +183,9 @@ object Eip712Encoder {
|
||||
"verifyingContract" to "address"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
val nameHash = encodeString("Polymarket CTF Exchange")
|
||||
val versionHash = encodeString("1")
|
||||
val versionHash = encodeString("2")
|
||||
val chainIdBytes = encodeUint256(BigInteger.valueOf(chainId))
|
||||
val contractBytes = encodeAddress(verifyingContract)
|
||||
|
||||
@@ -201,23 +200,21 @@ object Eip712Encoder {
|
||||
}
|
||||
|
||||
/**
|
||||
* 编码 ExchangeOrder 消息哈希
|
||||
* 参考: @polymarket/order-utils 的 ExchangeOrderBuilder
|
||||
* Order: { salt, maker, signer, taker, tokenId, makerAmount, takerAmount, expiration, nonce, feeRateBps, side, signatureType }
|
||||
* 编码 ExchangeOrder V2 消息哈希
|
||||
* V2 Order: { salt, maker, signer, tokenId, makerAmount, takerAmount, side, signatureType, timestamp, metadata, builder }
|
||||
*/
|
||||
fun encodeExchangeOrder(
|
||||
salt: Long,
|
||||
maker: String,
|
||||
signer: String,
|
||||
taker: String,
|
||||
tokenId: String,
|
||||
makerAmount: String,
|
||||
takerAmount: String,
|
||||
expiration: String,
|
||||
nonce: String,
|
||||
feeRateBps: String,
|
||||
side: String,
|
||||
signatureType: Int
|
||||
signatureType: Int,
|
||||
timestamp: String,
|
||||
metadata: String,
|
||||
builder: String
|
||||
): ByteArray {
|
||||
val orderTypeHash = encodeType(
|
||||
"Order",
|
||||
@@ -225,57 +222,51 @@ object Eip712Encoder {
|
||||
"salt" to "uint256",
|
||||
"maker" to "address",
|
||||
"signer" to "address",
|
||||
"taker" to "address",
|
||||
"tokenId" to "uint256",
|
||||
"makerAmount" to "uint256",
|
||||
"takerAmount" to "uint256",
|
||||
"expiration" to "uint256",
|
||||
"nonce" to "uint256",
|
||||
"feeRateBps" to "uint256",
|
||||
"side" to "uint8",
|
||||
"signatureType" to "uint8"
|
||||
"signatureType" to "uint8",
|
||||
"timestamp" to "uint256",
|
||||
"metadata" to "bytes32",
|
||||
"builder" to "bytes32"
|
||||
)
|
||||
)
|
||||
|
||||
// 编码订单字段
|
||||
|
||||
val saltBytes = encodeUint256(BigInteger.valueOf(salt))
|
||||
val makerBytes = encodeAddress(maker)
|
||||
val signerBytes = encodeAddress(signer)
|
||||
val takerBytes = encodeAddress(taker)
|
||||
val tokenIdBytes = encodeUint256(BigInteger(tokenId))
|
||||
val makerAmountBytes = encodeUint256(BigInteger(makerAmount))
|
||||
val takerAmountBytes = encodeUint256(BigInteger(takerAmount))
|
||||
val expirationBytes = encodeUint256(BigInteger(expiration))
|
||||
val nonceBytes = encodeUint256(BigInteger(nonce))
|
||||
val feeRateBpsBytes = encodeUint256(BigInteger(feeRateBps))
|
||||
|
||||
// side: BUY = 0, SELL = 1 (uint8,但需要编码为 32 字节)
|
||||
|
||||
val sideValue = when (side.uppercase()) {
|
||||
"BUY" -> 0
|
||||
"SELL" -> 1
|
||||
else -> throw IllegalArgumentException("side 必须是 BUY 或 SELL")
|
||||
}
|
||||
// uint8 类型,但 EIP-712 编码时仍需要 32 字节
|
||||
val sideBytes = encodeUint256(BigInteger.valueOf(sideValue.toLong()))
|
||||
val signatureTypeBytes = encodeUint256(BigInteger.valueOf(signatureType.toLong()))
|
||||
|
||||
// 组合所有字段
|
||||
val encoded = ByteArray(32 * 13) // 13 个字段,每个 32 字节
|
||||
|
||||
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'))
|
||||
|
||||
val encoded = ByteArray(32 * 12) // typeHash + 11 个字段
|
||||
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(takerBytes, 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(expirationBytes, 0, encoded, offset, 32); offset += 32
|
||||
System.arraycopy(nonceBytes, 0, encoded, offset, 32); offset += 32
|
||||
System.arraycopy(feeRateBpsBytes, 0, encoded, offset, 32); offset += 32
|
||||
System.arraycopy(sideBytes, 0, encoded, offset, 32); offset += 32
|
||||
System.arraycopy(signatureTypeBytes, 0, encoded, offset, 32)
|
||||
|
||||
System.arraycopy(signatureTypeBytes, 0, encoded, offset, 32); offset += 32
|
||||
System.arraycopy(timestampBytes, 0, encoded, offset, 32); offset += 32
|
||||
System.arraycopy(metadataBytes, 0, encoded, offset, 32); offset += 32
|
||||
System.arraycopy(builderBytes, 0, encoded, offset, 32)
|
||||
|
||||
return keccak256(encoded)
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import java.math.BigInteger
|
||||
object EthereumUtils {
|
||||
|
||||
// Polymarket 合约地址(Polygon 主网)
|
||||
private val COLLATERAL_TOKEN_ADDRESS = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174" // USDC
|
||||
private val COLLATERAL_TOKEN_ADDRESS = "0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB" // pUSD
|
||||
private val CONDITIONAL_TOKENS_ADDRESS = "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045" // ConditionalTokens
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Card, Table, Button, Space, Tag, Popconfirm, message, Typography, Spin, Modal, Descriptions, Divider, Form, Input, Alert, Tooltip, List, Empty } from 'antd'
|
||||
import { PlusOutlined, ReloadOutlined, EditOutlined, CopyOutlined, EyeOutlined, DeleteOutlined, WalletOutlined } from '@ant-design/icons'
|
||||
import { PlusOutlined, ReloadOutlined, EditOutlined, CopyOutlined, EyeOutlined, DeleteOutlined, WalletOutlined, SwapOutlined } from '@ant-design/icons'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAccountStore } from '../store/accountStore'
|
||||
import type { Account } from '../types'
|
||||
@@ -8,6 +8,7 @@ import { useMediaQuery } from 'react-responsive'
|
||||
import { formatUSDC } from '../utils'
|
||||
import AccountImportForm from '../components/AccountImportForm'
|
||||
import AccountSetupStatusBlock from '../components/AccountSetupStatusBlock'
|
||||
import apiService from '../services/api'
|
||||
|
||||
const { Title } = Typography
|
||||
|
||||
@@ -27,6 +28,43 @@ const AccountList: React.FC = () => {
|
||||
const [editLoading, setEditLoading] = useState(false)
|
||||
const [accountImportModalVisible, setAccountImportModalVisible] = useState(false)
|
||||
const [accountImportForm] = Form.useForm()
|
||||
const [wrapLoading, setWrapLoading] = useState<Record<number, boolean>>({})
|
||||
|
||||
const handleWrapToPusd = async (account: Account) => {
|
||||
try {
|
||||
setWrapLoading(prev => ({ ...prev, [account.id]: true }))
|
||||
const res = await apiService.accounts.getUsdceBalance(account.id)
|
||||
if (res.data.code !== 0 || !res.data.data) {
|
||||
message.error(res.data.msg || '查询 USDC.e 余额失败')
|
||||
return
|
||||
}
|
||||
const balance = parseFloat(res.data.data.balance)
|
||||
if (balance <= 0) {
|
||||
message.info('USDC.e 余额为 0,无需迁移')
|
||||
return
|
||||
}
|
||||
Modal.confirm({
|
||||
title: 'USDC.e → pUSD 迁移',
|
||||
content: `检测到 ${balance.toFixed(2)} USDC.e,将全部 wrap 为 pUSD。确认继续?`,
|
||||
okText: '确认迁移',
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
const wrapRes = await apiService.accounts.wrapToPusd(account.id)
|
||||
if (wrapRes.data.code === 0) {
|
||||
const txHash = wrapRes.data.data?.transactionHash
|
||||
message.success(txHash ? `迁移成功,交易: ${txHash.slice(0, 10)}...` : '迁移成功(无需操作)')
|
||||
fetchAccountBalance(account.id)
|
||||
} else {
|
||||
message.error(wrapRes.data.msg || '迁移失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '迁移失败')
|
||||
} finally {
|
||||
setWrapLoading(prev => ({ ...prev, [account.id]: false }))
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchAccounts()
|
||||
@@ -374,6 +412,26 @@ const AccountList: React.FC = () => {
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title="USDC.e → pUSD">
|
||||
<div
|
||||
onClick={() => handleWrapToPusd(record)}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
cursor: wrapLoading[record.id] ? 'wait' : 'pointer',
|
||||
borderRadius: '6px',
|
||||
transition: 'background-color 0.2s'
|
||||
}}
|
||||
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#fff7e6'}
|
||||
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = 'transparent'}
|
||||
>
|
||||
<SwapOutlined style={{ fontSize: '16px', color: '#fa8c16' }} spin={wrapLoading[record.id]} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
<Popconfirm
|
||||
title={t('accountList.deleteConfirm')}
|
||||
description={
|
||||
|
||||
@@ -284,9 +284,21 @@ export const apiService = {
|
||||
/**
|
||||
* 赎回仓位
|
||||
*/
|
||||
redeemPositions: (data: any) =>
|
||||
redeemPositions: (data: any) =>
|
||||
apiClient.post<ApiResponse<any>>('/accounts/positions/redeem', data),
|
||||
|
||||
|
||||
/**
|
||||
* 将 USDC.e wrap 为 pUSD(V2 迁移)
|
||||
*/
|
||||
wrapToPusd: (accountId: number) =>
|
||||
apiClient.post<ApiResponse<{ transactionHash: string | null }>>('/accounts/wrap-to-pusd', { accountId }),
|
||||
|
||||
/**
|
||||
* 查询 USDC.e 余额(V2 迁移用)
|
||||
*/
|
||||
getUsdceBalance: (accountId: number) =>
|
||||
apiClient.post<ApiResponse<{ balance: string }>>('/accounts/usdce-balance', { accountId }),
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user