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:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user