Skip to content

Commit c38d8c6

Browse files
committed
feat: migrate old embedded LND bbolt wallets to SQLite automatically
1 parent 6a5a700 commit c38d8c6

3 files changed

Lines changed: 208 additions & 5 deletions

File tree

utils/LndMobileUtils.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,85 @@ export async function initializeLnd({
306306
await initialize();
307307
}
308308

309+
export async function migrateBboltToSqlite({
310+
lndDir = 'lnd',
311+
isTestnet,
312+
walletPassword
313+
}: {
314+
lndDir: string;
315+
isTestnet?: boolean;
316+
walletPassword: string;
317+
}): Promise<boolean> {
318+
try {
319+
console.log(`Starting bbolt to SQLite migration for wallet: ${lndDir}`);
320+
321+
await stopLnd();
322+
323+
await sleep(1000);
324+
325+
await writeLndConfig({
326+
lndDir,
327+
isTestnet,
328+
isSqlite: true
329+
});
330+
331+
const { initialize, checkStatus } = lndMobile.index;
332+
await initialize();
333+
334+
await sleep(1000);
335+
336+
const status = await checkStatus();
337+
if (
338+
(status & ELndMobileStatusCodes.STATUS_PROCESS_STARTED) !==
339+
ELndMobileStatusCodes.STATUS_PROCESS_STARTED
340+
) {
341+
await startLnd({
342+
lndDir,
343+
walletPassword,
344+
isTorEnabled: false,
345+
isTestnet: isTestnet || false
346+
});
347+
}
348+
349+
await sleep(2000);
350+
351+
const finalStatus = await checkStatus();
352+
const isRunning =
353+
(finalStatus & ELndMobileStatusCodes.STATUS_PROCESS_STARTED) ===
354+
ELndMobileStatusCodes.STATUS_PROCESS_STARTED;
355+
356+
if (isRunning) {
357+
console.log(`Migration successful for wallet: ${lndDir}`);
358+
return true;
359+
} else {
360+
console.error(
361+
`Migration failed - LND did not start for wallet: ${lndDir}`
362+
);
363+
await writeLndConfig({
364+
lndDir,
365+
isTestnet,
366+
isSqlite: false
367+
});
368+
return false;
369+
}
370+
} catch (error) {
371+
console.error(
372+
`Error during bbolt to SQLite migration for wallet ${lndDir}:`,
373+
error
374+
);
375+
try {
376+
await writeLndConfig({
377+
lndDir,
378+
isTestnet,
379+
isSqlite: false
380+
});
381+
} catch (rollbackError) {
382+
console.error('Error rolling back config:', rollbackError);
383+
}
384+
return false;
385+
}
386+
}
387+
309388
export async function stopLnd() {
310389
const { checkStatus, stopLnd } = lndMobile.index;
311390
try {

utils/MigrationUtils.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ export const IS_BACKED_UP_KEY = 'backup-complete-v2';
7878

7979
const KEYCHAIN_MIGRATION_KEY = 'ios-keychain-cloud-sync-migration-v1';
8080
const CASHU_MIGRATION_KEY = 'ios-keychain-cashu-fix-v1';
81+
const BBOLT_TO_SQLITE_MIGRATION_KEY = 'bbolt-to-sqlite-migration-v1';
8182

8283
import EncryptedStorage from 'react-native-encrypted-storage';
8384
import Storage from '../storage';
@@ -1036,6 +1037,75 @@ class MigrationsUtils {
10361037
console.error('Error during keychain cloud sync migration:', error);
10371038
}
10381039
}
1040+
1041+
public async hasDatabaseMigrationBeenAttempted(
1042+
lndDir: string
1043+
): Promise<boolean> {
1044+
try {
1045+
const key = `${BBOLT_TO_SQLITE_MIGRATION_KEY}:${lndDir}`;
1046+
const attempted = await EncryptedStorage.getItem(key);
1047+
return attempted === 'true';
1048+
} catch (error) {
1049+
console.error(
1050+
'Error checking bbolt to SQLite migration status:',
1051+
error
1052+
);
1053+
return false;
1054+
}
1055+
}
1056+
1057+
public async markDatabaseMigrationAttempted(lndDir: string): Promise<void> {
1058+
try {
1059+
const key = `${BBOLT_TO_SQLITE_MIGRATION_KEY}:${lndDir}`;
1060+
await EncryptedStorage.setItem(key, 'true');
1061+
} catch (error) {
1062+
console.error(
1063+
'Error marking bbolt to SQLite migration as attempted:',
1064+
error
1065+
);
1066+
}
1067+
}
1068+
1069+
public async checkBboltWalletExists(
1070+
lndDir: string,
1071+
isSqlite?: boolean
1072+
): Promise<boolean> {
1073+
if (isSqlite === true) {
1074+
return false;
1075+
}
1076+
1077+
try {
1078+
const settingsJson = await Storage.getItem(STORAGE_KEY);
1079+
if (!settingsJson) {
1080+
return false;
1081+
}
1082+
1083+
const settings = JSON.parse(settingsJson);
1084+
if (!settings.nodes || !Array.isArray(settings.nodes)) {
1085+
return false;
1086+
}
1087+
1088+
const node = settings.nodes.find(
1089+
(n: any) =>
1090+
n.implementation === 'embedded-lnd' &&
1091+
(n.lndDir || 'lnd') === lndDir
1092+
);
1093+
1094+
if (!node) {
1095+
return false;
1096+
}
1097+
1098+
const nodeIsBbolt =
1099+
node.isSqlite === false || node.isSqlite === undefined;
1100+
const hasWalletData =
1101+
node.seedPhrase || node.adminMacaroon || node.walletPassword;
1102+
1103+
return nodeIsBbolt && hasWalletData;
1104+
} catch (error) {
1105+
console.error('Error checking bbolt wallet existence:', error);
1106+
return false;
1107+
}
1108+
}
10391109
}
10401110

10411111
const migrationsUtils = new MigrationsUtils();

views/Wallet/Wallet.tsx

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,12 @@ import {
5050
startLnd,
5151
stopLnd,
5252
expressGraphSync,
53-
LND_FOLDER_MISSING_ERROR
53+
LND_FOLDER_MISSING_ERROR,
54+
migrateBboltToSqlite
5455
} from '../../utils/LndMobileUtils';
5556
import { localeString, bridgeJavaStrings } from '../../utils/LocaleUtils';
5657
import { isBatterySaverEnabled } from '../../utils/BatteryUtils';
57-
import { IS_BACKED_UP_KEY } from '../../utils/MigrationUtils';
58+
import MigrationsUtils, { IS_BACKED_UP_KEY } from '../../utils/MigrationUtils';
5859
import { protectedNavigation } from '../../utils/NavigationUtils';
5960
import { isLightTheme, themeColor } from '../../utils/ThemeUtils';
6061
import { restartNeeded } from '../../utils/RestartUtils';
@@ -136,6 +137,7 @@ interface WalletState {
136137
initialLoad: boolean;
137138
loading: boolean;
138139
pendingShareIntent?: { qrData?: string; base64Image?: string };
140+
migratingDatabase: boolean;
139141
}
140142

141143
@inject(
@@ -177,7 +179,8 @@ export default class Wallet extends React.Component<WalletProps, WalletState> {
177179
unlocked: false,
178180
initialLoad: true,
179181
loading: false,
180-
pendingShareIntent: undefined
182+
pendingShareIntent: undefined,
183+
migratingDatabase: false
181184
};
182185
this.pan = new Animated.ValueXY();
183186
this.panResponder = PanResponder.create({
@@ -547,13 +550,63 @@ export default class Wallet extends React.Component<WalletProps, WalletState> {
547550
await CashuStore.initializeWallets();
548551

549552
console.log('lndDir', lndDir);
553+
const currLndDir = lndDir || 'lnd';
554+
const needsMigration =
555+
!isSqlite &&
556+
(await MigrationsUtils.checkBboltWalletExists(
557+
currLndDir,
558+
isSqlite
559+
)) &&
560+
!(await MigrationsUtils.hasDatabaseMigrationBeenAttempted(
561+
currLndDir
562+
));
563+
564+
if (needsMigration) {
565+
this.setState({ migratingDatabase: true });
566+
try {
567+
const success = await migrateBboltToSqlite({
568+
lndDir: currLndDir,
569+
isTestnet: embeddedLndNetwork === 'Testnet',
570+
walletPassword: walletPassword || ''
571+
});
572+
573+
if (success) {
574+
await MigrationsUtils.markDatabaseMigrationAttempted(
575+
currLndDir
576+
);
577+
const nodes = settings?.nodes || [];
578+
const nodeIndex = nodes.findIndex(
579+
(n: any) =>
580+
n.implementation === 'embedded-lnd' &&
581+
(n.lndDir || 'lnd') === currLndDir
582+
);
583+
if (nodeIndex !== -1) {
584+
const updatedNodes = [...nodes];
585+
updatedNodes[nodeIndex] = {
586+
...updatedNodes[nodeIndex],
587+
isSqlite: true
588+
};
589+
await updateSettings({
590+
nodes: updatedNodes
591+
});
592+
}
593+
}
594+
} catch (error) {
595+
console.error(
596+
'Error during database migration:',
597+
error
598+
);
599+
} finally {
600+
this.setState({ migratingDatabase: false });
601+
}
602+
}
550603

551604
await initializeLnd({
552605
lndDir: lndDir || 'lnd',
553606
isTestnet: embeddedLndNetwork === 'Testnet',
554607
rescan,
555608
compactDb,
556-
isSqlite
609+
isSqlite: needsMigration ? true : isSqlite
557610
});
558611
} catch (error: any) {
559612
console.log(
@@ -1383,7 +1436,8 @@ export default class Wallet extends React.Component<WalletProps, WalletState> {
13831436
padding: 8
13841437
}}
13851438
>
1386-
{isMigrating
1439+
{isMigrating ||
1440+
this.state.migratingDatabase
13871441
? localeString(
13881442
'views.Wallet.Wallet.migrating'
13891443
).replace('Zeus', 'ZEUS')

0 commit comments

Comments
 (0)