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

779 lines
22 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

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

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';
// =============================================================================
// sql.js WASM 文件路径
// =============================================================================
function getSqlJsWasmPath(): string {
// 在开发环境中WASM 文件在 node_modules 中
// 在生产环境中WASM 文件被复制到 extraResources 目录
const isDev = !app.isPackaged;
if (isDev) {
// 开发环境: 使用 node_modules 中的文件
return path.join(__dirname, '../../node_modules/sql.js/dist/sql-wasm.wasm');
} else {
// 生产环境: 使用 extraResources 中的文件
return path.join(process.resourcesPath, 'sql-wasm.wasm');
}
}
// =============================================================================
// 数据库路径
// =============================================================================
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> {
// 获取 WASM 文件路径
const wasmPath = getSqlJsWasmPath();
console.log('[Database] App packaged:', app.isPackaged);
console.log('[Database] Resources path:', process.resourcesPath);
console.log('[Database] WASM path:', wasmPath);
console.log('[Database] WASM exists:', fs.existsSync(wasmPath));
// 初始化 sql.js (加载 WASM)
// 使用 wasmBinary 直接加载 WASM 文件,这在打包环境中更可靠
let config: { wasmBinary?: ArrayBuffer; locateFile?: (file: string) => string } = {};
if (fs.existsSync(wasmPath)) {
// 直接读取 WASM 文件作为 ArrayBuffer - 这种方式更可靠
const wasmBuffer = fs.readFileSync(wasmPath);
config.wasmBinary = wasmBuffer.buffer.slice(
wasmBuffer.byteOffset,
wasmBuffer.byteOffset + wasmBuffer.byteLength
);
console.log('[Database] WASM loaded as binary, size:', wasmBuffer.length);
} else {
console.warn('[Database] WASM file not found, sql.js will try to load from default location');
// 作为备用方案,使用 locateFile
config.locateFile = (file: string) => {
console.log('[Database] locateFile called for:', file);
return file;
};
}
this.SQL = await initSqlJs(config);
// 如果数据库文件存在,加载它
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;
}
/**
* 等待数据库初始化完成(公开方法)
*/
async waitForReady(): 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 TABLE IF NOT EXISTS processed_messages (
message_id TEXT PRIMARY KEY,
session_id TEXT NOT NULL,
processed_at 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(`CREATE INDEX IF NOT EXISTS idx_processed_messages_session ON processed_messages(session_id)`);
// 插入默认设置
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;
}
// ===========================================================================
// 消息去重操作
// ===========================================================================
/**
* 检查消息是否已处理
*/
isMessageProcessed(messageId: string): boolean {
const row = this.queryOne<{ message_id: string }>(
`SELECT message_id FROM processed_messages WHERE message_id = ?`,
[messageId]
);
return !!row;
}
/**
* 标记消息为已处理
*/
markMessageProcessed(messageId: string, sessionId: string): void {
if (!this.db) return;
const now = new Date().toISOString();
this.db.run(
`INSERT OR IGNORE INTO processed_messages (message_id, session_id, processed_at) VALUES (?, ?, ?)`,
[messageId, sessionId, now]
);
this.saveToFile();
}
/**
* 清理指定会话的已处理消息记录
* 当会话完成后调用,释放空间
*/
clearProcessedMessages(sessionId: string): void {
if (!this.db) return;
this.db.run(`DELETE FROM processed_messages WHERE session_id = ?`, [sessionId]);
this.saveToFile();
}
/**
* 清理过期的已处理消息记录超过24小时
* 可在应用启动时调用
*/
cleanupOldProcessedMessages(): void {
if (!this.db) return;
const cutoff = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
this.db.run(`DELETE FROM processed_messages WHERE processed_at < ?`, [cutoff]);
this.saveToFile();
}
// ===========================================================================
// 导入导出
// ===========================================================================
/**
* 导出 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;
}
}
}