/** * Kava 交易服务 * * 使用 Kava LCD REST API 构建和广播交易 * 支持 TSS 多方签名 * * API 文档参考: * - https://docs.kava.io/docs/using-kava-endpoints/endpoints/ * - https://docs.cosmos.network/main/learn/advanced/grpc_rest */ import * as crypto from 'crypto'; // ============================================================================= // 配置 // ============================================================================= export interface KavaTxConfig { lcdEndpoint: string; rpcEndpoint: string; chainId: string; prefix: string; denom: string; gasPrice: number; // ukava per gas unit } export const KAVA_MAINNET_TX_CONFIG: KavaTxConfig = { lcdEndpoint: 'https://api.kava.io', rpcEndpoint: 'https://rpc.kava.io', chainId: 'kava_2222-10', prefix: 'kava', denom: 'ukava', gasPrice: 0.025, }; // 备用端点 const BACKUP_ENDPOINTS = [ 'https://api.kava.io', 'https://api.kava-rpc.com', 'https://api.kava.chainstacklabs.com', ]; // ============================================================================= // 类型定义 // ============================================================================= export interface Coin { denom: string; amount: string; } export interface AccountBalance { denom: string; amount: string; formatted: string; // 人类可读格式 (KAVA) } export interface AccountInfo { address: string; accountNumber: number; sequence: number; balances: AccountBalance[]; } export interface UnsignedTxData { // 用于 TSS 签名的数据 signBytes: Uint8Array; // 待签名的哈希 signBytesHex: string; // 十六进制格式 // 交易元数据 txBodyBytes: Uint8Array; authInfoBytes: Uint8Array; accountNumber: number; sequence: number; chainId: string; // 可读信息 from: string; to: string; amount: string; denom: string; memo: string; fee: string; gasLimit: number; } export interface SignedTxData { txBytes: Uint8Array; // 完整的已签名交易 txBytesBase64: string; // Base64 格式 txHash: string; // 交易哈希 } export interface TxBroadcastResult { success: boolean; txHash?: string; code?: number; rawLog?: string; gasUsed?: string; gasWanted?: string; height?: string; error?: string; } export interface TxStatus { found: boolean; status: 'pending' | 'success' | 'failed'; height?: number; code?: number; rawLog?: string; } // ============================================================================= // Kava 交易服务 // ============================================================================= export class KavaTxService { private config: KavaTxConfig; private currentEndpointIndex = 0; constructor(config: KavaTxConfig = KAVA_MAINNET_TX_CONFIG) { this.config = config; } /** * 获取当前 LCD 端点 */ private getLcdEndpoint(): string { return this.config.lcdEndpoint; } /** * 切换到备用端点 */ 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}`); } /** * HTTP 请求 */ private async request( path: string, method: 'GET' | 'POST' = 'GET', body?: unknown ): Promise { const url = `${this.getLcdEndpoint()}${path}`; const options: RequestInit = { method, headers: { 'Content-Type': 'application/json', }, }; if (body) { options.body = JSON.stringify(body); } try { const response = await fetch(url, options); if (!response.ok) { const errorText = await response.text(); throw new Error(`HTTP ${response.status}: ${errorText}`); } return await response.json() as T; } catch (error) { console.error(`Request failed for ${url}:`, error); this.switchToBackupEndpoint(); throw error; } } // =========================================================================== // 查询功能 // =========================================================================== /** * 查询 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 getAllBalances(address: string): Promise { const response = await this.request<{ balances: Coin[] }>( `/cosmos/bank/v1beta1/balances/${address}` ); return response.balances.map((coin: Coin) => ({ denom: coin.denom, amount: coin.amount, formatted: this.formatAmount(coin.amount, coin.denom), })); } /** * 查询账户信息 */ async getAccountInfo(address: string): Promise { // 获取账户信息 const accountResponse = await this.request<{ account: { '@type': string; address: string; account_number: string; sequence: string; }; }>(`/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, }; } /** * 查询交易状态 */ async getTxStatus(txHash: string): Promise { try { const response = await this.request<{ tx_response: { height: string; txhash: string; code: number; raw_log: 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, }; } catch { return { found: false, status: 'pending' }; } } // =========================================================================== // 交易构建 (用于 TSS 签名) // =========================================================================== /** * 构建转账交易 (待签名) * * 注意: 这是简化版本,实际生产环境应使用 Protobuf 编码 * * @param fromAddress - 发送方地址 * @param toAddress - 接收方地址 * @param amount - 金额 (KAVA,如 "1.5") * @param publicKeyHex - 发送方公钥 (压缩格式,33字节,十六进制) * @param memo - 备注 * @returns 待签名的交易数据 */ async buildSendTx( fromAddress: string, toAddress: string, amount: string, publicKeyHex: string, memo: string = '' ): Promise { // 获取账户信息 const accountInfo = await this.getAccountInfo(fromAddress); // 转换金额 (KAVA -> ukava) const amountUkava = this.toMinimalDenom(amount); // Gas 估算 const gasLimit = 100000; const feeAmount = Math.ceil(gasLimit * this.config.gasPrice).toString(); // 构建交易消息 (Amino JSON 格式) const msgSend = { '@type': '/cosmos.bank.v1beta1.MsgSend', from_address: fromAddress, to_address: toAddress, amount: [{ denom: this.config.denom, amount: amountUkava }], }; // 构建交易体 const txBody = { messages: [msgSend], 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' }, }, sequence: accountInfo.sequence.toString(), }], fee: { amount: [{ denom: this.config.denom, amount: feeAmount }], 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), accountNumber: accountInfo.accountNumber, sequence: accountInfo.sequence, chainId: this.config.chainId, from: fromAddress, to: toAddress, amount: amountUkava, denom: this.config.denom, memo, fee: feeAmount, gasLimit, }; } /** * 使用签名完成交易 * * @param unsignedTx - 未签名的交易数据 * @param signatureHex - TSS 签名 (64字节,R+S 格式,十六进制) * @returns 已签名的交易数据 */ async completeTx( unsignedTx: UnsignedTxData, signatureHex: string ): Promise { const signature = Buffer.from(signatureHex, 'hex'); // 构建 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 txBytes = Buffer.from(JSON.stringify(txRaw)); const txBytesBase64 = txBytes.toString('base64'); // 计算交易哈希 const txHash = crypto.createHash('sha256') .update(txBytes) .digest('hex') .toUpperCase(); return { txBytes: new Uint8Array(txBytes), txBytesBase64, txHash, }; } // =========================================================================== // 交易广播 // =========================================================================== /** * 广播已签名的交易 */ async broadcastTx(signedTx: SignedTxData): Promise { try { const response = await this.request<{ tx_response: { txhash: string; code: number; raw_log: string; gas_used: string; gas_wanted: string; height: string; }; }>('/cosmos/tx/v1beta1/txs', 'POST', { tx_bytes: signedTx.txBytesBase64, mode: 'BROADCAST_MODE_SYNC', }); return { success: response.tx_response.code === 0, txHash: response.tx_response.txhash, code: response.tx_response.code, rawLog: response.tx_response.raw_log, gasUsed: response.tx_response.gas_used, gasWanted: response.tx_response.gas_wanted, height: response.tx_response.height, }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error), }; } } // =========================================================================== // 工具方法 // =========================================================================== /** * 格式化金额 (ukava -> KAVA) */ formatAmount(amount: string, denom: string): string { if (denom === 'ukava') { const ukava = BigInt(amount); const kava = Number(ukava) / 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(); } /** * 从最小单位转换 (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), }; } } } // 导出默认服务实例 export const kavaTxService = new KavaTxService();