diff --git a/electron/src/rt/sqlite-error.ts b/electron/src/rt/sqlite-error.ts new file mode 100644 index 00000000..2c08dbf9 --- /dev/null +++ b/electron/src/rt/sqlite-error.ts @@ -0,0 +1,14 @@ +/** + * Custom error class for SQLite operations + * Provides additional context and error tracking for SQLite operations + */ +export class SQLiteError extends Error { + constructor( + message: string, + public operation: string, + public cause?: unknown + ) { + super(message); + this.name = 'SQLiteError'; + } +} \ No newline at end of file diff --git a/electron/src/rt/sqlite-init.ts b/electron/src/rt/sqlite-init.ts index eddfe89e..69c4c4b2 100644 --- a/electron/src/rt/sqlite-init.ts +++ b/electron/src/rt/sqlite-init.ts @@ -266,12 +266,16 @@ const verifyTransactionState = async (database: string): Promise => { // Check if we're in a transaction const isActive = await pluginState.instance.isTransactionActive({ database }); - transactionState.isActive = isActive; - transactionState.lastVerified = new Date(); - transactionState.database = database; + // Only update state if it's different + if (isActive !== transactionState.isActive || transactionState.database !== database) { + transactionState.isActive = isActive; + transactionState.lastVerified = new Date(); + transactionState.database = isActive ? database : null; + } return true; } catch (error) { + // Reset state on error transactionState.isActive = false; transactionState.lastVerified = new Date(); transactionState.database = null; @@ -896,4 +900,71 @@ export function setupSQLiteHandlers(): void { }); logger.info('SQLite IPC handlers setup complete'); -} \ No newline at end of file +} + +// Update transaction management to be more careful +const beginTransaction = async (database: string): Promise => { + if (!pluginState.instance || !pluginState.isAvailable) { + throw new SQLiteError('Database not available', 'beginTransaction'); + } + + // Verify current state first + await verifyTransactionState(database); + if (transactionState.isActive) { + throw new SQLiteError('Transaction already active', 'beginTransaction'); + } + + try { + await pluginState.instance.beginTransaction({ database }); + transactionState.isActive = true; + transactionState.lastVerified = new Date(); + transactionState.database = database; + } catch (error) { + transactionState.isActive = false; + transactionState.lastVerified = new Date(); + transactionState.database = null; + throw new SQLiteError('Failed to begin transaction', 'beginTransaction', error); + } +}; + +const commitTransaction = async (database: string): Promise => { + if (!pluginState.instance || !pluginState.isAvailable) { + throw new SQLiteError('Database not available', 'commitTransaction'); + } + + // Verify current state first + await verifyTransactionState(database); + if (!transactionState.isActive || transactionState.database !== database) { + throw new SQLiteError('No active transaction', 'commitTransaction'); + } + + try { + await pluginState.instance.commitTransaction({ database }); + transactionState.isActive = false; + transactionState.lastVerified = new Date(); + transactionState.database = null; + } catch (error) { + // Don't reset state on error - let rollback handle it + throw new SQLiteError('Failed to commit transaction', 'commitTransaction', error); + } +}; + +const rollbackTransaction = async (database: string): Promise => { + if (!pluginState.instance || !pluginState.isAvailable) { + return; // Just return if plugin not available + } + + // Only attempt rollback if we think we're in a transaction + if (transactionState.isActive && transactionState.database === database) { + try { + await pluginState.instance.rollbackTransaction({ database }); + } catch (error) { + logger.error('Rollback failed:', error); + } + } + + // Always reset state after rollback attempt + transactionState.isActive = false; + transactionState.lastVerified = new Date(); + transactionState.database = null; +}; \ No newline at end of file diff --git a/src/libs/util.ts b/src/libs/util.ts index b969c6e0..fc4a6b77 100644 --- a/src/libs/util.ts +++ b/src/libs/util.ts @@ -646,17 +646,29 @@ export async function saveNewIdentity( const secrets = await platformService.dbQuery( `SELECT secretBase64 FROM secret`, ); + + // If no secret exists, create one + let secretBase64: string; if (!secrets?.values?.length || !secrets.values[0]?.length) { - throw new Error( - "No initial encryption supported. We recommend you clear your data and start over.", + // Generate a new secret + const randomBytes = crypto.getRandomValues(new Uint8Array(32)); + secretBase64 = arrayBufferToBase64(randomBytes); + + // Store the new secret + await platformService.dbExec( + `INSERT INTO secret (id, secretBase64) VALUES (1, ?)`, + [secretBase64] ); + } else { + secretBase64 = secrets.values[0][0] as string; } - const secretBase64 = secrets.values[0][0] as string; + const secret = base64ToArrayBuffer(secretBase64); const encryptedIdentity = await simpleEncrypt(identity, secret); const encryptedMnemonic = await simpleEncrypt(mnemonic, secret); const encryptedIdentityBase64 = arrayBufferToBase64(encryptedIdentity); const encryptedMnemonicBase64 = arrayBufferToBase64(encryptedMnemonic); + await platformService.dbExec( `INSERT INTO accounts (dateCreated, derivationPath, did, identityEncrBase64, mnemonicEncrBase64, publicKeyHex) VALUES (?, ?, ?, ?, ?, ?)`, diff --git a/src/services/platforms/ElectronPlatformService.ts b/src/services/platforms/ElectronPlatformService.ts index eef3a297..b519c6a1 100644 --- a/src/services/platforms/ElectronPlatformService.ts +++ b/src/services/platforms/ElectronPlatformService.ts @@ -288,21 +288,38 @@ export class ElectronPlatformService implements PlatformService { if (this.isInitialized) return; if (this.sqliteReadyPromise) await this.sqliteReadyPromise; - if (!window.CapacitorSQLite) { - throw new Error("CapacitorSQLite not available"); + if (!window.electron?.sqlite) { + throw new Error("SQLite IPC bridge not available"); } - this.sqlite = window.CapacitorSQLite as unknown as SQLiteOperations; + // Use IPC bridge with specific methods + this.sqlite = { + createConnection: async (options) => { + await window.electron.ipcRenderer.invoke('sqlite-create-connection', options); + }, + query: async (options) => { + return await window.electron.ipcRenderer.invoke('sqlite-query', options); + }, + run: async (options) => { + return await window.electron.ipcRenderer.invoke('sqlite-run', options); + }, + execute: async (options) => { + await window.electron.ipcRenderer.invoke('sqlite-execute', { + database: options.database, + statements: [{ statement: options.statements }] + }); + } + } as SQLiteOperations; // Create the connection (idempotent) - await this.sqlite.createConnection({ + await this.sqlite!.createConnection({ database: this.dbName, encrypted: false, mode: "no-encryption", }); // Optionally, test the connection - await this.sqlite.query({ + await this.sqlite!.query({ database: this.dbName, statement: "SELECT 1", }); diff --git a/src/types/global.d.ts b/src/types/global.d.ts index 39cc76ef..93f09572 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -16,7 +16,7 @@ declare global { electron: { sqlite: { isAvailable: () => Promise; - execute: (method: string, ...args: unknown[]) => Promise; + execute: (options: { method: string; database?: string; statement?: string; values?: unknown[]; statements?: string; encrypted?: boolean; mode?: string }) => Promise; }; // Add other electron IPC methods as needed getPath: (pathType: string) => string;