feat: 实现RPC节点管理功能

- 后端功能:
  - 添加RPC节点配置管理(增删改查、优先级调整)
  - 实现节点健康检查和自动故障转移
  - 修复Retrofit baseUrl处理问题(使用拦截器动态替换URL)
  - 添加RPC节点可用性验证(创建前验证)
  - 默认节点作为兜底,不返回给前端,始终排在最后
  - API健康检查使用动态获取的可用节点

- 前端功能:
  - 添加RPC节点设置页面
  - 支持添加、删除、检查节点
  - 支持调整节点优先级
  - 完整的多语言支持(中文简体/繁体、英文)
  - 添加菜单项多语言key

- 数据库:
  - 添加RPC节点配置表迁移脚本
This commit is contained in:
WrBug
2025-12-25 23:58:50 +08:00
parent 7bc059ec34
commit 1580b60d24
20 changed files with 1856 additions and 70 deletions
+2 -1
View File
@@ -32,6 +32,7 @@ import CopyTradingMatchedOrders from './pages/CopyTradingMatchedOrders'
import FilteredOrdersList from './pages/FilteredOrdersList'
import SystemSettings from './pages/SystemSettings'
import ApiHealthStatus from './pages/ApiHealthStatus'
import RpcNodeSettings from './pages/RpcNodeSettings'
import Announcements from './pages/Announcements'
import { wsManager } from './services/websocket'
import type { OrderPushMessage } from './types'
@@ -263,7 +264,7 @@ function App() {
<Route path="/users" element={<ProtectedRoute><UserList /></ProtectedRoute>} />
<Route path="/announcements" element={<ProtectedRoute><Announcements /></ProtectedRoute>} />
<Route path="/system-settings" element={<ProtectedRoute><SystemSettings /></ProtectedRoute>} />
<Route path="/system-settings/api-health" element={<ProtectedRoute><ApiHealthStatus /></ProtectedRoute>} />
<Route path="/system-settings/rpc-nodes" element={<ProtectedRoute><RpcNodeSettings /></ProtectedRoute>} /> <Route path="/system-settings/api-health" element={<ProtectedRoute><ApiHealthStatus /></ProtectedRoute>} />
{/* 默认重定向到登录页 */}
<Route path="*" element={<Navigate to="/login" replace />} />
+172
View File
@@ -0,0 +1,172 @@
import { useState } from 'react'
import { Modal, Form, Input, Select, message, Space } from 'antd'
import { LinkOutlined } from '@ant-design/icons'
import { apiService } from '../services/api'
import type { RpcNodeAddRequest } from '../types'
import { useTranslation } from 'react-i18next'
interface AddRpcNodeModalProps {
visible: boolean
onCancel: () => void
onSuccess: () => void
}
const { Option } = Select
const AddRpcNodeModal: React.FC<AddRpcNodeModalProps> = ({ visible, onCancel, onSuccess }) => {
const { t } = useTranslation()
const [form] = Form.useForm()
const [selectedProvider, setSelectedProvider] = useState<string>('CUSTOM')
const [validating, setValidating] = useState(false)
const providerOptions = [
{ value: 'ALCHEMY', label: t('rpcNodeSettings.providerAlchemy'), url: 'https://dashboard.alchemy.com/' },
{ value: 'INFURA', label: t('rpcNodeSettings.providerInfura'), url: 'https://infura.io/' },
{ value: 'QUICKNODE', label: t('rpcNodeSettings.providerQuickNode'), url: 'https://www.quicknode.com/' },
{ value: 'CHAINSTACK', label: t('rpcNodeSettings.providerChainstack'), url: 'https://chainstack.com/' },
{ value: 'GETBLOCK', label: t('rpcNodeSettings.providerGetBlock'), url: 'https://getblock.io/' },
{ value: 'CUSTOM', label: t('rpcNodeSettings.customNode'), url: '' }
]
const handleSubmit = async () => {
try {
const values = await form.validateFields()
setValidating(true)
const request: RpcNodeAddRequest = {
providerType: values.providerType,
name: values.name,
apiKey: values.apiKey,
httpUrl: values.httpUrl,
wsUrl: values.wsUrl
}
// 先验证节点
const validateResponse = await apiService.rpcNodes.validate(request)
if (validateResponse.data.code === 0 && validateResponse.data.data) {
const result = validateResponse.data.data
if (!result.valid) {
message.error(`${t('rpcNodeSettings.validateFailed')} ${result.message}`)
setValidating(false)
return
}
// 验证通过,添加节点
const addResponse = await apiService.rpcNodes.add(request)
if (addResponse.data.code === 0) {
message.success(t('rpcNodeSettings.addSuccess'))
form.resetFields()
setSelectedProvider('CUSTOM')
onSuccess()
} else {
message.error(addResponse.data.msg || t('rpcNodeSettings.addFailed'))
}
} else {
message.error(validateResponse.data.msg || t('rpcNodeSettings.validateError'))
}
} catch (error: any) {
if (!error.errorFields) {
message.error(error.message || t('rpcNodeSettings.operationFailed'))
}
} finally {
setValidating(false)
}
}
const handleCancel = () => {
form.resetFields()
setSelectedProvider('CUSTOM')
onCancel()
}
const currentProvider = providerOptions.find(p => p.value === selectedProvider)
return (
<Modal
title={t('rpcNodeSettings.addNodeTitle')}
open={visible}
onOk={handleSubmit}
onCancel={handleCancel}
width={600}
confirmLoading={validating}
okText={t('rpcNodeSettings.validateAndAdd')}
cancelText={t('common.cancel')}
>
<Form
form={form}
layout="vertical"
initialValues={{ providerType: 'CUSTOM' }}
>
<Form.Item
label={t('rpcNodeSettings.providerTypeLabel')}
name="providerType"
rules={[{ required: true, message: t('rpcNodeSettings.providerTypeRequired') }]}
>
<Select onChange={setSelectedProvider}>
{providerOptions.map(opt => (
<Option key={opt.value} value={opt.value}>{opt.label}</Option>
))}
</Select>
</Form.Item>
<Form.Item
label={t('rpcNodeSettings.nodeNameLabel')}
name="name"
rules={[{ required: true, message: t('rpcNodeSettings.nodeNameRequired') }]}
>
<Input placeholder={t('rpcNodeSettings.nodeNamePlaceholder')} />
</Form.Item>
{selectedProvider !== 'CUSTOM' && (
<Form.Item
label={
<Space>
<span>{t('rpcNodeSettings.apiKeyLabel')}</span>
{currentProvider?.url && (
<a href={currentProvider.url} target="_blank" rel="noopener noreferrer">
<LinkOutlined /> {t('rpcNodeSettings.getApiKey')}
</a>
)}
</Space>
}
name="apiKey"
rules={[{ required: true, message: t('rpcNodeSettings.apiKeyRequired') }]}
>
<Input.Password
placeholder={t('rpcNodeSettings.apiKeyPlaceholder')}
autoComplete="off"
/>
</Form.Item>
)}
{selectedProvider === 'CUSTOM' && (
<>
<Form.Item
label={t('rpcNodeSettings.httpUrlLabel')}
name="httpUrl"
rules={[
{ required: true, message: t('rpcNodeSettings.httpUrlRequired') },
{ type: 'url', message: t('rpcNodeSettings.httpUrlInvalid') }
]}
>
<Input placeholder={t('rpcNodeSettings.httpUrlPlaceholder')} />
</Form.Item>
<Form.Item
label={t('rpcNodeSettings.wsUrlLabel')}
name="wsUrl"
rules={[{ type: 'url', message: t('rpcNodeSettings.wsUrlInvalid') }]}
>
<Input placeholder={t('rpcNodeSettings.wsUrlPlaceholder')} />
</Form.Item>
</>
)}
</Form>
</Modal>
)
}
export default AddRpcNodeModal
+6 -1
View File
@@ -19,7 +19,7 @@ import {
TwitterOutlined,
CheckCircleOutlined,
SendOutlined,
NotificationOutlined
ApiOutlined, NotificationOutlined
} from '@ant-design/icons'
import type { MenuProps } from 'antd'
import type { ReactNode } from 'react'
@@ -131,6 +131,11 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
icon: <SettingOutlined />,
label: t('menu.systemOverview') || '通用设置'
},
{
key: '/system-settings/rpc-nodes',
icon: <ApiOutlined />,
label: t('menu.rpcNodes') || 'RPC节点管理'
},
{
key: '/system-settings/api-health',
icon: <CheckCircleOutlined />,
+58
View File
@@ -237,6 +237,7 @@
"systemSettings": "System",
"systemOverview": "Overview",
"language": "Language",
"rpcNodes": "RPC Nodes",
"apiHealth": "API Health",
"builderApiKey": "Builder API Key",
"proxy": "Proxy",
@@ -1065,5 +1066,62 @@
"averagePnl": "Average PnL",
"maxPnl": "Max PnL",
"minPnl": "Min PnL"
},
"rpcNodeSettings": {
"title": "Polygon RPC Node Configuration",
"fetchFailed": "Failed to fetch node list",
"checkHealthSuccess": "Health check completed",
"checkHealthFailed": "Health check failed",
"deleteSuccess": "Deleted successfully",
"deleteFailed": "Failed to delete",
"adjustPrioritySuccess": "Priority adjusted successfully",
"adjustPriorityFailed": "Failed to adjust priority",
"priority": "Priority",
"providerType": "Provider",
"name": "Name",
"status": "Status",
"statusHealthy": "Available",
"statusUnhealthy": "Unavailable",
"statusUnknown": "Unknown",
"responseTime": "Response Time",
"actions": "Actions",
"check": "Check",
"checkSuccess": "Check completed",
"checkFailed": "Check failed",
"deleteConfirm": "Are you sure you want to delete this node?",
"deleteConfirmOk": "Confirm",
"deleteConfirmCancel": "Cancel",
"delete": "Delete",
"batchCheck": "Batch Check",
"addNode": "Add Node",
"addNodeTitle": "Add RPC Node",
"validateAndAdd": "Validate and Add",
"providerTypeLabel": "Provider Type",
"providerTypeRequired": "Please select provider type",
"nodeNameLabel": "Node Name",
"nodeNameRequired": "Please enter node name",
"nodeNamePlaceholder": "e.g.: My Alchemy Node",
"apiKeyLabel": "API Key",
"getApiKey": "Get API Key",
"apiKeyRequired": "Please enter API Key",
"apiKeyPlaceholder": "Enter your API Key",
"httpUrlLabel": "HTTP RPC URL",
"httpUrlRequired": "Please enter HTTP RPC URL",
"httpUrlInvalid": "Please enter a valid URL",
"httpUrlPlaceholder": "https://polygon-rpc.com",
"wsUrlLabel": "WebSocket URL (Optional)",
"wsUrlInvalid": "Please enter a valid URL",
"wsUrlPlaceholder": "wss://polygon-rpc.com",
"validateFailed": "Node validation failed:",
"addSuccess": "Added successfully",
"addFailed": "Failed to add",
"validateError": "Validation failed",
"operationFailed": "Operation failed",
"customNode": "Custom Node",
"providerAlchemy": "Alchemy",
"providerInfura": "Infura",
"providerQuickNode": "QuickNode",
"providerChainstack": "Chainstack",
"providerGetBlock": "GetBlock"
}
}
+58
View File
@@ -237,6 +237,7 @@
"systemSettings": "系统管理",
"systemOverview": "概览",
"language": "语言",
"rpcNodes": "RPC节点管理",
"apiHealth": "API健康",
"builderApiKey": "Builder API Key",
"proxy": "代理",
@@ -1065,5 +1066,62 @@
"averagePnl": "平均盈亏",
"maxPnl": "最大盈亏",
"minPnl": "最小盈亏"
},
"rpcNodeSettings": {
"title": "Polygon RPC 节点配置",
"fetchFailed": "获取节点列表失败",
"checkHealthSuccess": "健康检查完成",
"checkHealthFailed": "健康检查失败",
"deleteSuccess": "删除成功",
"deleteFailed": "删除失败",
"adjustPrioritySuccess": "调整成功",
"adjustPriorityFailed": "调整失败",
"priority": "优先级",
"providerType": "服务商",
"name": "名称",
"status": "状态",
"statusHealthy": "可用",
"statusUnhealthy": "不可用",
"statusUnknown": "未知",
"responseTime": "响应时间",
"actions": "操作",
"check": "检查",
"checkSuccess": "检查完成",
"checkFailed": "检查失败",
"deleteConfirm": "确定删除此节点吗?",
"deleteConfirmOk": "确定",
"deleteConfirmCancel": "取消",
"delete": "删除",
"batchCheck": "批量检查",
"addNode": "添加节点",
"addNodeTitle": "添加 RPC 节点",
"validateAndAdd": "验证并添加",
"providerTypeLabel": "服务商类型",
"providerTypeRequired": "请选择服务商类型",
"nodeNameLabel": "节点名称",
"nodeNameRequired": "请输入节点名称",
"nodeNamePlaceholder": "例如: My Alchemy Node",
"apiKeyLabel": "API Key",
"getApiKey": "获取 API Key",
"apiKeyRequired": "请输入 API Key",
"apiKeyPlaceholder": "输入您的 API Key",
"httpUrlLabel": "HTTP RPC URL",
"httpUrlRequired": "请输入 HTTP RPC URL",
"httpUrlInvalid": "请输入有效的 URL",
"httpUrlPlaceholder": "https://polygon-rpc.com",
"wsUrlLabel": "WebSocket URL (可选)",
"wsUrlInvalid": "请输入有效的 URL",
"wsUrlPlaceholder": "wss://polygon-rpc.com",
"validateFailed": "节点验证失败:",
"addSuccess": "添加成功",
"addFailed": "添加失败",
"validateError": "验证失败",
"operationFailed": "操作失败",
"customNode": "自定义节点",
"providerAlchemy": "Alchemy",
"providerInfura": "Infura",
"providerQuickNode": "QuickNode",
"providerChainstack": "Chainstack",
"providerGetBlock": "GetBlock"
}
}
+58
View File
@@ -237,6 +237,7 @@
"systemSettings": "系統管理",
"systemOverview": "概覽",
"language": "語言",
"rpcNodes": "RPC節點管理",
"apiHealth": "API健康",
"builderApiKey": "Builder API Key",
"proxy": "代理",
@@ -1065,5 +1066,62 @@
"averagePnl": "平均盈虧",
"maxPnl": "最大盈虧",
"minPnl": "最小盈虧"
},
"rpcNodeSettings": {
"title": "Polygon RPC 節點配置",
"fetchFailed": "獲取節點列表失敗",
"checkHealthSuccess": "健康檢查完成",
"checkHealthFailed": "健康檢查失敗",
"deleteSuccess": "刪除成功",
"deleteFailed": "刪除失敗",
"adjustPrioritySuccess": "調整成功",
"adjustPriorityFailed": "調整失敗",
"priority": "優先級",
"providerType": "服務商",
"name": "名稱",
"status": "狀態",
"statusHealthy": "可用",
"statusUnhealthy": "不可用",
"statusUnknown": "未知",
"responseTime": "響應時間",
"actions": "操作",
"check": "檢查",
"checkSuccess": "檢查完成",
"checkFailed": "檢查失敗",
"deleteConfirm": "確定刪除此節點嗎?",
"deleteConfirmOk": "確定",
"deleteConfirmCancel": "取消",
"delete": "刪除",
"batchCheck": "批量檢查",
"addNode": "添加節點",
"addNodeTitle": "添加 RPC 節點",
"validateAndAdd": "驗證並添加",
"providerTypeLabel": "服務商類型",
"providerTypeRequired": "請選擇服務商類型",
"nodeNameLabel": "節點名稱",
"nodeNameRequired": "請輸入節點名稱",
"nodeNamePlaceholder": "例如: My Alchemy Node",
"apiKeyLabel": "API Key",
"getApiKey": "獲取 API Key",
"apiKeyRequired": "請輸入 API Key",
"apiKeyPlaceholder": "輸入您的 API Key",
"httpUrlLabel": "HTTP RPC URL",
"httpUrlRequired": "請輸入 HTTP RPC URL",
"httpUrlInvalid": "請輸入有效的 URL",
"httpUrlPlaceholder": "https://polygon-rpc.com",
"wsUrlLabel": "WebSocket URL (可選)",
"wsUrlInvalid": "請輸入有效的 URL",
"wsUrlPlaceholder": "wss://polygon-rpc.com",
"validateFailed": "節點驗證失敗:",
"addSuccess": "添加成功",
"addFailed": "添加失敗",
"validateError": "驗證失敗",
"operationFailed": "操作失敗",
"customNode": "自定義節點",
"providerAlchemy": "Alchemy",
"providerInfura": "Infura",
"providerQuickNode": "QuickNode",
"providerChainstack": "Chainstack",
"providerGetBlock": "GetBlock"
}
}
+233
View File
@@ -0,0 +1,233 @@
import { useState, useEffect } from 'react'
import { Card, Table, Button, Space, Badge, message, Popconfirm, Tag } from 'antd'
import { UpOutlined, DownOutlined, DeleteOutlined, ReloadOutlined, PlusOutlined, ApiOutlined } from '@ant-design/icons'
import { apiService } from '../services/api'
import type { RpcNodeConfig } from '../types'
import AddRpcNodeModal from '../components/AddRpcNodeModal'
import { useTranslation } from 'react-i18next'
const RpcNodeSettings: React.FC = () => {
const { t } = useTranslation()
const [nodes, setNodes] = useState<RpcNodeConfig[]>([])
const [loading, setLoading] = useState(false)
const [checking, setChecking] = useState(false)
const [modalVisible, setModalVisible] = useState(false)
useEffect(() => {
fetchNodes()
}, [])
const fetchNodes = async () => {
setLoading(true)
try {
const response = await apiService.rpcNodes.list()
if (response.data.code === 0 && response.data.data) {
setNodes(response.data.data)
} else {
message.error(response.data.msg || t('rpcNodeSettings.fetchFailed'))
}
} catch (error: any) {
message.error(error.message || t('rpcNodeSettings.fetchFailed'))
} finally {
setLoading(false)
}
}
const handleCheckAllHealth = async () => {
setChecking(true)
try {
const response = await apiService.rpcNodes.checkHealth({})
if (response.data.code === 0) {
message.success(t('rpcNodeSettings.checkHealthSuccess'))
fetchNodes()
} else {
message.error(response.data.msg || t('rpcNodeSettings.checkHealthFailed'))
}
} catch (error: any) {
message.error(error.message || t('rpcNodeSettings.checkHealthFailed'))
} finally {
setChecking(false)
}
}
const handleDelete = async (id: number) => {
try {
const response = await apiService.rpcNodes.delete({ id })
if (response.data.code === 0) {
message.success(t('rpcNodeSettings.deleteSuccess'))
fetchNodes()
} else {
message.error(response.data.msg || t('rpcNodeSettings.deleteFailed'))
}
} catch (error: any) {
message.error(error.message || t('rpcNodeSettings.deleteFailed'))
}
}
const handleMovePriority = async (id: number, direction: 'up' | 'down') => {
const index = nodes.findIndex(n => n.id === id)
if (index === -1) return
if (direction === 'up' && index === 0) return
if (direction === 'down' && index === nodes.length - 1) return
const targetIndex = direction === 'up' ? index - 1 : index + 1
const newPriority = nodes[targetIndex].priority
try {
const response = await apiService.rpcNodes.updatePriority({ id, priority: newPriority })
if (response.data.code === 0) {
// 同时更新另一个节点的优先级
await apiService.rpcNodes.updatePriority({
id: nodes[targetIndex].id,
priority: nodes[index].priority
})
message.success(t('rpcNodeSettings.adjustPrioritySuccess'))
fetchNodes()
} else {
message.error(response.data.msg || t('rpcNodeSettings.adjustPriorityFailed'))
}
} catch (error: any) {
message.error(error.message || t('rpcNodeSettings.adjustPriorityFailed'))
}
}
const columns = [
{
title: t('rpcNodeSettings.priority'),
dataIndex: 'priority',
width: 120,
render: (_: any, record: RpcNodeConfig, index: number) => (
<Space>
<Button
size="small"
icon={<UpOutlined />}
onClick={() => handleMovePriority(record.id, 'up')}
disabled={index === 0}
/>
<Button
size="small"
icon={<DownOutlined />}
onClick={() => handleMovePriority(record.id, 'down')}
disabled={index === nodes.length - 1}
/>
<span>{index + 1}</span>
</Space>
)
},
{
title: t('rpcNodeSettings.providerType'),
dataIndex: 'providerType',
width: 120,
render: (type: string) => <Tag color="blue">{type}</Tag>
},
{
title: t('rpcNodeSettings.name'),
dataIndex: 'name',
ellipsis: true
},
{
title: t('rpcNodeSettings.status'),
dataIndex: 'lastCheckStatus',
width: 100,
render: (status: string | undefined) => {
const statusMap = {
HEALTHY: { status: 'success' as const, text: t('rpcNodeSettings.statusHealthy') },
UNHEALTHY: { status: 'error' as const, text: t('rpcNodeSettings.statusUnhealthy') },
UNKNOWN: { status: 'default' as const, text: t('rpcNodeSettings.statusUnknown') }
}
const config = statusMap[status as keyof typeof statusMap] || statusMap.UNKNOWN
return <Badge status={config.status} text={config.text} />
}
},
{
title: t('rpcNodeSettings.responseTime'),
dataIndex: 'responseTimeMs',
width: 100,
render: (time: number | undefined) => time ? `${time}ms` : '-'
},
{
title: t('rpcNodeSettings.actions'),
key: 'action',
width: 150,
render: (_: any, record: RpcNodeConfig) => (
<Space size="small">
<Button
size="small"
onClick={async () => {
try {
const response = await apiService.rpcNodes.checkHealth({ id: record.id })
if (response.data.code === 0) {
message.success(t('rpcNodeSettings.checkSuccess'))
fetchNodes()
}
} catch (error: any) {
message.error(t('rpcNodeSettings.checkFailed'))
}
}}
>
{t('rpcNodeSettings.check')}
</Button>
<Popconfirm
title={t('rpcNodeSettings.deleteConfirm')}
onConfirm={() => handleDelete(record.id)}
okText={t('rpcNodeSettings.deleteConfirmOk')}
cancelText={t('rpcNodeSettings.deleteConfirmCancel')}
>
<Button size="small" danger icon={<DeleteOutlined />}>
{t('rpcNodeSettings.delete')}
</Button>
</Popconfirm>
</Space>
)
}
]
return (
<Card
title={
<Space>
<ApiOutlined />
<span>{t('rpcNodeSettings.title')}</span>
</Space>
}
extra={
<Space>
<Button
icon={<ReloadOutlined />}
onClick={handleCheckAllHealth}
loading={checking}
>
{t('rpcNodeSettings.batchCheck')}
</Button>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => setModalVisible(true)}
>
{t('rpcNodeSettings.addNode')}
</Button>
</Space>
}
>
<Table
dataSource={nodes}
columns={columns}
rowKey="id"
loading={loading}
pagination={false}
/>
<AddRpcNodeModal
visible={modalVisible}
onCancel={() => setModalVisible(false)}
onSuccess={() => {
setModalVisible(false)
fetchNodes()
}}
/>
</Card>
)
}
export default RpcNodeSettings
+1
View File
@@ -887,6 +887,7 @@ const SystemSettings: React.FC = () => {
showIcon
/>
)}
</Card>
</div>
)
+27
View File
@@ -618,6 +618,33 @@ export const apiService = {
/**
* API
*/
/**
* RPC API
*/
rpcNodes: {
list: () =>
apiClient.post<ApiResponse<import('../types').RpcNodeConfig[]>>('/system/rpc-nodes/list', {}),
add: (data: import('../types').RpcNodeAddRequest) =>
apiClient.post<ApiResponse<import('../types').RpcNodeConfig>>('/system/rpc-nodes/add', data),
update: (data: import('../types').RpcNodeUpdateRequest) =>
apiClient.post<ApiResponse<import('../types').RpcNodeConfig>>('/system/rpc-nodes/update', data),
delete: (data: { id: number }) =>
apiClient.post<ApiResponse<void>>('/system/rpc-nodes/delete', data),
updatePriority: (data: { id: number; priority: number }) =>
apiClient.post<ApiResponse<void>>('/system/rpc-nodes/update-priority', data),
checkHealth: (data: { id?: number }) =>
apiClient.post<ApiResponse<any>>('/system/rpc-nodes/check-health', data),
validate: (data: import('../types').RpcNodeAddRequest) =>
apiClient.post<ApiResponse<{ valid: boolean; message: string; responseTimeMs?: number }>>('/system/rpc-nodes/validate', data)
},
announcements: {
/**
* 10
+51
View File
@@ -806,3 +806,54 @@ export interface NotificationConfigUpdateRequest {
}
}
/**
* RPC
*/
export interface RpcNodeConfig {
id: number
providerType: 'ALCHEMY' | 'INFURA' | 'QUICKNODE' | 'CHAINSTACK' | 'GETBLOCK' | 'CUSTOM' | 'PUBLIC'
name: string
httpUrl: string
wsUrl?: string
apiKeyMasked?: string // 脱敏后的 API Key
enabled: boolean
priority: number
lastCheckTime?: number
lastCheckStatus?: 'HEALTHY' | 'UNHEALTHY' | 'UNKNOWN'
responseTimeMs?: number
createdAt: number
updatedAt: number
}
/**
* RPC
*/
export interface RpcNodeAddRequest {
providerType: string
name: string
apiKey?: string // 主流服务商需要
httpUrl?: string // CUSTOM 需要
wsUrl?: string
}
/**
* RPC
*/
export interface RpcNodeUpdateRequest {
id: number
name?: string
enabled?: boolean
priority?: number
}
/**
*
*/
export interface NodeCheckResult {
status: 'HEALTHY' | 'UNHEALTHY' | 'UNKNOWN'
message: string
checkTime: number
responseTimeMs?: number
blockNumber?: string
}