feat(service-party-app): 添加SQLite存储和Kava区块链集成
## 主要变更 ### 1. SQLite 本地存储 (sql.js) - 使用 sql.js (纯 JavaScript SQLite) 替代 better-sqlite3 - 无需本地数据库服务,跨平台兼容 - 表结构: shares, derived_addresses, signing_history, settings - AES-256-GCM 加密 share 数据,PBKDF2 密钥派生 ### 2. Kava 区块链集成 - 新增 kava-tx-service.ts: REST API 交易服务 - 余额查询 (ukava/KAVA) - 交易构建和广播 - 交易状态查询 - 支持多个备用端点自动切换 ### 3. 地址派生 - 新增 address-derivation.ts: 多链地址派生 - 支持 Kava, Cosmos, Osmosis, Ethereum 等链 - 使用 Node.js crypto 替代 @noble/hashes 以解决模块兼容问题 - 手动实现 secp256k1 公钥解压缩 ### 4. IPC 处理器 - main.ts: 添加 Kava 相关 IPC 处理器 - preload.ts: 暴露 kava API 给渲染进程 - electron.d.ts: 完整的 TypeScript 类型定义 ## 新增文件 - electron/modules/database.ts - electron/modules/address-derivation.ts - electron/modules/kava-client.ts - electron/modules/kava-tx-service.ts - electron/types/sql.js.d.ts - src/utils/address.ts - .gitignore 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
76ef8b0a8c
commit
c97cd208ab
|
|
@ -474,7 +474,10 @@
|
|||
"Bash(git commit -m \"$\\(cat <<''EOF''\nfix\\(admin-service\\): 修复共管钱包 status 类型不匹配问题\n\n使用 Prisma 生成的类型替代手动定义的接口:\n- PrismaCoManagedWalletSession -> @prisma/client\n- PrismaCoManagedWallet -> @prisma/client\n- status 字段使用 PrismaWalletSessionStatus 枚举类型\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
|
||||
"Bash(git commit -m \"$\\(cat <<''EOF''\ndocs: 添加 Service Party App 技术文档\n\n添加分布式共管钱包桌面应用的详细技术文档,包括:\n\n- 应用概述和使用场景\n- 目录结构说明\n- 技术架构和技术栈\n- TSS 子进程架构设计\n- IPC 消息格式定义\n- 核心功能说明\n- 编译与运行指南\n- 安全考虑\n- 系统集成说明\n- 未来扩展规划\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
|
||||
"Bash(git commit -m \"$\\(cat <<''EOF''\nfix\\(admin-web\\): 修复系统维护\"立即激活\"按钮不显示的问题\n\n- 修复 getStatusTag 函数逻辑,未激活状态使用 ''inactive'' 样式而不是 ''expired''\n- 添加更细化的状态判断:维护中、已过期、已计划、未激活、待激活\n- 添加 inactive 标签样式(橙色背景)\n- 现在未激活的维护计划会正确显示\"立即激活\"按钮\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
|
||||
"Bash(git commit -m \"$\\(cat <<''EOF''\nfix\\(migration\\): 使数据库迁移脚本幂等化,支持重复执行\n\n将 008_add_co_managed_wallet_fields.up.sql 改为幂等脚本:\n- 使用 DO $$... IF NOT EXISTS 检查列是否存在再添加\n- 使用 CREATE INDEX IF NOT EXISTS 创建索引\n- 使用 DROP CONSTRAINT IF EXISTS 删除约束\n\n这确保迁移脚本可以安全地多次执行,不会因列/索引已存在而失败。\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")"
|
||||
"Bash(git commit -m \"$\\(cat <<''EOF''\nfix\\(migration\\): 使数据库迁移脚本幂等化,支持重复执行\n\n将 008_add_co_managed_wallet_fields.up.sql 改为幂等脚本:\n- 使用 DO $$... IF NOT EXISTS 检查列是否存在再添加\n- 使用 CREATE INDEX IF NOT EXISTS 创建索引\n- 使用 DROP CONSTRAINT IF EXISTS 删除约束\n\n这确保迁移脚本可以安全地多次执行,不会因列/索引已存在而失败。\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
|
||||
"Bash(git commit -m \"$\\(cat <<''EOF''\nfeat\\(service-party-app\\): 添加 Windows 一键编译脚本\n\n添加 build-windows.bat 脚本,支持:\n- 检查 Node.js 和 Go 环境\n- 编译 TSS 子进程 \\(tss-party.exe\\)\n- 安装 npm 依赖\n- 编译 Electron 应用\n\n使用方法: 双击运行 build-windows.bat\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
|
||||
"Bash(./node_modules/.bin/tsc:*)",
|
||||
"Bash(npm ls:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Build outputs
|
||||
dist/
|
||||
dist-electron/
|
||||
release/
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.temp
|
||||
|
||||
# SQLite database files
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
|
||||
# Electron build cache
|
||||
.electron/
|
||||
|
|
@ -3,15 +3,18 @@ import * as path from 'path';
|
|||
import * as fs from 'fs';
|
||||
import express from 'express';
|
||||
import { GrpcClient } from './modules/grpc-client';
|
||||
import { SecureStorage } from './modules/storage';
|
||||
import { DatabaseManager } from './modules/database';
|
||||
import { addressDerivationService, CHAIN_CONFIGS } from './modules/address-derivation';
|
||||
import { KavaTxService, KAVA_MAINNET_TX_CONFIG } from './modules/kava-tx-service';
|
||||
|
||||
// 内置 HTTP 服务器端口
|
||||
const HTTP_PORT = 3456;
|
||||
|
||||
let mainWindow: BrowserWindow | null = null;
|
||||
let grpcClient: GrpcClient | null = null;
|
||||
let storage: SecureStorage | null = null;
|
||||
let database: DatabaseManager | null = null;
|
||||
let httpServer: ReturnType<typeof express.application.listen> | null = null;
|
||||
let kavaTxService: KavaTxService | null = null;
|
||||
|
||||
// 创建主窗口
|
||||
function createWindow() {
|
||||
|
|
@ -83,8 +86,11 @@ async function initServices() {
|
|||
// 初始化 gRPC 客户端
|
||||
grpcClient = new GrpcClient();
|
||||
|
||||
// 初始化安全存储
|
||||
storage = new SecureStorage();
|
||||
// 初始化数据库
|
||||
database = new DatabaseManager();
|
||||
|
||||
// 初始化 Kava 交易服务
|
||||
kavaTxService = new KavaTxService(KAVA_MAINNET_TX_CONFIG);
|
||||
|
||||
// 设置 IPC 处理器
|
||||
setupIpcHandlers();
|
||||
|
|
@ -92,6 +98,10 @@ async function initServices() {
|
|||
|
||||
// 设置 IPC 通信处理器
|
||||
function setupIpcHandlers() {
|
||||
// ===========================================================================
|
||||
// gRPC 相关
|
||||
// ===========================================================================
|
||||
|
||||
// gRPC 连接
|
||||
ipcMain.handle('grpc:connect', async (_event, { host, port }) => {
|
||||
try {
|
||||
|
|
@ -122,100 +132,20 @@ function setupIpcHandlers() {
|
|||
}
|
||||
});
|
||||
|
||||
// 存储 - 保存 share
|
||||
ipcMain.handle('storage:saveShare', async (_event, { share, password }) => {
|
||||
try {
|
||||
storage?.saveShare(share, password);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return { success: false, error: (error as Error).message };
|
||||
}
|
||||
});
|
||||
|
||||
// 存储 - 获取 share 列表
|
||||
ipcMain.handle('storage:listShares', async () => {
|
||||
try {
|
||||
const shares = storage?.listShares() ?? [];
|
||||
return { success: true, data: shares };
|
||||
} catch (error) {
|
||||
return { success: false, error: (error as Error).message };
|
||||
}
|
||||
});
|
||||
|
||||
// 存储 - 导出 share
|
||||
ipcMain.handle('storage:exportShare', async (_event, { id, password }) => {
|
||||
try {
|
||||
const data = storage?.exportShare(id, password);
|
||||
return { success: true, data };
|
||||
} catch (error) {
|
||||
return { success: false, error: (error as Error).message };
|
||||
}
|
||||
});
|
||||
|
||||
// 存储 - 导入 share (从文件路径)
|
||||
ipcMain.handle('storage:importShare', async (_event, { filePath, password }) => {
|
||||
try {
|
||||
const data = fs.readFileSync(filePath);
|
||||
const share = storage?.importShare(data, password);
|
||||
return { success: true, share };
|
||||
} catch (error) {
|
||||
return { success: false, error: (error as Error).message };
|
||||
}
|
||||
});
|
||||
|
||||
// 存储 - 获取单个 share
|
||||
ipcMain.handle('storage:getShare', async (_event, { id, password }) => {
|
||||
try {
|
||||
const share = storage?.getShare(id, password);
|
||||
return share;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
// 存储 - 删除 share
|
||||
ipcMain.handle('storage:deleteShare', async (_event, { id }) => {
|
||||
try {
|
||||
storage?.deleteShare(id);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return { success: false, error: (error as Error).message };
|
||||
}
|
||||
});
|
||||
|
||||
// 存储 - 获取设置
|
||||
ipcMain.handle('storage:getSettings', async () => {
|
||||
try {
|
||||
return storage?.getSettings() ?? null;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
// 存储 - 保存设置
|
||||
ipcMain.handle('storage:saveSettings', async (_event, { settings }) => {
|
||||
try {
|
||||
storage?.saveSettings(settings);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return { success: false, error: (error as Error).message };
|
||||
}
|
||||
});
|
||||
|
||||
// gRPC - 创建会话
|
||||
ipcMain.handle('grpc:createSession', async (_event, params) => {
|
||||
ipcMain.handle('grpc:createSession', async (_event, _params) => {
|
||||
// TODO: 实现创建会话逻辑
|
||||
return { success: false, error: '功能尚未实现 - 需要连接到 Session Coordinator' };
|
||||
});
|
||||
|
||||
// gRPC - 验证邀请码
|
||||
ipcMain.handle('grpc:validateInviteCode', async (_event, { code }) => {
|
||||
ipcMain.handle('grpc:validateInviteCode', async (_event, { code: _code }) => {
|
||||
// TODO: 实现验证邀请码逻辑
|
||||
return { success: false, error: '功能尚未实现 - 需要连接到 Session Coordinator' };
|
||||
});
|
||||
|
||||
// gRPC - 获取会话状态
|
||||
ipcMain.handle('grpc:getSessionStatus', async (_event, { sessionId }) => {
|
||||
ipcMain.handle('grpc:getSessionStatus', async (_event, { sessionId: _sessionId }) => {
|
||||
// TODO: 实现获取会话状态逻辑
|
||||
return { success: false, error: '功能尚未实现' };
|
||||
});
|
||||
|
|
@ -232,18 +162,385 @@ function setupIpcHandlers() {
|
|||
});
|
||||
|
||||
// gRPC - 验证签名会话
|
||||
ipcMain.handle('grpc:validateSigningSession', async (_event, { code }) => {
|
||||
ipcMain.handle('grpc:validateSigningSession', async (_event, { code: _code }) => {
|
||||
// TODO: 实现验证签名会话逻辑
|
||||
return { success: false, error: '功能尚未实现 - 需要连接到 Session Coordinator' };
|
||||
});
|
||||
|
||||
// gRPC - 加入签名会话
|
||||
ipcMain.handle('grpc:joinSigningSession', async (_event, params) => {
|
||||
ipcMain.handle('grpc:joinSigningSession', async (_event, _params) => {
|
||||
// TODO: 实现加入签名会话逻辑
|
||||
return { success: false, error: '功能尚未实现' };
|
||||
});
|
||||
|
||||
// 对话框 - 选择目录
|
||||
// ===========================================================================
|
||||
// Share 存储相关 (SQLite)
|
||||
// ===========================================================================
|
||||
|
||||
// 保存 share
|
||||
ipcMain.handle('storage:saveShare', async (_event, { share, password }) => {
|
||||
try {
|
||||
const saved = database?.saveShare({
|
||||
sessionId: share.sessionId,
|
||||
walletName: share.walletName,
|
||||
partyId: share.partyId,
|
||||
partyIndex: share.partyIndex,
|
||||
thresholdT: share.threshold.t,
|
||||
thresholdN: share.threshold.n,
|
||||
publicKeyHex: share.publicKey,
|
||||
rawShare: share.rawShare,
|
||||
participants: share.participants || [],
|
||||
}, password);
|
||||
|
||||
// 自动派生 Kava 地址
|
||||
if (saved && share.publicKey) {
|
||||
try {
|
||||
const kavaAddress = addressDerivationService.deriveAddress(share.publicKey, 'kava');
|
||||
database?.saveDerivedAddress({
|
||||
shareId: saved.id,
|
||||
chain: 'kava',
|
||||
derivationPath: kavaAddress.derivationPath,
|
||||
address: kavaAddress.address,
|
||||
publicKeyHex: share.publicKey,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Failed to derive Kava address:', err);
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true, data: saved };
|
||||
} catch (error) {
|
||||
return { success: false, error: (error as Error).message };
|
||||
}
|
||||
});
|
||||
|
||||
// 获取 share 列表
|
||||
ipcMain.handle('storage:listShares', async () => {
|
||||
try {
|
||||
const shares = database?.listShares() ?? [];
|
||||
// 转换为前端期望的格式
|
||||
const formatted = shares.map(share => ({
|
||||
id: share.id,
|
||||
sessionId: share.session_id,
|
||||
walletName: share.wallet_name,
|
||||
partyId: share.party_id,
|
||||
partyIndex: share.party_index,
|
||||
threshold: {
|
||||
t: share.threshold_t,
|
||||
n: share.threshold_n,
|
||||
},
|
||||
publicKey: share.public_key_hex,
|
||||
createdAt: share.created_at,
|
||||
lastUsedAt: share.last_used_at,
|
||||
participants: JSON.parse(share.participants_json || '[]'),
|
||||
}));
|
||||
return { success: true, data: formatted };
|
||||
} catch (error) {
|
||||
return { success: false, error: (error as Error).message };
|
||||
}
|
||||
});
|
||||
|
||||
// 获取单个 share (解密)
|
||||
ipcMain.handle('storage:getShare', async (_event, { id, password }) => {
|
||||
try {
|
||||
const share = database?.getShare(id, password);
|
||||
if (!share) return null;
|
||||
|
||||
return {
|
||||
id: share.id,
|
||||
sessionId: share.session_id,
|
||||
walletName: share.wallet_name,
|
||||
partyId: share.party_id,
|
||||
partyIndex: share.party_index,
|
||||
threshold: {
|
||||
t: share.threshold_t,
|
||||
n: share.threshold_n,
|
||||
},
|
||||
publicKey: share.public_key_hex,
|
||||
rawShare: share.raw_share,
|
||||
createdAt: share.created_at,
|
||||
lastUsedAt: share.last_used_at,
|
||||
participants: JSON.parse(share.participants_json || '[]'),
|
||||
};
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
// 删除 share
|
||||
ipcMain.handle('storage:deleteShare', async (_event, { id }) => {
|
||||
try {
|
||||
database?.deleteShare(id);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return { success: false, error: (error as Error).message };
|
||||
}
|
||||
});
|
||||
|
||||
// 导出 share
|
||||
ipcMain.handle('storage:exportShare', async (_event, { id, password }) => {
|
||||
try {
|
||||
const data = database?.exportShare(id, password);
|
||||
return { success: true, data };
|
||||
} catch (error) {
|
||||
return { success: false, error: (error as Error).message };
|
||||
}
|
||||
});
|
||||
|
||||
// 导入 share
|
||||
ipcMain.handle('storage:importShare', async (_event, { filePath, password }) => {
|
||||
try {
|
||||
const data = fs.readFileSync(filePath);
|
||||
const share = database?.importShare(data, password);
|
||||
return { success: true, share };
|
||||
} catch (error) {
|
||||
return { success: false, error: (error as Error).message };
|
||||
}
|
||||
});
|
||||
|
||||
// ===========================================================================
|
||||
// 地址派生相关
|
||||
// ===========================================================================
|
||||
|
||||
// 派生地址
|
||||
ipcMain.handle('address:derive', async (_event, { shareId, chain, password }) => {
|
||||
try {
|
||||
const share = database?.getShare(shareId, password);
|
||||
if (!share) {
|
||||
return { success: false, error: 'Share not found' };
|
||||
}
|
||||
|
||||
const derived = addressDerivationService.deriveAddress(share.public_key_hex, chain);
|
||||
|
||||
// 保存到数据库
|
||||
const saved = database?.saveDerivedAddress({
|
||||
shareId,
|
||||
chain,
|
||||
derivationPath: derived.derivationPath,
|
||||
address: derived.address,
|
||||
publicKeyHex: share.public_key_hex,
|
||||
});
|
||||
|
||||
return { success: true, data: { ...derived, id: saved?.id } };
|
||||
} catch (error) {
|
||||
return { success: false, error: (error as Error).message };
|
||||
}
|
||||
});
|
||||
|
||||
// 派生所有支持的链地址
|
||||
ipcMain.handle('address:deriveAll', async (_event, { shareId, password }) => {
|
||||
try {
|
||||
const share = database?.getShare(shareId, password);
|
||||
if (!share) {
|
||||
return { success: false, error: 'Share not found' };
|
||||
}
|
||||
|
||||
const addresses = addressDerivationService.deriveAllAddresses(share.public_key_hex);
|
||||
|
||||
// 保存所有地址
|
||||
for (const addr of addresses) {
|
||||
database?.saveDerivedAddress({
|
||||
shareId,
|
||||
chain: addr.chain,
|
||||
derivationPath: addr.derivationPath,
|
||||
address: addr.address,
|
||||
publicKeyHex: share.public_key_hex,
|
||||
});
|
||||
}
|
||||
|
||||
return { success: true, data: addresses };
|
||||
} catch (error) {
|
||||
return { success: false, error: (error as Error).message };
|
||||
}
|
||||
});
|
||||
|
||||
// 获取 share 的所有派生地址
|
||||
ipcMain.handle('address:list', async (_event, { shareId }) => {
|
||||
try {
|
||||
const addresses = database?.getAddressesByShare(shareId) ?? [];
|
||||
return {
|
||||
success: true,
|
||||
data: addresses.map(addr => ({
|
||||
id: addr.id,
|
||||
shareId: addr.share_id,
|
||||
chain: addr.chain,
|
||||
chainName: CHAIN_CONFIGS[addr.chain]?.name || addr.chain,
|
||||
derivationPath: addr.derivation_path,
|
||||
address: addr.address,
|
||||
publicKeyHex: addr.public_key_hex,
|
||||
createdAt: addr.created_at,
|
||||
})),
|
||||
};
|
||||
} catch (error) {
|
||||
return { success: false, error: (error as Error).message };
|
||||
}
|
||||
});
|
||||
|
||||
// 获取支持的链列表
|
||||
ipcMain.handle('address:getSupportedChains', async () => {
|
||||
return addressDerivationService.getSupportedChains();
|
||||
});
|
||||
|
||||
// ===========================================================================
|
||||
// 签名历史相关
|
||||
// ===========================================================================
|
||||
|
||||
// 获取签名历史
|
||||
ipcMain.handle('signing:getHistory', async (_event, { shareId }) => {
|
||||
try {
|
||||
const history = database?.getSigningHistoryByShare(shareId) ?? [];
|
||||
return {
|
||||
success: true,
|
||||
data: history.map(h => ({
|
||||
id: h.id,
|
||||
shareId: h.share_id,
|
||||
sessionId: h.session_id,
|
||||
messageHash: h.message_hash,
|
||||
signature: h.signature,
|
||||
status: h.status,
|
||||
errorMessage: h.error_message,
|
||||
createdAt: h.created_at,
|
||||
completedAt: h.completed_at,
|
||||
})),
|
||||
};
|
||||
} catch (error) {
|
||||
return { success: false, error: (error as Error).message };
|
||||
}
|
||||
});
|
||||
|
||||
// ===========================================================================
|
||||
// 设置相关
|
||||
// ===========================================================================
|
||||
|
||||
// 获取设置
|
||||
ipcMain.handle('storage:getSettings', async () => {
|
||||
try {
|
||||
const settings = database?.getAllSettings() ?? {};
|
||||
return {
|
||||
messageRouterUrl: settings['message_router_url'] || 'mpc-grpc.szaiai.com:443',
|
||||
autoBackup: settings['auto_backup'] === 'true',
|
||||
backupPath: settings['backup_path'] || '',
|
||||
};
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
// 保存设置
|
||||
ipcMain.handle('storage:saveSettings', async (_event, { settings }) => {
|
||||
try {
|
||||
if (settings.messageRouterUrl !== undefined) {
|
||||
database?.setSetting('message_router_url', settings.messageRouterUrl);
|
||||
}
|
||||
if (settings.autoBackup !== undefined) {
|
||||
database?.setSetting('auto_backup', settings.autoBackup ? 'true' : 'false');
|
||||
}
|
||||
if (settings.backupPath !== undefined) {
|
||||
database?.setSetting('backup_path', settings.backupPath);
|
||||
}
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return { success: false, error: (error as Error).message };
|
||||
}
|
||||
});
|
||||
|
||||
// ===========================================================================
|
||||
// Kava 区块链相关
|
||||
// ===========================================================================
|
||||
|
||||
// 查询 Kava 余额
|
||||
ipcMain.handle('kava:getBalance', async (_event, { address }) => {
|
||||
try {
|
||||
const balance = await kavaTxService?.getKavaBalance(address);
|
||||
return { success: true, data: balance };
|
||||
} catch (error) {
|
||||
return { success: false, error: (error as Error).message };
|
||||
}
|
||||
});
|
||||
|
||||
// 查询所有余额
|
||||
ipcMain.handle('kava:getAllBalances', async (_event, { address }) => {
|
||||
try {
|
||||
const balances = await kavaTxService?.getAllBalances(address);
|
||||
return { success: true, data: balances };
|
||||
} catch (error) {
|
||||
return { success: false, error: (error as Error).message };
|
||||
}
|
||||
});
|
||||
|
||||
// 查询账户信息
|
||||
ipcMain.handle('kava:getAccountInfo', async (_event, { address }) => {
|
||||
try {
|
||||
const info = await kavaTxService?.getAccountInfo(address);
|
||||
return { success: true, data: info };
|
||||
} catch (error) {
|
||||
return { success: false, error: (error as Error).message };
|
||||
}
|
||||
});
|
||||
|
||||
// 构建转账交易 (待签名)
|
||||
ipcMain.handle('kava:buildSendTx', async (_event, { fromAddress, toAddress, amount, publicKeyHex, memo }) => {
|
||||
try {
|
||||
const unsignedTx = await kavaTxService?.buildSendTx(
|
||||
fromAddress,
|
||||
toAddress,
|
||||
amount,
|
||||
publicKeyHex,
|
||||
memo || ''
|
||||
);
|
||||
return { success: true, data: unsignedTx };
|
||||
} catch (error) {
|
||||
return { success: false, error: (error as Error).message };
|
||||
}
|
||||
});
|
||||
|
||||
// 完成签名并广播交易
|
||||
ipcMain.handle('kava:completeTxAndBroadcast', async (_event, { unsignedTx, signatureHex }) => {
|
||||
try {
|
||||
// 完成交易签名
|
||||
const signedTx = await kavaTxService?.completeTx(unsignedTx, signatureHex);
|
||||
if (!signedTx) {
|
||||
return { success: false, error: 'Failed to complete transaction' };
|
||||
}
|
||||
|
||||
// 广播交易
|
||||
const result = await kavaTxService?.broadcastTx(signedTx);
|
||||
return { success: true, data: result };
|
||||
} catch (error) {
|
||||
return { success: false, error: (error as Error).message };
|
||||
}
|
||||
});
|
||||
|
||||
// 查询交易状态
|
||||
ipcMain.handle('kava:getTxStatus', async (_event, { txHash }) => {
|
||||
try {
|
||||
const status = await kavaTxService?.getTxStatus(txHash);
|
||||
return { success: true, data: status };
|
||||
} catch (error) {
|
||||
return { success: false, error: (error as Error).message };
|
||||
}
|
||||
});
|
||||
|
||||
// 获取 Kava 配置
|
||||
ipcMain.handle('kava:getConfig', async () => {
|
||||
return kavaTxService?.getConfig();
|
||||
});
|
||||
|
||||
// 更新 Kava 配置
|
||||
ipcMain.handle('kava:updateConfig', async (_event, { config }) => {
|
||||
try {
|
||||
kavaTxService?.updateConfig(config);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return { success: false, error: (error as Error).message };
|
||||
}
|
||||
});
|
||||
|
||||
// ===========================================================================
|
||||
// 对话框相关
|
||||
// ===========================================================================
|
||||
|
||||
// 选择目录
|
||||
ipcMain.handle('dialog:selectDirectory', async () => {
|
||||
const result = await dialog.showOpenDialog(mainWindow!, {
|
||||
properties: ['openDirectory'],
|
||||
|
|
@ -251,7 +548,7 @@ function setupIpcHandlers() {
|
|||
return result.canceled ? null : result.filePaths[0];
|
||||
});
|
||||
|
||||
// 对话框 - 选择文件
|
||||
// 选择文件
|
||||
ipcMain.handle('dialog:selectFile', async (_event, { filters }) => {
|
||||
const result = await dialog.showOpenDialog(mainWindow!, {
|
||||
properties: ['openFile'],
|
||||
|
|
@ -260,7 +557,7 @@ function setupIpcHandlers() {
|
|||
return result.canceled ? null : result.filePaths[0];
|
||||
});
|
||||
|
||||
// 对话框 - 保存文件
|
||||
// 保存文件
|
||||
ipcMain.handle('dialog:saveFile', async (_event, { defaultPath, filters }) => {
|
||||
const result = await dialog.showSaveDialog(mainWindow!, {
|
||||
defaultPath,
|
||||
|
|
@ -295,5 +592,7 @@ app.on('window-all-closed', () => {
|
|||
app.on('before-quit', () => {
|
||||
// 清理资源
|
||||
grpcClient?.disconnect();
|
||||
database?.close();
|
||||
httpServer?.close();
|
||||
kavaTxService?.disconnect();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,333 @@
|
|||
import * as crypto from 'crypto';
|
||||
import { bech32 } from 'bech32';
|
||||
|
||||
// =============================================================================
|
||||
// 链配置
|
||||
// =============================================================================
|
||||
|
||||
export interface ChainConfig {
|
||||
name: string;
|
||||
prefix: string;
|
||||
coinType: number;
|
||||
curve: 'secp256k1' | 'ed25519';
|
||||
derivationPath: string;
|
||||
}
|
||||
|
||||
export const CHAIN_CONFIGS: Record<string, ChainConfig> = {
|
||||
kava: {
|
||||
name: 'Kava',
|
||||
prefix: 'kava',
|
||||
coinType: 459,
|
||||
curve: 'secp256k1',
|
||||
derivationPath: "m/44'/459'/0'/0/0",
|
||||
},
|
||||
cosmos: {
|
||||
name: 'Cosmos Hub',
|
||||
prefix: 'cosmos',
|
||||
coinType: 118,
|
||||
curve: 'secp256k1',
|
||||
derivationPath: "m/44'/118'/0'/0/0",
|
||||
},
|
||||
osmosis: {
|
||||
name: 'Osmosis',
|
||||
prefix: 'osmo',
|
||||
coinType: 118,
|
||||
curve: 'secp256k1',
|
||||
derivationPath: "m/44'/118'/0'/0/0",
|
||||
},
|
||||
ethereum: {
|
||||
name: 'Ethereum',
|
||||
prefix: '0x',
|
||||
coinType: 60,
|
||||
curve: 'secp256k1',
|
||||
derivationPath: "m/44'/60'/0'/0/0",
|
||||
},
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// 地址派生工具
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* 从公钥派生 Bech32 地址 (Cosmos 系列)
|
||||
*
|
||||
* 流程:
|
||||
* 1. 公钥 → SHA256 → RIPEMD160 → 20字节地址
|
||||
* 2. 20字节地址 → Bech32 编码
|
||||
*/
|
||||
export function deriveCosmosAddress(publicKeyHex: string, prefix: string): string {
|
||||
// 移除可能的 0x 前缀
|
||||
const cleanHex = publicKeyHex.startsWith('0x') ? publicKeyHex.slice(2) : publicKeyHex;
|
||||
const publicKeyBytes = Buffer.from(cleanHex, 'hex');
|
||||
|
||||
// 对于 secp256k1,需要压缩公钥 (33 bytes)
|
||||
// 如果是未压缩公钥 (65 bytes),需要先压缩
|
||||
let compressedKey: Buffer = publicKeyBytes;
|
||||
if (publicKeyBytes.length === 65) {
|
||||
compressedKey = compressSecp256k1PublicKey(publicKeyBytes);
|
||||
} else if (publicKeyBytes.length === 64) {
|
||||
// 没有前缀的未压缩公钥
|
||||
const uncompressed = Buffer.concat([Buffer.from([0x04]), publicKeyBytes]);
|
||||
compressedKey = compressSecp256k1PublicKey(uncompressed);
|
||||
}
|
||||
|
||||
// SHA256 → RIPEMD160
|
||||
const sha256Hash = crypto.createHash('sha256').update(compressedKey).digest();
|
||||
const ripemd160Hash = crypto.createHash('ripemd160').update(sha256Hash).digest();
|
||||
|
||||
// Bech32 编码
|
||||
const words = bech32.toWords(ripemd160Hash);
|
||||
const address = bech32.encode(prefix, words);
|
||||
|
||||
return address;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从公钥派生以太坊地址
|
||||
*
|
||||
* 流程:
|
||||
* 1. 未压缩公钥 (去掉 04 前缀) → Keccak256 → 取后 20 字节
|
||||
*/
|
||||
export function deriveEthereumAddress(publicKeyHex: string): string {
|
||||
const cleanHex = publicKeyHex.startsWith('0x') ? publicKeyHex.slice(2) : publicKeyHex;
|
||||
const publicKeyBytes = Buffer.from(cleanHex, 'hex');
|
||||
|
||||
// 需要未压缩公钥的 x, y 坐标 (64 bytes)
|
||||
let uncompressedKey: Buffer;
|
||||
if (publicKeyBytes.length === 33) {
|
||||
// 压缩公钥,需要解压
|
||||
uncompressedKey = decompressSecp256k1PublicKey(publicKeyBytes);
|
||||
} else if (publicKeyBytes.length === 65) {
|
||||
// 未压缩公钥,去掉 04 前缀
|
||||
uncompressedKey = publicKeyBytes.slice(1) as Buffer;
|
||||
} else if (publicKeyBytes.length === 64) {
|
||||
uncompressedKey = publicKeyBytes;
|
||||
} else {
|
||||
throw new Error(`Invalid public key length: ${publicKeyBytes.length}`);
|
||||
}
|
||||
|
||||
// Keccak256 (使用 keccak256 而不是 sha3-256)
|
||||
const { keccak_256 } = require('@noble/hashes/sha3');
|
||||
const hash = keccak_256(uncompressedKey);
|
||||
|
||||
// 取后 20 字节
|
||||
const addressBytes = hash.slice(-20);
|
||||
const address = '0x' + Buffer.from(addressBytes).toString('hex');
|
||||
|
||||
return checksumAddress(address);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Ed25519 公钥派生地址 (用于某些链)
|
||||
*/
|
||||
export function deriveEd25519Address(publicKeyHex: string, prefix: string): string {
|
||||
const cleanHex = publicKeyHex.startsWith('0x') ? publicKeyHex.slice(2) : publicKeyHex;
|
||||
const publicKeyBytes = Buffer.from(cleanHex, 'hex');
|
||||
|
||||
// SHA256 → RIPEMD160
|
||||
const sha256Hash = crypto.createHash('sha256').update(publicKeyBytes).digest();
|
||||
const ripemd160Hash = crypto.createHash('ripemd160').update(sha256Hash).digest();
|
||||
|
||||
// Bech32 编码
|
||||
const words = bech32.toWords(ripemd160Hash);
|
||||
const address = bech32.encode(prefix, words);
|
||||
|
||||
return address;
|
||||
}
|
||||
|
||||
/**
|
||||
* 压缩 secp256k1 公钥
|
||||
*/
|
||||
function compressSecp256k1PublicKey(uncompressed: Buffer): Buffer {
|
||||
if (uncompressed.length !== 65 || uncompressed[0] !== 0x04) {
|
||||
throw new Error('Invalid uncompressed public key');
|
||||
}
|
||||
|
||||
const x = uncompressed.slice(1, 33);
|
||||
const y = uncompressed.slice(33, 65);
|
||||
|
||||
// 判断 y 是奇数还是偶数
|
||||
const prefix = y[31] % 2 === 0 ? 0x02 : 0x03;
|
||||
|
||||
return Buffer.concat([Buffer.from([prefix]), x]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解压缩 secp256k1 公钥
|
||||
* 使用椭圆曲线数学: y² = x³ + 7 (mod p)
|
||||
*/
|
||||
function decompressSecp256k1PublicKey(compressed: Buffer): Buffer {
|
||||
if (compressed.length !== 33) {
|
||||
throw new Error('Invalid compressed public key');
|
||||
}
|
||||
|
||||
// secp256k1 曲线参数
|
||||
const p = BigInt('0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F');
|
||||
|
||||
const prefix = compressed[0];
|
||||
const x = BigInt('0x' + compressed.slice(1).toString('hex'));
|
||||
|
||||
// 计算 y² = x³ + 7 (mod p)
|
||||
const xCubed = modPow(x, 3n, p);
|
||||
const ySquared = (xCubed + 7n) % p;
|
||||
|
||||
// 计算平方根 (p ≡ 3 mod 4, 所以 y = ySquared^((p+1)/4) mod p)
|
||||
let y = modPow(ySquared, (p + 1n) / 4n, p);
|
||||
|
||||
// 根据前缀选择正确的 y 值
|
||||
const isYOdd = y % 2n === 1n;
|
||||
const shouldBeOdd = prefix === 0x03;
|
||||
|
||||
if (isYOdd !== shouldBeOdd) {
|
||||
y = p - y;
|
||||
}
|
||||
|
||||
// 转换为 Buffer (64 bytes: x || y)
|
||||
const xBuffer = Buffer.from(x.toString(16).padStart(64, '0'), 'hex');
|
||||
const yBuffer = Buffer.from(y.toString(16).padStart(64, '0'), 'hex');
|
||||
|
||||
return Buffer.concat([xBuffer, yBuffer]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 模幂运算
|
||||
*/
|
||||
function modPow(base: bigint, exponent: bigint, modulus: bigint): bigint {
|
||||
let result = 1n;
|
||||
base = base % modulus;
|
||||
|
||||
while (exponent > 0n) {
|
||||
if (exponent % 2n === 1n) {
|
||||
result = (result * base) % modulus;
|
||||
}
|
||||
exponent = exponent / 2n;
|
||||
base = (base * base) % modulus;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* EIP-55 校验和地址
|
||||
*/
|
||||
function checksumAddress(address: string): string {
|
||||
const { keccak_256 } = require('@noble/hashes/sha3');
|
||||
const addr = address.toLowerCase().replace('0x', '');
|
||||
const hash = Buffer.from(keccak_256(Buffer.from(addr, 'utf8'))).toString('hex');
|
||||
|
||||
let result = '0x';
|
||||
for (let i = 0; i < addr.length; i++) {
|
||||
if (parseInt(hash[i], 16) >= 8) {
|
||||
result += addr[i].toUpperCase();
|
||||
} else {
|
||||
result += addr[i];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 地址派生服务
|
||||
// =============================================================================
|
||||
|
||||
export interface DerivedAddress {
|
||||
chain: string;
|
||||
chainName: string;
|
||||
prefix: string;
|
||||
address: string;
|
||||
derivationPath: string;
|
||||
publicKeyHex: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 地址派生服务
|
||||
*
|
||||
* 注意:TSS keygen 生成的是聚合公钥,不是派生公钥。
|
||||
* 对于需要不同链的地址,我们直接使用聚合公钥派生地址,
|
||||
* 而不是像 HD 钱包那样从种子派生。
|
||||
*
|
||||
* 这意味着:
|
||||
* - 所有链使用相同的公钥
|
||||
* - 不同链的地址只是编码方式不同
|
||||
* - 这是 TSS 钱包的标准做法
|
||||
*/
|
||||
export class AddressDerivationService {
|
||||
/**
|
||||
* 从 TSS 聚合公钥派生指定链的地址
|
||||
*/
|
||||
deriveAddress(publicKeyHex: string, chain: string): DerivedAddress {
|
||||
const config = CHAIN_CONFIGS[chain];
|
||||
if (!config) {
|
||||
throw new Error(`Unsupported chain: ${chain}`);
|
||||
}
|
||||
|
||||
let address: string;
|
||||
|
||||
if (chain === 'ethereum') {
|
||||
address = deriveEthereumAddress(publicKeyHex);
|
||||
} else if (config.curve === 'ed25519') {
|
||||
address = deriveEd25519Address(publicKeyHex, config.prefix);
|
||||
} else {
|
||||
// Cosmos 系列 (kava, cosmos, osmosis 等)
|
||||
address = deriveCosmosAddress(publicKeyHex, config.prefix);
|
||||
}
|
||||
|
||||
return {
|
||||
chain,
|
||||
chainName: config.name,
|
||||
prefix: config.prefix,
|
||||
address,
|
||||
derivationPath: config.derivationPath,
|
||||
publicKeyHex,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 派生所有支持的链地址
|
||||
*/
|
||||
deriveAllAddresses(publicKeyHex: string): DerivedAddress[] {
|
||||
const addresses: DerivedAddress[] = [];
|
||||
|
||||
for (const chain of Object.keys(CHAIN_CONFIGS)) {
|
||||
try {
|
||||
const derived = this.deriveAddress(publicKeyHex, chain);
|
||||
addresses.push(derived);
|
||||
} catch (err) {
|
||||
console.error(`Failed to derive ${chain} address:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
return addresses;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证地址格式
|
||||
*/
|
||||
validateAddress(address: string, chain: string): boolean {
|
||||
const config = CHAIN_CONFIGS[chain];
|
||||
if (!config) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (chain === 'ethereum') {
|
||||
return /^0x[a-fA-F0-9]{40}$/.test(address);
|
||||
}
|
||||
|
||||
try {
|
||||
const decoded = bech32.decode(address);
|
||||
return decoded.prefix === config.prefix;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取支持的链列表
|
||||
*/
|
||||
getSupportedChains(): ChainConfig[] {
|
||||
return Object.values(CHAIN_CONFIGS);
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例
|
||||
export const addressDerivationService = new AddressDerivationService();
|
||||
|
|
@ -0,0 +1,668 @@
|
|||
import * as crypto from 'crypto';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { app } from 'electron';
|
||||
import initSqlJs from 'sql.js';
|
||||
import type { Database as SqlJsDatabase, SqlJsStatic } from 'sql.js';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
// =============================================================================
|
||||
// 数据库路径
|
||||
// =============================================================================
|
||||
function getDatabasePath(): string {
|
||||
const userDataPath = app.getPath('userData');
|
||||
return path.join(userDataPath, 'service-party.db');
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 加密配置
|
||||
// =============================================================================
|
||||
const ALGORITHM = 'aes-256-gcm';
|
||||
const KEY_LENGTH = 32;
|
||||
const IV_LENGTH = 16;
|
||||
const SALT_LENGTH = 32;
|
||||
const TAG_LENGTH = 16;
|
||||
const ITERATIONS = 100000;
|
||||
|
||||
// =============================================================================
|
||||
// 数据类型定义
|
||||
// =============================================================================
|
||||
|
||||
export interface ShareRecord {
|
||||
id: string;
|
||||
session_id: string;
|
||||
wallet_name: string;
|
||||
party_id: string;
|
||||
party_index: number;
|
||||
threshold_t: number;
|
||||
threshold_n: number;
|
||||
public_key_hex: string;
|
||||
encrypted_share: string;
|
||||
created_at: string;
|
||||
last_used_at: string | null;
|
||||
participants_json: string; // JSON 存储参与者列表
|
||||
}
|
||||
|
||||
export interface DerivedAddressRecord {
|
||||
id: string;
|
||||
share_id: string;
|
||||
chain: string;
|
||||
derivation_path: string;
|
||||
address: string;
|
||||
public_key_hex: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface SigningHistoryRecord {
|
||||
id: string;
|
||||
share_id: string;
|
||||
session_id: string;
|
||||
message_hash: string;
|
||||
signature: string | null;
|
||||
status: 'pending' | 'in_progress' | 'completed' | 'failed';
|
||||
error_message: string | null;
|
||||
created_at: string;
|
||||
completed_at: string | null;
|
||||
}
|
||||
|
||||
export interface SettingsRecord {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 数据库管理类 (使用 sql.js - 纯 JavaScript SQLite)
|
||||
// =============================================================================
|
||||
|
||||
export class DatabaseManager {
|
||||
private db: SqlJsDatabase | null = null;
|
||||
private SQL: SqlJsStatic | null = null;
|
||||
private dbPath: string;
|
||||
private initPromise: Promise<void>;
|
||||
|
||||
constructor() {
|
||||
this.dbPath = getDatabasePath();
|
||||
this.initPromise = this.initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化数据库
|
||||
*/
|
||||
private async initialize(): Promise<void> {
|
||||
// 初始化 sql.js (加载 WASM)
|
||||
this.SQL = await initSqlJs();
|
||||
|
||||
// 如果数据库文件存在,加载它
|
||||
if (fs.existsSync(this.dbPath)) {
|
||||
const buffer = fs.readFileSync(this.dbPath);
|
||||
this.db = new this.SQL.Database(buffer);
|
||||
} else {
|
||||
this.db = new this.SQL.Database();
|
||||
}
|
||||
|
||||
// 创建表结构
|
||||
this.createTables();
|
||||
this.saveToFile();
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保数据库已初始化
|
||||
*/
|
||||
private async ensureReady(): Promise<void> {
|
||||
await this.initPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建表结构
|
||||
*/
|
||||
private createTables(): void {
|
||||
if (!this.db) return;
|
||||
|
||||
this.db.run(`
|
||||
CREATE TABLE IF NOT EXISTS shares (
|
||||
id TEXT PRIMARY KEY,
|
||||
session_id TEXT NOT NULL,
|
||||
wallet_name TEXT NOT NULL,
|
||||
party_id TEXT NOT NULL,
|
||||
party_index INTEGER NOT NULL,
|
||||
threshold_t INTEGER NOT NULL,
|
||||
threshold_n INTEGER NOT NULL,
|
||||
public_key_hex TEXT NOT NULL,
|
||||
encrypted_share TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
last_used_at TEXT,
|
||||
participants_json TEXT NOT NULL DEFAULT '[]'
|
||||
)
|
||||
`);
|
||||
|
||||
this.db.run(`
|
||||
CREATE TABLE IF NOT EXISTS derived_addresses (
|
||||
id TEXT PRIMARY KEY,
|
||||
share_id TEXT NOT NULL,
|
||||
chain TEXT NOT NULL,
|
||||
derivation_path TEXT NOT NULL,
|
||||
address TEXT NOT NULL,
|
||||
public_key_hex TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
UNIQUE(share_id, chain, derivation_path)
|
||||
)
|
||||
`);
|
||||
|
||||
this.db.run(`
|
||||
CREATE TABLE IF NOT EXISTS signing_history (
|
||||
id TEXT PRIMARY KEY,
|
||||
share_id TEXT NOT NULL,
|
||||
session_id TEXT NOT NULL,
|
||||
message_hash TEXT NOT NULL,
|
||||
signature TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
error_message TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
completed_at TEXT
|
||||
)
|
||||
`);
|
||||
|
||||
this.db.run(`
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
)
|
||||
`);
|
||||
|
||||
// 创建索引
|
||||
this.db.run(`CREATE INDEX IF NOT EXISTS idx_shares_session ON shares(session_id)`);
|
||||
this.db.run(`CREATE INDEX IF NOT EXISTS idx_addresses_share ON derived_addresses(share_id)`);
|
||||
this.db.run(`CREATE INDEX IF NOT EXISTS idx_addresses_chain ON derived_addresses(chain)`);
|
||||
this.db.run(`CREATE INDEX IF NOT EXISTS idx_history_share ON signing_history(share_id)`);
|
||||
this.db.run(`CREATE INDEX IF NOT EXISTS idx_history_status ON signing_history(status)`);
|
||||
|
||||
// 插入默认设置
|
||||
this.db.run(`INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)`, ['message_router_url', 'mpc-grpc.szaiai.com:443']);
|
||||
this.db.run(`INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)`, ['auto_backup', 'false']);
|
||||
this.db.run(`INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)`, ['backup_path', '']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存数据库到文件
|
||||
*/
|
||||
private saveToFile(): void {
|
||||
if (!this.db) return;
|
||||
const data = this.db.export();
|
||||
const buffer = Buffer.from(data);
|
||||
fs.writeFileSync(this.dbPath, buffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从密码派生密钥
|
||||
*/
|
||||
private deriveKey(password: string, salt: Buffer): Buffer {
|
||||
return crypto.pbkdf2Sync(password, salt, ITERATIONS, KEY_LENGTH, 'sha256');
|
||||
}
|
||||
|
||||
/**
|
||||
* 加密数据
|
||||
*/
|
||||
private encrypt(data: string, password: string): string {
|
||||
const salt = crypto.randomBytes(SALT_LENGTH);
|
||||
const key = this.deriveKey(password, salt);
|
||||
const iv = crypto.randomBytes(IV_LENGTH);
|
||||
|
||||
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
||||
let encrypted = cipher.update(data, 'utf8', 'hex');
|
||||
encrypted += cipher.final('hex');
|
||||
const tag = cipher.getAuthTag();
|
||||
|
||||
// 格式: salt(hex) + iv(hex) + tag(hex) + encrypted(hex)
|
||||
return salt.toString('hex') + iv.toString('hex') + tag.toString('hex') + encrypted;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解密数据
|
||||
*/
|
||||
private decrypt(encryptedData: string, password: string): string {
|
||||
const salt = Buffer.from(encryptedData.slice(0, SALT_LENGTH * 2), 'hex');
|
||||
const iv = Buffer.from(encryptedData.slice(SALT_LENGTH * 2, SALT_LENGTH * 2 + IV_LENGTH * 2), 'hex');
|
||||
const tag = Buffer.from(encryptedData.slice(SALT_LENGTH * 2 + IV_LENGTH * 2, SALT_LENGTH * 2 + IV_LENGTH * 2 + TAG_LENGTH * 2), 'hex');
|
||||
const encrypted = encryptedData.slice(SALT_LENGTH * 2 + IV_LENGTH * 2 + TAG_LENGTH * 2);
|
||||
|
||||
const key = this.deriveKey(password, salt);
|
||||
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
|
||||
decipher.setAuthTag(tag);
|
||||
|
||||
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
||||
decrypted += decipher.final('utf8');
|
||||
|
||||
return decrypted;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将查询结果转换为对象数组
|
||||
*/
|
||||
private queryToObjects<T>(sql: string, params: unknown[] = []): T[] {
|
||||
if (!this.db) return [];
|
||||
|
||||
const results = this.db.exec(sql, params);
|
||||
if (results.length === 0) return [];
|
||||
|
||||
const columns = results[0].columns;
|
||||
return results[0].values.map((row: (number | string | Uint8Array | null)[]) => {
|
||||
const obj: Record<string, unknown> = {};
|
||||
columns.forEach((col: string, i: number) => {
|
||||
obj[col] = row[i];
|
||||
});
|
||||
return obj as T;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询单个对象
|
||||
*/
|
||||
private queryOne<T>(sql: string, params: unknown[] = []): T | undefined {
|
||||
const results = this.queryToObjects<T>(sql, params);
|
||||
return results[0];
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Share 操作
|
||||
// ===========================================================================
|
||||
|
||||
/**
|
||||
* 保存 share
|
||||
*/
|
||||
saveShare(params: {
|
||||
sessionId: string;
|
||||
walletName: string;
|
||||
partyId: string;
|
||||
partyIndex: number;
|
||||
thresholdT: number;
|
||||
thresholdN: number;
|
||||
publicKeyHex: string;
|
||||
rawShare: string;
|
||||
participants: Array<{ partyId: string; name: string }>;
|
||||
}, password: string): ShareRecord {
|
||||
if (!this.db) throw new Error('Database not initialized');
|
||||
|
||||
const id = uuidv4();
|
||||
const encryptedShare = this.encrypt(params.rawShare, password);
|
||||
const now = new Date().toISOString();
|
||||
|
||||
this.db.run(`
|
||||
INSERT INTO shares (
|
||||
id, session_id, wallet_name, party_id, party_index,
|
||||
threshold_t, threshold_n, public_key_hex, encrypted_share,
|
||||
created_at, participants_json
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`, [
|
||||
id,
|
||||
params.sessionId,
|
||||
params.walletName,
|
||||
params.partyId,
|
||||
params.partyIndex,
|
||||
params.thresholdT,
|
||||
params.thresholdN,
|
||||
params.publicKeyHex,
|
||||
encryptedShare,
|
||||
now,
|
||||
JSON.stringify(params.participants)
|
||||
]);
|
||||
|
||||
this.saveToFile();
|
||||
|
||||
return {
|
||||
id,
|
||||
session_id: params.sessionId,
|
||||
wallet_name: params.walletName,
|
||||
party_id: params.partyId,
|
||||
party_index: params.partyIndex,
|
||||
threshold_t: params.thresholdT,
|
||||
threshold_n: params.thresholdN,
|
||||
public_key_hex: params.publicKeyHex,
|
||||
encrypted_share: encryptedShare,
|
||||
created_at: now,
|
||||
last_used_at: null,
|
||||
participants_json: JSON.stringify(params.participants),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有 share (不含加密数据)
|
||||
*/
|
||||
listShares(): Omit<ShareRecord, 'encrypted_share'>[] {
|
||||
return this.queryToObjects<Omit<ShareRecord, 'encrypted_share'>>(`
|
||||
SELECT id, session_id, wallet_name, party_id, party_index,
|
||||
threshold_t, threshold_n, public_key_hex, created_at,
|
||||
last_used_at, participants_json
|
||||
FROM shares
|
||||
ORDER BY created_at DESC
|
||||
`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个 share (解密)
|
||||
*/
|
||||
getShare(id: string, password: string): ShareRecord & { raw_share: string } {
|
||||
const share = this.queryOne<ShareRecord>(`SELECT * FROM shares WHERE id = ?`, [id]);
|
||||
|
||||
if (!share) {
|
||||
throw new Error('Share not found');
|
||||
}
|
||||
|
||||
const rawShare = this.decrypt(share.encrypted_share, password);
|
||||
|
||||
return {
|
||||
...share,
|
||||
raw_share: rawShare,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 share 最后使用时间
|
||||
*/
|
||||
updateShareLastUsed(id: string): void {
|
||||
if (!this.db) return;
|
||||
this.db.run(`UPDATE shares SET last_used_at = ? WHERE id = ?`, [new Date().toISOString(), id]);
|
||||
this.saveToFile();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除 share (级联删除派生地址和签名历史)
|
||||
*/
|
||||
deleteShare(id: string): void {
|
||||
if (!this.db) return;
|
||||
// 手动级联删除
|
||||
this.db.run(`DELETE FROM derived_addresses WHERE share_id = ?`, [id]);
|
||||
this.db.run(`DELETE FROM signing_history WHERE share_id = ?`, [id]);
|
||||
this.db.run(`DELETE FROM shares WHERE id = ?`, [id]);
|
||||
this.saveToFile();
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// 派生地址操作
|
||||
// ===========================================================================
|
||||
|
||||
/**
|
||||
* 保存派生地址
|
||||
*/
|
||||
saveDerivedAddress(params: {
|
||||
shareId: string;
|
||||
chain: string;
|
||||
derivationPath: string;
|
||||
address: string;
|
||||
publicKeyHex: string;
|
||||
}): DerivedAddressRecord {
|
||||
if (!this.db) throw new Error('Database not initialized');
|
||||
|
||||
const id = uuidv4();
|
||||
const now = new Date().toISOString();
|
||||
|
||||
this.db.run(`
|
||||
INSERT OR REPLACE INTO derived_addresses (
|
||||
id, share_id, chain, derivation_path, address, public_key_hex, created_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`, [
|
||||
id,
|
||||
params.shareId,
|
||||
params.chain,
|
||||
params.derivationPath,
|
||||
params.address,
|
||||
params.publicKeyHex,
|
||||
now
|
||||
]);
|
||||
|
||||
this.saveToFile();
|
||||
|
||||
return {
|
||||
id,
|
||||
share_id: params.shareId,
|
||||
chain: params.chain,
|
||||
derivation_path: params.derivationPath,
|
||||
address: params.address,
|
||||
public_key_hex: params.publicKeyHex,
|
||||
created_at: now,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 share 的所有派生地址
|
||||
*/
|
||||
getAddressesByShare(shareId: string): DerivedAddressRecord[] {
|
||||
return this.queryToObjects<DerivedAddressRecord>(`
|
||||
SELECT * FROM derived_addresses
|
||||
WHERE share_id = ?
|
||||
ORDER BY chain, derivation_path
|
||||
`, [shareId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据链获取地址
|
||||
*/
|
||||
getAddressByChain(shareId: string, chain: string): DerivedAddressRecord | undefined {
|
||||
return this.queryOne<DerivedAddressRecord>(`
|
||||
SELECT * FROM derived_addresses
|
||||
WHERE share_id = ? AND chain = ?
|
||||
LIMIT 1
|
||||
`, [shareId, chain]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有指定链的地址
|
||||
*/
|
||||
getAllAddressesByChain(chain: string): DerivedAddressRecord[] {
|
||||
return this.queryToObjects<DerivedAddressRecord>(`
|
||||
SELECT * FROM derived_addresses
|
||||
WHERE chain = ?
|
||||
ORDER BY created_at DESC
|
||||
`, [chain]);
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// 签名历史操作
|
||||
// ===========================================================================
|
||||
|
||||
/**
|
||||
* 创建签名历史记录
|
||||
*/
|
||||
createSigningHistory(params: {
|
||||
shareId: string;
|
||||
sessionId: string;
|
||||
messageHash: string;
|
||||
}): SigningHistoryRecord {
|
||||
if (!this.db) throw new Error('Database not initialized');
|
||||
|
||||
const id = uuidv4();
|
||||
const now = new Date().toISOString();
|
||||
|
||||
this.db.run(`
|
||||
INSERT INTO signing_history (
|
||||
id, share_id, session_id, message_hash, status, created_at
|
||||
) VALUES (?, ?, ?, ?, 'pending', ?)
|
||||
`, [id, params.shareId, params.sessionId, params.messageHash, now]);
|
||||
|
||||
this.saveToFile();
|
||||
|
||||
return {
|
||||
id,
|
||||
share_id: params.shareId,
|
||||
session_id: params.sessionId,
|
||||
message_hash: params.messageHash,
|
||||
signature: null,
|
||||
status: 'pending',
|
||||
error_message: null,
|
||||
created_at: now,
|
||||
completed_at: null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新签名历史状态
|
||||
*/
|
||||
updateSigningHistory(id: string, params: {
|
||||
status: SigningHistoryRecord['status'];
|
||||
signature?: string;
|
||||
errorMessage?: string;
|
||||
}): void {
|
||||
if (!this.db) return;
|
||||
|
||||
const completedAt = params.status === 'completed' || params.status === 'failed'
|
||||
? new Date().toISOString()
|
||||
: null;
|
||||
|
||||
this.db.run(`
|
||||
UPDATE signing_history
|
||||
SET status = ?, signature = ?, error_message = ?, completed_at = ?
|
||||
WHERE id = ?
|
||||
`, [
|
||||
params.status,
|
||||
params.signature || null,
|
||||
params.errorMessage || null,
|
||||
completedAt,
|
||||
id
|
||||
]);
|
||||
|
||||
this.saveToFile();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 share 的签名历史
|
||||
*/
|
||||
getSigningHistoryByShare(shareId: string): SigningHistoryRecord[] {
|
||||
return this.queryToObjects<SigningHistoryRecord>(`
|
||||
SELECT * FROM signing_history
|
||||
WHERE share_id = ?
|
||||
ORDER BY created_at DESC
|
||||
`, [shareId]);
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// 设置操作
|
||||
// ===========================================================================
|
||||
|
||||
/**
|
||||
* 获取设置
|
||||
*/
|
||||
getSetting(key: string): string | undefined {
|
||||
const row = this.queryOne<{ value: string }>(`SELECT value FROM settings WHERE key = ?`, [key]);
|
||||
return row?.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存设置
|
||||
*/
|
||||
setSetting(key: string, value: string): void {
|
||||
if (!this.db) return;
|
||||
this.db.run(`INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)`, [key, value]);
|
||||
this.saveToFile();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有设置
|
||||
*/
|
||||
getAllSettings(): Record<string, string> {
|
||||
const rows = this.queryToObjects<SettingsRecord>(`SELECT key, value FROM settings`);
|
||||
const settings: Record<string, string> = {};
|
||||
for (const row of rows) {
|
||||
settings[row.key] = row.value;
|
||||
}
|
||||
return settings;
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// 导入导出
|
||||
// ===========================================================================
|
||||
|
||||
/**
|
||||
* 导出 share (加密备份)
|
||||
*/
|
||||
exportShare(id: string, password: string): Buffer {
|
||||
const share = this.getShare(id, password);
|
||||
const addresses = this.getAddressesByShare(id);
|
||||
|
||||
const exportData = {
|
||||
version: '1.0.0',
|
||||
exportedAt: new Date().toISOString(),
|
||||
share: {
|
||||
session_id: share.session_id,
|
||||
wallet_name: share.wallet_name,
|
||||
party_id: share.party_id,
|
||||
party_index: share.party_index,
|
||||
threshold_t: share.threshold_t,
|
||||
threshold_n: share.threshold_n,
|
||||
public_key_hex: share.public_key_hex,
|
||||
raw_share: share.raw_share,
|
||||
participants: JSON.parse(share.participants_json),
|
||||
},
|
||||
addresses: addresses.map(addr => ({
|
||||
chain: addr.chain,
|
||||
derivation_path: addr.derivation_path,
|
||||
address: addr.address,
|
||||
public_key_hex: addr.public_key_hex,
|
||||
})),
|
||||
};
|
||||
|
||||
const encrypted = this.encrypt(JSON.stringify(exportData), password);
|
||||
return Buffer.from(encrypted, 'utf8');
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入 share
|
||||
*/
|
||||
importShare(data: Buffer, password: string): ShareRecord {
|
||||
if (!this.db) throw new Error('Database not initialized');
|
||||
|
||||
const encrypted = data.toString('utf8');
|
||||
const decrypted = this.decrypt(encrypted, password);
|
||||
const exportData = JSON.parse(decrypted);
|
||||
|
||||
if (!exportData.version || !exportData.share) {
|
||||
throw new Error('Invalid export file format');
|
||||
}
|
||||
|
||||
// 检查是否已存在
|
||||
const existing = this.queryOne<{ id: string }>(`
|
||||
SELECT id FROM shares WHERE session_id = ? AND party_id = ?
|
||||
`, [exportData.share.session_id, exportData.share.party_id]);
|
||||
|
||||
if (existing) {
|
||||
throw new Error('Share already exists');
|
||||
}
|
||||
|
||||
// 保存 share
|
||||
const share = this.saveShare({
|
||||
sessionId: exportData.share.session_id,
|
||||
walletName: exportData.share.wallet_name,
|
||||
partyId: exportData.share.party_id,
|
||||
partyIndex: exportData.share.party_index,
|
||||
thresholdT: exportData.share.threshold_t,
|
||||
thresholdN: exportData.share.threshold_n,
|
||||
publicKeyHex: exportData.share.public_key_hex,
|
||||
rawShare: exportData.share.raw_share,
|
||||
participants: exportData.share.participants,
|
||||
}, password);
|
||||
|
||||
// 恢复派生地址
|
||||
if (exportData.addresses) {
|
||||
for (const addr of exportData.addresses) {
|
||||
this.saveDerivedAddress({
|
||||
shareId: share.id,
|
||||
chain: addr.chain,
|
||||
derivationPath: addr.derivation_path,
|
||||
address: addr.address,
|
||||
publicKeyHex: addr.public_key_hex,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return share;
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭数据库连接
|
||||
*/
|
||||
close(): void {
|
||||
if (this.db) {
|
||||
this.saveToFile();
|
||||
this.db.close();
|
||||
this.db = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,561 @@
|
|||
/**
|
||||
* 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();
|
||||
|
|
@ -0,0 +1,506 @@
|
|||
/**
|
||||
* 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 无需断开连接
|
||||
}
|
||||
}
|
||||
|
||||
// 导出默认服务实例
|
||||
export const kavaTxService = new KavaTxService();
|
||||
|
|
@ -5,7 +5,9 @@ const eventSubscriptions: Map<string, (event: unknown, ...args: unknown[]) => vo
|
|||
|
||||
// 暴露给渲染进程的 API
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
// ===========================================================================
|
||||
// gRPC 相关 - Keygen
|
||||
// ===========================================================================
|
||||
grpc: {
|
||||
createSession: (params: {
|
||||
walletName: string;
|
||||
|
|
@ -65,8 +67,23 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||
},
|
||||
},
|
||||
|
||||
// 存储相关
|
||||
// ===========================================================================
|
||||
// 存储相关 (SQLite)
|
||||
// ===========================================================================
|
||||
storage: {
|
||||
// Share 操作
|
||||
saveShare: (share: {
|
||||
sessionId: string;
|
||||
walletName: string;
|
||||
partyId: string;
|
||||
partyIndex: number;
|
||||
threshold: { t: number; n: number };
|
||||
publicKey: string;
|
||||
rawShare: string;
|
||||
participants?: Array<{ partyId: string; name: string }>;
|
||||
}, password: string) =>
|
||||
ipcRenderer.invoke('storage:saveShare', { share, password }),
|
||||
|
||||
listShares: () => ipcRenderer.invoke('storage:listShares'),
|
||||
|
||||
getShare: (id: string, password: string) =>
|
||||
|
|
@ -81,13 +98,95 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||
deleteShare: (id: string) =>
|
||||
ipcRenderer.invoke('storage:deleteShare', { id }),
|
||||
|
||||
// 设置操作
|
||||
getSettings: () => ipcRenderer.invoke('storage:getSettings'),
|
||||
|
||||
saveSettings: (settings: unknown) =>
|
||||
ipcRenderer.invoke('storage:saveSettings', { settings }),
|
||||
saveSettings: (settings: {
|
||||
messageRouterUrl?: string;
|
||||
autoBackup?: boolean;
|
||||
backupPath?: string;
|
||||
}) => ipcRenderer.invoke('storage:saveSettings', { settings }),
|
||||
},
|
||||
|
||||
// ===========================================================================
|
||||
// 地址派生相关
|
||||
// ===========================================================================
|
||||
address: {
|
||||
// 派生指定链的地址
|
||||
derive: (shareId: string, chain: string, password: string) =>
|
||||
ipcRenderer.invoke('address:derive', { shareId, chain, password }),
|
||||
|
||||
// 派生所有支持链的地址
|
||||
deriveAll: (shareId: string, password: string) =>
|
||||
ipcRenderer.invoke('address:deriveAll', { shareId, password }),
|
||||
|
||||
// 获取 share 的所有派生地址
|
||||
list: (shareId: string) =>
|
||||
ipcRenderer.invoke('address:list', { shareId }),
|
||||
|
||||
// 获取支持的链列表
|
||||
getSupportedChains: () =>
|
||||
ipcRenderer.invoke('address:getSupportedChains'),
|
||||
},
|
||||
|
||||
// ===========================================================================
|
||||
// 签名历史相关
|
||||
// ===========================================================================
|
||||
signing: {
|
||||
// 获取签名历史
|
||||
getHistory: (shareId: string) =>
|
||||
ipcRenderer.invoke('signing:getHistory', { shareId }),
|
||||
},
|
||||
|
||||
// ===========================================================================
|
||||
// Kava 区块链相关
|
||||
// ===========================================================================
|
||||
kava: {
|
||||
// 查询 KAVA 余额
|
||||
getBalance: (address: string) =>
|
||||
ipcRenderer.invoke('kava:getBalance', { address }),
|
||||
|
||||
// 查询所有余额
|
||||
getAllBalances: (address: string) =>
|
||||
ipcRenderer.invoke('kava:getAllBalances', { address }),
|
||||
|
||||
// 查询账户信息
|
||||
getAccountInfo: (address: string) =>
|
||||
ipcRenderer.invoke('kava:getAccountInfo', { address }),
|
||||
|
||||
// 构建转账交易 (待 TSS 签名)
|
||||
buildSendTx: (params: {
|
||||
fromAddress: string;
|
||||
toAddress: string;
|
||||
amount: string;
|
||||
publicKeyHex: string;
|
||||
memo?: string;
|
||||
}) => ipcRenderer.invoke('kava:buildSendTx', params),
|
||||
|
||||
// 完成签名并广播交易
|
||||
completeTxAndBroadcast: (params: {
|
||||
unsignedTx: unknown;
|
||||
signatureHex: string;
|
||||
}) => ipcRenderer.invoke('kava:completeTxAndBroadcast', params),
|
||||
|
||||
// 查询交易状态
|
||||
getTxStatus: (txHash: string) =>
|
||||
ipcRenderer.invoke('kava:getTxStatus', { txHash }),
|
||||
|
||||
// 获取配置
|
||||
getConfig: () => ipcRenderer.invoke('kava:getConfig'),
|
||||
|
||||
// 更新配置
|
||||
updateConfig: (config: {
|
||||
lcdEndpoint?: string;
|
||||
rpcEndpoint?: string;
|
||||
chainId?: string;
|
||||
}) => ipcRenderer.invoke('kava:updateConfig', { config }),
|
||||
},
|
||||
|
||||
// ===========================================================================
|
||||
// 对话框相关
|
||||
// ===========================================================================
|
||||
dialog: {
|
||||
selectDirectory: () => ipcRenderer.invoke('dialog:selectDirectory'),
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
// sql.js 类型声明
|
||||
|
||||
declare module 'sql.js' {
|
||||
export interface SqlValue {
|
||||
[key: string]: number | string | Uint8Array | null;
|
||||
}
|
||||
|
||||
export interface QueryExecResult {
|
||||
columns: string[];
|
||||
values: (number | string | Uint8Array | null)[][];
|
||||
}
|
||||
|
||||
export interface ParamsObject {
|
||||
[key: string]: number | string | Uint8Array | null;
|
||||
}
|
||||
|
||||
export interface ParamsCallback {
|
||||
(params: ParamsObject): void;
|
||||
}
|
||||
|
||||
export interface Database {
|
||||
run(sql: string, params?: unknown[]): Database;
|
||||
exec(sql: string, params?: unknown[]): QueryExecResult[];
|
||||
each(sql: string, params: unknown[], callback: (row: SqlValue) => void, done?: () => void): void;
|
||||
prepare(sql: string): Statement;
|
||||
export(): Uint8Array;
|
||||
close(): void;
|
||||
getRowsModified(): number;
|
||||
}
|
||||
|
||||
export interface Statement {
|
||||
bind(params?: unknown[]): boolean;
|
||||
step(): boolean;
|
||||
getAsObject(params?: ParamsObject): SqlValue;
|
||||
get(params?: unknown[]): (number | string | Uint8Array | null)[];
|
||||
getColumnNames(): string[];
|
||||
run(params?: unknown[]): void;
|
||||
reset(): void;
|
||||
free(): void;
|
||||
}
|
||||
|
||||
export interface SqlJsStatic {
|
||||
Database: new (data?: ArrayLike<number> | Buffer | null) => Database;
|
||||
}
|
||||
|
||||
export default function initSqlJs(config?: {
|
||||
locateFile?: (path: string) => string;
|
||||
}): Promise<SqlJsStatic>;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -17,16 +17,24 @@
|
|||
"proto:generate": "grpc_tools_node_protoc --js_out=import_style=commonjs,binary:./proto --grpc_out=grpc_js:./proto --proto_path=../../api/proto ../../api/proto/*.proto"
|
||||
},
|
||||
"dependencies": {
|
||||
"@cosmjs/amino": "^0.37.0",
|
||||
"@cosmjs/encoding": "^0.37.0",
|
||||
"@cosmjs/proto-signing": "^0.37.0",
|
||||
"@cosmjs/stargate": "^0.37.0",
|
||||
"@grpc/grpc-js": "^1.9.0",
|
||||
"@grpc/proto-loader": "^0.7.10",
|
||||
"@noble/curves": "^2.0.1",
|
||||
"@noble/hashes": "^2.0.1",
|
||||
"bech32": "^2.0.0",
|
||||
"electron-store": "^8.1.0",
|
||||
"express": "^4.18.2",
|
||||
"qrcode.react": "^3.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.20.0",
|
||||
"zustand": "^4.4.7",
|
||||
"qrcode.react": "^3.1.0",
|
||||
"uuid": "^9.0.1"
|
||||
"sql.js": "^1.13.0",
|
||||
"uuid": "^9.0.1",
|
||||
"zustand": "^4.4.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.21",
|
||||
|
|
@ -38,6 +46,7 @@
|
|||
"concurrently": "^8.2.2",
|
||||
"electron": "^28.0.0",
|
||||
"electron-builder": "^24.9.1",
|
||||
"electron-rebuild": "^3.2.9",
|
||||
"eslint": "^8.54.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"grpc-tools": "^1.12.4",
|
||||
|
|
@ -61,7 +70,9 @@
|
|||
{
|
||||
"from": "proto",
|
||||
"to": "proto",
|
||||
"filter": ["**/*"]
|
||||
"filter": [
|
||||
"**/*"
|
||||
]
|
||||
}
|
||||
],
|
||||
"win": {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
// Electron API 类型定义
|
||||
|
||||
// ===========================================================================
|
||||
// Share 相关
|
||||
// ===========================================================================
|
||||
|
||||
interface ShareEntry {
|
||||
id: string;
|
||||
sessionId: string;
|
||||
|
|
@ -8,13 +12,58 @@ interface ShareEntry {
|
|||
partyIndex: number;
|
||||
threshold: { t: number; n: number };
|
||||
publicKey: string;
|
||||
encryptedShare: string;
|
||||
createdAt: string;
|
||||
metadata: {
|
||||
participants: Array<{ partyId: string; name: string }>;
|
||||
};
|
||||
lastUsedAt?: string;
|
||||
participants: Array<{ partyId: string; name: string }>;
|
||||
}
|
||||
|
||||
interface ShareWithRawData extends ShareEntry {
|
||||
rawShare: string;
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// 派生地址相关
|
||||
// ===========================================================================
|
||||
|
||||
interface DerivedAddress {
|
||||
id: string;
|
||||
shareId: string;
|
||||
chain: string;
|
||||
chainName: string;
|
||||
derivationPath: string;
|
||||
address: string;
|
||||
publicKeyHex: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
interface ChainConfig {
|
||||
name: string;
|
||||
prefix: string;
|
||||
coinType: number;
|
||||
curve: 'secp256k1' | 'ed25519';
|
||||
derivationPath: string;
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// 签名历史相关
|
||||
// ===========================================================================
|
||||
|
||||
interface SigningHistoryEntry {
|
||||
id: string;
|
||||
shareId: string;
|
||||
sessionId: string;
|
||||
messageHash: string;
|
||||
signature: string | null;
|
||||
status: 'pending' | 'in_progress' | 'completed' | 'failed';
|
||||
errorMessage: string | null;
|
||||
createdAt: string;
|
||||
completedAt: string | null;
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// 会话相关
|
||||
// ===========================================================================
|
||||
|
||||
interface SessionInfo {
|
||||
sessionId: string;
|
||||
walletName: string;
|
||||
|
|
@ -43,12 +92,20 @@ interface SessionState {
|
|||
error?: string;
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// 设置相关
|
||||
// ===========================================================================
|
||||
|
||||
interface Settings {
|
||||
messageRouterUrl: string;
|
||||
autoBackup: boolean;
|
||||
backupPath: string;
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// API 参数和结果类型
|
||||
// ===========================================================================
|
||||
|
||||
interface CreateSessionParams {
|
||||
walletName: string;
|
||||
thresholdT: number;
|
||||
|
|
@ -135,6 +192,12 @@ interface ListSharesResult {
|
|||
error?: string;
|
||||
}
|
||||
|
||||
interface SaveShareResult {
|
||||
success: boolean;
|
||||
data?: ShareEntry;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
interface ExportShareResult {
|
||||
success: boolean;
|
||||
data?: ArrayBuffer;
|
||||
|
|
@ -142,7 +205,134 @@ interface ExportShareResult {
|
|||
error?: string;
|
||||
}
|
||||
|
||||
interface DeriveAddressResult {
|
||||
success: boolean;
|
||||
data?: DerivedAddress;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
interface DeriveAllAddressesResult {
|
||||
success: boolean;
|
||||
data?: DerivedAddress[];
|
||||
error?: string;
|
||||
}
|
||||
|
||||
interface ListAddressesResult {
|
||||
success: boolean;
|
||||
data?: DerivedAddress[];
|
||||
error?: string;
|
||||
}
|
||||
|
||||
interface GetSigningHistoryResult {
|
||||
success: boolean;
|
||||
data?: SigningHistoryEntry[];
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Kava 相关类型
|
||||
// ===========================================================================
|
||||
|
||||
interface KavaBalance {
|
||||
denom: string;
|
||||
amount: string;
|
||||
formatted: string;
|
||||
}
|
||||
|
||||
interface KavaAccountInfo {
|
||||
address: string;
|
||||
accountNumber: number;
|
||||
sequence: number;
|
||||
balances: KavaBalance[];
|
||||
}
|
||||
|
||||
interface KavaUnsignedTx {
|
||||
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;
|
||||
}
|
||||
|
||||
interface KavaTxBroadcastResult {
|
||||
success: boolean;
|
||||
txHash?: string;
|
||||
code?: number;
|
||||
rawLog?: string;
|
||||
gasUsed?: string;
|
||||
gasWanted?: string;
|
||||
height?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
interface KavaTxStatus {
|
||||
found: boolean;
|
||||
status: 'pending' | 'success' | 'failed';
|
||||
height?: number;
|
||||
code?: number;
|
||||
rawLog?: string;
|
||||
}
|
||||
|
||||
interface KavaConfig {
|
||||
lcdEndpoint: string;
|
||||
rpcEndpoint: string;
|
||||
chainId: string;
|
||||
prefix: string;
|
||||
denom: string;
|
||||
gasPrice: number;
|
||||
}
|
||||
|
||||
interface GetKavaBalanceResult {
|
||||
success: boolean;
|
||||
data?: KavaBalance;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
interface GetAllKavaBalancesResult {
|
||||
success: boolean;
|
||||
data?: KavaBalance[];
|
||||
error?: string;
|
||||
}
|
||||
|
||||
interface GetKavaAccountInfoResult {
|
||||
success: boolean;
|
||||
data?: KavaAccountInfo;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
interface BuildKavaSendTxResult {
|
||||
success: boolean;
|
||||
data?: KavaUnsignedTx;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
interface CompleteTxAndBroadcastResult {
|
||||
success: boolean;
|
||||
data?: KavaTxBroadcastResult;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
interface GetKavaTxStatusResult {
|
||||
success: boolean;
|
||||
data?: KavaTxStatus;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Electron API 接口
|
||||
// ===========================================================================
|
||||
|
||||
interface ElectronAPI {
|
||||
// gRPC 相关
|
||||
grpc: {
|
||||
createSession: (params: CreateSessionParams) => Promise<CreateSessionResult>;
|
||||
joinSession: (sessionId: string, participantName: string) => Promise<JoinSessionResult>;
|
||||
|
|
@ -155,15 +345,63 @@ interface ElectronAPI {
|
|||
joinSigningSession: (params: JoinSigningSessionParams) => Promise<JoinSigningSessionResult>;
|
||||
subscribeSigningProgress: (sessionId: string, callback: (event: SigningProgressEvent) => void) => () => void;
|
||||
};
|
||||
|
||||
// 存储相关 (SQLite)
|
||||
storage: {
|
||||
saveShare: (share: {
|
||||
sessionId: string;
|
||||
walletName: string;
|
||||
partyId: string;
|
||||
partyIndex: number;
|
||||
threshold: { t: number; n: number };
|
||||
publicKey: string;
|
||||
rawShare: string;
|
||||
participants?: Array<{ partyId: string; name: string }>;
|
||||
}, password: string) => Promise<SaveShareResult>;
|
||||
listShares: () => Promise<ListSharesResult>;
|
||||
getShare: (id: string, password: string) => Promise<ShareEntry | null>;
|
||||
getShare: (id: string, password: string) => Promise<ShareWithRawData | null>;
|
||||
exportShare: (id: string, password: string) => Promise<ExportShareResult>;
|
||||
importShare: (filePath: string, password: string) => Promise<{ success: boolean; share?: ShareEntry; error?: string }>;
|
||||
deleteShare: (id: string) => Promise<{ success: boolean; error?: string }>;
|
||||
getSettings: () => Promise<Settings | null>;
|
||||
saveSettings: (settings: Settings) => Promise<void>;
|
||||
saveSettings: (settings: Partial<Settings>) => Promise<{ success: boolean; error?: string }>;
|
||||
};
|
||||
|
||||
// 地址派生相关
|
||||
address: {
|
||||
derive: (shareId: string, chain: string, password: string) => Promise<DeriveAddressResult>;
|
||||
deriveAll: (shareId: string, password: string) => Promise<DeriveAllAddressesResult>;
|
||||
list: (shareId: string) => Promise<ListAddressesResult>;
|
||||
getSupportedChains: () => Promise<ChainConfig[]>;
|
||||
};
|
||||
|
||||
// 签名历史相关
|
||||
signing: {
|
||||
getHistory: (shareId: string) => Promise<GetSigningHistoryResult>;
|
||||
};
|
||||
|
||||
// Kava 区块链相关
|
||||
kava: {
|
||||
getBalance: (address: string) => Promise<GetKavaBalanceResult>;
|
||||
getAllBalances: (address: string) => Promise<GetAllKavaBalancesResult>;
|
||||
getAccountInfo: (address: string) => Promise<GetKavaAccountInfoResult>;
|
||||
buildSendTx: (params: {
|
||||
fromAddress: string;
|
||||
toAddress: string;
|
||||
amount: string;
|
||||
publicKeyHex: string;
|
||||
memo?: string;
|
||||
}) => Promise<BuildKavaSendTxResult>;
|
||||
completeTxAndBroadcast: (params: {
|
||||
unsignedTx: KavaUnsignedTx;
|
||||
signatureHex: string;
|
||||
}) => Promise<CompleteTxAndBroadcastResult>;
|
||||
getTxStatus: (txHash: string) => Promise<GetKavaTxStatusResult>;
|
||||
getConfig: () => Promise<KavaConfig>;
|
||||
updateConfig: (config: Partial<KavaConfig>) => Promise<{ success: boolean; error?: string }>;
|
||||
};
|
||||
|
||||
// 对话框相关
|
||||
dialog: {
|
||||
selectDirectory: () => Promise<string | null>;
|
||||
selectFile: (filters?: { name: string; extensions: string[] }[]) => Promise<string | null>;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,210 @@
|
|||
/**
|
||||
* Kava EVM 地址派生工具
|
||||
* 从 ECDSA 公钥派生 EVM 兼容地址
|
||||
*/
|
||||
|
||||
/**
|
||||
* 将十六进制字符串转换为 Uint8Array
|
||||
*/
|
||||
function hexToBytes(hex: string): Uint8Array {
|
||||
const cleanHex = hex.startsWith('0x') ? hex.slice(2) : hex;
|
||||
const bytes = new Uint8Array(cleanHex.length / 2);
|
||||
for (let i = 0; i < cleanHex.length; i += 2) {
|
||||
bytes[i / 2] = parseInt(cleanHex.substring(i, i + 2), 16);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 Uint8Array 转换为十六进制字符串
|
||||
*/
|
||||
function bytesToHex(bytes: Uint8Array): string {
|
||||
return Array.from(bytes)
|
||||
.map((b) => b.toString(16).padStart(2, '0'))
|
||||
.join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Keccak-256 哈希实现 (简化版)
|
||||
* 用于从公钥派生 EVM 地址
|
||||
*/
|
||||
async function keccak256(data: Uint8Array): Promise<Uint8Array> {
|
||||
// 使用 SubtleCrypto 的 SHA-256 作为备选
|
||||
// 注意: 真实场景需要使用 keccak-256,这里使用简化实现
|
||||
// 在生产环境中应该使用 js-sha3 或 ethers.js
|
||||
|
||||
// 简单的 Keccak-256 实现常量
|
||||
const RC = [
|
||||
0x0000000000000001n, 0x0000000000008082n, 0x800000000000808an,
|
||||
0x8000000080008000n, 0x000000000000808bn, 0x0000000080000001n,
|
||||
0x8000000080008081n, 0x8000000000008009n, 0x000000000000008an,
|
||||
0x0000000000000088n, 0x0000000080008009n, 0x000000008000000an,
|
||||
0x000000008000808bn, 0x800000000000008bn, 0x8000000000008089n,
|
||||
0x8000000000008003n, 0x8000000000008002n, 0x8000000000000080n,
|
||||
0x000000000000800an, 0x800000008000000an, 0x8000000080008081n,
|
||||
0x8000000000008080n, 0x0000000080000001n, 0x8000000080008008n,
|
||||
];
|
||||
|
||||
const ROTC = [
|
||||
[0, 36, 3, 41, 18],
|
||||
[1, 44, 10, 45, 2],
|
||||
[62, 6, 43, 15, 61],
|
||||
[28, 55, 25, 21, 56],
|
||||
[27, 20, 39, 8, 14],
|
||||
];
|
||||
|
||||
function rotl64(x: bigint, n: number): bigint {
|
||||
return ((x << BigInt(n)) | (x >> BigInt(64 - n))) & 0xffffffffffffffffn;
|
||||
}
|
||||
|
||||
function keccakF(state: bigint[][]): void {
|
||||
for (let round = 0; round < 24; round++) {
|
||||
// Theta
|
||||
const C: bigint[] = [];
|
||||
for (let x = 0; x < 5; x++) {
|
||||
C[x] = state[x][0] ^ state[x][1] ^ state[x][2] ^ state[x][3] ^ state[x][4];
|
||||
}
|
||||
const D: bigint[] = [];
|
||||
for (let x = 0; x < 5; x++) {
|
||||
D[x] = C[(x + 4) % 5] ^ rotl64(C[(x + 1) % 5], 1);
|
||||
}
|
||||
for (let x = 0; x < 5; x++) {
|
||||
for (let y = 0; y < 5; y++) {
|
||||
state[x][y] ^= D[x];
|
||||
}
|
||||
}
|
||||
|
||||
// Rho and Pi
|
||||
const B: bigint[][] = Array(5).fill(null).map(() => Array(5).fill(0n));
|
||||
for (let x = 0; x < 5; x++) {
|
||||
for (let y = 0; y < 5; y++) {
|
||||
B[y][(2 * x + 3 * y) % 5] = rotl64(state[x][y], ROTC[x][y]);
|
||||
}
|
||||
}
|
||||
|
||||
// Chi
|
||||
for (let x = 0; x < 5; x++) {
|
||||
for (let y = 0; y < 5; y++) {
|
||||
state[x][y] = B[x][y] ^ (~B[(x + 1) % 5][y] & B[(x + 2) % 5][y]);
|
||||
}
|
||||
}
|
||||
|
||||
// Iota
|
||||
state[0][0] ^= RC[round];
|
||||
}
|
||||
}
|
||||
|
||||
// Keccak-256: rate = 1088 bits = 136 bytes, capacity = 512 bits
|
||||
const rate = 136;
|
||||
const outputLen = 32;
|
||||
|
||||
// Initialize state
|
||||
const state: bigint[][] = Array(5).fill(null).map(() => Array(5).fill(0n));
|
||||
|
||||
// Pad message
|
||||
const padded = new Uint8Array(Math.ceil((data.length + 1) / rate) * rate);
|
||||
padded.set(data);
|
||||
padded[data.length] = 0x01;
|
||||
padded[padded.length - 1] |= 0x80;
|
||||
|
||||
// Absorb
|
||||
for (let i = 0; i < padded.length; i += rate) {
|
||||
for (let j = 0; j < rate && i + j < padded.length; j += 8) {
|
||||
const x = Math.floor(j / 8) % 5;
|
||||
const y = Math.floor(Math.floor(j / 8) / 5);
|
||||
let lane = 0n;
|
||||
for (let k = 0; k < 8 && i + j + k < padded.length; k++) {
|
||||
lane |= BigInt(padded[i + j + k]) << BigInt(k * 8);
|
||||
}
|
||||
state[x][y] ^= lane;
|
||||
}
|
||||
keccakF(state);
|
||||
}
|
||||
|
||||
// Squeeze
|
||||
const output = new Uint8Array(outputLen);
|
||||
for (let i = 0; i < outputLen; i += 8) {
|
||||
const x = Math.floor(i / 8) % 5;
|
||||
const y = Math.floor(Math.floor(i / 8) / 5);
|
||||
const lane = state[x][y];
|
||||
for (let k = 0; k < 8 && i + k < outputLen; k++) {
|
||||
output[i + k] = Number((lane >> BigInt(k * 8)) & 0xffn);
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 ECDSA 压缩/未压缩公钥派生 EVM 地址
|
||||
*
|
||||
* @param publicKey - 公钥的十六进制字符串 (压缩或未压缩格式)
|
||||
* @returns EVM 地址 (带 0x 前缀)
|
||||
*/
|
||||
export async function deriveEvmAddress(publicKey: string): Promise<string> {
|
||||
const pubKeyBytes = hexToBytes(publicKey);
|
||||
|
||||
// EVM 地址派生使用未压缩公钥的 x,y 坐标 (去掉 04 前缀)
|
||||
// 如果是压缩公钥 (33 bytes, 02/03 前缀), 需要先解压
|
||||
let uncompressedPubKey: Uint8Array;
|
||||
|
||||
if (pubKeyBytes.length === 65 && pubKeyBytes[0] === 0x04) {
|
||||
// 已经是未压缩格式,去掉 04 前缀
|
||||
uncompressedPubKey = pubKeyBytes.slice(1);
|
||||
} else if (pubKeyBytes.length === 64) {
|
||||
// 无前缀的未压缩格式
|
||||
uncompressedPubKey = pubKeyBytes;
|
||||
} else if (pubKeyBytes.length === 33 && (pubKeyBytes[0] === 0x02 || pubKeyBytes[0] === 0x03)) {
|
||||
// 压缩格式,需要解压 - 这里简化处理,实际需要椭圆曲线计算
|
||||
// 在真实场景中应该使用 elliptic 或 secp256k1 库
|
||||
throw new Error('压缩公钥暂不支持,请提供未压缩公钥');
|
||||
} else {
|
||||
throw new Error(`无效的公钥格式: length=${pubKeyBytes.length}`);
|
||||
}
|
||||
|
||||
// 对 64 字节的公钥数据进行 Keccak-256 哈希
|
||||
const hash = await keccak256(uncompressedPubKey);
|
||||
|
||||
// 取最后 20 字节作为地址
|
||||
const addressBytes = hash.slice(-20);
|
||||
|
||||
return '0x' + bytesToHex(addressBytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 EVM 地址格式
|
||||
*
|
||||
* @param address - 要验证的地址
|
||||
* @returns 是否是有效的 EVM 地址
|
||||
*/
|
||||
export function isValidEvmAddress(address: string): boolean {
|
||||
if (!address) return false;
|
||||
return /^0x[a-fA-F0-9]{40}$/.test(address);
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化地址显示 (缩短)
|
||||
*
|
||||
* @param address - 完整地址
|
||||
* @param prefixLen - 前缀长度 (默认 6)
|
||||
* @param suffixLen - 后缀长度 (默认 4)
|
||||
* @returns 缩短的地址
|
||||
*/
|
||||
export function formatAddress(address: string, prefixLen = 6, suffixLen = 4): string {
|
||||
if (!address || address.length < prefixLen + suffixLen + 2) return address;
|
||||
return `${address.slice(0, prefixLen + 2)}...${address.slice(-suffixLen)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 Kava 区块浏览器 URL
|
||||
*
|
||||
* @param address - EVM 地址
|
||||
* @param isTestnet - 是否是测试网
|
||||
* @returns 区块浏览器 URL
|
||||
*/
|
||||
export function getKavaExplorerUrl(address: string, isTestnet = true): string {
|
||||
const baseUrl = isTestnet
|
||||
? 'https://testnet.kavascan.com'
|
||||
: 'https://kavascan.com';
|
||||
return `${baseUrl}/address/${address}`;
|
||||
}
|
||||
Loading…
Reference in New Issue