779 lines
22 KiB
TypeScript
779 lines
22 KiB
TypeScript
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;
|
||
}
|
||
}
|
||
}
|