/** * Kava 区块链客户端 * * 功能: * 1. 查询账户余额 * 2. 查询账户信息 (用于获取 sequence 和 account_number) * 3. 构建交易 * 4. 广播交易 * * 使用 Kava 官方 LCD REST API: * - 主网: https://api.kava.io * - 备用: https://api.kava-rpc.com */ import * as crypto from 'crypto'; // ============================================================================= // 配置 // ============================================================================= export interface KavaClientConfig { lcdEndpoint: string; // LCD REST API 端点 chainId: string; // 链 ID (kava_2222-10 for mainnet) gasPrice: string; // Gas 价格 (如 "0.025ukava") defaultGasLimit: number; // 默认 Gas 限制 } export const KAVA_MAINNET_CONFIG: KavaClientConfig = { lcdEndpoint: 'https://api.kava.io', chainId: 'kava_2222-10', gasPrice: '0.025ukava', defaultGasLimit: 200000, }; export const KAVA_TESTNET_CONFIG: KavaClientConfig = { lcdEndpoint: 'https://api.testnet.kava.io', chainId: 'kava_2221-16000', gasPrice: '0.025ukava', defaultGasLimit: 200000, }; // 备用端点列表 export const KAVA_LCD_ENDPOINTS = [ 'https://api.kava.io', 'https://api.kava-rpc.com', 'https://api.kava.chainstacklabs.com', ]; // ============================================================================= // 类型定义 // ============================================================================= export interface Coin { denom: string; amount: string; } export interface AccountInfo { address: string; accountNumber: string; sequence: string; pubKey?: { type: string; value: string; }; } export interface BalanceResponse { balances: Coin[]; pagination: { next_key: string | null; total: string; }; } export interface AccountResponse { account: { '@type': string; address: string; pub_key?: { '@type': string; key: string; }; account_number: string; sequence: string; }; } export interface TxResponse { height: string; txhash: string; codespace: string; code: number; data: string; raw_log: string; logs: unknown[]; info: string; gas_wanted: string; gas_used: string; tx: unknown; timestamp: string; events: unknown[]; } export interface BroadcastTxResponse { tx_response: TxResponse; } export interface SimulateTxResponse { gas_info: { gas_wanted: string; gas_used: string; }; result: { data: string; log: string; events: unknown[]; }; } // 交易消息类型 export interface MsgSend { '@type': '/cosmos.bank.v1beta1.MsgSend'; from_address: string; to_address: string; amount: Coin[]; } export interface Fee { amount: Coin[]; gas_limit: string; payer?: string; granter?: string; } export interface SignerInfo { public_key: { '@type': string; key: string; }; mode_info: { single: { mode: string; }; }; sequence: string; } export interface AuthInfo { signer_infos: SignerInfo[]; fee: Fee; } export interface TxBody { messages: MsgSend[]; memo: string; timeout_height: string; extension_options: unknown[]; non_critical_extension_options: unknown[]; } export interface TxRaw { body_bytes: string; auth_info_bytes: string; signatures: string[]; } // ============================================================================= // Kava 客户端类 // ============================================================================= export class KavaClient { private config: KavaClientConfig; private currentEndpointIndex = 0; constructor(config: KavaClientConfig = KAVA_MAINNET_CONFIG) { this.config = config; } /** * 获取当前 LCD 端点 */ private getLcdEndpoint(): string { return this.config.lcdEndpoint; } /** * 切换到备用端点 */ private switchToBackupEndpoint(): void { this.currentEndpointIndex = (this.currentEndpointIndex + 1) % KAVA_LCD_ENDPOINTS.length; this.config.lcdEndpoint = KAVA_LCD_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; } } // =========================================================================== // 查询功能 // =========================================================================== /** * 查询账户余额 * * @param address - Kava 地址 (bech32 格式,以 "kava" 开头) * @returns 余额列表 */ async getBalances(address: string): Promise { const response = await this.request( `/cosmos/bank/v1beta1/balances/${address}` ); return response.balances; } /** * 查询指定代币余额 * * @param address - Kava 地址 * @param denom - 代币单位 (如 "ukava") * @returns 余额 */ async getBalance(address: string, denom: string = 'ukava'): Promise { const response = await this.request<{ balance: Coin }>( `/cosmos/bank/v1beta1/balances/${address}/by_denom?denom=${denom}` ); return response.balance; } /** * 查询账户信息 (用于构建交易) * * @param address - Kava 地址 * @returns 账户信息 (包含 account_number 和 sequence) */ async getAccountInfo(address: string): Promise { const response = await this.request( `/cosmos/auth/v1beta1/accounts/${address}` ); const account = response.account; return { address: account.address, accountNumber: account.account_number, sequence: account.sequence, pubKey: account.pub_key ? { type: account.pub_key['@type'], value: account.pub_key.key, } : undefined, }; } /** * 查询交易详情 * * @param txHash - 交易哈希 * @returns 交易详情 */ async getTx(txHash: string): Promise { const response = await this.request<{ tx_response: TxResponse }>( `/cosmos/tx/v1beta1/txs/${txHash}` ); return response.tx_response; } /** * 查询最新区块高度 */ async getLatestBlockHeight(): Promise { const response = await this.request<{ block: { header: { height: string } } }>( `/cosmos/base/tendermint/v1beta1/blocks/latest` ); return parseInt(response.block.header.height, 10); } // =========================================================================== // 交易构建 // =========================================================================== /** * 构建转账交易消息 * * @param fromAddress - 发送方地址 * @param toAddress - 接收方地址 * @param amount - 金额 * @param denom - 代币单位 * @returns MsgSend 消息 */ buildMsgSend( fromAddress: string, toAddress: string, amount: string, denom: string = 'ukava' ): MsgSend { return { '@type': '/cosmos.bank.v1beta1.MsgSend', from_address: fromAddress, to_address: toAddress, amount: [{ denom, amount }], }; } /** * 构建交易体 * * @param messages - 交易消息列表 * @param memo - 备注 * @returns TxBody */ buildTxBody(messages: MsgSend[], memo: string = ''): TxBody { return { messages, memo, timeout_height: '0', extension_options: [], non_critical_extension_options: [], }; } /** * 构建 AuthInfo * * @param publicKeyBase64 - 压缩公钥 (Base64) * @param sequence - 账户序列号 * @param gasLimit - Gas 限制 * @param feeAmount - 手续费金额 * @returns AuthInfo */ buildAuthInfo( publicKeyBase64: string, sequence: string, gasLimit: number = this.config.defaultGasLimit, feeAmount?: Coin[] ): AuthInfo { // 计算手续费 (如果未提供) if (!feeAmount) { const gasPrice = parseFloat(this.config.gasPrice.replace('ukava', '')); const fee = Math.ceil(gasLimit * gasPrice); feeAmount = [{ denom: 'ukava', amount: fee.toString() }]; } return { signer_infos: [{ public_key: { '@type': '/cosmos.crypto.secp256k1.PubKey', key: publicKeyBase64, }, mode_info: { single: { mode: 'SIGN_MODE_DIRECT', }, }, sequence, }], fee: { amount: feeAmount, gas_limit: gasLimit.toString(), }, }; } /** * 构建待签名的交易数据 * * @param txBody - 交易体 * @param authInfo - 认证信息 * @param accountNumber - 账户编号 * @returns 签名数据 (用于 TSS 签名) */ buildSignDoc( txBody: TxBody, authInfo: AuthInfo, accountNumber: string ): { bodyBytes: Buffer; authInfoBytes: Buffer; chainId: string; accountNumber: string; signBytes: Buffer; } { // 注意:这里需要使用 protobuf 编码 // 简化版本:使用 JSON 编码后进行 SHA256 哈希 // 生产环境应使用 @cosmjs/proto-signing const bodyBytes = Buffer.from(JSON.stringify(txBody)); const authInfoBytes = Buffer.from(JSON.stringify(authInfo)); // 构建 SignDoc const signDoc = { body_bytes: bodyBytes.toString('base64'), auth_info_bytes: authInfoBytes.toString('base64'), chain_id: this.config.chainId, account_number: accountNumber, }; // 计算签名哈希 (SHA256) const signBytes = crypto.createHash('sha256') .update(JSON.stringify(signDoc)) .digest(); return { bodyBytes, authInfoBytes, chainId: this.config.chainId, accountNumber, signBytes, }; } // =========================================================================== // 交易广播 // =========================================================================== /** * 模拟交易 (估算 Gas) * * @param txBytes - 交易字节 (Base64) * @returns 模拟结果 */ async simulateTx(txBytes: string): Promise { return this.request( '/cosmos/tx/v1beta1/simulate', 'POST', { tx_bytes: txBytes } ); } /** * 广播交易 * * @param txBytes - 已签名的交易字节 (Base64) * @param mode - 广播模式 (BROADCAST_MODE_SYNC | BROADCAST_MODE_ASYNC | BROADCAST_MODE_BLOCK) * @returns 广播结果 */ async broadcastTx( txBytes: string, mode: 'BROADCAST_MODE_SYNC' | 'BROADCAST_MODE_ASYNC' | 'BROADCAST_MODE_BLOCK' = 'BROADCAST_MODE_SYNC' ): Promise { return this.request( '/cosmos/tx/v1beta1/txs', 'POST', { tx_bytes: txBytes, mode, } ); } /** * 构建并编码完整的已签名交易 * * @param bodyBytes - 交易体字节 * @param authInfoBytes - 认证信息字节 * @param signature - 签名 (Buffer) * @returns Base64 编码的交易字节 */ encodeSignedTx( bodyBytes: Buffer, authInfoBytes: Buffer, signature: Buffer ): string { // 简化版本:使用 JSON 编码 // 生产环境应使用 protobuf 编码 const txRaw = { body_bytes: bodyBytes.toString('base64'), auth_info_bytes: authInfoBytes.toString('base64'), signatures: [signature.toString('base64')], }; return Buffer.from(JSON.stringify(txRaw)).toString('base64'); } // =========================================================================== // 便捷方法 // =========================================================================== /** * 格式化 KAVA 金额 (ukava -> KAVA) * * @param amount - 金额 (ukava) * @returns 格式化的金额字符串 */ formatKava(amount: string): string { const ukava = BigInt(amount); const kava = Number(ukava) / 1_000_000; return kava.toFixed(6); } /** * 转换 KAVA 到 ukava * * @param kava - KAVA 金额 * @returns ukava 金额字符串 */ toUkava(kava: number | string): string { const ukava = Math.floor(Number(kava) * 1_000_000); return ukava.toString(); } /** * 检查地址格式 * * @param address - 地址 * @returns 是否有效 */ isValidAddress(address: string): boolean { return address.startsWith('kava') && address.length === 43; } /** * 获取配置 */ getConfig(): KavaClientConfig { return { ...this.config }; } /** * 更新配置 */ updateConfig(config: Partial): void { this.config = { ...this.config, ...config }; } } // 导出默认客户端实例 export const kavaClient = new KavaClient();