feat: 实现 Builder API Key 系统级配置和 Gasless 交易支持

- 新增系统配置表(SystemConfig)用于存储 Builder API Key
- 实现 Builder API Key 的前端配置页面
- 集成 Builder Relayer API 支持 Gasless 交易
- 在 API 健康检查中添加 Builder Relayer API 检测
- 移除前端 API 测试功能,简化配置页面
- 修复赎回仓位时的 Builder API Key 检查逻辑
- 更新错误处理,引导用户配置 Builder API Key
This commit is contained in:
WrBug
2025-12-06 23:10:26 +08:00
parent 3369dbb248
commit bfbbbdd1de
27 changed files with 1971 additions and 233 deletions
+2
View File
@@ -35,6 +35,7 @@ import LanguageSettings from './pages/LanguageSettings'
import ApiHealthStatus from './pages/ApiHealthStatus'
import ProxySettings from './pages/ProxySettings'
import NotificationSettings from './pages/NotificationSettings'
import BuilderApiKeySettings from './pages/BuilderApiKeySettings'
import { wsManager } from './services/websocket'
import type { OrderPushMessage } from './types'
import { apiService } from './services/api'
@@ -253,6 +254,7 @@ function App() {
<Route path="/system-settings/api-health" element={<ProtectedRoute><ApiHealthStatus /></ProtectedRoute>} />
<Route path="/system-settings/proxy" element={<ProtectedRoute><ProxySettings /></ProtectedRoute>} />
<Route path="/system-settings/notifications" element={<ProtectedRoute><NotificationSettings /></ProtectedRoute>} />
<Route path="/system-settings/builder-api-key" element={<ProtectedRoute><BuilderApiKeySettings /></ProtectedRoute>} />
{/* 默认重定向到登录页 */}
<Route path="*" element={<Navigate to="/login" replace />} />
+14 -3
View File
@@ -19,7 +19,8 @@ import {
TwitterOutlined,
GlobalOutlined,
CheckCircleOutlined,
NotificationOutlined
NotificationOutlined,
KeyOutlined
} from '@ant-design/icons'
import type { MenuProps } from 'antd'
import type { ReactNode } from 'react'
@@ -121,6 +122,11 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
icon: <SettingOutlined />,
label: t('menu.systemSettings'),
children: [
{
key: '/system-settings',
icon: <SettingOutlined />,
label: t('menu.systemOverview') || '概览'
},
{
key: '/system-settings/language',
icon: <GlobalOutlined />,
@@ -136,6 +142,11 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
icon: <LinkOutlined />,
label: t('menu.proxy')
},
{
key: '/system-settings/builder-api-key',
icon: <KeyOutlined />,
label: t('menu.builderApiKey') || 'Builder API Key'
},
{
key: '/system-settings/notifications',
icon: <NotificationOutlined />,
@@ -173,8 +184,8 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
}
const handleMenuClick = ({ key }: { key: string }) => {
// 如果是父菜单,不导航
if (key === '/copy-trading-management' || key === '/system-settings') {
// 如果是父菜单,不导航(但 /system-settings 作为子菜单项时可以导航)
if (key === '/copy-trading-management') {
return
}
+45
View File
@@ -233,8 +233,10 @@
"statistics": "Statistics",
"users": "User Management",
"systemSettings": "System",
"systemOverview": "Overview",
"language": "Language",
"apiHealth": "API Health",
"builderApiKey": "Builder API Key",
"proxy": "Proxy",
"notifications": "Notifications",
"logout": "Logout",
@@ -302,6 +304,49 @@
"systemSettingsDesc": "Configure proxy, view API health status",
"footer": "Please use the above pages for configuration management."
},
"builderApiKey": {
"title": "Builder API Key Configuration",
"alertTitle": "What is Builder API Key?",
"description": "Builder API Key is a system-level configuration, not related to your account. It is an API credential for Polymarket's Builder service, used to execute Gasless (no gas fee) transactions.",
"purposeTitle": "Main purposes:",
"purpose1": "Execute Gasless transactions: Operations like redeeming positions and creating orders don't require gas fees",
"purpose2": "Improve user experience: No need to prepare MATIC tokens in wallet for gas fees",
"purpose3": "Reduce transaction costs: Gas fees are covered by Builder service",
"getApiKey": "Get API Key:",
"openSettings": "Go to Polymarket Settings",
"apiKey": "Builder API Key",
"secret": "Builder Secret",
"passphrase": "Builder Passphrase",
"apiKeyPlaceholder": "Please enter Builder API Key",
"secretPlaceholder": "Please enter Builder Secret",
"passphrasePlaceholder": "Please enter Builder Passphrase",
"apiKeyHelp": "Configured, enter new value to update",
"secretHelp": "Configured, enter new value to update",
"passphraseHelp": "Configured, enter new value to update",
"saveSuccess": "Builder API Key configuration saved successfully",
"saveFailed": "Failed to save Builder API Key configuration",
"getFailed": "Failed to get Builder API Key configuration",
"notConfigured": "Builder API Key not configured, please go to System Settings to configure",
"notConfiguredError": "Builder API Key not configured, cannot execute Gasless transactions. Please go to System Settings to configure Builder API Key.",
"apiReference": "API Reference Documentation",
"apiReferenceDesc": "Learn more about Builder API usage:",
"relayerApiDocs": "Builder Relayer API Documentation",
"builderDocs": "Builder Development Documentation",
"apiTest": "API Test",
"apiTestDesc": "Test if Builder API is working correctly",
"testNonce": "Test Get Nonce",
"testNonceHelp": "Enter Safe address (proxyAddress)",
"testDeployed": "Test Check Deployed Status",
"testDeployedHelp": "Enter Safe address (proxyAddress)",
"testTransaction": "Test Get Transaction Status",
"testTransactionHelp": "Enter transaction ID",
"testAddressRequired": "Please enter address",
"testTransactionIdRequired": "Please enter transaction ID",
"testAddressPlaceholder": "Enter address, e.g., 0x...",
"testTransactionIdPlaceholder": "Enter transaction ID",
"testSuccess": "Test successful",
"testFailed": "Test failed"
},
"resetPassword": {
"title": "Reset Password",
"firstUse": "First Time Using System",
+45
View File
@@ -150,8 +150,10 @@
"statistics": "统计信息",
"users": "用户管理",
"systemSettings": "系统管理",
"systemOverview": "概览",
"language": "语言",
"apiHealth": "API健康",
"builderApiKey": "Builder API Key",
"proxy": "代理",
"notifications": "消息推送",
"logout": "退出登录",
@@ -214,6 +216,49 @@
"systemSettingsDesc": "配置代理、查看 API 健康状态",
"footer": "请使用上述页面进行配置管理。"
},
"builderApiKey": {
"title": "Builder API Key 配置",
"alertTitle": "什么是 Builder API Key",
"description": "Builder API Key 是系统级别的配置,与您的账户无关。它是 Polymarket 提供的 Builder 服务的 API 凭证,用于执行 Gasless(免 gas 费)交易。",
"purposeTitle": "主要用途:",
"purpose1": "执行 Gasless 交易:赎回仓位、创建订单等操作无需支付 gas 费用",
"purpose2": "提升用户体验:无需在钱包中准备 MATIC 代币作为 gas 费",
"purpose3": "降低交易成本:由 Builder 服务承担 gas 费用",
"getApiKey": "获取 API Key",
"openSettings": "前往 Polymarket 设置页面",
"apiKey": "Builder API Key",
"secret": "Builder Secret",
"passphrase": "Builder Passphrase",
"apiKeyPlaceholder": "请输入 Builder API Key",
"secretPlaceholder": "请输入 Builder Secret",
"passphrasePlaceholder": "请输入 Builder Passphrase",
"apiKeyHelp": "已配置,输入新值以更新",
"secretHelp": "已配置,输入新值以更新",
"passphraseHelp": "已配置,输入新值以更新",
"saveSuccess": "保存 Builder API Key 配置成功",
"saveFailed": "保存 Builder API Key 配置失败",
"getFailed": "获取 Builder API Key 配置失败",
"notConfigured": "Builder API Key 未配置,请前往系统设置页面配置",
"notConfiguredError": "Builder API Key 未配置,无法执行 Gasless 交易。请前往系统设置页面配置 Builder API Key。",
"apiReference": "API 参考文档",
"apiReferenceDesc": "了解更多关于 Builder API 的使用方法:",
"relayerApiDocs": "Builder Relayer API 文档",
"builderDocs": "Builder 开发文档",
"apiTest": "API 测试",
"apiTestDesc": "测试 Builder API 是否正常工作",
"testNonce": "测试获取 Nonce",
"testNonceHelp": "输入 Safe 地址(proxyAddress",
"testDeployed": "测试检查部署状态",
"testDeployedHelp": "输入 Safe 地址(proxyAddress",
"testTransaction": "测试获取交易状态",
"testTransactionHelp": "输入交易ID",
"testAddressRequired": "请输入地址",
"testTransactionIdRequired": "请输入交易ID",
"testAddressPlaceholder": "输入地址,例如:0x...",
"testTransactionIdPlaceholder": "输入交易ID",
"testSuccess": "测试成功",
"testFailed": "测试失败"
},
"resetPassword": {
"title": "重置密码",
"firstUse": "首次使用系统",
+45
View File
@@ -233,8 +233,10 @@
"statistics": "統計信息",
"users": "用戶管理",
"systemSettings": "系統管理",
"systemOverview": "概覽",
"language": "語言",
"apiHealth": "API健康",
"builderApiKey": "Builder API Key",
"proxy": "代理",
"notifications": "消息推送",
"logout": "退出登錄",
@@ -302,6 +304,49 @@
"systemSettingsDesc": "配置代理、查看 API 健康狀態",
"footer": "請使用上述頁面進行配置管理。"
},
"builderApiKey": {
"title": "Builder API Key 配置",
"alertTitle": "什麼是 Builder API Key",
"description": "Builder API Key 是系統級別的配置,與您的賬戶無關。它是 Polymarket 提供的 Builder 服務的 API 憑證,用於執行 Gasless(免 gas 費)交易。",
"purposeTitle": "主要用途:",
"purpose1": "執行 Gasless 交易:贖回倉位、創建訂單等操作無需支付 gas 費用",
"purpose2": "提升用戶體驗:無需在錢包中準備 MATIC 代幣作為 gas 費",
"purpose3": "降低交易成本:由 Builder 服務承擔 gas 費用",
"getApiKey": "獲取 API Key",
"openSettings": "前往 Polymarket 設置頁面",
"apiKey": "Builder API Key",
"secret": "Builder Secret",
"passphrase": "Builder Passphrase",
"apiKeyPlaceholder": "請輸入 Builder API Key",
"secretPlaceholder": "請輸入 Builder Secret",
"passphrasePlaceholder": "請輸入 Builder Passphrase",
"apiKeyHelp": "已配置,輸入新值以更新",
"secretHelp": "已配置,輸入新值以更新",
"passphraseHelp": "已配置,輸入新值以更新",
"saveSuccess": "保存 Builder API Key 配置成功",
"saveFailed": "保存 Builder API Key 配置失敗",
"getFailed": "獲取 Builder API Key 配置失敗",
"notConfigured": "Builder API Key 未配置,請前往系統設置頁面配置",
"notConfiguredError": "Builder API Key 未配置,無法執行 Gasless 交易。請前往系統設置頁面配置 Builder API Key。",
"apiReference": "API 參考文檔",
"apiReferenceDesc": "了解更多關於 Builder API 的使用方法:",
"relayerApiDocs": "Builder Relayer API 文檔",
"builderDocs": "Builder 開發文檔",
"apiTest": "API 測試",
"apiTestDesc": "測試 Builder API 是否正常工作",
"testNonce": "測試獲取 Nonce",
"testNonceHelp": "輸入 Safe 地址(proxyAddress",
"testDeployed": "測試檢查部署狀態",
"testDeployedHelp": "輸入 Safe 地址(proxyAddress",
"testTransaction": "測試獲取交易狀態",
"testTransactionHelp": "輸入交易ID",
"testAddressRequired": "請輸入地址",
"testTransactionIdRequired": "請輸入交易ID",
"testAddressPlaceholder": "輸入地址,例如:0x...",
"testTransactionIdPlaceholder": "輸入交易ID",
"testSuccess": "測試成功",
"testFailed": "測試失敗"
},
"resetPassword": {
"title": "重置密碼",
"firstUse": "首次使用系統",
@@ -0,0 +1,165 @@
import { useEffect, useState } from 'react'
import { Card, Form, Button, Input, message, Typography, Space, Alert } from 'antd'
import { SaveOutlined, LinkOutlined } from '@ant-design/icons'
import { apiService } from '../services/api'
import { useMediaQuery } from 'react-responsive'
import { useTranslation } from 'react-i18next'
import type { SystemConfig, BuilderApiKeyUpdateRequest } from '../types'
const { Title, Paragraph, Text } = Typography
const BuilderApiKeySettings: React.FC = () => {
const { t } = useTranslation()
const isMobile = useMediaQuery({ maxWidth: 768 })
const [builderApiKeyForm] = Form.useForm()
const [builderApiKeyConfig, setBuilderApiKeyConfig] = useState<SystemConfig | null>(null)
const [builderApiKeyLoading, setBuilderApiKeyLoading] = useState(false)
useEffect(() => {
fetchBuilderApiKeyConfig()
}, [])
const fetchBuilderApiKeyConfig = async () => {
try {
const response = await apiService.systemConfig.get()
if (response.data.code === 0 && response.data.data) {
const config = response.data.data
setBuilderApiKeyConfig(config)
// 预填充字段(如果已配置,显示占位符)
builderApiKeyForm.setFieldsValue({
builderApiKey: config.builderApiKeyConfigured ? '***' : '',
builderSecret: config.builderSecretConfigured ? '***' : '',
builderPassphrase: config.builderPassphraseConfigured ? '***' : '',
})
} else {
message.error(response.data.msg || t('builderApiKey.getFailed'))
}
} catch (error: any) {
message.error(error.message || t('builderApiKey.getFailed'))
}
}
const handleBuilderApiKeySubmit = async (values: BuilderApiKeyUpdateRequest) => {
setBuilderApiKeyLoading(true)
try {
// 如果值是 '***',表示已配置但未修改,不发送
const updateData: BuilderApiKeyUpdateRequest = {}
if (values.builderApiKey && values.builderApiKey !== '***') {
updateData.builderApiKey = values.builderApiKey
}
if (values.builderSecret && values.builderSecret !== '***') {
updateData.builderSecret = values.builderSecret
}
if (values.builderPassphrase && values.builderPassphrase !== '***') {
updateData.builderPassphrase = values.builderPassphrase
}
const response = await apiService.systemConfig.updateBuilderApiKey(updateData)
if (response.data.code === 0) {
message.success(t('builderApiKey.saveSuccess'))
fetchBuilderApiKeyConfig()
} else {
message.error(response.data.msg || t('builderApiKey.saveFailed'))
}
} catch (error: any) {
message.error(error.message || t('builderApiKey.saveFailed'))
} finally {
setBuilderApiKeyLoading(false)
}
}
return (
<div>
<div style={{ marginBottom: '16px' }}>
<Title level={2} style={{ margin: 0 }}>{t('builderApiKey.title')}</Title>
</div>
<Card>
<Alert
message={t('builderApiKey.alertTitle')}
description={
<div>
<Paragraph style={{ marginBottom: '8px' }}>
{t('builderApiKey.description')}
</Paragraph>
<Paragraph style={{ marginBottom: '8px' }}>
<Text strong>{t('builderApiKey.purposeTitle')}</Text>
<ul style={{ marginTop: '8px', marginBottom: 0, paddingLeft: '20px' }}>
<li>{t('builderApiKey.purpose1')}</li>
<li>{t('builderApiKey.purpose2')}</li>
<li>{t('builderApiKey.purpose3')}</li>
</ul>
</Paragraph>
<Paragraph style={{ marginBottom: 0 }}>
<Text strong>{t('builderApiKey.getApiKey')}</Text>
<Space style={{ marginLeft: '8px' }}>
<a
href="https://polymarket.com/settings?tab=builder"
target="_blank"
rel="noopener noreferrer"
>
<LinkOutlined /> {t('builderApiKey.openSettings')}
</a>
</Space>
</Paragraph>
</div>
}
type="info"
showIcon
style={{ marginBottom: '24px' }}
/>
<Form
form={builderApiKeyForm}
layout="vertical"
onFinish={handleBuilderApiKeySubmit}
size={isMobile ? 'middle' : 'large'}
>
<Form.Item
label={t('builderApiKey.apiKey')}
name="builderApiKey"
help={builderApiKeyConfig?.builderApiKeyConfigured ? t('builderApiKey.apiKeyHelp') : t('builderApiKey.apiKeyPlaceholder')}
>
<Input
placeholder={builderApiKeyConfig?.builderApiKeyConfigured ? t('builderApiKey.apiKeyHelp') : t('builderApiKey.apiKeyPlaceholder')}
/>
</Form.Item>
<Form.Item
label={t('builderApiKey.secret')}
name="builderSecret"
help={builderApiKeyConfig?.builderSecretConfigured ? t('builderApiKey.secretHelp') : t('builderApiKey.secretPlaceholder')}
>
<Input
placeholder={builderApiKeyConfig?.builderSecretConfigured ? t('builderApiKey.secretHelp') : t('builderApiKey.secretPlaceholder')}
/>
</Form.Item>
<Form.Item
label={t('builderApiKey.passphrase')}
name="builderPassphrase"
help={builderApiKeyConfig?.builderPassphraseConfigured ? t('builderApiKey.passphraseHelp') : t('builderApiKey.passphrasePlaceholder')}
>
<Input
placeholder={builderApiKeyConfig?.builderPassphraseConfigured ? t('builderApiKey.passphraseHelp') : t('builderApiKey.passphrasePlaceholder')}
/>
</Form.Item>
<Form.Item>
<Button
type="primary"
htmlType="submit"
icon={<SaveOutlined />}
loading={builderApiKeyLoading}
>
{t('common.save') || '保存配置'}
</Button>
</Form.Item>
</Form>
</Card>
</div>
)
}
export default BuilderApiKeySettings
+28 -2
View File
@@ -1,6 +1,7 @@
import { useEffect, useState, useMemo } from 'react'
import { Card, Table, Tag, message, Space, Input, Radio, Select, Button, Row, Col, Empty, Modal, Form, Descriptions } from 'antd'
import { SearchOutlined, AppstoreOutlined, UnorderedListOutlined, UpOutlined, DownOutlined } from '@ant-design/icons'
import { useNavigate } from 'react-router-dom'
import { apiService } from '../services/api'
import type { AccountPosition, Account, PositionPushMessage, PositionSellRequest, MarketPriceResponse, RedeemablePositionsSummary, PositionRedeemRequest } from '../types'
import { getPositionKey } from '../types'
@@ -13,6 +14,7 @@ type PositionFilter = 'current' | 'historical'
type ViewMode = 'card' | 'list'
const PositionList: React.FC = () => {
const navigate = useNavigate()
const isMobile = useMediaQuery({ maxWidth: 768 })
const [currentPositions, setCurrentPositions] = useState<AccountPosition[]>([])
const [historyPositions, setHistoryPositions] = useState<AccountPosition[]>([])
@@ -113,10 +115,34 @@ const PositionList: React.FC = () => {
// 刷新可赎回统计
await fetchRedeemableSummary()
} else {
message.error(response.data.msg || '赎回失败')
// 检查是否是 Builder API Key 未配置的错误
if (response.data.code === 2014 || response.data.msg?.includes('Builder API Key 未配置')) {
message.error({
content: response.data.msg || 'Builder API Key 未配置',
duration: 5,
})
// 延迟跳转,让用户看到错误消息
setTimeout(() => {
navigate('/system-settings/builder-api-key')
}, 1500)
} else {
message.error(response.data.msg || '赎回失败')
}
}
} catch (error: any) {
message.error('赎回失败: ' + (error.message || '未知错误'))
// 检查是否是 Builder API Key 未配置的错误
if (error.response?.data?.code === 2014 || error.message?.includes('Builder API Key 未配置')) {
message.error({
content: error.response?.data?.msg || error.message || 'Builder API Key 未配置,请前往系统设置页面配置',
duration: 5,
})
// 延迟跳转,让用户看到错误消息
setTimeout(() => {
navigate('/system-settings/builder-api-key')
}, 1500)
} else {
message.error('赎回失败: ' + (error.message || '未知错误'))
}
} finally {
setRedeeming(false)
}
+17
View File
@@ -578,6 +578,23 @@ export const apiService = {
*/
getTelegramChatIds: (data: { botToken: string }) =>
apiClient.post<ApiResponse<string[]>>('/notifications/telegram/get-chat-ids', data)
},
/**
* 系统配置 API
*/
systemConfig: {
/**
* 获取系统配置
*/
get: () =>
apiClient.post<ApiResponse<import('../types').SystemConfig>>('/system/config/get', {}),
/**
* 更新 Builder API Key 配置
*/
updateBuilderApiKey: (data: import('../types').BuilderApiKeyUpdateRequest) =>
apiClient.post<ApiResponse<import('../types').SystemConfig>>('/system/config/builder-api-key/update', data)
}
}
+18
View File
@@ -753,6 +753,24 @@ export interface NotificationConfigRequest {
/**
* 通知配置更新请求
*/
/**
* 系统配置响应
*/
export interface SystemConfig {
builderApiKeyConfigured: boolean
builderSecretConfigured: boolean
builderPassphraseConfigured: boolean
}
/**
* Builder API Key 更新请求
*/
export interface BuilderApiKeyUpdateRequest {
builderApiKey?: string
builderSecret?: string
builderPassphrase?: string
}
export interface NotificationConfigUpdateRequest {
id: number
type: string