Compare commits

..

7 Commits

Author SHA1 Message Date
WrBug 6a6af6f5e9 fix: handle neg_risk market in sellPosition
Fixes #38

The sellPosition method was not checking if the market uses Neg Risk
Exchange contract. For neg_risk markets, the order signing must use the
Neg Risk Exchange address, otherwise the order will be rejected by
Polymarket.

The copy trading service already handles this correctly, but the manual
sell position flow was missing this check.
2026-04-29 14:35:43 +08:00
WrBug 4b58b7cd46 Merge pull request #57 from WrBug/dev 2026-04-28 22:32:45 +08:00
WrBug 8ab8ae5205 style: 全局将 USDC 展示替换为 $ 符号
前端所有金额展示从 "100.00 USDC" 改为 "$100.00" 格式,
后端 Telegram 通知模板和错误提示同步更新。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 22:23:49 +08:00
WrBug 0769022878 feat: 添加 CLOB 2.0 迁移弹窗提醒与账户页迁移引导
登录后首次进入显示迁移提醒弹窗,引导用户前往账户页;
账户页首次进入时显示 Alert 提示 USDC.e → pUSD 迁移按钮;
移动端账户卡片补充 swap 操作按钮。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 21:59:15 +08:00
WrBug 06a122dd34 fix: 迁移链路外统一改为仅识别 pUSD
移除链上监听中对 USDC.e 的抵押品匹配,并将解包与审批相关说明统一为 pUSD 语义,避免在非 swap/wrap 流程继续使用 USDC.e。

Made-with: Cursor
2026-04-28 21:40:41 +08:00
WrBug fb6b469e9f fix: 修复 CLOB V2 下代理交易与链上成交解析问题
调整 CLOB API 地址并优化 Magic Proxy 的执行与 gas 限制,避免 wrap 与 relay 调用在 V2 环境下异常回滚。同时补充 pUSD/USDC.e 兼容与 ERC1155-only 成交价格回填逻辑,提升链上成交解析稳定性。

Made-with: Cursor
2026-04-28 21:34:43 +08:00
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
54 changed files with 1905 additions and 441 deletions
+7 -11
View File
@@ -91,11 +91,6 @@ jobs:
env:
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
# 经 env 注入,避免 Issue 标题等含引号/换行/$ 时被写入 run 脚本导致截断或语法错误
ISSUE_NUM: ${{ steps.pr.outputs.ISSUE_NUMBER }}
ISSUE_TITLE: ${{ steps.issue.outputs.title }}
ISSUE_URL: ${{ steps.issue.outputs.url }}
PR_URL: ${{ github.event.pull_request.html_url }}
run: |
# 与 docker-build 一致:未配置则跳过
if [ -z "$TELEGRAM_BOT_TOKEN" ] || [ -z "$TELEGRAM_CHAT_ID" ]; then
@@ -103,12 +98,13 @@ jobs:
exit 0
fi
MESSAGE=$(jq -n -r \
--arg num "$ISSUE_NUM" \
--arg title "$ISSUE_TITLE" \
--arg iu "$ISSUE_URL" \
--arg pu "$PR_URL" \
'"✅ <b>AI 修复的 Issue 已关闭</b>\n\n🔢 <b>Issue:</b> #" + $num + " " + $title + "\n📎 <a href=\"" + $iu + "\">查看 Issue</a>\n🔗 <a href=\"" + $pu + "\">查看 PR</a>"')
ISSUE_NUM="${{ steps.pr.outputs.ISSUE_NUMBER }}"
ISSUE_TITLE="${{ steps.issue.outputs.title }}"
ISSUE_URL="${{ steps.issue.outputs.url }}"
PR_URL="${{ github.event.pull_request.html_url }}"
# 与 docker-build 相同的 HTML 消息格式
MESSAGE="✅ <b>AI 修复的 Issue 已关闭</b>"$'\n'$'\n'"🔢 <b>Issue:</b> #${ISSUE_NUM} ${ISSUE_TITLE}"$'\n'"📎 <a href=\"${ISSUE_URL}\">查看 Issue</a>"$'\n'"🔗 <a href=\"${PR_URL}\">查看 PR</a>"
curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
-H "Content-Type: application/json" \
+2 -1
View File
@@ -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>
)
@@ -572,5 +572,53 @@ class AccountController(
}
}
/**
* 将 USDC.e wrap 为 pUSDV2 迁移)
*/
@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))
}
}
}
@@ -267,7 +267,7 @@ class CryptoTailStrategyController(
"当前周期已下单" -> ErrorCode.PARAM_ERROR
"价格必须在 0~1 之间" -> ErrorCode.PARAM_ERROR
"数量不能少于 1" -> ErrorCode.PARAM_ERROR
"总金额不能少于 1 USDC" -> ErrorCode.PARAM_ERROR
"总金额不能少于 $1" -> ErrorCode.PARAM_ERROR
"总金额超过策略配置的投入金额" -> ErrorCode.PARAM_ERROR
else -> ErrorCode.SERVER_ERROR
}
@@ -365,14 +365,14 @@ class AccountService(
}
/**
* Polymarket 代币批准检查:USDC.e 需授权的 spender 合约地址(Polygon 主网)
* Polymarket 代币批准检查:pUSD 需授权的 spender 合约地址(Polygon 主网)
* 来源:Polymarket/magic-safe-builder-example README §6 Token Approvals
* 及 neg-risk-ctf-adapter 仓库 addresses.json (chainId 137)
*/
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 地址)
)
@@ -941,7 +941,7 @@ class AccountService(
}
/**
* 轮询用:遍历所有账户,对代理地址 WCOL 余额 > 0 的执行解包为 USDC.e
* 轮询用:遍历所有账户,对代理地址 WCOL 余额 > 0 的执行解包。
* 由 WcolUnwrapJobService 每 20 秒调用,赎回后无需在赎回流程内等待确认与解包。
*/
suspend fun runWcolUnwrapForAllAccounts() {
@@ -1244,23 +1244,17 @@ 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. 检查市场是否为 Neg Risk 市场,获取正确的 Exchange 合约地址
val negRisk = marketService.getNegRiskByConditionId(request.marketId) == true
val exchangeContract = orderSigningService.getExchangeContract(negRisk)
if (negRisk) {
logger.debug("市场为 Neg Risk,使用 Neg Risk Exchange 签约: conditionId=${request.marketId}")
}
// 11. 创建并签名订单(使用计算后的卖出数量,按账户钱包类型使用对应 signatureType
// 12. 创建并签名订单(使用计算后的卖出数量,按账户钱包类型使用对应 signatureType
val signedOrder = try {
orderSigningService.createAndSignOrder(
privateKey = decryptedPrivateKey,
@@ -1270,22 +1264,19 @@ class AccountService(
price = sellPrice,
size = sellQuantity.toPlainString(), // 使用计算后的卖出数量
signatureType = orderSigningService.getSignatureTypeForWalletType(account.walletType),
nonce = "0",
feeRateBps = feeRateBps, // 使用动态获取的费率
expiration = expiration
exchangeContract = exchangeContract
)
} catch (e: Exception) {
logger.error("创建并签名订单失败", e)
return Result.failure(Exception("创建并签名订单失败: ${e.message}"))
}
// 12. 构建订单请求
// 13. 构建订单请求
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 +1916,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)
}
}
@@ -11,7 +11,7 @@ import org.springframework.stereotype.Service
/**
* WCOL 解包轮询任务
* 每 20 秒轮询一次,遍历所有账户的代理地址:若 WCOL 余额 > 0 则解包为 USDC.e
* 每 20 秒轮询一次,遍历所有账户的代理地址:若 WCOL 余额 > 0 则执行解包。
* 同一时间仅允许单次执行;若上次执行未结束则本次忽略(与现有轮询逻辑一致)。
* 若未配置 Builder API Key,直接跳过本轮(解包依赖 Relayer Gasless,未配置则无法执行)。
*/
@@ -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
@@ -56,7 +56,7 @@ class BlockchainService(
// ConditionalTokens 合约地址(Polygon 主网)
private val conditionalTokensAddress = "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045"
// Neg Risk WrappedCollateral 合约地址(Polygon,解包后得 USDC.e
// Neg Risk WrappedCollateral 合约地址(Polygon
private val wcolContractAddress = "0x3A3BD7bb9528E159577F7C2e685CC81A765002E2"
// 空集合ID(用于计算collectionId
@@ -812,11 +812,11 @@ class BlockchainService(
}
/**
* 将代理钱包内的 WCOL 解包为 USDC.e(解包后转入代理地址)
* 赎回 Neg Risk 仓位后到账为 WCOL,调用此方法可转为 USDC.e 以便显示/使用
* 将代理钱包内的 WCOL 执行解包(解包后转入代理地址)
* 赎回 Neg Risk 仓位后到账为 WCOL,调用此方法可执行解包后续资产处理
*
* Safe 与 Magic 使用同一套逻辑:同一 [createUnwrapWcolTx] + [RelayClientService.execute]
* Safe 走 execTransactionMagic 走 PROXY 编码,最终均为代理合约调用 WCOL.unwrap(proxyAddress, amount)USDC.e 转入 proxyAddress
* Safe 走 execTransactionMagic 走 PROXY 编码,最终均为代理合约调用 WCOL.unwrap(proxyAddress, amount)。
*
* @param privateKey 主钱包私钥
* @param proxyAddress 代理地址(Safe 或 Magic 代理)
@@ -855,6 +855,99 @@ 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)
if (walletType == WalletType.MAGIC) {
// MAGIC 账户走 PROXY 时,不使用 Safe MultiSend(delegatecall)
// 改为顺序执行两笔 CALL,避免内层 delegatecall 回滚导致“外层成功但业务失败”。
val approveResult = relayClientService.execute(privateKey, proxyAddress, approveTx, walletType)
val approveHash = approveResult.getOrElse {
logger.error("USDC.e approve 失败: ${it.message}", it)
return Result.failure(it)
}
logger.info("USDC.e approve 成功: txHash=$approveHash")
val wrapResult = relayClientService.execute(privateKey, proxyAddress, wrapTx, walletType)
wrapResult.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)
}
)
} else {
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 交易)
*/
@@ -223,15 +223,7 @@ class PolymarketClobService(
}
}
/**
* 创建订单已废弃使用 createSignedOrder 代替
* @deprecated 使用 createSignedOrder 代替需要签名的订单对象
*/
@Deprecated("使用 createSignedOrder 代替")
suspend fun createOrder(request: CreateOrderRequest): Result<OrderResponse> {
return Result.failure(UnsupportedOperationException("已废弃,请使用 createSignedOrder 方法"))
}
/**
* 创建签名的订单
* 注意此方法需要完整的订单签名逻辑当前为占位实现
@@ -45,7 +45,9 @@ object OnChainWsUtils {
}
// 合约地址
const val USDC_CONTRACT = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174"
const val PUSD_CONTRACT = "0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB" // V2 pUSD
const val USDC_CONTRACT = PUSD_CONTRACT // 默认使用 pUSD
private val COLLATERAL_CONTRACTS = setOf(PUSD_CONTRACT.lowercase())
const val ERC1155_CONTRACT = "0x4d97dcd97ec945f40cf65f87097ace5ea0476045"
const val ERC20_TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
const val ERC1155_TRANSFER_SINGLE_TOPIC = "0xc3d58168c5ae7397731d063d5bbf3d657854427343f4c083240f7aacaa2d0f62"
@@ -96,8 +98,8 @@ object OnChainWsUtils {
val t0 = topics[0].lowercase()
val data = log.get("data")?.asString ?: "0x"
// USDC ERC20 Transfer
if (address == USDC_CONTRACT.lowercase() && t0 == ERC20_TRANSFER_TOPIC && topics.size >= 3) {
// 抵押品 ERC20 Transfer(当前仅匹配 pUSD
if (address in COLLATERAL_CONTRACTS && t0 == ERC20_TRANSFER_TOPIC && topics.size >= 3) {
val from = topicToAddress(topics[1])
val to = topicToAddress(topics[2])
val value = hexToBigInt(data)
@@ -149,6 +151,42 @@ object OnChainWsUtils {
return Pair(erc20, erc1155)
}
/**
* 通过 CLOB 交易历史获取 Leader 真实成交价用于无 USDC 转账的 ERC1155-only 场景
* 查询该 token 最近的成交记录匹配 side 方向的第一条作为成交价
* 返回 usdcRaw = price × sizeRaw
*/
private suspend fun fetchEstimatedUsdcRaw(
tokenId: String,
side: String,
sizeRaw: BigInteger,
retrofitFactory: RetrofitFactory
): BigInteger? {
return try {
val clobApi = retrofitFactory.createClobApiWithoutAuth()
val response = clobApi.getTrades(asset_id = tokenId)
if (!response.isSuccessful || response.body() == null) {
logger.warn("CLOB 交易历史查询失败: tokenId=$tokenId, code=${response.code()}")
return null
}
val trades = response.body()!!.data
val clobSide = side.lowercase()
val matchedTrade = trades.firstOrNull { it.side.equals(clobSide, ignoreCase = true) }
if (matchedTrade == null) {
logger.warn("CLOB 交易历史中未找到匹配成交: tokenId=$tokenId, side=$clobSide, totalTrades=${trades.size}")
return null
}
val price = matchedTrade.price.toBigDecimal()
val usdcRaw = price.multiply(sizeRaw.toBigDecimal())
.setScale(0, java.math.RoundingMode.DOWN).toBigInteger()
logger.debug("CLOB 交易历史估算: tokenId=$tokenId, side=$clobSide, tradePrice=${matchedTrade.price}, tradeSize=${matchedTrade.size}, sizeRaw=$sizeRaw, usdcRaw=$usdcRaw")
usdcRaw
} catch (e: Exception) {
logger.warn("获取 CLOB 交易历史失败: tokenId=$tokenId, side=$side, error=${e.message}")
null
}
}
/**
* Transfer 日志解析交易信息
*/
@@ -205,6 +243,32 @@ object OnChainWsUtils {
asset = bestOutId
sizeRaw = bestOutVal
usdcRaw = usdcIn
} else if (bestInId != null && bestInVal > BigInteger.ZERO && bestOutId == null
&& usdcOut == BigInteger.ZERO && usdcIn == BigInteger.ZERO
) {
// BUY(无 USDC: 只收到 ERC1155 token,无 USDC 流动(CLOB 内部结算等场景)
side = "BUY"
asset = bestInId
sizeRaw = bestInVal
usdcRaw = fetchEstimatedUsdcRaw(bestInId.toString(), "BUY", bestInVal, retrofitFactory)
?: run {
logger.warn("无法获取估算价格(ERC1155-only BUY: txHash=$txHash, tokenId=$bestInId")
return null
}
logger.debug("ERC1155-only BUY: txHash=$txHash, tokenId=$bestInId, sizeRaw=$sizeRaw, usdcRaw=$usdcRaw")
} else if (bestOutId != null && bestOutVal > BigInteger.ZERO && bestInId == null
&& usdcOut == BigInteger.ZERO && usdcIn == BigInteger.ZERO
) {
// SELL(无 USDC: 只发出 ERC1155 token,无 USDC 流动
side = "SELL"
asset = bestOutId
sizeRaw = bestOutVal
usdcRaw = fetchEstimatedUsdcRaw(bestOutId.toString(), "SELL", bestOutVal, retrofitFactory)
?: run {
logger.warn("无法获取估算价格(ERC1155-only SELL: txHash=$txHash, tokenId=$bestOutId")
return null
}
logger.debug("ERC1155-only SELL: txHash=$txHash, tokenId=$bestOutId, sizeRaw=$sizeRaw, usdcRaw=$usdcRaw")
} else {
// 无法判断交易方向
logger.debug("无法判断交易方向: txHash=$txHash, bestInId=$bestInId, bestInVal=$bestInVal, bestOutId=$bestOutId, bestOutVal=$bestOutVal, usdcOut=$usdcOut, usdcIn=$usdcIn")
@@ -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 Exchangeneg 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 Exchangeneg 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 和 timestampV2: 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
* 签名订单 V2EIP-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. 编码订单消息哈希
// signatureType1 = 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)
}
}
@@ -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 创建订单
@@ -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()
)
@@ -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,
@@ -717,7 +702,7 @@ class CryptoTailStrategyExecutionService(
val amountUsdc = priceRounded.multi(size).setScale(2, RoundingMode.HALF_UP)
if (amountUsdc < BigDecimal.ONE) {
return Result.failure(IllegalArgumentException("总金额不能少于 1 USDC"))
return Result.failure(IllegalArgumentException("总金额不能少于 \$1"))
}
val mutex = getTriggerMutex(strategy.id!!, request.periodStartUnix)
@@ -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(
@@ -169,9 +169,9 @@ class NotificationTemplateService(
方向: <b>{{side}}</b>
价格: <code>{{price}}</code>
数量: <code>{{quantity}}</code> shares
金额: <code>{{amount}}</code> USDC
金额: <code>${'$'}{{amount}}</code>
账户: {{account_name}}
可用余额: <code>{{available_balance}}</code> USDC
可用余额: <code>${'$'}{{available_balance}}</code>
时间: <code>{{time}}</code>
""".trimIndent(),
@@ -184,7 +184,7 @@ class NotificationTemplateService(
方向: <b>{{side}}</b>
价格: <code>{{price}}</code>
数量: <code>{{quantity}}</code> shares
金额: <code>{{amount}}</code> USDC
金额: <code>${'$'}{{amount}}</code>
账户: {{account_name}}
<b>错误信息</b>
@@ -201,7 +201,7 @@ class NotificationTemplateService(
方向: <b>{{side}}</b>
价格: <code>{{price}}</code>
数量: <code>{{quantity}}</code> shares
金额: <code>{{amount}}</code> USDC
金额: <code>${'$'}{{amount}}</code>
账户: {{account_name}}
<b>过滤类型</b> <code>{{filter_type}}</code>
@@ -222,7 +222,7 @@ class NotificationTemplateService(
方向: <b>{{side}}</b>
价格: <code>{{price}}</code>
数量: <code>{{quantity}}</code> shares
金额: <code>{{amount}}</code> USDC
金额: <code>${'$'}{{amount}}</code>
账户: {{account_name}}
时间: <code>{{time}}</code>
@@ -233,8 +233,8 @@ class NotificationTemplateService(
📊 <b>赎回信息</b>
账户: {{account_name}}
交易哈希: <code>{{transaction_hash}}</code>
赎回总价值: <code>{{total_value}}</code> USDC
可用余额: <code>{{available_balance}}</code> USDC
赎回总价值: <code>${'$'}{{total_value}}</code>
可用余额: <code>${'$'}{{available_balance}}</code>
时间: <code>{{time}}</code>
""".trimIndent(),
@@ -246,7 +246,7 @@ class NotificationTemplateService(
账户: {{account_name}}
交易哈希: <code>{{transaction_hash}}</code>
可用余额: <code>{{available_balance}}</code> USDC
可用余额: <code>${'$'}{{available_balance}}</code>
时间: <code>{{time}}</code>
""".trimIndent()
@@ -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 合约地址(Polygonneg-risk-ctf-adapter
private val negRiskWrappedCollateralAddress = "0x3A3BD7bb9528E159577F7C2e685CC81A765002E2"
@@ -51,7 +57,9 @@ class RelayClientService(
// Polygon PROXYMagic)合约地址,参考 builder-relayer-client config
private val proxyFactoryAddress = "0xaB45c5A4B0c941a2F231C04C3f49182e1A254052"
private val relayHubAddress = "0xD216153c06E857cD7f72665E0aF1d7D82172F494"
private val defaultProxyGasLimit = "10000000"
// PROXY relayCall 内层 gasLimit(签名参数)不能给过大值,否则 RelayHub 会因 gasleft 校验失败回滚。
private val defaultProxyGasLimit = "2400000"
private val maxProxyGasLimit = BigInteger.valueOf(2400000)
// Safe MultiSend 合约地址(Polygon 主网)
private val safeMultisendAddress = "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761"
@@ -282,14 +290,14 @@ class RelayClientService(
}
/**
* 创建 WCOL 解包交易 Wrapped Collateral 解包为 USDC.e
* 创建 WCOL 解包交易 Wrapped Collateral 执行解包
* 合约: Neg Risk WrappedCollateral 0x3A3BD7bb9528E159577F7C2e685CC81A765002E2
* 方法: unwrap(address _to, uint256 _amount)解包后 USDC.e 转到 _to
* 方法: unwrap(address _to, uint256 _amount)
*
* Safe Magic 共用此交易对象Safe [executeViaBuilderRelayer] / [executeManually]execTransaction
* Magic [executeViaBuilderRelayerProxy]encodeProxyTransactionData语义一致
*
* @param toAddress 接收 USDC.e 的地址通常为 proxy 自身使余额留在代理钱包
* @param toAddress 接收解包资产的地址通常为 proxy 自身使余额留在代理钱包
* @param amountWei WCOL 数量6 位小数对应的 raw balanceOf 返回一致
* @return Safe 交易对象
*/
@@ -323,6 +331,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
@@ -489,7 +532,16 @@ class RelayClientService(
// 估算 gas limit(参考 builder-relayer-client builder/proxy.ts getGasLimit
val gasLimit = try {
estimateProxyGasLimit(fromAddress, proxyFactoryAddress, proxyCallData)
val estimatedGasLimit = estimateProxyGasLimit(fromAddress, proxyFactoryAddress, proxyCallData)
val estimatedBigInt = BigInteger(estimatedGasLimit)
if (estimatedBigInt > maxProxyGasLimit) {
logger.warn(
"估算 PROXY gas limit 过大,进行截断: estimated=$estimatedGasLimit, capped=$maxProxyGasLimit"
)
maxProxyGasLimit.toString()
} else {
estimatedGasLimit
}
} catch (e: Exception) {
logger.warn("估算 PROXY gas limit 失败,使用默认值: ${e.message}", e)
defaultProxyGasLimit
@@ -691,7 +691,7 @@ class TelegramNotificationService(
$sideLabel: <b>$sideDisplay</b>
$priceLabel: <code>$priceDisplay</code>
$quantityLabel: <code>$sizeDisplay</code> shares
$amountLabel: <code>$amountDisplay</code> USDC
$amountLabel: <code>${'$'}$amountDisplay</code>
$accountLabel: $escapedAccountInfo
<b>$filterTypeLabel</b> <code>$filterTypeDisplay</code>
@@ -1169,9 +1169,9 @@ class TelegramNotificationService(
} else {
balanceDecimal.stripTrailingZeros()
}
"\n$availableBalanceLabel: <code>${formatted.toPlainString()}</code> USDC"
"\n$availableBalanceLabel: <code>${'$'}${formatted.toPlainString()}</code>"
} catch (e: Exception) {
"\n$availableBalanceLabel: <code>$availableBalance</code> USDC"
"\n$availableBalanceLabel: <code>${'$'}$availableBalance</code>"
}
} else {
""
@@ -1185,7 +1185,7 @@ class TelegramNotificationService(
$sideLabel: <b>$sideDisplay</b>
$priceLabel: <code>$priceDisplay</code>
$quantityLabel: <code>$sizeDisplay</code> shares
$amountLabel: <code>$amountDisplay</code> USDC
$amountLabel: <code>${'$'}$amountDisplay</code>
$accountLabel: $escapedAccountInfo$escapedCopyTradingInfo$availableBalanceDisplay
$timeLabel: <code>$time</code>"""
@@ -1264,7 +1264,7 @@ class TelegramNotificationService(
$sideLabel: <b>$sideDisplay</b>
$priceLabel: <code>$priceDisplay</code>
$quantityLabel: <code>$sizeDisplay</code> shares
$amountLabel: <code>$amountDisplay</code> USDC
$amountLabel: <code>${'$'}$amountDisplay</code>
$accountLabel: $escapedAccountInfo
$timeLabel: <code>$time</code>"""
@@ -1381,7 +1381,7 @@ class TelegramNotificationService(
$sideLabel: <b>$sideDisplay</b>
$priceLabel: <code>$priceDisplay</code>
$quantityLabel: <code>$sizeDisplay</code> shares
$amountLabel: <code>$amountDisplay</code> USDC
$amountLabel: <code>${'$'}$amountDisplay</code>
$accountLabel: $escapedAccountInfo
<b>$errorInfo</b>
@@ -1515,7 +1515,7 @@ class TelegramNotificationService(
} catch (e: Exception) {
position.value
}
"${position.marketId.substring(0, 8)}... (${position.side}): $quantityDisplay shares = $valueDisplay USDC"
"${position.marketId.substring(0, 8)}... (${position.side}): $quantityDisplay shares = ${'$'}$valueDisplay"
}
// 格式化可用余额
@@ -1527,9 +1527,9 @@ class TelegramNotificationService(
} else {
balanceDecimal.stripTrailingZeros()
}
"\n$availableBalanceLabel: <code>${formatted.toPlainString()}</code> USDC"
"\n$availableBalanceLabel: <code>${'$'}${formatted.toPlainString()}</code>"
} catch (e: Exception) {
"\n$availableBalanceLabel: <code>$availableBalance</code> USDC"
"\n$availableBalanceLabel: <code>${'$'}$availableBalance</code>"
}
} else {
""
@@ -1540,7 +1540,7 @@ class TelegramNotificationService(
📊 <b>$redeemInfo</b>
$accountLabel: $escapedAccountInfo
$transactionHashLabel: <code>$escapedTxHash</code>
$totalValueLabel: <code>$totalValueDisplay</code> USDC$availableBalanceDisplay
$totalValueLabel: <code>${'$'}$totalValueDisplay</code>$availableBalanceDisplay
📦 <b>$positionsLabel</b>
$positionsText
@@ -1642,9 +1642,9 @@ $positionsText
} else {
balanceDecimal.stripTrailingZeros()
}
"\n$availableBalanceLabel: <code>${formatted.toPlainString()}</code> USDC"
"\n$availableBalanceLabel: <code>${'$'}${formatted.toPlainString()}</code>"
} catch (e: Exception) {
"\n$availableBalanceLabel: <code>$availableBalance</code> USDC"
"\n$availableBalanceLabel: <code>${'$'}$availableBalance</code>"
}
} else {
""
@@ -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
/**
+10
View File
@@ -41,6 +41,7 @@ import { wsManager } from './services/websocket'
import type { OrderPushMessage } from './types'
import { apiService } from './services/api'
import { hasToken } from './utils'
import ClobMigrationModal, { STORAGE_KEY as CLOB_MIGRATION_KEY } from './components/ClobMigrationModal'
/**
*
@@ -64,6 +65,7 @@ function App() {
const { t, i18n } = useTranslation()
const [isFirstUse, setIsFirstUse] = useState<boolean | null>(null)
const [checking, setChecking] = useState(true)
const [clobMigrationVisible, setClobMigrationVisible] = useState(false)
// 根据当前语言设置 Ant Design 的 locale
const getAntdLocale = () => {
@@ -199,6 +201,13 @@ function App() {
wsManager.disconnect()
}
}, [checking, isFirstUse])
// 已登录且未查看过 CLOB V2 迁移提醒时显示弹窗
useEffect(() => {
if (!checking && isFirstUse === false && hasToken() && !localStorage.getItem(CLOB_MIGRATION_KEY)) {
setClobMigrationVisible(true)
}
}, [checking, isFirstUse])
// 订阅订单推送并显示全局通知
useEffect(() => {
@@ -284,6 +293,7 @@ function App() {
{/* 默认重定向到登录页 */}
<Route path="*" element={<Navigate to="/login" replace />} />
</Routes>
<ClobMigrationModal open={clobMigrationVisible} onClose={() => setClobMigrationVisible(false)} />
</BrowserRouter>
</ConfigProvider>
)
@@ -525,7 +525,7 @@ const AccountImportForm: React.FC<AccountImportFormProps> = ({
</Space>
{!option.error && (
<span style={{ fontSize: 13, fontWeight: 500, color: 'var(--ant-color-primary)' }}>
{formatUSDC(option.totalBalance)} USDC
${formatUSDC(option.totalBalance)}
</span>
)}
</div>
@@ -247,7 +247,7 @@ const AccountSetupStatusBlock: React.FC<AccountSetupStatusBlockProps> = ({
const displayText = isUnlimited
? t('accountSetup.approvalDetails.unlimited')
: isApproved
? `${parseFloat(allowance).toFixed(2)} USDC`
? `$${parseFloat(allowance).toFixed(2)}`
: t('accountSetup.approvalDetails.notApproved')
return (
<div
@@ -0,0 +1,64 @@
import { Modal, Button, Space } from 'antd'
import { SwapOutlined } from '@ant-design/icons'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router-dom'
const STORAGE_KEY = 'clob_v2_migration_dismissed'
interface ClobMigrationModalProps {
open: boolean
onClose: () => void
}
const ClobMigrationModal: React.FC<ClobMigrationModalProps> = ({ open, onClose }) => {
const { t } = useTranslation()
const navigate = useNavigate()
const handleClose = (dontRemind = false) => {
if (dontRemind) {
localStorage.setItem(STORAGE_KEY, 'true')
}
onClose()
}
const handleGoToAccounts = () => {
localStorage.setItem(STORAGE_KEY, 'true')
onClose()
navigate('/accounts')
}
return (
<Modal
title={
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<SwapOutlined style={{ color: '#fa8c16' }} />
<span>{t('clobMigration.title')}</span>
</div>
}
open={open}
onCancel={() => handleClose()}
footer={
<Space>
<Button onClick={() => handleClose(true)}>
{t('clobMigration.dontRemind')}
</Button>
<Button onClick={() => handleClose()}>
{t('common.later')}
</Button>
<Button type="primary" onClick={handleGoToAccounts}>
{t('clobMigration.goToAccounts')}
</Button>
</Space>
}
closable
maskClosable={false}
>
<p style={{ fontSize: 14, lineHeight: 1.8, color: 'rgba(0, 0, 0, 0.65)' }}>
{t('clobMigration.description')}
</p>
</Modal>
)
}
export default ClobMigrationModal
export { STORAGE_KEY }
+9
View File
@@ -1801,5 +1801,14 @@
"maxSizeUpdated": "Updated to max size",
"periodChanged": "Period has changed, popup closed"
}
},
"clobMigration": {
"title": "CLOB 2.0 Migration Notice",
"description": "Polymarket CLOB has been upgraded to V2. Please go to the Accounts page to complete the USDC migration to ensure trading functions properly.",
"goToAccounts": "Go to Accounts",
"dontRemind": "Don't remind again",
"accountGuide": "CLOB V2 requires pUSD for trading. Click the USDC.e → pUSD button in the account actions to complete the migration.",
"accountGuideButton": "USDC.e → pUSD",
"dismissGuide": "Got it"
}
}
+9
View File
@@ -1801,5 +1801,14 @@
"maxSizeUpdated": "已更新为最大数量",
"periodChanged": "周期已切换,弹窗已关闭"
}
},
"clobMigration": {
"title": "CLOB 2.0 迁移提醒",
"description": "Polymarket CLOB 已升级至 V2 版本,您需要前往账户页完成 USDC 迁移操作,以确保交易功能正常使用。",
"goToAccounts": "前往账户页",
"dontRemind": "不再提醒",
"accountGuide": "CLOB V2 需要 pUSD 进行交易,请点击账户操作栏中的 USDC.e → pUSD 按钮完成迁移。",
"accountGuideButton": "USDC.e → pUSD",
"dismissGuide": "知道了"
}
}
+9
View File
@@ -1801,5 +1801,14 @@
"maxSizeUpdated": "已更新為最大數量",
"periodChanged": "週期已切換,彈窗已關閉"
}
},
"clobMigration": {
"title": "CLOB 2.0 遷移提醒",
"description": "Polymarket CLOB 已升級至 V2 版本,您需要前往帳戶頁完成 USDC 遷移操作,以確保交易功能正常使用。",
"goToAccounts": "前往帳戶頁",
"dontRemind": "不再提醒",
"accountGuide": "CLOB V2 需要 pUSD 進行交易,請點擊帳戶操作欄中的 USDC.e → pUSD 按鈕完成遷移。",
"accountGuideButton": "USDC.e → pUSD",
"dismissGuide": "知道了"
}
}
+2 -2
View File
@@ -202,7 +202,7 @@ const AccountDetail: React.FC = () => {
<Spin size="small" />
) : balance ? (
<span style={{ fontWeight: 'bold', color: '#1890ff' }}>
{formatUSDC(balance)} USDC
${formatUSDC(balance)}
</span>
) : (
<span style={{ color: '#999' }}>-</span>
@@ -274,7 +274,7 @@ const AccountDetail: React.FC = () => {
fontWeight: 'bold',
color: account.totalPnl.startsWith('-') ? '#ff4d4f' : '#52c41a'
}}>
{formatUSDC(account.totalPnl)} USDC
${formatUSDC(account.totalPnl)}
</span>
</Descriptions.Item>
)}
+116 -7
View File
@@ -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,11 +28,57 @@ 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 [migrationGuideVisible, setMigrationGuideVisible] = useState(false)
const ACCOUNT_GUIDE_KEY = 'clob_v2_account_guide_dismissed'
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()
}, [fetchAccounts])
// 首次进入且有账户时显示迁移引导
useEffect(() => {
if (!loading && accounts.length > 0 && !localStorage.getItem(ACCOUNT_GUIDE_KEY)) {
setMigrationGuideVisible(true)
}
}, [loading, accounts.length])
const handleAccountImportSuccess = async () => {
message.success(t('accountImport.importSuccess'))
setAccountImportModalVisible(false)
@@ -325,7 +372,7 @@ const AccountList: React.FC = () => {
}
const balanceObj = balanceMap[record.id]
const balance = balanceObj?.total || record.balance || '-'
return balance && balance !== '-' && typeof balance === 'string' ? `${formatUSDC(balance)} USDC` : '-'
return balance && balance !== '-' && typeof balance === 'string' ? `$${formatUSDC(balance)}` : '-'
}
},
{
@@ -374,6 +421,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={
@@ -438,6 +505,38 @@ const AccountList: React.FC = () => {
</Tooltip>
</div>
{migrationGuideVisible && !loading && accounts.length > 0 && (
<Alert
message={t('clobMigration.accountGuide')}
type="warning"
showIcon
icon={<SwapOutlined />}
closable
onClose={() => {
localStorage.setItem(ACCOUNT_GUIDE_KEY, 'true')
setMigrationGuideVisible(false)
}}
afterClose={() => {
localStorage.setItem(ACCOUNT_GUIDE_KEY, 'true')
setMigrationGuideVisible(false)
}}
style={{ marginBottom: 16, ...(isMobile ? { margin: '0 8px 12px' } : {}) }}
action={
<Button
size="small"
type="primary"
danger
onClick={() => {
localStorage.setItem(ACCOUNT_GUIDE_KEY, 'true')
setMigrationGuideVisible(false)
}}
>
{t('clobMigration.dismissGuide')}
</Button>
}
/>
)}
<Card style={{
margin: isMobile ? '0 -8px' : '0',
borderRadius: isMobile ? '0' : undefined
@@ -504,7 +603,7 @@ const AccountList: React.FC = () => {
{t('accountList.totalBalance')}
</div>
<div style={{ fontSize: '14px', fontWeight: '600', color: '#52c41a' }}>
{balance?.total && balance.total !== '-' ? `${formatUSDC(balance.total)} USDC` : '- USDC'}
{balance?.total && balance.total !== '-' ? `$${formatUSDC(balance.total)}` : '-'}
</div>
</div>
<div style={{ textAlign: 'right' }}>
@@ -568,6 +667,16 @@ const AccountList: React.FC = () => {
</div>
</Tooltip>
<Tooltip title={t('clobMigration.accountGuideButton')}>
<div
onClick={() => handleWrapToPusd(account)}
style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', cursor: wrapLoading[account.id] ? 'wait' : 'pointer', padding: '4px 8px' }}
>
<SwapOutlined style={{ fontSize: '18px', color: '#fa8c16' }} spin={wrapLoading[account.id]} />
<span style={{ fontSize: '10px', color: '#8c8c8c', marginTop: '2px' }}>{t('clobMigration.accountGuideButton')}</span>
</div>
</Tooltip>
<Popconfirm
title={t('accountList.deleteConfirm')}
description={
@@ -728,7 +837,7 @@ const AccountList: React.FC = () => {
<Spin size="small" />
) : detailBalance ? (
<span style={{ fontWeight: 'bold', color: '#1890ff', fontSize: '16px' }}>
{formatUSDC(detailBalance.total)} USDC
${formatUSDC(detailBalance.total)}
</span>
) : (
<span style={{ color: '#999' }}>-</span>
@@ -739,7 +848,7 @@ const AccountList: React.FC = () => {
<Spin size="small" />
) : detailBalance ? (
<span style={{ color: '#52c41a' }}>
{formatUSDC(detailBalance.available)} USDC
${formatUSDC(detailBalance.available)}
</span>
) : (
<span style={{ color: '#999' }}>-</span>
@@ -750,7 +859,7 @@ const AccountList: React.FC = () => {
<Spin size="small" />
) : detailBalance ? (
<span style={{ color: '#1890ff' }}>
{formatUSDC(detailBalance.position)} USDC
${formatUSDC(detailBalance.position)}
</span>
) : (
<span style={{ color: '#999' }}>-</span>
@@ -806,7 +915,7 @@ const AccountList: React.FC = () => {
fontWeight: 'bold',
color: detailAccount.totalPnl && detailAccount.totalPnl.startsWith('-') ? '#ff4d4f' : '#52c41a'
}}>
{formatUSDC(detailAccount.totalPnl)} USDC
${formatUSDC(detailAccount.totalPnl)}
</span>
</Descriptions.Item>
)}
+3 -3
View File
@@ -75,9 +75,9 @@ const BacktestChart: React.FC<BacktestChartProps> = ({ trades }) => {
return `
<div>
<div>${t('backtest.tradeTime')}: ${param.name}</div>
<div>${t('backtest.balanceAfter')}: ${value} USDC</div>
<div>${t('backtest.balanceAfter')}: $${value}</div>
<div style="color: ${color}">
${t('backtest.profitLoss')}: ${diff} USDC (${diffPercent}%)
${t('backtest.profitLoss')}: $${diff} (${diffPercent}%)
</div>
</div>
`
@@ -121,7 +121,7 @@ const BacktestChart: React.FC<BacktestChartProps> = ({ trades }) => {
},
yAxis: {
type: 'value',
name: 'USDC',
name: '$',
nameLocation: 'end',
nameGap: 10,
axisLabel: {
+5 -5
View File
@@ -279,14 +279,14 @@ const BacktestDetail: React.FC = () => {
render: (value: string) => parseFloat(value).toFixed(4)
},
{
title: t('backtest.amount') + ' (USDC)',
title: t('backtest.amount') + ' ($)',
dataIndex: 'amount',
key: 'amount',
width: 120,
render: (value: string) => formatUSDC(value)
},
{
title: t('backtest.balanceAfter') + ' (USDC)',
title: t('backtest.balanceAfter') + ' ($)',
dataIndex: 'balanceAfter',
key: 'balanceAfter',
width: 120,
@@ -346,14 +346,14 @@ const BacktestDetail: React.FC = () => {
{task.leaderName || task.leaderAddress}
</Descriptions.Item>
<Descriptions.Item label={t('backtest.initialBalance')}>
{formatUSDC(task.initialBalance)} USDC
${formatUSDC(task.initialBalance)}
</Descriptions.Item>
<Descriptions.Item label={t('backtest.finalBalance')}>
{task.finalBalance ? formatUSDC(task.finalBalance) + ' USDC' : '-'}
{task.finalBalance ? '$' + formatUSDC(task.finalBalance) : '-'}
</Descriptions.Item>
<Descriptions.Item label={t('backtest.profitAmount')}>
<span style={{ color: task.profitAmount && parseFloat(task.profitAmount) >= 0 ? '#52c41a' : '#ff4d4f' }}>
{task.profitAmount ? formatUSDC(task.profitAmount) + ' USDC' : '-'}
{task.profitAmount ? '$' + formatUSDC(task.profitAmount) : '-'}
</span>
</Descriptions.Item>
<Descriptions.Item label={t('backtest.profitRate')}>
+17 -17
View File
@@ -461,14 +461,14 @@ const BacktestList: React.FC = () => {
render: (value: string) => parseFloat(value).toFixed(4)
},
{
title: t('backtest.amount') + ' (USDC)',
title: t('backtest.amount') + ' ($)',
dataIndex: 'amount',
key: 'amount',
width: 120,
render: (value: string) => formatUSDC(value)
},
{
title: t('backtest.balanceAfter') + ' (USDC)',
title: t('backtest.balanceAfter') + ' ($)',
dataIndex: 'balanceAfter',
key: 'balanceAfter',
width: 120,
@@ -815,7 +815,7 @@ const BacktestList: React.FC = () => {
<div>
<div style={{ fontSize: '10px', color: '#8c8c8c' }}>{t('backtest.profitAmount')}</div>
{task.profitAmount != null ? (
<div style={{ fontSize: '14px', fontWeight: '600', color: parseFloat(task.profitAmount) >= 0 ? '#52c41a' : '#ff4d4f' }}>{formatUSDC(task.profitAmount)} USDC</div>
<div style={{ fontSize: '14px', fontWeight: '600', color: parseFloat(task.profitAmount) >= 0 ? '#52c41a' : '#ff4d4f' }}>${formatUSDC(task.profitAmount)}</div>
) : (
<div style={{ fontSize: '14px', color: '#8c8c8c' }}>-</div>
)}
@@ -997,7 +997,7 @@ const BacktestList: React.FC = () => {
<Row gutter={24}>
<Col xs={24} sm={24} md={12}>
<Form.Item
label={t('backtest.initialBalance') + ' (USDC)'}
label={t('backtest.initialBalance') + ' ($)'}
name="initialBalance"
rules={[
{ required: true, message: t('backtest.initialBalanceRequired') || '请输入初始资金' },
@@ -1082,7 +1082,7 @@ const BacktestList: React.FC = () => {
{copyMode === 'FIXED' && (
<Form.Item
label={t('backtest.fixedAmount') + ' (USDC)'}
label={t('backtest.fixedAmount') + ' ($)'}
name="fixedAmount"
rules={[
{ required: true, message: t('backtest.fixedAmountRequired') || '请输入固定金额' },
@@ -1101,7 +1101,7 @@ const BacktestList: React.FC = () => {
<Row gutter={24}>
<Col xs={24} sm={24} md={12}>
<Form.Item
label={t('backtest.maxOrderSize') + ' (USDC)'}
label={t('backtest.maxOrderSize') + ' ($)'}
name="maxOrderSize"
rules={[{ required: true }]}
>
@@ -1110,7 +1110,7 @@ const BacktestList: React.FC = () => {
</Col>
<Col xs={24} sm={24} md={12}>
<Form.Item
label={t('backtest.minOrderSize') + ' (USDC)'}
label={t('backtest.minOrderSize') + ' ($)'}
name="minOrderSize"
rules={[{ required: true }]}
>
@@ -1122,7 +1122,7 @@ const BacktestList: React.FC = () => {
<Row gutter={24}>
<Col xs={24} sm={24} md={12}>
<Form.Item
label={t('backtest.maxDailyLoss') + ' (USDC)'}
label={t('backtest.maxDailyLoss') + ' ($)'}
name="maxDailyLoss"
rules={[{ required: true }]}
>
@@ -1141,7 +1141,7 @@ const BacktestList: React.FC = () => {
</Row>
<Form.Item
label={t('backtest.maxPositionValue') + ' (USDC)'}
label={t('backtest.maxPositionValue') + ' ($)'}
name="maxPositionValue"
>
<InputNumber
@@ -1316,14 +1316,14 @@ const BacktestList: React.FC = () => {
{detailTask.leaderName || `Leader ${detailTask.leaderId}`}
</Descriptions.Item>
<Descriptions.Item label={t('backtest.initialBalance')}>
{formatUSDC(detailTask.initialBalance)} USDC
${formatUSDC(detailTask.initialBalance)}
</Descriptions.Item>
<Descriptions.Item label={t('backtest.finalBalance')}>
{detailTask.finalBalance ? formatUSDC(detailTask.finalBalance) + ' USDC' : '-'}
{detailTask.finalBalance ? '$' + formatUSDC(detailTask.finalBalance) : '-'}
</Descriptions.Item>
<Descriptions.Item label={t('backtest.profitAmount')}>
<span style={{ color: detailTask.profitAmount && parseFloat(detailTask.profitAmount) >= 0 ? '#52c41a' : '#ff4d4f' }}>
{detailTask.profitAmount ? formatUSDC(detailTask.profitAmount) + ' USDC' : '-'}
{detailTask.profitAmount ? '$' + formatUSDC(detailTask.profitAmount) : '-'}
</span>
</Descriptions.Item>
<Descriptions.Item label={t('backtest.profitRate')}>
@@ -1439,17 +1439,17 @@ const BacktestList: React.FC = () => {
<Descriptions.Item label={t('backtest.copyMode')}>
{detailConfig.copyMode === 'RATIO'
? `${t('backtest.copyModeRatio')} ${parseFloat(detailConfig.copyRatio) * 100}%`
: `${t('backtest.copyModeFixed')} ${formatUSDC(detailConfig.fixedAmount)} USDC`
: `${t('backtest.copyModeFixed')} $${formatUSDC(detailConfig.fixedAmount)}`
}
</Descriptions.Item>
<Descriptions.Item label={t('backtest.maxOrderSize')}>
{formatUSDC(detailConfig.maxOrderSize)} USDC
{'$' + formatUSDC(detailConfig.maxOrderSize)}
</Descriptions.Item>
<Descriptions.Item label={t('backtest.minOrderSize')}>
{formatUSDC(detailConfig.minOrderSize)} USDC
{'$' + formatUSDC(detailConfig.minOrderSize)}
</Descriptions.Item>
<Descriptions.Item label={t('backtest.maxDailyLoss')}>
{formatUSDC(detailConfig.maxDailyLoss)} USDC
{'$' + formatUSDC(detailConfig.maxDailyLoss)}
</Descriptions.Item>
<Descriptions.Item label={t('backtest.maxDailyOrders')}>
{detailConfig.maxDailyOrders}
@@ -1471,7 +1471,7 @@ const BacktestList: React.FC = () => {
)}
{detailConfig.maxPositionValue && (
<Descriptions.Item label={t('backtest.maxPositionValue')}>
{formatUSDC(detailConfig.maxPositionValue)} USDC
{'$' + formatUSDC(detailConfig.maxPositionValue)}
</Descriptions.Item>
)}
{(detailConfig.minPrice || detailConfig.maxPrice) && (
+2 -2
View File
@@ -150,7 +150,7 @@ const CopyTradingBuyOrdersPage: React.FC = () => {
const amount = (parseFloat(record.quantity) * parseFloat(record.price)).toString()
return (
<span style={{ fontSize: isMobile ? 12 : 14 }}>
{isMobile ? formatUSDC(amount) : `${formatUSDC(amount)} USDC`}
{isMobile ? formatUSDC(amount) : `$${formatUSDC(amount)}`}
</span>
)
}
@@ -305,7 +305,7 @@ const CopyTradingBuyOrdersPage: React.FC = () => {
: {formatUSDC(order.quantity)} | : {formatUSDC(order.price)}
</div>
<div style={{ fontSize: '14px', fontWeight: '500', marginTop: '4px' }}>
: {formatUSDC(amount)} USDC
金额: ${formatUSDC(amount)}
</div>
</div>
+3 -3
View File
@@ -279,7 +279,7 @@ const CopyTradingList: React.FC = () => {
fontSize: isMobile ? 12 : 14
}}>
{getPnlIcon(stats.totalPnl)}
{isMobile ? formatUSDC(stats.totalPnl) : `${formatUSDC(stats.totalPnl)} USDC`}
{isMobile ? formatUSDC(stats.totalPnl) : `$${formatUSDC(stats.totalPnl)}`}
</div>
{!isMobile && (
<div style={{
@@ -520,7 +520,7 @@ const CopyTradingList: React.FC = () => {
<div style={{ fontSize: '12px', opacity: '0.9' }}>
{record.copyMode === 'RATIO'
? `${t('copyTradingList.ratioMode') || '比例'} ${(parseFloat(record.copyRatio || '0') * 100).toFixed(0).replace(/\.0+$/, '')}%`
: `${t('copyTradingList.fixedAmountMode') || '固定'} ${formatUSDC(record.fixedAmount || '0')} USDC`
: `${t('copyTradingList.fixedAmountMode') || '固定'} $${formatUSDC(record.fixedAmount || '0')}`
}
</div>
</div>
@@ -549,7 +549,7 @@ const CopyTradingList: React.FC = () => {
gap: '4px'
}}>
{getPnlIcon(stats.totalPnl)}
{formatUSDC(stats.totalPnl)} USDC
${formatUSDC(stats.totalPnl)}
</div>
) : loadingStatistics.has(record.id) ? (
<Spin size="small" />
@@ -130,7 +130,7 @@ const CopyTradingMatchedOrdersPage: React.FC = () => {
fontWeight: 500,
fontSize: isMobile ? 12 : 14
}}>
{isMobile ? formatUSDC(value) : `${formatUSDC(value)} USDC`}
{isMobile ? formatUSDC(value) : `$${formatUSDC(value)}`}
</span>
)
},
@@ -262,7 +262,7 @@ const CopyTradingMatchedOrdersPage: React.FC = () => {
fontWeight: 'bold',
color: getPnlColor(order.realizedPnl)
}}>
{formatUSDC(order.realizedPnl)} USDC
${formatUSDC(order.realizedPnl)}
</div>
</div>
@@ -518,7 +518,7 @@ const AddModal: React.FC<AddModalProps> = ({
value={parseFloat(leaderAssetInfo.total)}
precision={4}
valueStyle={{ color: '#52c41a', fontWeight: 'bold', fontSize: '16px' }}
suffix="USDC"
prefix="$"
formatter={(value) => formatUSDC(value?.toString() || '0')}
/>
</Col>
@@ -528,7 +528,7 @@ const AddModal: React.FC<AddModalProps> = ({
value={parseFloat(leaderAssetInfo.available)}
precision={4}
valueStyle={{ color: '#1890ff', fontSize: '14px' }}
suffix="USDC"
prefix="$"
formatter={(value) => formatUSDC(value?.toString() || '0')}
/>
</Col>
@@ -538,7 +538,7 @@ const AddModal: React.FC<AddModalProps> = ({
value={parseFloat(leaderAssetInfo.position)}
precision={4}
valueStyle={{ color: '#722ed1', fontSize: '14px' }}
suffix="USDC"
prefix="$"
formatter={(value) => formatUSDC(value?.toString() || '0')}
/>
</Col>
@@ -606,7 +606,7 @@ const AddModal: React.FC<AddModalProps> = ({
{copyMode === 'FIXED' && (
<Form.Item
label={t('copyTradingAdd.fixedAmount') || '固定跟单金额 (USDC)'}
label={t('copyTradingAdd.fixedAmount') || '固定跟单金额 ($)'}
name="fixedAmount"
rules={[
{ required: true, message: t('copyTradingAdd.fixedAmountRequired') || '请输入固定跟单金额' },
@@ -645,7 +645,7 @@ const AddModal: React.FC<AddModalProps> = ({
{copyMode === 'RATIO' && (
<>
<Form.Item
label={t('copyTradingAdd.maxOrderSize') || '单笔订单最大金额 (USDC)'}
label={t('copyTradingAdd.maxOrderSize') || '单笔订单最大金额 ($)'}
name="maxOrderSize"
tooltip={t('copyTradingAdd.maxOrderSizeTooltip') || '比例模式下,限制单笔跟单订单的最大金额上限'}
>
@@ -665,7 +665,7 @@ const AddModal: React.FC<AddModalProps> = ({
</Form.Item>
<Form.Item
label={t('copyTradingAdd.minOrderSize') || '单笔订单最小金额 (USDC)'}
label={t('copyTradingAdd.minOrderSize') || '单笔订单最小金额 ($)'}
name="minOrderSize"
tooltip={t('copyTradingAdd.minOrderSizeTooltip') || '比例模式下,限制单笔跟单订单的最小金额下限,必须 >= 1'}
rules={[
@@ -700,7 +700,7 @@ const AddModal: React.FC<AddModalProps> = ({
)}
<Form.Item
label={t('copyTradingAdd.maxDailyLoss') || '每日最大亏损限制 (USDC)'}
label={t('copyTradingAdd.maxDailyLoss') || '每日最大亏损限制 ($)'}
name="maxDailyLoss"
tooltip={t('copyTradingAdd.maxDailyLossTooltip') || '限制每日最大亏损金额,用于风险控制'}
>
@@ -709,7 +709,7 @@ const AddModal: React.FC<AddModalProps> = ({
step={0.0001}
precision={4}
style={{ width: '100%' }}
placeholder={t('copyTradingAdd.maxDailyLossPlaceholder') || '默认 10000 USDC(可选)'}
placeholder={t('copyTradingAdd.maxDailyLossPlaceholder') || '默认 10000 $(可选)'}
formatter={(value) => {
if (!value && value !== 0) return ''
const num = parseFloat(value.toString())
@@ -767,7 +767,7 @@ const AddModal: React.FC<AddModalProps> = ({
</Form.Item>
<Form.Item
label={t('copyTradingAdd.minOrderDepth') || '最小订单深度 (USDC)'}
label={t('copyTradingAdd.minOrderDepth') || '最小订单深度 ($)'}
name="minOrderDepth"
tooltip={t('copyTradingAdd.minOrderDepthTooltip') || '检查订单簿的总订单金额(买盘+卖盘),确保市场有足够的流动性。不填写则不启用此过滤'}
>
@@ -853,7 +853,7 @@ const AddModal: React.FC<AddModalProps> = ({
<Divider>{t('copyTradingAdd.positionLimitFilter') || '最大仓位限制'}</Divider>
<Form.Item
label={t('copyTradingAdd.maxPositionValue') || '最大仓位金额 (USDC)'}
label={t('copyTradingAdd.maxPositionValue') || '最大仓位金额 ($)'}
name="maxPositionValue"
tooltip={t('copyTradingAdd.maxPositionValueTooltip') || '限制单个市场的最大仓位金额。如果该市场的当前仓位金额 + 跟单金额超过此限制,则不会下单。不填写则不启用此限制'}
>
@@ -1082,7 +1082,7 @@ const AddModal: React.FC<AddModalProps> = ({
<span>
{record.copyMode === 'RATIO'
? `${t('copyTradingAdd.ratioMode') || '比例'} ${record.copyRatio}x`
: `${t('copyTradingAdd.fixedAmountMode') || '固定'} ${formatUSDC(record.fixedAmount || '0')} USDC`
: `${t('copyTradingAdd.fixedAmountMode') || '固定'} $${formatUSDC(record.fixedAmount || '0')}`
}
</span>
)
@@ -261,7 +261,7 @@ const BuyOrdersTab: React.FC<BuyOrdersTabProps> = ({ copyTradingId, active = fal
const amount = (parseFloat(record.quantity) * parseFloat(record.price)).toString()
return (
<span style={{ fontSize: isMobile ? 12 : 14 }}>
{isMobile ? formatUSDC(amount) : `${formatUSDC(amount)} USDC`}
{isMobile ? formatUSDC(amount) : `$${formatUSDC(amount)}`}
</span>
)
}
@@ -384,7 +384,7 @@ const BuyOrdersTab: React.FC<BuyOrdersTabProps> = ({ copyTradingId, active = fal
</div>
<div style={{ display: 'flex', gap: '16px', flexWrap: 'wrap', fontSize: isMobile ? '12px' : '13px', color: '#666' }}>
<span>{t('copyTradingOrders.orderCount') || '订单数'}: {group.stats.count}</span>
<span>{t('copyTradingOrders.totalAmount') || '总金额'}: {formatUSDC(group.stats.totalAmount)} USDC</span>
<span>{t('copyTradingOrders.totalAmount') || '总金额'}: ${formatUSDC(group.stats.totalAmount)}</span>
<span>
{t('copyTradingOrders.statusBreakdown') || '状态'}:
{group.stats.fullyMatchedCount > 0 && ` ${t('copyTradingOrders.allFullySold') || '全部卖出'} ${group.stats.fullyMatchedCount}`}
@@ -460,7 +460,7 @@ const BuyOrdersTab: React.FC<BuyOrdersTabProps> = ({ copyTradingId, active = fal
<div style={{ fontSize: '12px', color: '#666' }}>
<div>{t('copyTradingOrders.quantity') || '数量'}: {formatUSDC(order.quantity)} | {t('copyTradingOrders.price') || '价格'}: {formatUSDC(order.price)}</div>
<div>{t('copyTradingOrders.amount') || '金额'}: {formatUSDC(amount)} USDC</div>
<div>{t('copyTradingOrders.amount') || '金额'}: ${formatUSDC(amount)}</div>
<div>{t('copyTradingOrders.matched') || '已匹配'}: {formatUSDC(order.matchedQuantity)} | {t('copyTradingOrders.remaining') || '剩余'}: {formatUSDC(order.remainingQuantity)}</div>
<div style={{ color: '#999', marginTop: '4px' }}>{formattedDate}</div>
</div>
@@ -557,7 +557,7 @@ const BuyOrdersTab: React.FC<BuyOrdersTabProps> = ({ copyTradingId, active = fal
{t('copyTradingOrders.quantity') || '数量'}: {formatUSDC(order.quantity)} | {t('copyTradingOrders.price') || '价格'}: {formatUSDC(order.price)}
</div>
<div style={{ fontSize: '14px', fontWeight: '500', marginTop: '4px' }}>
{t('copyTradingOrders.amount') || '金额'}: {formatUSDC(amount)} USDC
{t('copyTradingOrders.amount') || '金额'}: ${formatUSDC(amount)}
</div>
</div>
@@ -356,7 +356,7 @@ const EditModal: React.FC<EditModalProps> = ({
value={parseFloat(leaderAssetInfo.total)}
precision={4}
valueStyle={{ color: '#52c41a', fontWeight: 'bold', fontSize: '16px' }}
suffix="USDC"
prefix="$"
formatter={(value) => formatUSDC(value?.toString() || '0')}
/>
</Col>
@@ -366,7 +366,7 @@ const EditModal: React.FC<EditModalProps> = ({
value={parseFloat(leaderAssetInfo.available)}
precision={4}
valueStyle={{ color: '#1890ff', fontSize: '14px' }}
suffix="USDC"
prefix="$"
formatter={(value) => formatUSDC(value?.toString() || '0')}
/>
</Col>
@@ -376,7 +376,7 @@ const EditModal: React.FC<EditModalProps> = ({
value={parseFloat(leaderAssetInfo.position)}
precision={4}
valueStyle={{ color: '#722ed1', fontSize: '14px' }}
suffix="USDC"
prefix="$"
formatter={(value) => formatUSDC(value?.toString() || '0')}
/>
</Col>
@@ -456,7 +456,7 @@ const EditModal: React.FC<EditModalProps> = ({
{copyMode === 'FIXED' && (
<Form.Item
label={t('copyTradingEdit.fixedAmount') || '固定跟单金额 (USDC)'}
label={t('copyTradingEdit.fixedAmount') || '固定跟单金额 ($)'}
name="fixedAmount"
rules={[
{ required: true, message: t('copyTradingEdit.fixedAmountRequired') || '请输入固定跟单金额' },
@@ -495,7 +495,7 @@ const EditModal: React.FC<EditModalProps> = ({
{copyMode === 'RATIO' && (
<>
<Form.Item
label={t('copyTradingEdit.maxOrderSize') || '单笔订单最大金额 (USDC)'}
label={t('copyTradingEdit.maxOrderSize') || '单笔订单最大金额 ($)'}
name="maxOrderSize"
tooltip={t('copyTradingEdit.maxOrderSizeTooltip') || '比例模式下,限制单笔跟单订单的最大金额上限'}
>
@@ -515,7 +515,7 @@ const EditModal: React.FC<EditModalProps> = ({
</Form.Item>
<Form.Item
label={t('copyTradingEdit.minOrderSize') || '单笔订单最小金额 (USDC)'}
label={t('copyTradingEdit.minOrderSize') || '单笔订单最小金额 ($)'}
name="minOrderSize"
tooltip={t('copyTradingEdit.minOrderSizeTooltip') || '比例模式下,限制单笔跟单订单的最小金额下限,必须 >= 1'}
rules={[
@@ -550,7 +550,7 @@ const EditModal: React.FC<EditModalProps> = ({
)}
<Form.Item
label={t('copyTradingEdit.maxDailyLoss') || '每日最大亏损限制 (USDC)'}
label={t('copyTradingEdit.maxDailyLoss') || '每日最大亏损限制 ($)'}
name="maxDailyLoss"
tooltip={t('copyTradingEdit.maxDailyLossTooltip') || '限制每日最大亏损金额,用于风险控制'}
>
@@ -559,7 +559,7 @@ const EditModal: React.FC<EditModalProps> = ({
step={0.0001}
precision={4}
style={{ width: '100%' }}
placeholder={t('copyTradingEdit.maxDailyLossPlaceholder') || '默认 10000 USDC(可选)'}
placeholder={t('copyTradingEdit.maxDailyLossPlaceholder') || '默认 10000 $(可选)'}
formatter={(value) => {
if (!value && value !== 0) return ''
const num = parseFloat(value.toString())
@@ -617,7 +617,7 @@ const EditModal: React.FC<EditModalProps> = ({
</Form.Item>
<Form.Item
label={t('copyTradingEdit.minOrderDepth') || '最小订单深度 (USDC)'}
label={t('copyTradingEdit.minOrderDepth') || '最小订单深度 ($)'}
name="minOrderDepth"
tooltip={t('copyTradingEdit.minOrderDepthTooltip') || '检查订单簿的总订单金额(买盘+卖盘),确保市场有足够的流动性。不填写则不启用此过滤'}
>
@@ -703,7 +703,7 @@ const EditModal: React.FC<EditModalProps> = ({
<Divider>{t('copyTradingEdit.positionLimitFilter') || '最大仓位限制'}</Divider>
<Form.Item
label={t('copyTradingEdit.maxPositionValue') || '最大仓位金额 (USDC)'}
label={t('copyTradingEdit.maxPositionValue') || '最大仓位金额 ($)'}
name="maxPositionValue"
tooltip={t('copyTradingEdit.maxPositionValueTooltip') || '限制单个市场的最大仓位金额。如果该市场的当前仓位金额 + 跟单金额超过此限制,则不会下单。不填写则不启用此限制'}
>
@@ -228,7 +228,7 @@ const MatchedOrdersTab: React.FC<MatchedOrdersTabProps> = ({ copyTradingId, acti
fontWeight: 500,
fontSize: isMobile ? 12 : 14
}}>
{isMobile ? formatUSDC(value) : `${formatUSDC(value)} USDC`}
{isMobile ? formatUSDC(value) : `$${formatUSDC(value)}`}
</span>
)
},
@@ -408,7 +408,7 @@ const MatchedOrdersTab: React.FC<MatchedOrdersTabProps> = ({ copyTradingId, acti
fontWeight: 'bold',
color: getPnlColor(order.realizedPnl)
}}>
{formatUSDC(order.realizedPnl)} USDC
${formatUSDC(order.realizedPnl)}
</div>
</div>
@@ -258,7 +258,7 @@ const SellOrdersTab: React.FC<SellOrdersTabProps> = ({ copyTradingId, active = f
const amount = (parseFloat(record.quantity) * parseFloat(record.price)).toString()
return (
<span style={{ fontSize: isMobile ? 12 : 14 }}>
{isMobile ? formatUSDC(amount) : `${formatUSDC(amount)} USDC`}
{isMobile ? formatUSDC(amount) : `$${formatUSDC(amount)}`}
</span>
)
}
@@ -274,7 +274,7 @@ const SellOrdersTab: React.FC<SellOrdersTabProps> = ({ copyTradingId, active = f
fontWeight: 500,
fontSize: isMobile ? 12 : 14
}}>
{isMobile ? formatUSDC(value) : `${formatUSDC(value)} USDC`}
{isMobile ? formatUSDC(value) : `$${formatUSDC(value)}`}
</span>
)
},
@@ -362,10 +362,10 @@ const SellOrdersTab: React.FC<SellOrdersTabProps> = ({ copyTradingId, active = f
</div>
<div style={{ display: 'flex', gap: '16px', flexWrap: 'wrap', fontSize: isMobile ? '12px' : '13px', color: '#666' }}>
<span>{t('copyTradingOrders.orderCount') || '订单数'}: {group.stats.count}</span>
<span>{t('copyTradingOrders.totalAmount') || '总金额'}: {formatUSDC(group.stats.totalAmount)} USDC</span>
<span>{t('copyTradingOrders.totalAmount') || '总金额'}: ${formatUSDC(group.stats.totalAmount)}</span>
{group.stats.totalPnl && (
<span style={{ color: pnlColor, fontWeight: 500 }}>
{t('copyTradingOrders.totalPnl') || '总盈亏'}: {formatUSDC(group.stats.totalPnl)} USDC
{t('copyTradingOrders.totalPnl') || '总盈亏'}: ${formatUSDC(group.stats.totalPnl)}
</span>
)}
</div>
@@ -434,9 +434,9 @@ const SellOrdersTab: React.FC<SellOrdersTabProps> = ({ copyTradingId, active = f
<div style={{ fontSize: '12px', color: '#666' }}>
<div>{t('copyTradingOrders.quantity') || '数量'}: {formatUSDC(order.quantity)} | {t('copyTradingOrders.price') || '价格'}: {formatUSDC(order.price)}</div>
<div>{t('copyTradingOrders.amount') || '金额'}: {formatUSDC(amount)} USDC</div>
<div>{t('copyTradingOrders.amount') || '金额'}: ${formatUSDC(amount)}</div>
<div style={{ color: getPnlColor(order.realizedPnl), fontWeight: 500 }}>
{t('copyTradingOrders.realizedPnl') || '已实现盈亏'}: {formatUSDC(order.realizedPnl)} USDC
{t('copyTradingOrders.realizedPnl') || '已实现盈亏'}: ${formatUSDC(order.realizedPnl)}
</div>
<div style={{ color: '#999', marginTop: '4px' }}>{formattedDate}</div>
</div>
@@ -530,7 +530,7 @@ const SellOrdersTab: React.FC<SellOrdersTabProps> = ({ copyTradingId, active = f
{t('copyTradingOrders.quantity') || '数量'}: {formatUSDC(order.quantity)} | {t('copyTradingOrders.price') || '价格'}: {formatUSDC(order.price)}
</div>
<div style={{ fontSize: '14px', fontWeight: '500', marginTop: '4px' }}>
{t('copyTradingOrders.amount') || '金额'}: {formatUSDC(amount)} USDC
{t('copyTradingOrders.amount') || '金额'}: ${formatUSDC(amount)}
</div>
</div>
@@ -541,7 +541,7 @@ const SellOrdersTab: React.FC<SellOrdersTabProps> = ({ copyTradingId, active = f
fontWeight: 'bold',
color: getPnlColor(order.realizedPnl)
}}>
{formatUSDC(order.realizedPnl)} USDC
${formatUSDC(order.realizedPnl)}
</div>
</div>
@@ -104,7 +104,7 @@ const StatisticsModal: React.FC<StatisticsModalProps> = ({
</div>
<div style={{ fontSize: '16px', fontWeight: '500', color: '#333', flex: '1', textAlign: 'right', display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: '4px' }}>
<ArrowUpOutlined style={{ color: '#1890ff', fontSize: '14px' }} />
<span style={{ fontSize: 'clamp(12px, 4vw, 16px)' }}>{formatUSDC(statistics.totalBuyAmount)} USDC</span>
<span style={{ fontSize: 'clamp(12px, 4vw, 16px)' }}>${formatUSDC(statistics.totalBuyAmount)}</span>
</div>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px', borderBottom: '1px solid #f0f0f0' }}>
@@ -113,7 +113,7 @@ const StatisticsModal: React.FC<StatisticsModalProps> = ({
</div>
<div style={{ fontSize: '16px', fontWeight: '500', color: '#333', flex: '1', textAlign: 'right', display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: '4px' }}>
<ArrowDownOutlined style={{ color: '#ff4d4f', fontSize: '14px' }} />
<span style={{ fontSize: 'clamp(12px, 4vw, 16px)' }}>{formatUSDC(statistics.totalSellAmount)} USDC</span>
<span style={{ fontSize: 'clamp(12px, 4vw, 16px)' }}>${formatUSDC(statistics.totalSellAmount)}</span>
</div>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px', borderBottom: '1px solid #f0f0f0' }}>
@@ -122,7 +122,7 @@ const StatisticsModal: React.FC<StatisticsModalProps> = ({
</div>
<div style={{ fontSize: '16px', fontWeight: 'bold', color: getPnlColor(statistics.totalPnl), flex: '1', textAlign: 'right', display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: '4px' }}>
{getPnlIcon(statistics.totalPnl)}
<span style={{ fontSize: 'clamp(12px, 4vw, 16px)' }}>{formatUSDC(statistics.totalPnl)} USDC</span>
<span style={{ fontSize: 'clamp(12px, 4vw, 16px)' }}>${formatUSDC(statistics.totalPnl)}</span>
</div>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px', borderBottom: '1px solid #f0f0f0' }}>
@@ -131,7 +131,7 @@ const StatisticsModal: React.FC<StatisticsModalProps> = ({
</div>
<div style={{ fontSize: '16px', fontWeight: '500', color: getPnlColor(statistics.totalRealizedPnl), flex: '1', textAlign: 'right', display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: '4px' }}>
{getPnlIcon(statistics.totalRealizedPnl)}
<span style={{ fontSize: 'clamp(12px, 4vw, 16px)' }}>{formatUSDC(statistics.totalRealizedPnl)} USDC</span>
<span style={{ fontSize: 'clamp(12px, 4vw, 16px)' }}>${formatUSDC(statistics.totalRealizedPnl)}</span>
</div>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px', borderBottom: '1px solid #f0f0f0' }}>
@@ -140,7 +140,7 @@ const StatisticsModal: React.FC<StatisticsModalProps> = ({
</div>
<div style={{ fontSize: '16px', fontWeight: '500', color: getPnlColor(statistics.totalUnrealizedPnl), flex: '1', textAlign: 'right', display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: '4px' }}>
{getPnlIcon(statistics.totalUnrealizedPnl)}
<span style={{ fontSize: 'clamp(12px, 4vw, 16px)' }}>{formatUSDC(statistics.totalUnrealizedPnl)} USDC</span>
<span style={{ fontSize: 'clamp(12px, 4vw, 16px)' }}>${formatUSDC(statistics.totalUnrealizedPnl)}</span>
</div>
</div>
</div>
@@ -165,43 +165,38 @@ const StatisticsModal: React.FC<StatisticsModalProps> = ({
<Statistic
title={t('copyTradingOrders.totalBuyAmount') || '总买入金额'}
value={formatUSDC(statistics.totalBuyAmount)}
suffix="USDC"
prefix={<ArrowUpOutlined style={{ color: '#1890ff' }} />}
prefix={<><ArrowUpOutlined style={{ color: '#1890ff' }} /> $</>}
/>
</Col>
<Col xs={24} sm={12} md={8}>
<Statistic
title={t('copyTradingOrders.totalSellAmount') || '总卖出金额'}
value={formatUSDC(statistics.totalSellAmount)}
suffix="USDC"
prefix={<ArrowDownOutlined style={{ color: '#ff4d4f' }} />}
prefix={<><ArrowDownOutlined style={{ color: '#ff4d4f' }} /> $</>}
/>
</Col>
<Col xs={24} sm={12} md={8}>
<Statistic
title={t('copyTradingOrders.totalPnl') || '总盈亏'}
value={formatUSDC(statistics.totalPnl)}
suffix="USDC"
valueStyle={{ color: getPnlColor(statistics.totalPnl) }}
prefix={getPnlIcon(statistics.totalPnl)}
prefix={<>{getPnlIcon(statistics.totalPnl)} $</>}
/>
</Col>
<Col xs={24} sm={12} md={8}>
<Statistic
title={t('copyTradingOrders.totalRealizedPnl') || '总已实现盈亏'}
value={formatUSDC(statistics.totalRealizedPnl)}
suffix="USDC"
valueStyle={{ color: getPnlColor(statistics.totalRealizedPnl) }}
prefix={getPnlIcon(statistics.totalRealizedPnl)}
prefix={<>{getPnlIcon(statistics.totalRealizedPnl)} $</>}
/>
</Col>
<Col xs={24} sm={12} md={8}>
<Statistic
title={t('copyTradingOrders.totalUnrealizedPnl') || '总未实现盈亏'}
value={formatUSDC(statistics.totalUnrealizedPnl)}
suffix="USDC"
valueStyle={{ color: getPnlColor(statistics.totalUnrealizedPnl) }}
prefix={getPnlIcon(statistics.totalUnrealizedPnl)}
prefix={<>{getPnlIcon(statistics.totalUnrealizedPnl)} $</>}
/>
</Col>
</Row>
+4 -4
View File
@@ -145,7 +145,7 @@ const CopyTradingSellOrdersPage: React.FC = () => {
const amount = (parseFloat(record.quantity) * parseFloat(record.price)).toString()
return (
<span style={{ fontSize: isMobile ? 12 : 14 }}>
{isMobile ? formatUSDC(amount) : `${formatUSDC(amount)} USDC`}
{isMobile ? formatUSDC(amount) : `$${formatUSDC(amount)}`}
</span>
)
}
@@ -161,7 +161,7 @@ const CopyTradingSellOrdersPage: React.FC = () => {
fontWeight: 500,
fontSize: isMobile ? 12 : 14
}}>
{isMobile ? formatUSDC(value) : `${formatUSDC(value)} USDC`}
{isMobile ? formatUSDC(value) : `$${formatUSDC(value)}`}
</span>
)
},
@@ -277,7 +277,7 @@ const CopyTradingSellOrdersPage: React.FC = () => {
: {formatUSDC(order.quantity)} | : {formatUSDC(order.price)}
</div>
<div style={{ fontSize: '14px', fontWeight: '500', marginTop: '4px' }}>
: {formatUSDC(amount)} USDC
金额: ${formatUSDC(amount)}
</div>
</div>
@@ -289,7 +289,7 @@ const CopyTradingSellOrdersPage: React.FC = () => {
fontWeight: 'bold',
color: getPnlColor(order.realizedPnl)
}}>
{formatUSDC(order.realizedPnl)} USDC
${formatUSDC(order.realizedPnl)}
</div>
</div>
+5 -8
View File
@@ -145,7 +145,7 @@ const CopyTradingStatisticsPage: React.FC = () => {
<Statistic
title="总买入金额"
value={formatUSDC(statistics.totalBuyAmount)}
suffix="USDC"
prefix="$"
/>
</Col>
<Col xs={24} sm={12} md={6}>
@@ -179,7 +179,7 @@ const CopyTradingStatisticsPage: React.FC = () => {
<Statistic
title="总卖出金额"
value={formatUSDC(statistics.totalSellAmount)}
suffix="USDC"
prefix="$"
/>
</Col>
<Col xs={24} sm={12} md={8}>
@@ -220,8 +220,7 @@ const CopyTradingStatisticsPage: React.FC = () => {
title="总已实现盈亏"
value={formatUSDC(statistics.totalRealizedPnl)}
valueStyle={{ color: getPnlColor(statistics.totalRealizedPnl) }}
prefix={getPnlIcon(statistics.totalRealizedPnl)}
suffix="USDC"
prefix={<>{getPnlIcon(statistics.totalRealizedPnl)} $</>}
/>
</Col>
<Col xs={24} sm={12} md={6}>
@@ -229,8 +228,7 @@ const CopyTradingStatisticsPage: React.FC = () => {
title="总未实现盈亏"
value={formatUSDC(statistics.totalUnrealizedPnl)}
valueStyle={{ color: getPnlColor(statistics.totalUnrealizedPnl) }}
prefix={getPnlIcon(statistics.totalUnrealizedPnl)}
suffix="USDC"
prefix={<>{getPnlIcon(statistics.totalUnrealizedPnl)} $</>}
/>
</Col>
<Col xs={24} sm={12} md={6}>
@@ -238,8 +236,7 @@ const CopyTradingStatisticsPage: React.FC = () => {
title="总盈亏"
value={formatUSDC(statistics.totalPnl)}
valueStyle={{ color: getPnlColor(statistics.totalPnl) }}
prefix={getPnlIcon(statistics.totalPnl)}
suffix="USDC"
prefix={<>{getPnlIcon(statistics.totalPnl)} $</>}
/>
</Col>
<Col xs={24} sm={12} md={6}>
+1 -1
View File
@@ -493,7 +493,7 @@ const CryptoTailMonitor: React.FC = () => {
} else {
timeStr = '--'
}
return `<span style="font-size:12px">${timeStr} &nbsp; ${Number(val).toFixed(2)} USDC</span>`
return `<span style="font-size:12px">${timeStr} &nbsp; $${Number(val).toFixed(2)}</span>`
}
},
legend: {
@@ -45,12 +45,12 @@ const CryptoTailPnlCurveModal: React.FC<CryptoTailPnlCurveModalProps> = (props)
if (!v) return ''
const d = data.curveData.find((p) => p.timestamp === v[0])
if (!d) return ''
return dayjs(v[0]).format('YYYY-MM-DD HH:mm') + '<br/>' + t('cryptoTailStrategy.pnlCurve.totalPnl') + ': ' + formatUSDC(d.cumulativePnl) + ' USDC'
return dayjs(v[0]).format('YYYY-MM-DD HH:mm') + '<br/>' + t('cryptoTailStrategy.pnlCurve.totalPnl') + ': $' + formatUSDC(d.cumulativePnl)
}
},
grid: { left: '3%', right: '4%', bottom: '3%', top: '10%', containLabel: true },
xAxis: { type: 'time' },
yAxis: { type: 'value', axisLabel: { formatter: (val: number) => String(val) + ' USDC' } },
yAxis: { type: 'value', axisLabel: { formatter: (val: number) => '$' + String(val) } },
series: [{
name: t('cryptoTailStrategy.pnlCurve.totalPnl'),
type: 'line',
@@ -108,7 +108,7 @@ const CryptoTailPnlCurveModal: React.FC<CryptoTailPnlCurveModalProps> = (props)
<Statistic
title={t('cryptoTailStrategy.pnlCurve.totalPnl')}
value={data?.totalRealizedPnl != null ? formatUSDC(data.totalRealizedPnl) : '-'}
suffix="USDC"
prefix="$"
valueStyle={{ color: pnlColor(data?.totalRealizedPnl ?? null) }}
/>
</Col>
@@ -125,7 +125,7 @@ const CryptoTailPnlCurveModal: React.FC<CryptoTailPnlCurveModalProps> = (props)
<Statistic
title={t('cryptoTailStrategy.pnlCurve.maxDrawdown')}
value={data?.maxDrawdown != null ? '-' + formatUSDC(data.maxDrawdown) : '-'}
suffix="USDC"
prefix="$"
valueStyle={{ color: data?.maxDrawdown ? '#ff4d4f' : undefined }}
/>
</Col>
@@ -773,7 +773,7 @@ const CryptoTailStrategyList: React.FC = () => {
<div>
<div style={{ fontSize: '10px', color: '#8c8c8c' }}>{t('cryptoTailStrategy.list.totalRealizedPnl')}</div>
{item.totalRealizedPnl != null ? (
<div style={{ fontSize: '14px', fontWeight: '600', color: pnlColor(item.totalRealizedPnl) }}>{formatUSDC(item.totalRealizedPnl)} USDC</div>
<div style={{ fontSize: '14px', fontWeight: '600', color: pnlColor(item.totalRealizedPnl) }}>${formatUSDC(item.totalRealizedPnl)}</div>
) : (
<div style={{ fontSize: '14px', color: '#8c8c8c' }}>-</div>
)}
@@ -966,7 +966,7 @@ const CryptoTailStrategyList: React.FC = () => {
</Form.Item>
) : (
<Form.Item name="amountValue" label={t('cryptoTailStrategy.form.fixedUsdc')} rules={[{ required: true }]}>
<InputNumber min={1} style={{ width: '100%' }} addonAfter="USDC" stringMode />
<InputNumber min={1} style={{ width: '100%' }} addonBefore="$" stringMode />
</Form.Item>
)
}
@@ -1111,7 +1111,7 @@ const CryptoTailStrategyList: React.FC = () => {
dataIndex: 'amountUsdc',
key: 'amountUsdc',
width: 110,
render: (v: string) => `${formatUSDC(v)} USDC`
render: (v: string) => `$${formatUSDC(v)}`
},
{
title: t('cryptoTailStrategy.triggerRecords.realizedPnl'),
@@ -1186,7 +1186,7 @@ const CryptoTailStrategyList: React.FC = () => {
dataIndex: 'amountUsdc',
key: 'amountUsdc',
width: 110,
render: (v: string) => `${formatUSDC(v)} USDC`
render: (v: string) => `$${formatUSDC(v)}`
},
{
title: t('cryptoTailStrategy.triggerRecords.failReason'),
+5 -5
View File
@@ -254,7 +254,7 @@ const LeaderList: React.FC = () => {
return (
<Space direction="vertical" size={0}>
<Text style={{ color: '#52c41a', fontSize: '14px', fontWeight: '500' }}>
{balance.available === '-' ? '-' : `${formatUSDC(balance.available)} USDC`}
{balance.available === '-' ? '-' : `$${formatUSDC(balance.available)}`}
</Text>
<Text type="secondary" style={{ fontSize: '12px' }}>
{t('leaderDetail.positionBalance')}: {formatUSDC(balance.position)}
@@ -488,7 +488,7 @@ const LeaderList: React.FC = () => {
{t('leaderDetail.availableBalance')}
</div>
<div style={{ fontSize: '14px', fontWeight: '600', color: '#52c41a' }}>
{balance?.available && balance.available !== '-' ? `${formatUSDC(balance.available)} USDC` : '- USDC'}
{balance?.available && balance.available !== '-' ? `$${formatUSDC(balance.available)}` : '-'}
</div>
</div>
<div style={{ textAlign: 'right' }}>
@@ -700,7 +700,7 @@ const LeaderList: React.FC = () => {
value={parseFloat(detailBalance.availableBalance)}
precision={4}
valueStyle={{ color: '#1890ff' }}
suffix="USDC"
prefix="$"
formatter={(value) => formatUSDC(value?.toString() || '0')}
/>
</Card>
@@ -712,7 +712,7 @@ const LeaderList: React.FC = () => {
value={parseFloat(detailBalance.positionBalance)}
precision={4}
valueStyle={{ color: '#722ed1' }}
suffix="USDC"
prefix="$"
formatter={(value) => formatUSDC(value?.toString() || '0')}
/>
</Card>
@@ -724,7 +724,7 @@ const LeaderList: React.FC = () => {
value={parseFloat(detailBalance.totalBalance)}
precision={4}
valueStyle={{ color: '#52c41a', fontWeight: 'bold' }}
suffix="USDC"
prefix="$"
formatter={(value) => formatUSDC(value?.toString() || '0')}
/>
</Card>
+1 -1
View File
@@ -118,7 +118,7 @@ const OrderList: React.FC = () => {
key: 'pnl',
render: (pnl: string | undefined) => pnl ? (
<span style={{ color: pnl.startsWith('-') ? 'red' : 'green' }}>
{formatUSDC(pnl)} USDC
${formatUSDC(pnl)}
</span>
) : '-'
},
+15 -15
View File
@@ -695,7 +695,7 @@ const PositionList: React.FC = () => {
fontWeight: '500',
color: isProfit ? '#52c41a' : '#f5222d'
}}>
{pnlNum >= 0 ? '+' : ''}{formatUSDC(position.pnl)} USDC
{pnlNum >= 0 ? '+' : ''}${formatUSDC(position.pnl)}
</span>
</div>
)}
@@ -718,7 +718,7 @@ const PositionList: React.FC = () => {
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
<span style={{ fontSize: '13px', color: '#666' }}></span>
<span style={{ fontSize: '13px', fontWeight: '500' }}>
{formatUSDC(position.initialValue)} USDC
${formatUSDC(position.initialValue)}
</span>
</div>
{positionFilter === 'current' && position.currentPrice && (
@@ -732,7 +732,7 @@ const PositionList: React.FC = () => {
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
<span style={{ fontSize: '13px', color: '#666' }}></span>
<span style={{ fontSize: '13px', fontWeight: '600' }}>
{formatUSDC(position.currentValue)} USDC
${formatUSDC(position.currentValue)}
</span>
</div>
</>
@@ -775,7 +775,7 @@ const PositionList: React.FC = () => {
fontWeight: 'bold',
color: isProfit ? '#52c41a' : '#f5222d'
}}>
{pnlNum >= 0 ? '+' : ''}{formatUSDC(position.pnl)} USDC
{pnlNum >= 0 ? '+' : ''}${formatUSDC(position.pnl)}
</span>
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
@@ -802,7 +802,7 @@ const PositionList: React.FC = () => {
color: parseFloat(position.realizedPnl) >= 0 ? '#52c41a' : '#f5222d',
fontWeight: '500'
}}>
{parseFloat(position.realizedPnl) >= 0 ? '+' : ''}{formatUSDC(position.realizedPnl)} USDC
{parseFloat(position.realizedPnl) >= 0 ? '+' : ''}${formatUSDC(position.realizedPnl)}
</span>
</div>
)}
@@ -951,7 +951,7 @@ const PositionList: React.FC = () => {
dataIndex: 'initialValue',
key: 'initialValue',
render: (value: string) => (
<span>{formatUSDC(value)} USDC</span>
<span>${formatUSDC(value)}</span>
),
align: 'right' as const,
width: 110
@@ -971,7 +971,7 @@ const PositionList: React.FC = () => {
return (
<div>
<div style={{ fontWeight: '600', marginBottom: '2px' }}>
{formatUSDC(record.currentValue)} USDC
${formatUSDC(record.currentValue)}
</div>
<div style={{
fontSize: '13px',
@@ -1215,7 +1215,7 @@ const PositionList: React.FC = () => {
borderColor: '#52c41a'
}}
>
({redeemableSummary.totalCount}, {formatUSDC(redeemableSummary.totalValue)} USDC)
({redeemableSummary.totalCount}, ${formatUSDC(redeemableSummary.totalValue)})
</Button>
)}
</div>
@@ -1238,13 +1238,13 @@ const PositionList: React.FC = () => {
<span>
{' '}
<span style={{ fontWeight: 600 }}>
{formatUSDC(positionTotals.totalInitialValue.toString())} USDC
${formatUSDC(positionTotals.totalInitialValue.toString())}
</span>
</span>
<span>
{' '}
<span style={{ fontWeight: 600 }}>
{formatUSDC(positionTotals.totalCurrentValue.toString())} USDC
${formatUSDC(positionTotals.totalCurrentValue.toString())}
</span>
</span>
<span>
@@ -1256,7 +1256,7 @@ const PositionList: React.FC = () => {
}}
>
{positionTotals.totalPnl >= 0 ? '+' : ''}
{formatUSDC(positionTotals.totalPnl.toString())} USDC
${formatUSDC(positionTotals.totalPnl.toString())}
</span>
</span>
<span>
@@ -1268,7 +1268,7 @@ const PositionList: React.FC = () => {
}}
>
{positionTotals.totalRealizedPnl >= 0 ? '+' : ''}
{formatUSDC(positionTotals.totalRealizedPnl.toString())} USDC
${formatUSDC(positionTotals.totalRealizedPnl.toString())}
</span>
</span>
</div>
@@ -1532,7 +1532,7 @@ const PositionList: React.FC = () => {
color: currentPnl.pnl >= 0 ? '#52c41a' : '#f5222d',
marginBottom: '4px'
}}>
{currentPnl.pnl >= 0 ? '+' : ''}{formatUSDC(currentPnl.pnl)} USDC
{currentPnl.pnl >= 0 ? '+' : ''}${formatUSDC(currentPnl.pnl)}
</div>
<div style={{
fontSize: '14px',
@@ -1572,7 +1572,7 @@ const PositionList: React.FC = () => {
</Descriptions.Item>
<Descriptions.Item label="可赎回总价值">
<span style={{ fontSize: '18px', fontWeight: 'bold', color: '#52c41a' }}>
{formatUSDC(redeemableSummary.totalValue)} USDC
${formatUSDC(redeemableSummary.totalValue)}
</span>
</Descriptions.Item>
<Descriptions.Item label="涉及账户">
@@ -1625,7 +1625,7 @@ const PositionList: React.FC = () => {
width: 120
},
{
title: '价值 (USDC)',
title: '价值 ($)',
dataIndex: 'value',
key: 'value',
align: 'right' as const,
+4 -8
View File
@@ -101,9 +101,8 @@ const Statistics: React.FC = () => {
<Statistic
title={t('statistics.totalPnl') || '总盈亏'}
value={formatUSDC(stats?.totalPnl || '0')}
prefix={stats?.totalPnl && parseFloat(stats.totalPnl) >= 0 ? <ArrowUpOutlined /> : <ArrowDownOutlined />}
prefix={<>{stats?.totalPnl && parseFloat(stats.totalPnl) >= 0 ? <ArrowUpOutlined /> : <ArrowDownOutlined />} $</>}
valueStyle={{ color: stats?.totalPnl && parseFloat(stats.totalPnl || '0') >= 0 ? '#3f8600' : '#cf1322' }}
suffix="USDC"
loading={loading}
/>
</Card>
@@ -124,9 +123,8 @@ const Statistics: React.FC = () => {
<Statistic
title={t('statistics.avgPnl') || '平均盈亏'}
value={formatUSDC(stats?.avgPnl || '0')}
prefix={stats?.avgPnl && parseFloat(stats.avgPnl || '0') >= 0 ? <ArrowUpOutlined /> : <ArrowDownOutlined />}
prefix={<>{stats?.avgPnl && parseFloat(stats.avgPnl || '0') >= 0 ? <ArrowUpOutlined /> : <ArrowDownOutlined />} $</>}
valueStyle={{ color: stats?.avgPnl && parseFloat(stats.avgPnl || '0') >= 0 ? '#3f8600' : '#cf1322' }}
suffix="USDC"
loading={loading}
/>
</Card>
@@ -136,9 +134,8 @@ const Statistics: React.FC = () => {
<Statistic
title={t('statistics.maxProfit') || '最大盈利'}
value={formatUSDC(stats?.maxProfit || '0')}
prefix={<ArrowUpOutlined />}
prefix={<><ArrowUpOutlined /> $</>}
valueStyle={{ color: '#3f8600' }}
suffix="USDC"
loading={loading}
/>
</Card>
@@ -148,9 +145,8 @@ const Statistics: React.FC = () => {
<Statistic
title={t('statistics.maxLoss') || '最大亏损'}
value={formatUSDC(stats?.maxLoss || '0')}
prefix={<ArrowDownOutlined />}
prefix={<><ArrowDownOutlined /> $</>}
valueStyle={{ color: '#cf1322' }}
suffix="USDC"
loading={loading}
/>
</Card>
+6 -6
View File
@@ -154,7 +154,7 @@ const TemplateAdd: React.FC = () => {
{copyMode === 'FIXED' && (
<Form.Item
label={t('templateAdd.fixedAmount') || '固定跟单金额 (USDC)'}
label={t('templateAdd.fixedAmount') || '固定跟单金额 ($)'}
name="fixedAmount"
rules={[
{ required: true, message: t('templateAdd.fixedAmountRequired') || '请输入固定跟单金额' },
@@ -193,9 +193,9 @@ const TemplateAdd: React.FC = () => {
{copyMode === 'RATIO' && (
<>
<Form.Item
label={t('templateAdd.maxOrderSize') || '单笔订单最大金额 (USDC)'}
label={t('templateAdd.maxOrderSize') || '单笔订单最大金额 ($)'}
name="maxOrderSize"
tooltip={t('templateAdd.maxOrderSizeTooltip') || '比例模式下,限制单笔跟单订单的最大金额上限,用于防止跟单金额过大,控制风险。例如:设置为 1000,即使计算出的跟单金额超过 1000,也会限制为 1000 USDC。'}
tooltip={t('templateAdd.maxOrderSizeTooltip') || '比例模式下,限制单笔跟单订单的最大金额上限,用于防止跟单金额过大,控制风险。例如:设置为 1000,即使计算出的跟单金额超过 1000,也会限制为 $1000。'}
>
<InputNumber
min={0.0001}
@@ -213,9 +213,9 @@ const TemplateAdd: React.FC = () => {
</Form.Item>
<Form.Item
label={t('templateAdd.minOrderSize') || '单笔订单最小金额 (USDC)'}
label={t('templateAdd.minOrderSize') || '单笔订单最小金额 ($)'}
name="minOrderSize"
tooltip={t('templateAdd.minOrderSizeTooltip') || '比例模式下,限制单笔跟单订单的最小金额下限,用于过滤掉金额过小的订单,避免频繁小额交易。如果填写,必须 >= 1 USDC。例如:设置为 10,如果计算出的跟单金额小于 10,则跳过该订单。'}
tooltip={t('templateAdd.minOrderSizeTooltip') || '比例模式下,限制单笔跟单订单的最小金额下限,用于过滤掉金额过小的订单,避免频繁小额交易。如果填写,必须 >= $1。例如:设置为 10,如果计算出的跟单金额小于 10,则跳过该订单。'}
rules={[
{
validator: (_, value) => {
@@ -282,7 +282,7 @@ const TemplateAdd: React.FC = () => {
</Form.Item>
<Form.Item
label={t('templateAdd.minOrderDepth') || '最小订单深度 (USDC)'}
label={t('templateAdd.minOrderDepth') || '最小订单深度 ($)'}
name="minOrderDepth"
tooltip={t('templateAdd.minOrderDepthTooltip') || '检查订单簿的总订单金额(买盘+卖盘),确保市场有足够的流动性。不填写则不启用此过滤'}
>
+7 -7
View File
@@ -189,9 +189,9 @@ const TemplateEdit: React.FC = () => {
{copyMode === 'FIXED' && (
<Form.Item
label={t('templateEdit.fixedAmount') || '固定跟单金额 (USDC)'}
label={t('templateEdit.fixedAmount') || '固定跟单金额 ($)'}
name="fixedAmount"
tooltip={t('templateEdit.fixedAmountTooltip') || '固定金额模式下,每次跟单的固定金额,不随 Leader 订单大小变化。必须 >= 1 USDC。例如:设置为 10,则无论 Leader 买入多少,跟单金额始终为 10 USDC。'}
tooltip={t('templateEdit.fixedAmountTooltip') || '固定金额模式下,每次跟单的固定金额,不随 Leader 订单大小变化。必须 >= $1。例如:设置为 10,则无论 Leader 买入多少,跟单金额始终为 $10。'}
rules={[
{ required: true, message: t('templateEdit.fixedAmountRequired') || '请输入固定跟单金额' },
{
@@ -229,9 +229,9 @@ const TemplateEdit: React.FC = () => {
{copyMode === 'RATIO' && (
<>
<Form.Item
label={t('templateEdit.maxOrderSize') || '单笔订单最大金额 (USDC)'}
label={t('templateEdit.maxOrderSize') || '单笔订单最大金额 ($)'}
name="maxOrderSize"
tooltip={t('templateEdit.maxOrderSizeTooltip') || '比例模式下,限制单笔跟单订单的最大金额上限,用于防止跟单金额过大,控制风险。例如:设置为 1000,即使计算出的跟单金额超过 1000,也会限制为 1000 USDC。'}
tooltip={t('templateEdit.maxOrderSizeTooltip') || '比例模式下,限制单笔跟单订单的最大金额上限,用于防止跟单金额过大,控制风险。例如:设置为 1000,即使计算出的跟单金额超过 1000,也会限制为 $1000。'}
>
<InputNumber
min={0.0001}
@@ -249,9 +249,9 @@ const TemplateEdit: React.FC = () => {
</Form.Item>
<Form.Item
label={t('templateEdit.minOrderSize') || '单笔订单最小金额 (USDC)'}
label={t('templateEdit.minOrderSize') || '单笔订单最小金额 ($)'}
name="minOrderSize"
tooltip={t('templateEdit.minOrderSizeTooltip') || '比例模式下,限制单笔跟单订单的最小金额下限,用于过滤掉金额过小的订单,避免频繁小额交易。如果填写,必须 >= 1 USDC。例如:设置为 10,如果计算出的跟单金额小于 10,则跳过该订单。'}
tooltip={t('templateEdit.minOrderSizeTooltip') || '比例模式下,限制单笔跟单订单的最小金额下限,用于过滤掉金额过小的订单,避免频繁小额交易。如果填写,必须 >= $1。例如:设置为 10,如果计算出的跟单金额小于 10,则跳过该订单。'}
rules={[
{
validator: (_, value) => {
@@ -318,7 +318,7 @@ const TemplateEdit: React.FC = () => {
</Form.Item>
<Form.Item
label={t('templateEdit.minOrderDepth') || '最小订单深度 (USDC)'}
label={t('templateEdit.minOrderDepth') || '最小订单深度 ($)'}
name="minOrderDepth"
tooltip={t('templateEdit.minOrderDepthTooltip') || '检查订单簿的总订单金额(买盘+卖盘),确保市场有足够的流动性。不填写则不启用此过滤'}
>
+10 -10
View File
@@ -176,7 +176,7 @@ const TemplateList: React.FC = () => {
if (record.copyMode === 'RATIO') {
return `${t('templateList.ratio') || '比例'} ${record.copyRatio}x`
} else if (record.copyMode === 'FIXED' && record.fixedAmount) {
return `${t('templateList.fixedAmount') || '固定'} ${formatUSDC(record.fixedAmount)} USDC`
return `$${formatUSDC(record.fixedAmount)}`
}
return '-'
}
@@ -350,7 +350,7 @@ const TemplateList: React.FC = () => {
<div style={{ fontSize: '12px', opacity: '0.9' }}>
{template.copyMode === 'RATIO'
? `${t('templateList.ratioMode') || '比例模式'} ${(parseFloat(template.copyRatio || '0') * 100).toFixed(0).replace(/\.0+$/, '')}%`
: `${t('templateList.fixedAmountMode') || '固定金额'} ${formatUSDC(template.fixedAmount || '0')} USDC`
: `$${formatUSDC(template.fixedAmount || '0')}`
}
</div>
</div>
@@ -396,11 +396,11 @@ const TemplateList: React.FC = () => {
}}>
<span style={{ color: '#d48806' }}>{t('templateList.amountLimit') || '金额限制'}: </span>
{template.maxOrderSize && (
<span>{t('templateList.max') || '最大'} {formatUSDC(template.maxOrderSize)} USDC</span>
<span>{t('templateList.max') || '最大'} ${formatUSDC(template.maxOrderSize)}</span>
)}
{template.maxOrderSize && template.minOrderSize && <span> | </span>}
{template.minOrderSize && (
<span>{t('templateList.min') || '最小'} {formatUSDC(template.minOrderSize)} USDC</span>
<span>{t('templateList.min') || '最小'} ${formatUSDC(template.minOrderSize)}</span>
)}
{!template.maxOrderSize && !template.minOrderSize && <span style={{ color: '#bfbfbf' }}>{t('templateList.notSet') || '未设置'}</span>}
</div>
@@ -551,7 +551,7 @@ const TemplateList: React.FC = () => {
{copyMode === 'FIXED' && (
<Form.Item
label="固定跟单金额 (USDC)"
label="固定跟单金额 ($)"
name="fixedAmount"
rules={[
{ required: true, message: '请输入固定跟单金额' },
@@ -589,9 +589,9 @@ const TemplateList: React.FC = () => {
{copyMode === 'RATIO' && (
<>
<Form.Item
label="单笔订单最大金额 (USDC)"
label="单笔订单最大金额 ($)"
name="maxOrderSize"
tooltip="比例模式下,限制单笔跟单订单的最大金额上限,用于防止跟单金额过大,控制风险。例如:设置为 1000,即使计算出的跟单金额超过 1000,也会限制为 1000 USDC。"
tooltip="比例模式下,限制单笔跟单订单的最大金额上限,用于防止跟单金额过大,控制风险。例如:设置为 1000,即使计算出的跟单金额超过 1000,也会限制为 $1000。"
>
<InputNumber
min={0.01}
@@ -609,9 +609,9 @@ const TemplateList: React.FC = () => {
</Form.Item>
<Form.Item
label="单笔订单最小金额 (USDC)"
label="单笔订单最小金额 ($)"
name="minOrderSize"
tooltip="比例模式下,限制单笔跟单订单的最小金额下限,用于过滤掉金额过小的订单,避免频繁小额交易。如果填写,必须 >= 1 USDC。例如:设置为 10,如果计算出的跟单金额小于 10,则跳过该订单。"
tooltip="比例模式下,限制单笔跟单订单的最小金额下限,用于过滤掉金额过小的订单,避免频繁小额交易。如果填写,必须 >= $1。例如:设置为 10,如果计算出的跟单金额小于 10,则跳过该订单。"
rules={[
{
validator: (_, value) => {
@@ -698,7 +698,7 @@ const TemplateList: React.FC = () => {
<Divider></Divider>
<Form.Item
label="最小订单深度 (USDC)"
label="最小订单深度 ($)"
name="minOrderDepth"
tooltip="检查订单簿的总订单金额(买盘+卖盘),确保市场有足够的流动性。不填写则不启用此过滤"
>
+14 -2
View File
@@ -284,9 +284,21 @@ export const apiService = {
/**
*
*/
redeemPositions: (data: any) =>
redeemPositions: (data: any) =>
apiClient.post<ApiResponse<any>>('/accounts/positions/redeem', data),
/**
* USDC.e wrap pUSDV2
*/
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 }),
},
/**