From 3405a1cda3e59d7a4cf0af25149de084c2ba6479 Mon Sep 17 00:00:00 2001 From: WrBug Date: Sat, 14 Feb 2026 01:14:04 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=92=B1=E5=8C=85=E7=B1=BB=E5=9E=8B?= =?UTF-8?q?=E6=9E=9A=E4=B8=BE=E4=B8=8E=E5=89=8D=E7=AB=AF=E8=B4=A6=E5=8F=B7?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 后端: - 新增 WalletType 枚举(MAGIC/SAFE),移除 safe/magic 字符串硬编码 - RelayClientService/BlockchainService/AccountService/OrderSigningService 使用枚举 - Builder Relayer API 类型使用常量 RELAYER_TYPE_PROXY/SAFE 前端: - 账户列表、详情、导入 Modal 显示账号类型(Magic/Safe Tag) - 导入账户 Modal 移除安全提示 Alert,移除 showAlert 与相关 i18n - 移除无用 i18n key:securityTip、securityTipDesc、walletTypeMagic、walletTypeSafe、magicNotSupported - 钱包类型 Tag 简化为仅显示 Magic 或 Safe Co-authored-by: Cursor --- .../wrbug/polymarketbot/enums/WalletType.kt | 51 +++++++++++++++++++ .../service/accounts/AccountService.kt | 21 +++++--- .../service/common/BlockchainService.kt | 15 +++--- .../copytrading/orders/OrderSigningService.kt | 8 +-- .../service/system/RelayClientService.kt | 19 ++++--- frontend/src/components/AccountImportForm.tsx | 14 +---- frontend/src/locales/en/common.json | 7 +-- frontend/src/locales/zh-CN/common.json | 6 +-- frontend/src/locales/zh-TW/common.json | 7 +-- frontend/src/pages/AccountDetail.tsx | 7 +++ frontend/src/pages/AccountImport.tsx | 2 - frontend/src/pages/AccountList.tsx | 35 +++++++++++-- .../src/pages/CopyTradingOrders/AddModal.tsx | 2 - 13 files changed, 133 insertions(+), 61 deletions(-) create mode 100644 backend/src/main/kotlin/com/wrbug/polymarketbot/enums/WalletType.kt diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/enums/WalletType.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/enums/WalletType.kt new file mode 100644 index 0000000..7ab2fe4 --- /dev/null +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/enums/WalletType.kt @@ -0,0 +1,51 @@ +package com.wrbug.polymarketbot.enums + +/** + * 钱包类型枚举 + */ +enum class WalletType(val value: String, val description: String) { + /** + * Magic 钱包(邮箱/OAuth 登录) + * 使用 PROXY 代理合约,通过 Builder Relayer 执行 Gasless 交易 + */ + MAGIC("magic", "Magic(邮箱/OAuth登录)"), + + /** + * Safe 钱包(MetaMask 等 Web3 钱包) + * 使用 Gnosis Safe 代理合约,支持 Builder Relayer Gasless 或手动交易 + */ + SAFE("safe", "Safe(Web3钱包)"); + + companion object { + /** + * 从字符串值解析钱包类型(不区分大小写) + */ + fun fromString(value: String?): WalletType { + if (value.isNullOrBlank()) { + return SAFE // 默认返回 SAFE + } + return values().find { it.value.equals(value, ignoreCase = true) } + ?: throw IllegalArgumentException("未知的钱包类型: $value") + } + + /** + * 安全地从字符串值解析钱包类型(不区分大小写),解析失败返回默认值 + */ + fun fromStringOrDefault(value: String?, default: WalletType = SAFE): WalletType { + if (value.isNullOrBlank()) { + return default + } + return values().find { it.value.equals(value, ignoreCase = true) } ?: default + } + + /** + * 检查字符串是否为有效的钱包类型 + */ + fun isValid(value: String?): Boolean { + if (value.isNullOrBlank()) { + return false + } + return values().any { it.value.equals(value, ignoreCase = true) } + } + } +} diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/accounts/AccountService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/accounts/AccountService.kt index e0aa1b6..54a83ec 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/accounts/AccountService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/accounts/AccountService.kt @@ -3,6 +3,7 @@ package com.wrbug.polymarketbot.service.accounts import com.wrbug.polymarketbot.api.TradeResponse import com.wrbug.polymarketbot.dto.* import com.wrbug.polymarketbot.entity.Account +import com.wrbug.polymarketbot.enums.WalletType import com.wrbug.polymarketbot.repository.AccountRepository import com.wrbug.polymarketbot.util.RetrofitFactory import com.wrbug.polymarketbot.util.toSafeBigDecimal @@ -105,7 +106,8 @@ class AccountService( // 5. 获取代理地址(必须成功,否则导入失败) // 根据用户选择的钱包类型计算代理地址 val proxyAddress = runBlocking { - val proxyResult = blockchainService.getProxyAddress(request.walletAddress, request.walletType) + val walletTypeEnum = WalletType.fromStringOrDefault(request.walletType, WalletType.MAGIC) + val proxyResult = blockchainService.getProxyAddress(request.walletAddress, walletTypeEnum) if (proxyResult.isSuccess) { val address = proxyResult.getOrNull() if (address != null) { @@ -199,11 +201,11 @@ class AccountService( coroutineScope { val magicDeferred = async { try { - val proxyAddress = blockchainService.getProxyAddress(request.walletAddress, "magic").getOrNull() + val proxyAddress = blockchainService.getProxyAddress(request.walletAddress, WalletType.MAGIC).getOrNull() if (proxyAddress != null) { val balance = blockchainService.getWalletBalance(proxyAddress).getOrNull() ProxyOptionDto( - walletType = "magic", + walletType = WalletType.MAGIC.value, proxyAddress = proxyAddress, descriptionKey = "accountImport.proxyOption.magic.description", availableBalance = balance?.availableBalance ?: "0", @@ -246,11 +248,11 @@ class AccountService( val safeDeferred = async { try { - val proxyAddress = blockchainService.getProxyAddress(request.walletAddress, "safe").getOrNull() + val proxyAddress = blockchainService.getProxyAddress(request.walletAddress, WalletType.SAFE).getOrNull() if (proxyAddress != null) { val balance = blockchainService.getWalletBalance(proxyAddress).getOrNull() ProxyOptionDto( - walletType = "safe", + walletType = WalletType.SAFE.value, proxyAddress = proxyAddress, descriptionKey = "accountImport.proxyOption.safe.description", availableBalance = balance?.availableBalance ?: "0", @@ -300,7 +302,7 @@ class AccountService( } else { // 助记词导入:仅获取 Safe 代理地址及资产 try { - val proxyAddress = blockchainService.getProxyAddress(request.walletAddress, "safe").getOrNull() + val proxyAddress = blockchainService.getProxyAddress(request.walletAddress, WalletType.SAFE).getOrNull() if (proxyAddress != null) { val balance = blockchainService.getWalletBalance(proxyAddress).getOrNull() options.add( @@ -1398,7 +1400,9 @@ class AccountService( } // 4. 若涉及 Magic 账户,必须已配置 Builder API Key(提前判断,避免执行到深层再报错) - val hasMagicAccount = accounts.values.any { it.walletType.equals("magic", ignoreCase = true) } + val hasMagicAccount = accounts.values.any { + WalletType.fromStringOrDefault(it.walletType, WalletType.SAFE) == WalletType.MAGIC + } if (hasMagicAccount && !relayClientService.isBuilderApiKeyConfigured()) { return Result.failure( IllegalStateException("Builder API Key 未配置,无法执行 Magic 账户赎回(Gasless)。请前往系统设置页面配置 Builder API Key。") @@ -1468,12 +1472,13 @@ class AccountService( val decryptedPrivateKey = decryptPrivateKey(account) // 调用区块链服务赎回仓位 + val walletTypeEnum = WalletType.fromStringOrDefault(account.walletType, WalletType.SAFE) val redeemResult = blockchainService.redeemPositions( privateKey = decryptedPrivateKey, proxyAddress = account.proxyAddress, conditionId = marketId, indexSets = indexSets, - walletType = account.walletType + walletType = walletTypeEnum ) redeemResult.fold( diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/common/BlockchainService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/common/BlockchainService.kt index 30a80c5..21e4d74 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/common/BlockchainService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/common/BlockchainService.kt @@ -10,6 +10,7 @@ import com.wrbug.polymarketbot.api.ValueResponse import com.wrbug.polymarketbot.constants.PolymarketConstants import com.wrbug.polymarketbot.dto.PositionDto import com.wrbug.polymarketbot.dto.WalletBalanceResponse +import com.wrbug.polymarketbot.enums.WalletType import com.wrbug.polymarketbot.util.EthereumUtils import com.wrbug.polymarketbot.util.RetrofitFactory import com.wrbug.polymarketbot.util.createClient @@ -93,13 +94,13 @@ class BlockchainService( * 2. Safe Proxy(MetaMask 钱包用户)- 通过合约调用获取地址 * * @param walletAddress 用户的钱包地址(EOA) - * @param walletType 钱包类型:"magic"(默认)或 "safe" + * @param walletType 钱包类型:MAGIC(默认)或 SAFE * @return 代理钱包地址 */ - suspend fun getProxyAddress(walletAddress: String, walletType: String = "magic"): Result { + suspend fun getProxyAddress(walletAddress: String, walletType: WalletType = WalletType.MAGIC): Result { return try { - when (walletType.lowercase()) { - "safe" -> { + when (walletType) { + WalletType.SAFE -> { // Safe Proxy(MetaMask 用户) val safeProxyResult = getSafeProxyAddress(walletAddress) if (safeProxyResult.isSuccess) { @@ -110,7 +111,7 @@ class BlockchainService( Result.failure(safeProxyResult.exceptionOrNull() ?: Exception("获取 Safe Proxy 地址失败")) } } - else -> { + WalletType.MAGIC -> { // Magic Proxy(邮箱/OAuth 登录用户)- 默认 val magicProxyAddress = calculateMagicProxyAddress(walletAddress) logger.debug("使用 Magic Proxy 地址: $magicProxyAddress") @@ -586,7 +587,7 @@ class BlockchainService( * @param proxyAddress 代理地址(Safe 或 Magic 代理钱包地址) * @param conditionId 市场条件ID(bytes32,必须是 0x 开头的 66 位十六进制字符串) * @param indexSets 要赎回的索引集合列表(每个元素是 2^outcomeIndex) - * @param walletType 钱包类型:"magic" 或 "safe",用于选择执行路径 + * @param walletType 钱包类型:MAGIC 或 SAFE,用于选择执行路径 * @return 交易哈希 */ suspend fun redeemPositions( @@ -594,7 +595,7 @@ class BlockchainService( proxyAddress: String, conditionId: String, indexSets: List, - walletType: String = "safe" + walletType: WalletType = WalletType.SAFE ): Result { return try { if (indexSets.isEmpty()) { diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/orders/OrderSigningService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/orders/OrderSigningService.kt index 1245c43..7d760b6 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/orders/OrderSigningService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/orders/OrderSigningService.kt @@ -24,11 +24,13 @@ class OrderSigningService { /** * 根据钱包类型返回 CLOB 订单签名类型 - * @param walletType magic=邮箱/社交登录, safe=Web3 钱包 + * @param walletType Magic=邮箱/社交登录, Safe=Web3 钱包 * @return 1=POLY_PROXY(Magic), 2=POLY_GNOSIS_SAFE(Safe), 默认 2 */ - fun getSignatureTypeForWalletType(walletType: String?): Int = - if (walletType?.lowercase() == "magic") 1 else 2 + fun getSignatureTypeForWalletType(walletType: String?): Int { + val walletTypeEnum = com.wrbug.polymarketbot.enums.WalletType.fromStringOrDefault(walletType, com.wrbug.polymarketbot.enums.WalletType.SAFE) + return if (walletTypeEnum == com.wrbug.polymarketbot.enums.WalletType.MAGIC) 1 else 2 + } // Polygon 主网合约地址 private val EXCHANGE_CONTRACT = "0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E" diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/system/RelayClientService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/system/RelayClientService.kt index 63f6b44..ceffa71 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/system/RelayClientService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/system/RelayClientService.kt @@ -4,6 +4,7 @@ import com.wrbug.polymarketbot.api.BuilderRelayerApi import com.wrbug.polymarketbot.api.EthereumRpcApi import com.wrbug.polymarketbot.api.JsonRpcRequest import com.wrbug.polymarketbot.constants.PolymarketConstants +import com.wrbug.polymarketbot.enums.WalletType import com.wrbug.polymarketbot.util.EthereumUtils import com.wrbug.polymarketbot.util.RetrofitFactory import com.wrbug.polymarketbot.util.createClient @@ -44,6 +45,10 @@ class RelayClientService( private val proxyFactoryAddress = "0xaB45c5A4B0c941a2F231C04C3f49182e1A254052" private val relayHubAddress = "0xD216153c06E857cD7f72665E0aF1d7D82172F494" private val defaultProxyGasLimit = "10000000" + + // Builder Relayer API 交易类型常量 + private val RELAYER_TYPE_PROXY = "PROXY" + private val RELAYER_TYPE_SAFE = "SAFE" private val polygonRpcApi: EthereumRpcApi by lazy { val rpcUrl = rpcNodeService.getHttpUrl() @@ -212,14 +217,14 @@ class RelayClientService( * @param privateKey 私钥 * @param proxyAddress 代理钱包地址 * @param safeTx 交易对象(to/data/value) - * @param walletType 钱包类型:"magic" 使用 PROXY Gasless,"safe" 使用 Safe 流程 + * @param walletType 钱包类型:MAGIC 使用 PROXY Gasless,SAFE 使用 Safe 流程 * @return 交易哈希 */ suspend fun execute( privateKey: String, proxyAddress: String, safeTx: SafeTransaction, - walletType: String = "safe" + walletType: WalletType = WalletType.SAFE ): Result { return try { if (proxyAddress.isBlank() || !proxyAddress.startsWith("0x") || proxyAddress.length != 42) { @@ -230,7 +235,7 @@ class RelayClientService( val builderSecret = systemConfigService.getBuilderSecret() val builderPassphrase = systemConfigService.getBuilderPassphrase() - if (walletType.lowercase() == "magic") { + if (walletType == WalletType.MAGIC) { if (!isBuilderRelayerEnabled(builderApiKey, builderSecret, builderPassphrase)) { return Result.failure(IllegalStateException("Magic 账户赎回必须配置 Builder API Key(Gasless)")) } @@ -289,7 +294,7 @@ class RelayClientService( val credentials = org.web3j.crypto.Credentials.create(privateKeyBigInt.toString(16)) val fromAddress = credentials.address - val relayPayloadResponse = relayerApi.getRelayPayload(fromAddress, "PROXY") + val relayPayloadResponse = relayerApi.getRelayPayload(fromAddress, RELAYER_TYPE_PROXY) if (!relayPayloadResponse.isSuccessful || relayPayloadResponse.body() == null) { val errorBody = relayPayloadResponse.errorBody()?.string() ?: "未知错误" logger.error("获取 Relay Payload 失败: code=${relayPayloadResponse.code()}, body=$errorBody") @@ -338,7 +343,7 @@ class RelayClientService( String.format("%02x", (signature.v as ByteArray).getOrElse(0) { 0 }.toInt() and 0xff) val request = BuilderRelayerApi.TransactionRequest( - type = "PROXY", + type = RELAYER_TYPE_PROXY, from = fromAddress, to = proxyFactoryAddress, proxyWallet = proxyAddress, @@ -520,7 +525,7 @@ class RelayClientService( val redeemCallData = safeTx.data // 获取 Proxy 的 nonce(通过 Builder Relayer API) - val nonceResponse = relayerApi.getNonce(fromAddress, "SAFE") + val nonceResponse = relayerApi.getNonce(fromAddress, RELAYER_TYPE_SAFE) if (!nonceResponse.isSuccessful || nonceResponse.body() == null) { val errorBody = nonceResponse.errorBody()?.string() ?: "未知错误" logger.error("获取 nonce 失败: code=${nonceResponse.code()}, body=$errorBody") @@ -587,7 +592,7 @@ class RelayClientService( // 构建 TransactionRequest(参考 builder-relayer-client/src/builder/safe.ts) // 注意:根据 TypeScript 实现,data 和 signature 都应该带 0x 前缀 val request = BuilderRelayerApi.TransactionRequest( - type = "SAFE", + type = RELAYER_TYPE_SAFE, from = fromAddress, to = safeTx.to, proxyWallet = proxyAddress, diff --git a/frontend/src/components/AccountImportForm.tsx b/frontend/src/components/AccountImportForm.tsx index 17c9937..9bb1e4a 100644 --- a/frontend/src/components/AccountImportForm.tsx +++ b/frontend/src/components/AccountImportForm.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react' -import { Form, Input, Button, Radio, Space, Alert, Card, Spin, message } from 'antd' +import { Form, Input, Button, Radio, Space, Card, Spin, message, Alert } from 'antd' import { CheckCircleOutlined, ExclamationCircleOutlined } from '@ant-design/icons' import { useTranslation } from 'react-i18next' import { useAccountStore } from '../store/accountStore' @@ -22,7 +22,6 @@ interface AccountImportFormProps { form: any onSuccess?: (accountId: number) => void onCancel?: () => void - showAlert?: boolean showCancelButton?: boolean } @@ -30,7 +29,6 @@ const AccountImportForm: React.FC = ({ form, onSuccess, onCancel, - showAlert = true, showCancelButton = true }) => { const { t } = useTranslation() @@ -266,16 +264,6 @@ const AccountImportForm: React.FC = ({ return ( <> - {showAlert && ( - - )} -
{ {account.accountName || '-'} + {account.walletType && ( + + + {account.walletType.toLowerCase() === 'magic' ? 'Magic' : 'Safe'} + + + )} { form={form} onSuccess={handleSuccess} onCancel={() => navigate('/accounts')} - showAlert={true} - showCancelButton={true} /> diff --git a/frontend/src/pages/AccountList.tsx b/frontend/src/pages/AccountList.tsx index 4066038..8d93a80 100644 --- a/frontend/src/pages/AccountList.tsx +++ b/frontend/src/pages/AccountList.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react' -import { Card, Table, Button, Space, Tag, Popconfirm, message, Typography, Spin, Modal, Descriptions, Divider, Form, Input, Alert } from 'antd' +import { Card, Table, Button, Space, Tag, Popconfirm, message, Typography, Spin, Modal, Descriptions, Divider, Form, Input } from 'antd' import { PlusOutlined, ReloadOutlined, EditOutlined, CopyOutlined } from '@ant-design/icons' import { useTranslation } from 'react-i18next' import { useAccountStore } from '../store/accountStore' @@ -303,6 +303,20 @@ const AccountList: React.FC = () => { ) } }, + { + title: t('accountList.walletType'), + dataIndex: 'walletType', + key: 'walletType', + render: (walletType: string) => { + if (!walletType) return '-' + const type = walletType.toLowerCase() + return ( + + {type === 'magic' ? 'Magic' : 'Safe'} + + ) + } + }, { title: t('accountList.balance'), dataIndex: 'balance', @@ -392,7 +406,7 @@ const AccountList: React.FC = () => { style={{ marginLeft: '4px', padding: '0 4px' }} /> -
+
{t('accountList.proxyAddress')}: {record.proxyAddress ? `${record.proxyAddress.slice(0, 6)}...${record.proxyAddress.slice(-4)}` : '-'}
+ {record.walletType && ( +
+ {t('accountList.walletType')}:{' '} + + {record.walletType.toLowerCase() === 'magic' ? 'Magic' : 'Safe'} + +
+ )}
{ /> + {detailAccount.walletType && ( + + + {detailAccount.walletType.toLowerCase() === 'magic' ? 'Magic' : 'Safe'} + + + )} {detailBalanceLoading ? ( @@ -868,8 +897,6 @@ const AccountList: React.FC = () => { setAccountImportModalVisible(false) accountImportForm.resetFields() }} - showAlert={true} - showCancelButton={true} />
diff --git a/frontend/src/pages/CopyTradingOrders/AddModal.tsx b/frontend/src/pages/CopyTradingOrders/AddModal.tsx index 8619835..b581c9b 100644 --- a/frontend/src/pages/CopyTradingOrders/AddModal.tsx +++ b/frontend/src/pages/CopyTradingOrders/AddModal.tsx @@ -1120,8 +1120,6 @@ const AddModal: React.FC = ({ setAccountImportModalVisible(false) accountImportForm.resetFields() }} - showAlert={true} - showCancelButton={true} />