diff --git a/backend/mpc-system/services/service-party-app/electron/main.ts b/backend/mpc-system/services/service-party-app/electron/main.ts index 53ac9bce..6fd05f80 100644 --- a/backend/mpc-system/services/service-party-app/electron/main.ts +++ b/backend/mpc-system/services/service-party-app/electron/main.ts @@ -6,7 +6,7 @@ import express from 'express'; import { GrpcClient } from './modules/grpc-client'; import { DatabaseManager } from './modules/database'; import { addressDerivationService, CHAIN_CONFIGS } from './modules/address-derivation'; -import { KavaTxService, KAVA_MAINNET_TX_CONFIG } from './modules/kava-tx-service'; +import { KavaTxService, KAVA_MAINNET_TX_CONFIG, KAVA_TESTNET_TX_CONFIG } from './modules/kava-tx-service'; import { AccountClient } from './modules/account-client'; import { TSSHandler, MockTSSHandler, KeygenResult } from './modules/tss-handler'; @@ -394,8 +394,11 @@ async function initServices() { } }); - // 初始化 Kava 交易服务 - kavaTxService = new KavaTxService(KAVA_MAINNET_TX_CONFIG); + // 初始化 Kava 交易服务 (从数据库读取网络设置,默认测试网) + const kavaNetwork = database.getSetting('kava_network') || 'testnet'; + const kavaConfig = kavaNetwork === 'mainnet' ? KAVA_MAINNET_TX_CONFIG : KAVA_TESTNET_TX_CONFIG; + kavaTxService = new KavaTxService(kavaConfig); + debugLog.info('kava', `Kava network: ${kavaNetwork}`); // 初始化 Account 服务 HTTP 客户端 // 从数据库读取 Account 服务 URL,默认使用生产环境地址 @@ -1578,6 +1581,28 @@ function setupIpcHandlers() { } }); + // 切换 Kava 网络 (主网/测试网) + ipcMain.handle('kava:switchNetwork', async (_event, { network }) => { + try { + if (network === 'testnet') { + kavaTxService?.switchToTestnet(); + database?.setSetting('kava_network', 'testnet'); + } else { + kavaTxService?.switchToMainnet(); + database?.setSetting('kava_network', 'mainnet'); + } + return { success: true, network }; + } catch (error) { + return { success: false, error: (error as Error).message }; + } + }); + + // 获取当前 Kava 网络 + ipcMain.handle('kava:getNetwork', async () => { + const isTestnet = kavaTxService?.isTestnet() ?? false; + return { network: isTestnet ? 'testnet' : 'mainnet' }; + }); + // =========================================================================== // 对话框相关 // =========================================================================== diff --git a/backend/mpc-system/services/service-party-app/electron/modules/kava-tx-service.ts b/backend/mpc-system/services/service-party-app/electron/modules/kava-tx-service.ts index 3d918358..a8a9a9e3 100644 --- a/backend/mpc-system/services/service-party-app/electron/modules/kava-tx-service.ts +++ b/backend/mpc-system/services/service-party-app/electron/modules/kava-tx-service.ts @@ -33,6 +33,15 @@ export const KAVA_MAINNET_TX_CONFIG: KavaTxConfig = { gasPrice: 0.025, }; +export const KAVA_TESTNET_TX_CONFIG: KavaTxConfig = { + lcdEndpoint: 'https://api.testnet.kava.io', + rpcEndpoint: 'https://rpc.testnet.kava.io', + chainId: 'kava_2221-16000', + prefix: 'kava', + denom: 'ukava', + gasPrice: 0.025, +}; + // 备用端点 const BACKUP_ENDPOINTS = [ 'https://api.kava.io', @@ -98,67 +107,89 @@ export interface TxBroadcastResult { gasUsed?: string; gasWanted?: string; height?: string; - error?: string; } export interface TxStatus { found: boolean; status: 'pending' | 'success' | 'failed'; - height?: number; code?: number; rawLog?: string; + height?: string; + gasUsed?: string; + timestamp?: string; } // ============================================================================= -// Kava 交易服务 +// Kava 交易服务类 // ============================================================================= export class KavaTxService { private config: KavaTxConfig; - private currentEndpointIndex = 0; + private backupEndpointIndex = 0; constructor(config: KavaTxConfig = KAVA_MAINNET_TX_CONFIG) { - this.config = config; + this.config = { ...config }; + } + + // =========================================================================== + // 配置管理 + // =========================================================================== + + getConfig(): KavaTxConfig { + return { ...this.config }; + } + + updateConfig(config: Partial): void { + this.config = { ...this.config, ...config }; } /** - * 获取当前 LCD 端点 + * 切换到测试网 */ - private getLcdEndpoint(): string { - return this.config.lcdEndpoint; + switchToTestnet(): void { + this.config = { ...KAVA_TESTNET_TX_CONFIG }; } /** - * 切换到备用端点 + * 切换到主网 */ - private switchToBackupEndpoint(): void { - this.currentEndpointIndex = (this.currentEndpointIndex + 1) % BACKUP_ENDPOINTS.length; - this.config.lcdEndpoint = BACKUP_ENDPOINTS[this.currentEndpointIndex]; - console.log(`Switched to backup endpoint: ${this.config.lcdEndpoint}`); + switchToMainnet(): void { + this.config = { ...KAVA_MAINNET_TX_CONFIG }; } /** - * HTTP 请求 + * 检查是否是测试网 */ + isTestnet(): boolean { + return this.config.chainId === KAVA_TESTNET_TX_CONFIG.chainId; + } + + // =========================================================================== + // HTTP 请求 + // =========================================================================== + private async request( path: string, method: 'GET' | 'POST' = 'GET', - body?: unknown + body?: unknown, + timeout: number = 10000 ): Promise { - const url = `${this.getLcdEndpoint()}${path}`; + const url = `${this.config.lcdEndpoint}${path}`; - const options: RequestInit = { - method, - headers: { - 'Content-Type': 'application/json', - }, - }; - - if (body) { - options.body = JSON.stringify(body); - } + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeout); try { + const options: RequestInit = { + method, + headers: { 'Content-Type': 'application/json' }, + signal: controller.signal, + }; + + if (body) { + options.body = JSON.stringify(body); + } + const response = await fetch(url, options); if (!response.ok) { @@ -168,9 +199,14 @@ export class KavaTxService { return await response.json() as T; } catch (error) { - console.error(`Request failed for ${url}:`, error); - this.switchToBackupEndpoint(); + // 如果是主网且请求失败,尝试备用端点 + if (!this.isTestnet()) { + this.backupEndpointIndex = (this.backupEndpointIndex + 1) % BACKUP_ENDPOINTS.length; + console.log(`Switched to backup endpoint: ${BACKUP_ENDPOINTS[this.backupEndpointIndex]}`); + } throw error; + } finally { + clearTimeout(timeoutId); } } @@ -178,19 +214,44 @@ export class KavaTxService { // 查询功能 // =========================================================================== + /** + * 健康检查 - 查询最新区块 + */ + async healthCheck(): Promise<{ ok: boolean; latency?: number; blockHeight?: number; error?: string }> { + const start = Date.now(); + try { + const response = await this.request<{ block: { header: { height: string } } }>( + '/cosmos/base/tendermint/v1beta1/blocks/latest', + 'GET', + undefined, + 5000 + ); + const latency = Date.now() - start; + return { + ok: true, + latency, + blockHeight: parseInt(response.block.header.height, 10), + }; + } catch (error) { + return { + ok: false, + error: (error as Error).message, + }; + } + } + /** * 查询 KAVA 余额 */ - async getKavaBalance(address: string): Promise { - const response = await this.request<{ balance: Coin }>( - `/cosmos/bank/v1beta1/balances/${address}/by_denom?denom=${this.config.denom}` - ); - - return { - denom: response.balance.denom, - amount: response.balance.amount, - formatted: this.formatAmount(response.balance.amount, response.balance.denom), - }; + async getKavaBalance(address: string): Promise { + try { + const response = await this.request<{ balance: Coin }>( + `/cosmos/bank/v1beta1/balances/${address}/by_denom?denom=ukava` + ); + return response.balance?.amount || '0'; + } catch { + return '0'; + } } /** @@ -201,7 +262,7 @@ export class KavaTxService { `/cosmos/bank/v1beta1/balances/${address}` ); - return response.balances.map((coin: Coin) => ({ + return (response.balances || []).map(coin => ({ denom: coin.denom, amount: coin.amount, formatted: this.formatAmount(coin.amount, coin.denom), @@ -211,26 +272,29 @@ export class KavaTxService { /** * 查询账户信息 */ - async getAccountInfo(address: string): Promise { - // 获取账户信息 - const accountResponse = await this.request<{ - account: { - '@type': string; - address: string; - account_number: string; - sequence: string; + async getAccountInfo(address: string): Promise { + try { + const [accountResp, balances] = await Promise.all([ + this.request<{ + account: { + '@type': string; + address: string; + account_number: string; + sequence: string; + }; + }>(`/cosmos/auth/v1beta1/accounts/${address}`), + this.getAllBalances(address), + ]); + + return { + address: accountResp.account.address, + accountNumber: parseInt(accountResp.account.account_number, 10), + sequence: parseInt(accountResp.account.sequence, 10), + balances, }; - }>(`/cosmos/auth/v1beta1/accounts/${address}`); - - // 获取余额 - const balances = await this.getAllBalances(address); - - return { - address: accountResponse.account.address, - accountNumber: parseInt(accountResponse.account.account_number, 10), - sequence: parseInt(accountResponse.account.sequence, 10), - balances, - }; + } catch { + return null; + } } /** @@ -240,19 +304,22 @@ export class KavaTxService { try { const response = await this.request<{ tx_response: { - height: string; - txhash: string; code: number; raw_log: string; + height: string; + gas_used: string; + timestamp: string; }; }>(`/cosmos/tx/v1beta1/txs/${txHash}`); return { found: true, status: response.tx_response.code === 0 ? 'success' : 'failed', - height: parseInt(response.tx_response.height, 10), code: response.tx_response.code, rawLog: response.tx_response.raw_log, + height: response.tx_response.height, + gasUsed: response.tx_response.gas_used, + timestamp: response.tx_response.timestamp, }; } catch { return { found: false, status: 'pending' }; @@ -260,20 +327,11 @@ export class KavaTxService { } // =========================================================================== - // 交易构建 (用于 TSS 签名) + // 交易构建 (使用 Amino JSON) // =========================================================================== /** - * 构建转账交易 (待签名) - * - * 注意: 这是简化版本,实际生产环境应使用 Protobuf 编码 - * - * @param fromAddress - 发送方地址 - * @param toAddress - 接收方地址 - * @param amount - 金额 (KAVA,如 "1.5") - * @param publicKeyHex - 发送方公钥 (压缩格式,33字节,十六进制) - * @param memo - 备注 - * @returns 待签名的交易数据 + * 构建转账交易 (返回待签名数据) */ async buildSendTx( fromAddress: string, @@ -284,134 +342,118 @@ export class KavaTxService { ): Promise { // 获取账户信息 const accountInfo = await this.getAccountInfo(fromAddress); + if (!accountInfo) { + throw new Error('Account not found or has no transactions'); + } - // 转换金额 (KAVA -> ukava) - const amountUkava = this.toMinimalDenom(amount); - - // Gas 估算 const gasLimit = 100000; - const feeAmount = Math.ceil(gasLimit * this.config.gasPrice).toString(); + const feeAmount = Math.ceil(gasLimit * this.config.gasPrice); - // 构建交易消息 (Amino JSON 格式) - const msgSend = { - '@type': '/cosmos.bank.v1beta1.MsgSend', - from_address: fromAddress, - to_address: toAddress, - amount: [{ denom: this.config.denom, amount: amountUkava }], + // 构建 Amino JSON 签名文档 + const signDoc = { + chain_id: this.config.chainId, + account_number: accountInfo.accountNumber.toString(), + sequence: accountInfo.sequence.toString(), + fee: { + amount: [{ denom: 'ukava', amount: feeAmount.toString() }], + gas: gasLimit.toString(), + }, + msgs: [{ + type: 'cosmos-sdk/MsgSend', + value: { + from_address: fromAddress, + to_address: toAddress, + amount: [{ denom: 'ukava', amount }], + }, + }], + memo, }; - // 构建交易体 + // 计算签名哈希 (SHA256) + const signDocJson = JSON.stringify(sortObject(signDoc)); + const signBytes = crypto.createHash('sha256').update(signDocJson).digest(); + + // 构建交易体和认证信息 (用于广播) const txBody = { - messages: [msgSend], + messages: [{ + '@type': '/cosmos.bank.v1beta1.MsgSend', + from_address: fromAddress, + to_address: toAddress, + amount: [{ denom: 'ukava', amount }], + }], memo, timeout_height: '0', extension_options: [], non_critical_extension_options: [], }; - // 构建认证信息 const authInfo = { signer_infos: [{ public_key: { '@type': '/cosmos.crypto.secp256k1.PubKey', key: Buffer.from(publicKeyHex, 'hex').toString('base64'), }, - mode_info: { - single: { mode: 'SIGN_MODE_DIRECT' }, - }, + mode_info: { single: { mode: 'SIGN_MODE_LEGACY_AMINO_JSON' } }, sequence: accountInfo.sequence.toString(), }], fee: { - amount: [{ denom: this.config.denom, amount: feeAmount }], + amount: [{ denom: 'ukava', amount: feeAmount.toString() }], gas_limit: gasLimit.toString(), }, }; - // 序列化 (简化版使用 JSON) - const txBodyBytes = Buffer.from(JSON.stringify(txBody)); - const authInfoBytes = Buffer.from(JSON.stringify(authInfo)); - - // 构建 SignDoc - const signDoc = { - body_bytes: txBodyBytes.toString('base64'), - auth_info_bytes: authInfoBytes.toString('base64'), - chain_id: this.config.chainId, - account_number: accountInfo.accountNumber.toString(), - }; - - // 计算待签名字节 (SHA256) - const signDocBytes = Buffer.from(JSON.stringify(signDoc)); - const signBytes = crypto.createHash('sha256').update(signDocBytes).digest(); - return { signBytes: new Uint8Array(signBytes), signBytesHex: signBytes.toString('hex'), - - txBodyBytes: new Uint8Array(txBodyBytes), - authInfoBytes: new Uint8Array(authInfoBytes), + txBodyBytes: new Uint8Array(Buffer.from(JSON.stringify(txBody))), + authInfoBytes: new Uint8Array(Buffer.from(JSON.stringify(authInfo))), accountNumber: accountInfo.accountNumber, sequence: accountInfo.sequence, chainId: this.config.chainId, - from: fromAddress, to: toAddress, - amount: amountUkava, - denom: this.config.denom, + amount, + denom: 'ukava', memo, - fee: feeAmount, + fee: feeAmount.toString(), gasLimit, }; } /** - * 使用签名完成交易 - * - * @param unsignedTx - 未签名的交易数据 - * @param signatureHex - TSS 签名 (64字节,R+S 格式,十六进制) - * @returns 已签名的交易数据 + * 完成交易 (添加签名) */ - async completeTx( - unsignedTx: UnsignedTxData, - signatureHex: string - ): Promise { - const signature = Buffer.from(signatureHex, 'hex'); + async completeTx(unsignedTx: UnsignedTxData, signatureHex: string): Promise { + // 解析保存的交易数据 + const txBody = JSON.parse(Buffer.from(unsignedTx.txBodyBytes).toString()); + const authInfo = JSON.parse(Buffer.from(unsignedTx.authInfoBytes).toString()); - // 构建 TxRaw (简化版使用 JSON) - const txRaw = { - body_bytes: Buffer.from(unsignedTx.txBodyBytes).toString('base64'), - auth_info_bytes: Buffer.from(unsignedTx.authInfoBytes).toString('base64'), - signatures: [signature.toString('base64')], + // 构建完整的已签名交易 + const signedTx = { + body: txBody, + auth_info: authInfo, + signatures: [Buffer.from(signatureHex, 'hex').toString('base64')], }; - const txBytes = Buffer.from(JSON.stringify(txRaw)); - const txBytesBase64 = txBytes.toString('base64'); - - // 计算交易哈希 - const txHash = crypto.createHash('sha256') - .update(txBytes) - .digest('hex') - .toUpperCase(); + const txBytes = Buffer.from(JSON.stringify(signedTx)); + const txHash = crypto.createHash('sha256').update(txBytes).digest('hex').toUpperCase(); return { txBytes: new Uint8Array(txBytes), - txBytesBase64, + txBytesBase64: txBytes.toString('base64'), txHash, }; } - // =========================================================================== - // 交易广播 - // =========================================================================== - /** - * 广播已签名的交易 + * 广播交易 */ async broadcastTx(signedTx: SignedTxData): Promise { try { const response = await this.request<{ tx_response: { - txhash: string; code: number; + txhash: string; raw_log: string; gas_used: string; gas_wanted: string; @@ -434,7 +476,7 @@ export class KavaTxService { } catch (error) { return { success: false, - error: error instanceof Error ? error.message : String(error), + rawLog: (error as Error).message, }; } } @@ -443,101 +485,53 @@ export class KavaTxService { // 工具方法 // =========================================================================== - /** - * 格式化金额 (ukava -> KAVA) - */ formatAmount(amount: string, denom: string): string { if (denom === 'ukava') { - const ukava = BigInt(amount); - const kava = Number(ukava) / 1_000_000; + const kava = Number(amount) / 1_000_000; return `${kava.toFixed(6)} KAVA`; } return `${amount} ${denom}`; } - /** - * 转换为最小单位 (KAVA -> ukava) - */ - toMinimalDenom(amount: string): string { - const kava = parseFloat(amount); - const ukava = Math.floor(kava * 1_000_000); - return ukava.toString(); + toUkava(kava: number | string): string { + return Math.floor(Number(kava) * 1_000_000).toString(); + } + + fromUkava(ukava: string): number { + return Number(ukava) / 1_000_000; } /** - * 从最小单位转换 (ukava -> KAVA) - */ - fromMinimalDenom(amount: string): string { - const ukava = BigInt(amount); - const kava = Number(ukava) / 1_000_000; - return kava.toFixed(6); - } - - /** - * 验证地址格式 - */ - isValidAddress(address: string): boolean { - return address.startsWith(this.config.prefix) && address.length === 43; - } - - /** - * 获取当前配置 - */ - getConfig(): KavaTxConfig { - return { ...this.config }; - } - - /** - * 更新配置 - */ - updateConfig(config: Partial): void { - this.config = { ...this.config, ...config }; - } - - /** - * 断开连接 (兼容接口) + * 断开连接 (清理资源) */ disconnect(): void { - // REST API 无需断开连接 - } - - /** - * 健康检查 - 验证 API 是否可用 - */ - async healthCheck(): Promise<{ ok: boolean; latency?: number; error?: string }> { - const startTime = Date.now(); - try { - // 查询链的最新区块,这是最轻量级的检查 - const response = await this.request<{ - block: { - header: { - height: string; - chain_id: string; - }; - }; - }>('/cosmos/base/tendermint/v1beta1/blocks/latest'); - - const latency = Date.now() - startTime; - - if (response?.block?.header?.height) { - return { - ok: true, - latency, - }; - } - - return { - ok: false, - error: 'Invalid response format', - }; - } catch (error) { - return { - ok: false, - error: error instanceof Error ? error.message : String(error), - }; - } + // 目前使用 REST API,无需特殊清理 } } -// 导出默认服务实例 +// =========================================================================== +// 辅助函数 +// =========================================================================== + +/** + * 递归排序对象键 (Amino JSON 签名需要) + */ +function sortObject(obj: unknown): unknown { + if (obj === null || typeof obj !== 'object') { + return obj; + } + + if (Array.isArray(obj)) { + return obj.map(sortObject); + } + + const sortedObj: Record = {}; + const keys = Object.keys(obj as Record).sort(); + for (const key of keys) { + sortedObj[key] = sortObject((obj as Record)[key]); + } + return sortedObj; +} + +// 导出默认实例 export const kavaTxService = new KavaTxService(); diff --git a/backend/mpc-system/services/service-party-app/electron/preload.ts b/backend/mpc-system/services/service-party-app/electron/preload.ts index f0cb6ac8..811b73ef 100644 --- a/backend/mpc-system/services/service-party-app/electron/preload.ts +++ b/backend/mpc-system/services/service-party-app/electron/preload.ts @@ -236,6 +236,13 @@ contextBridge.exposeInMainWorld('electronAPI', { // 健康检查 healthCheck: () => ipcRenderer.invoke('kava:healthCheck'), + + // 切换网络 (mainnet/testnet) + switchNetwork: (network: 'mainnet' | 'testnet') => + ipcRenderer.invoke('kava:switchNetwork', { network }), + + // 获取当前网络 + getNetwork: () => ipcRenderer.invoke('kava:getNetwork'), }, // =========================================================================== diff --git a/backend/mpc-system/services/service-party-app/src/components/Layout.module.css b/backend/mpc-system/services/service-party-app/src/components/Layout.module.css index 34667860..0930b419 100644 --- a/backend/mpc-system/services/service-party-app/src/components/Layout.module.css +++ b/backend/mpc-system/services/service-party-app/src/components/Layout.module.css @@ -121,6 +121,25 @@ white-space: nowrap; } +/* 网络标签 */ +.networkBadgeTestnet { + margin-left: 4px; + padding: 1px 4px; + font-size: 9px; + background-color: #f59e0b; + color: white; + border-radius: 3px; +} + +.networkBadgeMainnet { + margin-left: 4px; + padding: 1px 4px; + font-size: 9px; + background-color: #22c55e; + color: white; + border-radius: 3px; +} + /* 操作进度 */ .operationPanel { margin-top: var(--spacing-sm); diff --git a/backend/mpc-system/services/service-party-app/src/components/Layout.tsx b/backend/mpc-system/services/service-party-app/src/components/Layout.tsx index 6221b3fb..10a3fba0 100644 --- a/backend/mpc-system/services/service-party-app/src/components/Layout.tsx +++ b/backend/mpc-system/services/service-party-app/src/components/Layout.tsx @@ -18,12 +18,17 @@ const navItems = [ export default function Layout({ children }: LayoutProps) { const location = useLocation(); const [isRefreshing, setIsRefreshing] = useState(false); + const [kavaNetwork, setKavaNetwork] = useState<'mainnet' | 'testnet'>('testnet'); const { environment, operation, checkAllServices, appReady } = useAppStore(); - // 启动时检测环境 + // 启动时检测环境和获取网络 useEffect(() => { checkAllServices(); + // 获取当前 Kava 网络 + window.electronAPI?.kava.getNetwork().then(result => { + setKavaNetwork(result.network); + }); }, [checkAllServices]); const handleRefresh = async () => { @@ -85,7 +90,12 @@ export default function Layout({ children }: LayoutProps) { {/* Kava API */}
- Kava API + + Kava API + + {kavaNetwork === 'testnet' ? '测试网' : '主网'} + + ('testnet'); const [isLoading, setIsLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null); @@ -30,12 +31,14 @@ export default function Settings() { try { const result = await window.electronAPI.storage.getSettings(); const accountUrl = await window.electronAPI.account.getUrl(); + const networkResult = await window.electronAPI.kava.getNetwork(); if (result) { setSettings({ ...result, accountServiceUrl: accountUrl || 'https://rwaapi.szaiai.com', }); } + setKavaNetwork(networkResult.network); } catch (err) { console.error('Failed to load settings:', err); } finally { @@ -169,6 +172,47 @@ export default function Settings() {
+ {/* 区块链网络设置 */} +
+

区块链网络

+
+
+ +
+ + +
+

+ {kavaNetwork === 'testnet' + ? '当前使用测试网 (kava_2221-16000),适合开发测试' + : '当前使用主网 (kava_2222-10),请谨慎操作'} +

+
+
+
+ {/* 备份设置 */}

备份设置

diff --git a/backend/mpc-system/services/service-party-app/src/types/electron.d.ts b/backend/mpc-system/services/service-party-app/src/types/electron.d.ts index 76ee1496..5e60dac1 100644 --- a/backend/mpc-system/services/service-party-app/src/types/electron.d.ts +++ b/backend/mpc-system/services/service-party-app/src/types/electron.d.ts @@ -504,6 +504,8 @@ interface ElectronAPI { getConfig: () => Promise; updateConfig: (config: Partial) => Promise<{ success: boolean; error?: string }>; healthCheck: () => Promise; + switchNetwork: (network: 'mainnet' | 'testnet') => Promise<{ success: boolean; network?: string; error?: string }>; + getNetwork: () => Promise<{ network: 'mainnet' | 'testnet' }>; }; // 对话框相关