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

544 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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<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;
}
}
// ===========================================================================
// 查询功能
// ===========================================================================
/**
* 查询 KAVA 余额
*/
async getKavaBalance(address: string): Promise<AccountBalance> {
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<AccountBalance[]> {
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<AccountInfo> {
// 获取账户信息
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<TxStatus> {
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<UnsignedTxData> {
// 获取账户信息
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<SignedTxData> {
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<TxBroadcastResult> {
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<KavaTxConfig>): 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();