diff --git a/.gitignore b/.gitignore index c773d98..0c705f8 100644 --- a/.gitignore +++ b/.gitignore @@ -19,7 +19,7 @@ backend/gradle-app.setting backend/.gradle # 注意:gradle-wrapper.jar 应该被提交,不要忽略 # backend/gradle/wrapper/gradle-wrapper.jar - +polyhub/ # Kotlin *.kt.bak *.class diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/api/BuilderRelayerApi.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/api/BuilderRelayerApi.kt index d69fac0..01b07eb 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/api/BuilderRelayerApi.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/api/BuilderRelayerApi.kt @@ -41,6 +41,17 @@ interface BuilderRelayerApi { @Query("address") address: String, @Query("type") type: String ): Response + + /** + * 获取 Relay Payload(PROXY 类型执行时使用) + * GET /relay-payload?address={address}&type=PROXY + * 参考: builder-relayer-client endpoints GET_RELAY_PAYLOAD + */ + @GET("/relay-payload") + suspend fun getRelayPayload( + @Query("address") address: String, + @Query("type") type: String + ): Response /** * 获取交易状态 @@ -96,6 +107,7 @@ interface BuilderRelayerApi { /** * 签名参数 * 参考: builder-relayer-client/src/types.ts 的 SignatureParams + * Safe 使用 operation/safeTxnGas/baseGas 等,PROXY 使用 relayHub/relay/relayerFee 等 */ data class SignatureParams( @SerializedName("gasPrice") @@ -114,7 +126,19 @@ interface BuilderRelayerApi { val gasToken: String? = null, @SerializedName("refundReceiver") - val refundReceiver: String? = null + val refundReceiver: String? = null, + + @SerializedName("relayerFee") + val relayerFee: String? = null, + + @SerializedName("gasLimit") + val gasLimit: String? = null, + + @SerializedName("relayHub") + val relayHub: String? = null, + + @SerializedName("relay") + val relay: String? = null ) /** @@ -142,6 +166,17 @@ interface BuilderRelayerApi { @SerializedName("nonce") val nonce: String ) + + /** + * Relay Payload(PROXY 执行时获取 relay 地址与 nonce) + * 参考: builder-relayer-client types RelayPayload + */ + data class RelayPayload( + @SerializedName("address") + val address: String, + @SerializedName("nonce") + val nonce: String + ) /** * Relayer 交易详情 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 fc20724..65abab8 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 @@ -827,7 +827,7 @@ class AccountService( "0" } - // 11. 创建并签名订单(使用计算后的卖出数量) + // 11. 创建并签名订单(使用计算后的卖出数量,按账户钱包类型使用对应 signatureType) val signedOrder = try { orderSigningService.createAndSignOrder( privateKey = decryptedPrivateKey, @@ -836,7 +836,7 @@ class AccountService( side = "SELL", price = sellPrice, size = sellQuantity.toPlainString(), // 使用计算后的卖出数量 - signatureType = 2, // Browser Wallet(与正确订单数据一致) + signatureType = orderSigningService.getSignatureTypeForWalletType(account.walletType), nonce = "0", feeRateBps = feeRateBps, // 使用动态获取的费率 expiration = expiration @@ -1260,7 +1260,7 @@ class AccountService( accountRedeemedInfo[accountId] = accountInfo } - // 5. 对每个账户执行赎回 + // 5. 对每个账户执行赎回(Safe 与 Magic 均支持,Magic 通过 Builder Relayer PROXY Gasless 执行) val accountTransactions = mutableListOf() var totalRedeemedValue = BigDecimal.ZERO @@ -1284,7 +1284,8 @@ class AccountService( privateKey = decryptedPrivateKey, proxyAddress = account.proxyAddress, conditionId = marketId, - indexSets = indexSets + indexSets = indexSets, + walletType = account.walletType ) 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 6445c34..30a80c5 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 @@ -580,39 +580,35 @@ class BlockchainService( /** * 赎回仓位 - * 通过代理钱包的 execTransaction 调用 ConditionalTokens 合约的 redeemPositions 函数 - * - * 使用 RelayClientService 实现,完全参考 TypeScript 项目的实现方式 - * + * Safe 账户通过代理 execTransaction 调用,Magic 账户通过 Builder Relayer PROXY(Gasless)执行 + * * @param privateKey 私钥(原始钱包的私钥,用于签名交易) - * @param proxyAddress 代理地址(Gnosis Safe 代理钱包地址) + * @param proxyAddress 代理地址(Safe 或 Magic 代理钱包地址) * @param conditionId 市场条件ID(bytes32,必须是 0x 开头的 66 位十六进制字符串) - * @param indexSets 要赎回的索引集合列表(每个元素是 2^outcomeIndex,例如 [1] 表示 outcome 0,[2] 表示 outcome 1) + * @param indexSets 要赎回的索引集合列表(每个元素是 2^outcomeIndex) + * @param walletType 钱包类型:"magic" 或 "safe",用于选择执行路径 * @return 交易哈希 */ suspend fun redeemPositions( privateKey: String, proxyAddress: String, conditionId: String, - indexSets: List + indexSets: List, + walletType: String = "safe" ): Result { return try { - // 验证参数 if (indexSets.isEmpty()) { return Result.failure(IllegalArgumentException("indexSets 不能为空")) } - if (conditionId.isBlank() || !conditionId.startsWith("0x") || conditionId.length != 66) { return Result.failure(IllegalArgumentException("conditionId 格式错误,必须是 0x 开头的 66 位十六进制字符串")) } - if (proxyAddress.isBlank() || !proxyAddress.startsWith("0x") || proxyAddress.length != 42) { return Result.failure(IllegalArgumentException("proxyAddress 格式错误,必须是有效的以太坊地址")) } - // 使用 RelayClientService 创建赎回交易并执行 val redeemTx = relayClientService.createRedeemTx(conditionId, indexSets) - relayClientService.execute(privateKey, proxyAddress, redeemTx) + relayClientService.execute(privateKey, proxyAddress, redeemTx, walletType) } catch (e: Exception) { logger.error("赎回仓位失败: ${e.message}", e) Result.failure(e) 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 5e04636..1245c43 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 @@ -19,9 +19,17 @@ import java.math.RoundingMode */ @Service class OrderSigningService { - + private val logger = LoggerFactory.getLogger(OrderSigningService::class.java) - + + /** + * 根据钱包类型返回 CLOB 订单签名类型 + * @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 + // Polygon 主网合约地址 private val EXCHANGE_CONTRACT = "0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E" private val CHAIN_ID = 137L diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/statistics/CopyOrderTrackingService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/statistics/CopyOrderTrackingService.kt index 1b1f4cc..cf58159 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/statistics/CopyOrderTrackingService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/statistics/CopyOrderTrackingService.kt @@ -570,7 +570,8 @@ open class CopyOrderTrackingService( owner = account.apiKey, copyTradingId = copyTrading.id!!, tradeId = trade.id, - feeRateBps = feeRateBps + feeRateBps = feeRateBps, + signatureType = orderSigningService.getSignatureTypeForWalletType(account.walletType) ) // 处理订单创建失败 @@ -994,7 +995,7 @@ open class CopyOrderTrackingService( "0" } - // 9. 创建并签名卖出订单 + // 9. 创建并签名卖出订单(按账户钱包类型使用对应 signatureType) val signedOrder = try { orderSigningService.createAndSignOrder( privateKey = decryptedPrivateKey, @@ -1003,7 +1004,7 @@ open class CopyOrderTrackingService( side = "SELL", price = sellPrice.toString(), size = totalMatched.toString(), - signatureType = 2, // Browser Wallet + signatureType = orderSigningService.getSignatureTypeForWalletType(account.walletType), nonce = "0", feeRateBps = feeRateBps, // 使用动态获取的费率 expiration = "0" @@ -1044,7 +1045,8 @@ open class CopyOrderTrackingService( owner = account.apiKey, copyTradingId = copyTrading.id, tradeId = leaderSellTrade.id, - feeRateBps = feeRateBps + feeRateBps = feeRateBps, + signatureType = orderSigningService.getSignatureTypeForWalletType(account.walletType) ) if (createOrderResult.isFailure) { @@ -1137,6 +1139,7 @@ open class CopyOrderTrackingService( * @param copyTradingId 跟单配置ID(用于日志) * @param tradeId Leader 交易ID(用于日志) * @param feeRateBps 费率基点(从API动态获取) + * @param signatureType 签名类型(1=Magic, 2=Safe) * @return 成功返回订单ID,失败返回异常 */ private suspend fun createOrderWithRetry( @@ -1150,7 +1153,8 @@ open class CopyOrderTrackingService( owner: String, copyTradingId: Long, tradeId: String, - feeRateBps: String + feeRateBps: String, + signatureType: Int ): Result { var lastError: Exception? = null @@ -1165,7 +1169,7 @@ open class CopyOrderTrackingService( side = side, price = price, size = size, - signatureType = 2, // Browser Wallet + signatureType = signatureType, nonce = "0", feeRateBps = feeRateBps, // 使用动态获取的费率 expiration = "0" 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 698533d..69a38f0 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 @@ -40,6 +40,11 @@ class RelayClientService( // 空集合ID private val EMPTY_SET = "0x0000000000000000000000000000000000000000000000000000000000000000" + // Polygon PROXY(Magic)合约地址,参考 builder-relayer-client config + private val proxyFactoryAddress = "0xaB45c5A4B0c941a2F231C04C3f49182e1A254052" + private val relayHubAddress = "0xD216153c06E857cD7f72665E0aF1d7D82172F494" + private val defaultProxyGasLimit = "10000000" + private val polygonRpcApi: EthereumRpcApi by lazy { val rpcUrl = rpcNodeService.getHttpUrl() retrofitFactory.createEthereumRpcApi(rpcUrl) @@ -201,33 +206,45 @@ class RelayClientService( } /** - * 执行 Safe 交易(通过 Proxy.execTransaction) + * 执行代理交易(Safe 或 Magic PROXY) * 参考 TypeScript: RelayClient.execute() * - * 优先使用 Builder Relayer(Gasless),如果未配置则回退到手动发送交易 - * * @param privateKey 私钥 * @param proxyAddress 代理钱包地址 - * @param safeTx Safe 交易对象 + * @param safeTx 交易对象(to/data/value) + * @param walletType 钱包类型:"magic" 使用 PROXY Gasless,"safe" 使用 Safe 流程 * @return 交易哈希 */ suspend fun execute( privateKey: String, proxyAddress: String, - safeTx: SafeTransaction + safeTx: SafeTransaction, + walletType: String = "safe" ): Result { return try { - // 验证参数 if (proxyAddress.isBlank() || !proxyAddress.startsWith("0x") || proxyAddress.length != 42) { return Result.failure(IllegalArgumentException("proxyAddress 格式错误,必须是有效的以太坊地址")) } - // 检查 Builder API Key 是否已配置 val builderApiKey = systemConfigService.getBuilderApiKey() val builderSecret = systemConfigService.getBuilderSecret() val builderPassphrase = systemConfigService.getBuilderPassphrase() - // 优先使用 Builder Relayer(Gasless) + if (walletType.lowercase() == "magic") { + if (!isBuilderRelayerEnabled(builderApiKey, builderSecret, builderPassphrase)) { + return Result.failure(IllegalStateException("Magic 账户赎回必须配置 Builder API Key(Gasless)")) + } + logger.info("使用 Builder Relayer PROXY 执行 Magic 赎回") + return executeViaBuilderRelayerProxy( + privateKey, + proxyAddress, + safeTx, + builderApiKey!!, + builderSecret!!, + builderPassphrase!! + ) + } + if (isBuilderRelayerEnabled(builderApiKey, builderSecret, builderPassphrase)) { logger.info("使用 Builder Relayer 执行 Gasless 交易") return executeViaBuilderRelayer( @@ -240,15 +257,166 @@ class RelayClientService( ) } - // 回退到手动发送交易(需要用户支付 gas) logger.info("Builder Relayer 未配置,使用手动发送交易(需要用户支付 gas)") return executeManually(privateKey, proxyAddress, safeTx) } catch (e: Exception) { - logger.error("执行 Safe 交易失败: ${e.message}", e) + logger.error("执行交易失败: ${e.message}", e) Result.failure(e) } } + /** + * 通过 Builder Relayer 执行 PROXY(Magic)交易(Gasless) + * 参考: builder-relayer-client client.ts executeProxyTransactions, builder/proxy.ts + */ + private suspend fun executeViaBuilderRelayerProxy( + privateKey: String, + proxyAddress: String, + safeTx: SafeTransaction, + builderApiKey: String, + builderSecret: String, + builderPassphrase: String + ): Result { + val relayerApi = retrofitFactory.createBuilderRelayerApi( + relayerUrl = PolymarketConstants.BUILDER_RELAYER_URL, + apiKey = builderApiKey, + secret = builderSecret, + passphrase = builderPassphrase + ) + + val cleanPrivateKey = privateKey.removePrefix("0x") + val privateKeyBigInt = BigInteger(cleanPrivateKey, 16) + val credentials = org.web3j.crypto.Credentials.create(privateKeyBigInt.toString(16)) + val fromAddress = credentials.address + + val relayPayloadResponse = relayerApi.getRelayPayload(fromAddress, "PROXY") + if (!relayPayloadResponse.isSuccessful || relayPayloadResponse.body() == null) { + val errorBody = relayPayloadResponse.errorBody()?.string() ?: "未知错误" + logger.error("获取 Relay Payload 失败: code=${relayPayloadResponse.code()}, body=$errorBody") + return Result.failure(Exception("获取 Relay Payload 失败: ${relayPayloadResponse.code()} - $errorBody")) + } + val relayPayload = relayPayloadResponse.body()!! + val relayAddress = relayPayload.address + val nonce = relayPayload.nonce + + val proxyCallData = encodeProxyTransactionData(safeTx) + val gasLimit = defaultProxyGasLimit + + val structHash = createProxyStructHash( + from = fromAddress, + to = proxyFactoryAddress, + data = proxyCallData, + txFee = "0", + gasPrice = "0", + gasLimit = gasLimit, + nonce = nonce, + relayHubAddress = relayHubAddress, + relayAddress = relayAddress + ) + + val prefix = "\u0019Ethereum Signed Message:\n32".toByteArray(Charsets.UTF_8) + val messageWithPrefix = ByteArray(prefix.size + structHash.size) + System.arraycopy(prefix, 0, messageWithPrefix, 0, prefix.size) + System.arraycopy(structHash, 0, messageWithPrefix, prefix.size, structHash.size) + + val keccak256 = org.bouncycastle.crypto.digests.KeccakDigest(256) + keccak256.update(messageWithPrefix, 0, messageWithPrefix.size) + val hashWithPrefix = ByteArray(keccak256.digestSize) + keccak256.doFinal(hashWithPrefix, 0) + + val ecKeyPair = org.web3j.crypto.ECKeyPair.create(privateKeyBigInt) + val signature = org.web3j.crypto.Sign.signMessage(hashWithPrefix, ecKeyPair, false) + val sigHex = "0x" + org.web3j.utils.Numeric.toHexString(signature.r).removePrefix("0x").padStart(64, '0') + + org.web3j.utils.Numeric.toHexString(signature.s).removePrefix("0x").padStart(64, '0') + + String.format("%02x", (signature.v as ByteArray).getOrElse(0) { 0 }.toInt() and 0xff) + + val request = BuilderRelayerApi.TransactionRequest( + type = "PROXY", + from = fromAddress, + to = proxyFactoryAddress, + proxyWallet = proxyAddress, + data = proxyCallData, + nonce = nonce, + signature = sigHex, + signatureParams = BuilderRelayerApi.SignatureParams( + gasPrice = "0", + gasLimit = gasLimit, + relayerFee = "0", + relayHub = relayHubAddress, + relay = relayAddress + ), + metadata = "Redeem positions via Builder Relayer PROXY" + ) + + val response = relayerApi.submitTransaction(request) + if (!response.isSuccessful || response.body() == null) { + val errorBody = response.errorBody()?.string() ?: "未知错误" + logger.error("Builder Relayer PROXY API 调用失败: code=${response.code()}, body=$errorBody") + return Result.failure(Exception("Builder Relayer PROXY 调用失败: ${response.code()} - $errorBody")) + } + + val relayerResponse = response.body()!! + val txHash = relayerResponse.transactionHash ?: relayerResponse.hash + ?: return Result.failure(Exception("Builder Relayer 返回的交易哈希为空")) + logger.info("Builder Relayer PROXY 执行成功: transactionID=${relayerResponse.transactionID}, txHash=$txHash") + return Result.success(txHash) + } + + /** + * 编码 ProxyFactory.proxy(calls) 调用数据 + * 参考: builder-relayer-client encode/proxy.ts, abis proxyFactory proxy((uint8,address,uint256,bytes)[]) + */ + private fun encodeProxyTransactionData(safeTx: SafeTransaction): String { + val selector = EthereumUtils.getFunctionSelector("proxy((uint8,address,uint256,bytes)[])") + val callData = safeTx.data.removePrefix("0x") + val dataLen = callData.length / 2 + val dataLenPadded = (dataLen + 31) / 32 * 32 * 2 + val dataPadded = callData.padEnd(dataLenPadded, '0') + + val arrayOffset = EthereumUtils.encodeUint256(BigInteger.valueOf(32)) + val arrayLength = EthereumUtils.encodeUint256(BigInteger.ONE) + val typeCode = EthereumUtils.encodeUint256(BigInteger.ONE) + val toEncoded = EthereumUtils.encodeAddress(safeTx.to) + val valueEncoded = EthereumUtils.encodeUint256(BigInteger.ZERO) + val dataOffsetInTuple = BigInteger.valueOf(192) + val dataOffsetEncoded = EthereumUtils.encodeUint256(dataOffsetInTuple) + val dataLengthEncoded = EthereumUtils.encodeUint256(BigInteger.valueOf(dataLen.toLong())) + return "0x" + selector.removePrefix("0x") + arrayOffset + arrayLength + + typeCode + toEncoded + valueEncoded + dataOffsetEncoded + + dataLengthEncoded + dataPadded + } + + /** + * 创建 PROXY 结构哈希,参考 builder-relayer-client builder/proxy.ts createStructHash + * concat: "rlx:" + from + to + data + txFee + gasPrice + gasLimit + nonce + relayHub + relay, then keccak256 + */ + private fun createProxyStructHash( + from: String, + to: String, + data: String, + txFee: String, + gasPrice: String, + gasLimit: String, + nonce: String, + relayHubAddress: String, + relayAddress: String + ): ByteArray { + val rlxPrefix = "rlx:".toByteArray(Charsets.UTF_8) + val fromBytes = EthereumUtils.hexToBytes(from.lowercase().removePrefix("0x").padStart(40, '0')) + val toBytes = EthereumUtils.hexToBytes(to.lowercase().removePrefix("0x").padStart(40, '0')) + val dataBytes = EthereumUtils.hexToBytes(data.removePrefix("0x")) + val txFeeBytes = EthereumUtils.encodeUint256(BigInteger(txFee)).let { EthereumUtils.hexToBytes(it) } + val gasPriceBytes = EthereumUtils.encodeUint256(BigInteger(gasPrice)).let { EthereumUtils.hexToBytes(it) } + val gasLimitBytes = EthereumUtils.encodeUint256(BigInteger(gasLimit)).let { EthereumUtils.hexToBytes(it) } + val nonceBytes = EthereumUtils.encodeUint256(BigInteger(nonce)).let { EthereumUtils.hexToBytes(it) } + val relayHubBytes = EthereumUtils.hexToBytes(relayHubAddress.lowercase().removePrefix("0x").padStart(40, '0')) + val relayBytes = EthereumUtils.hexToBytes(relayAddress.lowercase().removePrefix("0x").padStart(40, '0')) + + val concat = rlxPrefix + fromBytes + toBytes + dataBytes + txFeeBytes + gasPriceBytes + + gasLimitBytes + nonceBytes + relayHubBytes + relayBytes + return EthereumUtils.keccak256(concat) + } + /** * 通过 Builder Relayer 执行交易(Gasless) * 参考: builder-relayer-client/src/client.ts 的 execute 方法 diff --git a/frontend/src/components/AccountImportForm.tsx b/frontend/src/components/AccountImportForm.tsx index 35237b4..b1b0a60 100644 --- a/frontend/src/components/AccountImportForm.tsx +++ b/frontend/src/components/AccountImportForm.tsx @@ -227,8 +227,8 @@ const AccountImportForm: React.FC = ({ {t('accountImport.walletTypeSafe')} - - {t('accountImport.walletTypeMagic')} {t('accountImport.magicNotSupported')} + + {t('accountImport.walletTypeMagic')} diff --git a/frontend/src/locales/en/common.json b/frontend/src/locales/en/common.json index b968220..86d7136 100644 --- a/frontend/src/locales/en/common.json +++ b/frontend/src/locales/en/common.json @@ -215,7 +215,7 @@ "walletTypeHelp": "Web3 Wallet: Polymarket accounts connected via browser wallets like MetaMask\nMagic: Polymarket accounts logged in via email or social accounts (Google, Twitter, etc.)", "walletTypeMagic": "Magic (Email/Social Login)", "walletTypeSafe": "Web3 Wallet", - "magicNotSupported": "(Not Supported)" + "magicNotSupported": "" }, "leader": { "title": "Leader Management", diff --git a/frontend/src/locales/zh-CN/common.json b/frontend/src/locales/zh-CN/common.json index 88276ab..1e2871c 100644 --- a/frontend/src/locales/zh-CN/common.json +++ b/frontend/src/locales/zh-CN/common.json @@ -215,7 +215,7 @@ "walletTypeHelp": "Web3钱包:使用 MetaMask 等浏览器钱包连接的 Polymarket 账户\nMagic:通过邮箱或社交账号(如 Google、Twitter)登录的 Polymarket 账户", "walletTypeMagic": "Magic(邮箱/社交账号登录)", "walletTypeSafe": "Web3钱包", - "magicNotSupported": "(暂不支持)" + "magicNotSupported": "" }, "leader": { "title": "Leader 管理", diff --git a/frontend/src/locales/zh-TW/common.json b/frontend/src/locales/zh-TW/common.json index e4747ab..638fa60 100644 --- a/frontend/src/locales/zh-TW/common.json +++ b/frontend/src/locales/zh-TW/common.json @@ -215,7 +215,7 @@ "walletTypeHelp": "Web3錢包:使用 MetaMask 等瀏覽器錢包連接的 Polymarket 帳戶\nMagic:透過郵箱或社群帳號(如 Google、Twitter)登入的 Polymarket 帳戶", "walletTypeMagic": "Magic(郵箱/社群帳號登入)", "walletTypeSafe": "Web3錢包", - "magicNotSupported": "(暫不支持)" + "magicNotSupported": "" }, "leader": { "title": "Leader 管理",