Skip to content

Commit 5acf3dd

Browse files
committed
feat: migrate old embedded LND bbolt wallets to SQLite automatically
1 parent 5a8baeb commit 5acf3dd

4 files changed

Lines changed: 214 additions & 6 deletions

File tree

locales/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,7 @@
443443
"views.Wallet.Wallet.loadingAccount": "Zeus is loading your account.",
444444
"views.Wallet.Wallet.startingNode": "Zeus is starting your node.",
445445
"views.Wallet.Wallet.expressGraphSync": "Zeus is running express graph sync. Hang tight.",
446+
"views.Wallet.Wallet.migratingDatabase": "Zeus is upgrading your database. This may take a moment.",
446447
"views.Wallet.startupSlowTitle": "Startup is taking longer than expected",
447448
"views.Wallet.startupSlowMsg.ios": "We recommend either viewing the LND logs for more details, or restarting the app manually.",
448449
"views.Wallet.startupSlowMsg.android": "We recommend either viewing the LND logs for more details, or restarting the app directly from here.",

utils/LndMobileUtils.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,85 @@ export async function initializeLnd({
278278
await initialize();
279279
}
280280

281+
export async function migrateBboltToSqlite({
282+
lndDir = 'lnd',
283+
isTestnet,
284+
walletPassword
285+
}: {
286+
lndDir: string;
287+
isTestnet?: boolean;
288+
walletPassword: string;
289+
}): Promise<boolean> {
290+
try {
291+
console.log(`Starting bbolt to SQLite migration for wallet: ${lndDir}`);
292+
293+
await stopLnd();
294+
295+
await sleep(1000);
296+
297+
await writeLndConfig({
298+
lndDir,
299+
isTestnet,
300+
isSqlite: true
301+
});
302+
303+
const { initialize, checkStatus } = lndMobile.index;
304+
await initialize();
305+
306+
await sleep(1000);
307+
308+
const status = await checkStatus();
309+
if (
310+
(status & ELndMobileStatusCodes.STATUS_PROCESS_STARTED) !==
311+
ELndMobileStatusCodes.STATUS_PROCESS_STARTED
312+
) {
313+
await startLnd({
314+
lndDir,
315+
walletPassword,
316+
isTorEnabled: false,
317+
isTestnet: isTestnet || false
318+
});
319+
}
320+
321+
await sleep(2000);
322+
323+
const finalStatus = await checkStatus();
324+
const isRunning =
325+
(finalStatus & ELndMobileStatusCodes.STATUS_PROCESS_STARTED) ===
326+
ELndMobileStatusCodes.STATUS_PROCESS_STARTED;
327+
328+
if (isRunning) {
329+
console.log(`Migration successful for wallet: ${lndDir}`);
330+
return true;
331+
} else {
332+
console.error(
333+
`Migration failed - LND did not start for wallet: ${lndDir}`
334+
);
335+
await writeLndConfig({
336+
lndDir,
337+
isTestnet,
338+
isSqlite: false
339+
});
340+
return false;
341+
}
342+
} catch (error) {
343+
console.error(
344+
`Error during bbolt to SQLite migration for wallet ${lndDir}:`,
345+
error
346+
);
347+
try {
348+
await writeLndConfig({
349+
lndDir,
350+
isTestnet,
351+
isSqlite: false
352+
});
353+
} catch (rollbackError) {
354+
console.error('Error rolling back config:', rollbackError);
355+
}
356+
return false;
357+
}
358+
}
359+
281360
export async function stopLnd() {
282361
const { checkStatus, stopLnd } = lndMobile.index;
283362
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';
8080
const CASHU_MIGRATION_KEY = 'ios-keychain-cashu-fix';
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';
@@ -965,6 +966,75 @@ class MigrationsUtils {
965966
console.error('Error during keychain cloud sync migration:', error);
966967
}
967968
}
969+
970+
public async hasDatabaseMigrationBeenAttempted(
971+
lndDir: string
972+
): Promise<boolean> {
973+
try {
974+
const key = `${BBOLT_TO_SQLITE_MIGRATION_KEY}:${lndDir}`;
975+
const attempted = await EncryptedStorage.getItem(key);
976+
return attempted === 'true';
977+
} catch (error) {
978+
console.error(
979+
'Error checking bbolt to SQLite migration status:',
980+
error
981+
);
982+
return false;
983+
}
984+
}
985+
986+
public async markDatabaseMigrationAttempted(lndDir: string): Promise<void> {
987+
try {
988+
const key = `${BBOLT_TO_SQLITE_MIGRATION_KEY}:${lndDir}`;
989+
await EncryptedStorage.setItem(key, 'true');
990+
} catch (error) {
991+
console.error(
992+
'Error marking bbolt to SQLite migration as attempted:',
993+
error
994+
);
995+
}
996+
}
997+
998+
public async checkBboltWalletExists(
999+
lndDir: string,
1000+
isSqlite?: boolean
1001+
): Promise<boolean> {
1002+
if (isSqlite === true) {
1003+
return false;
1004+
}
1005+
1006+
try {
1007+
const settingsJson = await Storage.getItem(STORAGE_KEY);
1008+
if (!settingsJson) {
1009+
return false;
1010+
}
1011+
1012+
const settings = JSON.parse(settingsJson);
1013+
if (!settings.nodes || !Array.isArray(settings.nodes)) {
1014+
return false;
1015+
}
1016+
1017+
const node = settings.nodes.find(
1018+
(n: any) =>
1019+
n.implementation === 'embedded-lnd' &&
1020+
(n.lndDir || 'lnd') === lndDir
1021+
);
1022+
1023+
if (!node) {
1024+
return false;
1025+
}
1026+
1027+
const nodeIsBbolt =
1028+
node.isSqlite === false || node.isSqlite === undefined;
1029+
const hasWalletData =
1030+
node.seedPhrase || node.adminMacaroon || node.walletPassword;
1031+
1032+
return nodeIsBbolt && hasWalletData;
1033+
} catch (error) {
1034+
console.error('Error checking bbolt wallet existence:', error);
1035+
return false;
1036+
}
1037+
}
9681038
}
9691039

9701040
const migrationsUtils = new MigrationsUtils();

views/Wallet/Wallet.tsx

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,12 @@ import {
4949
initializeLnd,
5050
startLnd,
5151
stopLnd,
52-
expressGraphSync
52+
expressGraphSync,
53+
migrateBboltToSqlite
5354
} from '../../utils/LndMobileUtils';
5455
import { localeString, bridgeJavaStrings } from '../../utils/LocaleUtils';
5556
import { isBatterySaverEnabled } from '../../utils/BatteryUtils';
56-
import { IS_BACKED_UP_KEY } from '../../utils/MigrationUtils';
57+
import MigrationsUtils, { IS_BACKED_UP_KEY } from '../../utils/MigrationUtils';
5758
import { protectedNavigation } from '../../utils/NavigationUtils';
5859
import { isLightTheme, themeColor } from '../../utils/ThemeUtils';
5960
import { restartNeeded } from '../../utils/RestartUtils';
@@ -129,6 +130,7 @@ interface WalletState {
129130
initialLoad: boolean;
130131
loading: boolean;
131132
pendingShareIntent?: { qrData?: string; base64Image?: string };
133+
migratingDatabase: boolean;
132134
}
133135

134136
@inject(
@@ -169,7 +171,8 @@ export default class Wallet extends React.Component<WalletProps, WalletState> {
169171
unlocked: false,
170172
initialLoad: true,
171173
loading: false,
172-
pendingShareIntent: undefined
174+
pendingShareIntent: undefined,
175+
migratingDatabase: false
173176
};
174177
this.pan = new Animated.ValueXY();
175178
this.panResponder = PanResponder.create({
@@ -475,12 +478,63 @@ export default class Wallet extends React.Component<WalletProps, WalletState> {
475478
await CashuStore.initializeWallets();
476479

477480
console.log('lndDir', lndDir);
481+
const currLndDir = lndDir || 'lnd';
482+
const needsMigration =
483+
!isSqlite &&
484+
(await MigrationsUtils.checkBboltWalletExists(
485+
currLndDir,
486+
isSqlite
487+
)) &&
488+
!(await MigrationsUtils.hasDatabaseMigrationBeenAttempted(
489+
currLndDir
490+
));
491+
492+
if (needsMigration) {
493+
this.setState({ migratingDatabase: true });
494+
try {
495+
const success = await migrateBboltToSqlite({
496+
lndDir: currLndDir,
497+
isTestnet: embeddedLndNetwork === 'Testnet',
498+
walletPassword: walletPassword || ''
499+
});
500+
501+
if (success) {
502+
await MigrationsUtils.markDatabaseMigrationAttempted(
503+
currLndDir
504+
);
505+
const nodes = settings?.nodes || [];
506+
const nodeIndex = nodes.findIndex(
507+
(n: any) =>
508+
n.implementation === 'embedded-lnd' &&
509+
(n.lndDir || 'lnd') === currLndDir
510+
);
511+
if (nodeIndex !== -1) {
512+
const updatedNodes = [...nodes];
513+
updatedNodes[nodeIndex] = {
514+
...updatedNodes[nodeIndex],
515+
isSqlite: true
516+
};
517+
await updateSettings({
518+
nodes: updatedNodes
519+
});
520+
}
521+
}
522+
} catch (error) {
523+
console.error(
524+
'Error during database migration:',
525+
error
526+
);
527+
} finally {
528+
this.setState({ migratingDatabase: false });
529+
}
530+
}
531+
478532
await initializeLnd({
479-
lndDir: lndDir || 'lnd',
533+
lndDir: currLndDir,
480534
isTestnet: embeddedLndNetwork === 'Testnet',
481535
rescan,
482536
compactDb,
483-
isSqlite
537+
isSqlite: needsMigration ? true : isSqlite
484538
});
485539

486540
// on initial load, do not run EGS
@@ -1250,7 +1304,11 @@ export default class Wallet extends React.Component<WalletProps, WalletState> {
12501304
padding: 8
12511305
}}
12521306
>
1253-
{CashuStore.initializing
1307+
{this.state.migratingDatabase
1308+
? localeString(
1309+
'views.Wallet.Wallet.migratingDatabase'
1310+
).replace('Zeus', 'ZEUS')
1311+
: CashuStore.initializing
12541312
? CashuStore.loadingMsg
12551313
: settings.nodes &&
12561314
loggedIn &&

0 commit comments

Comments
 (0)