- 重置密码后自动跳转到登录页,登录页添加重置密码入口 - 移除默认账户相关逻辑和UI - 移除前端API Key自动获取提示文案(后端保留自动获取逻辑) - 修复SELL订单takerAmount计算精度问题,使用精确计算避免精度丢失 - 修复导入账户页面白屏问题(缺少Alert组件导入) - 持仓出售支持百分比模式,使用原始精度数据计算,支持小数百分比 - 优化订单金额计算逻辑,确保与SDK一致
328 lines
11 KiB
TypeScript
328 lines
11 KiB
TypeScript
import { useState } from 'react'
|
|
import { useNavigate } from 'react-router-dom'
|
|
import { Card, Form, Input, Button, message, Typography, Radio, Space, Alert } from 'antd'
|
|
import { ArrowLeftOutlined } from '@ant-design/icons'
|
|
import { useAccountStore } from '../store/accountStore'
|
|
import {
|
|
getAddressFromPrivateKey,
|
|
getAddressFromMnemonic,
|
|
getPrivateKeyFromMnemonic,
|
|
isValidWalletAddress,
|
|
isValidPrivateKey,
|
|
isValidMnemonic
|
|
} from '../utils'
|
|
import { useMediaQuery } from 'react-responsive'
|
|
|
|
const { Title } = Typography
|
|
|
|
type ImportType = 'privateKey' | 'mnemonic'
|
|
|
|
const AccountImport: React.FC = () => {
|
|
const navigate = useNavigate()
|
|
const isMobile = useMediaQuery({ maxWidth: 768 })
|
|
const { importAccount, loading } = useAccountStore()
|
|
const [form] = Form.useForm()
|
|
const [importType, setImportType] = useState<ImportType>('privateKey')
|
|
const [derivedAddress, setDerivedAddress] = useState<string>('')
|
|
const [addressError, setAddressError] = useState<string>('')
|
|
|
|
// 当私钥输入时,自动推导地址
|
|
const handlePrivateKeyChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
const privateKey = e.target.value.trim()
|
|
if (!privateKey) {
|
|
setDerivedAddress('')
|
|
setAddressError('')
|
|
return
|
|
}
|
|
|
|
// 验证私钥格式
|
|
if (!isValidPrivateKey(privateKey)) {
|
|
setAddressError('私钥格式不正确(应为64位十六进制字符串)')
|
|
setDerivedAddress('')
|
|
return
|
|
}
|
|
|
|
try {
|
|
const address = getAddressFromPrivateKey(privateKey)
|
|
setDerivedAddress(address)
|
|
setAddressError('')
|
|
|
|
// 自动填充钱包地址字段
|
|
form.setFieldsValue({ walletAddress: address })
|
|
} catch (error: any) {
|
|
setAddressError(error.message || '无法从私钥推导地址')
|
|
setDerivedAddress('')
|
|
}
|
|
}
|
|
|
|
// 当助记词输入时,自动推导地址
|
|
const handleMnemonicChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
const mnemonic = e.target.value.trim()
|
|
if (!mnemonic) {
|
|
setDerivedAddress('')
|
|
setAddressError('')
|
|
return
|
|
}
|
|
|
|
// 验证助记词格式
|
|
if (!isValidMnemonic(mnemonic)) {
|
|
setAddressError('助记词格式不正确(应为12或24个单词,用空格分隔)')
|
|
setDerivedAddress('')
|
|
return
|
|
}
|
|
|
|
try {
|
|
const address = getAddressFromMnemonic(mnemonic, 0)
|
|
setDerivedAddress(address)
|
|
setAddressError('')
|
|
|
|
// 自动填充钱包地址字段
|
|
form.setFieldsValue({ walletAddress: address })
|
|
} catch (error: any) {
|
|
setAddressError(error.message || '无法从助记词推导地址')
|
|
setDerivedAddress('')
|
|
}
|
|
}
|
|
|
|
const handleSubmit = async (values: any) => {
|
|
try {
|
|
let privateKey: string
|
|
let walletAddress: string
|
|
|
|
if (importType === 'privateKey') {
|
|
// 私钥模式
|
|
privateKey = values.privateKey
|
|
walletAddress = values.walletAddress
|
|
|
|
// 验证推导的地址和输入的地址是否一致
|
|
if (derivedAddress && walletAddress !== derivedAddress) {
|
|
message.error('钱包地址与私钥不匹配')
|
|
return
|
|
}
|
|
} else {
|
|
// 助记词模式
|
|
if (!values.mnemonic) {
|
|
message.error('请输入助记词')
|
|
return
|
|
}
|
|
|
|
// 从助记词导出私钥和地址
|
|
privateKey = getPrivateKeyFromMnemonic(values.mnemonic, 0)
|
|
const derivedAddressFromMnemonic = getAddressFromMnemonic(values.mnemonic, 0)
|
|
|
|
// 如果用户手动输入了地址,验证是否与推导的地址一致
|
|
if (values.walletAddress) {
|
|
if (values.walletAddress !== derivedAddressFromMnemonic) {
|
|
// 地址不匹配,使用推导的地址(因为私钥是从助记词导出的,必须使用对应的地址)
|
|
message.warning(`输入的地址与助记词推导的地址不一致。推导的地址: ${derivedAddressFromMnemonic},将使用推导的地址`)
|
|
walletAddress = derivedAddressFromMnemonic
|
|
} else {
|
|
// 地址匹配,使用用户输入的地址
|
|
walletAddress = values.walletAddress
|
|
}
|
|
} else {
|
|
// 如果用户没有输入地址,使用推导的地址
|
|
walletAddress = derivedAddressFromMnemonic
|
|
}
|
|
}
|
|
|
|
// 验证钱包地址格式
|
|
if (!isValidWalletAddress(walletAddress)) {
|
|
message.error('钱包地址格式不正确')
|
|
return
|
|
}
|
|
|
|
await importAccount({
|
|
privateKey: privateKey,
|
|
walletAddress: walletAddress,
|
|
accountName: values.accountName
|
|
})
|
|
|
|
message.success('导入账户成功')
|
|
navigate('/accounts')
|
|
} catch (error: any) {
|
|
message.error(error.message || '导入账户失败')
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<div style={{ marginBottom: '16px' }}>
|
|
<Button
|
|
icon={<ArrowLeftOutlined />}
|
|
onClick={() => navigate('/accounts')}
|
|
style={{ marginBottom: '16px' }}
|
|
>
|
|
返回
|
|
</Button>
|
|
<Title level={2} style={{ margin: 0 }}>导入账户</Title>
|
|
</div>
|
|
|
|
<Card>
|
|
<Alert
|
|
message="安全提示"
|
|
description="私钥将存储在后端数据库中,请确保数据库访问安全。建议使用 HTTPS 连接。"
|
|
type="warning"
|
|
showIcon
|
|
style={{ marginBottom: '24px' }}
|
|
/>
|
|
|
|
<Form
|
|
form={form}
|
|
layout="vertical"
|
|
onFinish={handleSubmit}
|
|
size={isMobile ? 'middle' : 'large'}
|
|
>
|
|
<Form.Item label="导入方式">
|
|
<Radio.Group
|
|
value={importType}
|
|
onChange={(e) => {
|
|
setImportType(e.target.value)
|
|
setDerivedAddress('')
|
|
setAddressError('')
|
|
form.setFieldsValue({ walletAddress: '' })
|
|
}}
|
|
>
|
|
<Radio value="privateKey">私钥</Radio>
|
|
<Radio value="mnemonic">助记词</Radio>
|
|
</Radio.Group>
|
|
</Form.Item>
|
|
|
|
{importType === 'privateKey' ? (
|
|
<>
|
|
<Form.Item
|
|
label="私钥"
|
|
name="privateKey"
|
|
rules={[
|
|
{ required: true, message: '请输入私钥' },
|
|
{
|
|
validator: (_, value) => {
|
|
if (!value) return Promise.resolve()
|
|
if (!isValidPrivateKey(value)) {
|
|
return Promise.reject(new Error('私钥格式不正确(应为64位十六进制字符串)'))
|
|
}
|
|
return Promise.resolve()
|
|
}
|
|
}
|
|
]}
|
|
help={addressError || (derivedAddress ? `推导地址: ${derivedAddress}` : '')}
|
|
validateStatus={addressError ? 'error' : derivedAddress ? 'success' : ''}
|
|
>
|
|
<Input.TextArea
|
|
rows={3}
|
|
placeholder="请输入私钥(64位十六进制字符串,可选0x前缀)"
|
|
onChange={handlePrivateKeyChange}
|
|
/>
|
|
</Form.Item>
|
|
|
|
<Form.Item
|
|
label="钱包地址"
|
|
name="walletAddress"
|
|
rules={[
|
|
{ required: true, message: '请输入钱包地址' },
|
|
{
|
|
validator: (_, value) => {
|
|
if (!value) return Promise.resolve()
|
|
if (!isValidWalletAddress(value)) {
|
|
return Promise.reject(new Error('钱包地址格式不正确'))
|
|
}
|
|
if (derivedAddress && value !== derivedAddress) {
|
|
return Promise.reject(new Error('钱包地址与私钥不匹配'))
|
|
}
|
|
return Promise.resolve()
|
|
}
|
|
}
|
|
]}
|
|
>
|
|
<Input
|
|
placeholder="钱包地址(将从私钥自动推导)"
|
|
readOnly={!!derivedAddress}
|
|
/>
|
|
</Form.Item>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Form.Item
|
|
label="助记词"
|
|
name="mnemonic"
|
|
rules={[
|
|
{ required: true, message: '请输入助记词' },
|
|
{
|
|
validator: (_, value) => {
|
|
if (!value) return Promise.resolve()
|
|
if (!isValidMnemonic(value)) {
|
|
return Promise.reject(new Error('助记词格式不正确(应为12或24个单词,用空格分隔)'))
|
|
}
|
|
return Promise.resolve()
|
|
}
|
|
}
|
|
]}
|
|
help={addressError || (derivedAddress ? `推导地址: ${derivedAddress}` : '')}
|
|
validateStatus={addressError ? 'error' : derivedAddress ? 'success' : ''}
|
|
>
|
|
<Input.TextArea
|
|
rows={4}
|
|
placeholder="请输入12或24个单词的助记词(用空格分隔)"
|
|
onChange={handleMnemonicChange}
|
|
/>
|
|
</Form.Item>
|
|
|
|
<Form.Item
|
|
label="钱包地址"
|
|
name="walletAddress"
|
|
rules={[
|
|
{ required: true, message: '请输入钱包地址' },
|
|
{
|
|
validator: (_, value) => {
|
|
if (!value) return Promise.resolve()
|
|
if (!isValidWalletAddress(value)) {
|
|
return Promise.reject(new Error('钱包地址格式不正确'))
|
|
}
|
|
if (derivedAddress && value !== derivedAddress) {
|
|
return Promise.reject(new Error('钱包地址与助记词不匹配'))
|
|
}
|
|
return Promise.resolve()
|
|
}
|
|
}
|
|
]}
|
|
>
|
|
<Input
|
|
placeholder="钱包地址(将从助记词自动推导)"
|
|
readOnly={!!derivedAddress}
|
|
/>
|
|
</Form.Item>
|
|
</>
|
|
)}
|
|
|
|
<Form.Item
|
|
label="账户名称"
|
|
name="accountName"
|
|
>
|
|
<Input placeholder="可选,用于标识账户" />
|
|
</Form.Item>
|
|
|
|
|
|
<Form.Item>
|
|
<Space>
|
|
<Button
|
|
type="primary"
|
|
htmlType="submit"
|
|
loading={loading}
|
|
size={isMobile ? 'middle' : 'large'}
|
|
>
|
|
导入账户
|
|
</Button>
|
|
<Button onClick={() => navigate('/accounts')}>
|
|
取消
|
|
</Button>
|
|
</Space>
|
|
</Form.Item>
|
|
</Form>
|
|
</Card>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default AccountImport
|
|
|