rwadurian/backend/mpc-system/services/service-party-app/electron/modules/kava-client.ts

562 lines
13 KiB
TypeScript

/**
* 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<T>(
path: string,
method: 'GET' | 'POST' = 'GET',
body?: unknown
): Promise<T> {
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<Coin[]> {
const response = await this.request<BalanceResponse>(
`/cosmos/bank/v1beta1/balances/${address}`
);
return response.balances;
}
/**
* 查询指定代币余额
*
* @param address - Kava 地址
* @param denom - 代币单位 (如 "ukava")
* @returns 余额
*/
async getBalance(address: string, denom: string = 'ukava'): Promise<Coin> {
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<AccountInfo> {
const response = await this.request<AccountResponse>(
`/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<TxResponse> {
const response = await this.request<{ tx_response: TxResponse }>(
`/cosmos/tx/v1beta1/txs/${txHash}`
);
return response.tx_response;
}
/**
* 查询最新区块高度
*/
async getLatestBlockHeight(): Promise<number> {
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<SimulateTxResponse> {
return this.request<SimulateTxResponse>(
'/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<BroadcastTxResponse> {
return this.request<BroadcastTxResponse>(
'/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<KavaClientConfig>): void {
this.config = { ...this.config, ...config };
}
}
// 导出默认客户端实例
export const kavaClient = new KavaClient();