Skip to content

Commit ef3f36a

Browse files
committed
feat: adding channel export in the Backup wallet view and made a separate channel migration utility file
1 parent d3221aa commit ef3f36a

File tree

6 files changed

+261
-201
lines changed

6 files changed

+261
-201
lines changed

locales/en.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -888,7 +888,7 @@
888888
"views.Tools.migration.export.title": "Export Channels Backup",
889889
"views.Tools.migration.export.text1": "This will export your active channel database. You can use this file to restore your channels on a different device.",
890890
"views.Tools.migration.export.text2": "WARNING: After exporting, this wallet will be locked, as restoring the same wallet on a new device will force close the existing channels.",
891-
"views.Tools.migration.databaseNotFound": "Could not find wallet database.",
891+
"views.Tools.migration.databaseNotFound": "Could not find channel database.",
892892
"views.Tools.migration.export.confirm": "Export",
893893
"views.Tools.migration.export.success": "Backup Successful",
894894
"views.Tools.migration.import.success": "Import Successful",

utils/ChannelMigrationUtils.ts

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import { Alert, Platform } from 'react-native';
2+
import RNFS from 'react-native-fs';
3+
import Share from 'react-native-share';
4+
import RNRestart from 'react-native-restart';
5+
import { localeString } from './LocaleUtils';
6+
7+
import Storage from '../storage';
8+
9+
export const CHANNEL_MIGRATION_ACTIVE = 'channel_migration_active';
10+
11+
export const getChannelDbPath = async (
12+
lndDir: string,
13+
isTestnet: boolean,
14+
isSqlite: boolean
15+
) => {
16+
const network = isTestnet ? 'testnet' : 'mainnet';
17+
const rootPath = Platform.select({
18+
android: RNFS.DocumentDirectoryPath,
19+
ios: `${RNFS.LibraryDirectoryPath}/Application Support`
20+
});
21+
const basePath = `${rootPath}/${lndDir}/data/graph/${network}`;
22+
const fileName = isSqlite ? 'channel.sqlite' : 'channel.db';
23+
const fullPath = `${basePath}/${fileName}`;
24+
25+
if (await RNFS.exists(fullPath)) {
26+
return { path: fullPath, type: isSqlite ? 'sqlite' : 'bolt' };
27+
}
28+
return null;
29+
};
30+
31+
export const exportChannelDb = async (
32+
lndDir: string | any,
33+
isTestnet: boolean,
34+
isSqlite: boolean,
35+
setLoading?: (loading: boolean) => void
36+
) => {
37+
try {
38+
if (setLoading) setLoading(true);
39+
40+
const dbPath = await getChannelDbPath(lndDir, isTestnet, isSqlite);
41+
42+
if (!dbPath) {
43+
Alert.alert(
44+
localeString('general.error'),
45+
localeString('views.Tools.migration.databaseNotFound')
46+
);
47+
if (setLoading) setLoading(false);
48+
return;
49+
}
50+
51+
const { path, type } = dbPath;
52+
const extension = type === 'sqlite' ? 'sqlite' : 'db';
53+
const backupFileName = `zeus-channels-${
54+
isTestnet ? 'testnet' : 'mainnet'
55+
}-${Date.now()}.${extension}`;
56+
const stagingPath = `${RNFS.DocumentDirectoryPath}/${backupFileName}`;
57+
58+
if (await RNFS.exists(stagingPath)) {
59+
await RNFS.unlink(stagingPath);
60+
}
61+
62+
await RNFS.copyFile(path, stagingPath);
63+
64+
if (setLoading) setLoading(false);
65+
66+
console.log('Opening Share Sheet...');
67+
68+
const shareResult = await Share.open({
69+
title: localeString('views.Tools.migration.export.title'),
70+
url: `file://${stagingPath}`,
71+
type: 'application/octet-stream',
72+
filename: backupFileName,
73+
failOnCancel: false
74+
});
75+
76+
const isDismissed =
77+
shareResult.dismissedAction || shareResult.success === false;
78+
79+
if (isDismissed) {
80+
await RNFS.unlink(stagingPath);
81+
return;
82+
}
83+
84+
console.log(' Export successful. Locking wallet...');
85+
86+
await Storage.setItem(
87+
CHANNEL_MIGRATION_ACTIVE,
88+
JSON.stringify({ migrationStatus: true, lndDir: lndDir })
89+
);
90+
await RNFS.unlink(stagingPath);
91+
92+
Alert.alert(
93+
localeString('views.Tools.migration.export.success'),
94+
localeString('views.Tools.migration.export.success.text'),
95+
[
96+
{
97+
text: localeString('views.Wallet.restart'),
98+
onPress: () => RNRestart.Restart()
99+
}
100+
],
101+
{ cancelable: false }
102+
);
103+
} catch (error) {
104+
console.error('Export Failed:', error);
105+
if (setLoading) setLoading(false);
106+
Alert.alert(localeString('general.error'));
107+
RNRestart.Restart();
108+
}
109+
};
110+
111+
export const restoreChannels = async (
112+
sourceUri: string,
113+
fileName: string,
114+
lndDir: string,
115+
isTestnet: boolean,
116+
setLoading?: (loading: boolean) => void
117+
) => {
118+
try {
119+
if (setLoading) setLoading(true);
120+
121+
console.log(`Restoring from: ${sourceUri}`);
122+
123+
const isSqliteBackup = fileName.toLowerCase().endsWith('.sqlite');
124+
const dbName = isSqliteBackup ? 'channel.sqlite' : 'channel.db';
125+
126+
const rootPath = Platform.select({
127+
android: RNFS.DocumentDirectoryPath,
128+
ios: `${RNFS.LibraryDirectoryPath}/Application Support`
129+
});
130+
131+
const destFolder = `${rootPath}/${lndDir}/data/graph/${
132+
isTestnet ? 'testnet' : 'mainnet'
133+
}`;
134+
const destPath = `${destFolder}/${dbName}`;
135+
136+
console.log(`Target Destination: ${destPath}`);
137+
138+
if (await RNFS.exists(destPath)) {
139+
await RNFS.unlink(destPath);
140+
}
141+
await RNFS.copyFile(sourceUri, destPath);
142+
143+
if (setLoading) setLoading(false);
144+
145+
Alert.alert(
146+
localeString('views.Tools.nodeConfigExportImport.importSuccess'),
147+
`${localeString(
148+
'views.Tools.migration.import.success.text1'
149+
)}\n\n` +
150+
`ⓘ ${localeString(
151+
'views.Tools.migration.import.success.text2'
152+
)}`,
153+
[
154+
{
155+
text: localeString('views.Wallet.restart'),
156+
onPress: () => RNRestart.Restart()
157+
}
158+
],
159+
{ cancelable: false }
160+
);
161+
} catch (error) {
162+
console.error('Import Failed:', error);
163+
if (setLoading) setLoading(false);
164+
Alert.alert(localeString('general.error'));
165+
}
166+
};

views/Settings/Seed.tsx

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import Header from '../../components/Header';
2525
import ModalBox from '../../components/ModalBox';
2626

2727
import SettingsStore from '../../stores/SettingsStore';
28+
import NodeInfoStore from '../../stores/NodeInfoStore';
2829

2930
import {
3031
SWAPS_KEY,
@@ -35,6 +36,7 @@ import {
3536
import { themeColor } from '../../utils/ThemeUtils';
3637
import { localeString } from '../../utils/LocaleUtils';
3738
import { IS_BACKED_UP_KEY } from '../../utils/MigrationUtils';
39+
import { exportChannelDb } from '../../utils/ChannelMigrationUtils';
3840

3941
import Storage from '../../storage';
4042

@@ -44,6 +46,7 @@ import QR from '../../assets/images/SVG/QR.svg';
4446
interface SeedProps {
4547
navigation: StackNavigationProp<any, any>;
4648
SettingsStore: SettingsStore;
49+
NodeInfoStore: NodeInfoStore;
4750
route: Route<
4851
'Seed',
4952
{
@@ -56,6 +59,7 @@ interface SeedState {
5659
understood: boolean;
5760
showModal: boolean;
5861
isDeleteModalVisible: boolean;
62+
isChannelExporting: boolean;
5963
}
6064

6165
const MnemonicWord = ({ index, word }: { index: any; word: any }) => {
@@ -106,13 +110,14 @@ const MnemonicWord = ({ index, word }: { index: any; word: any }) => {
106110
);
107111
};
108112

109-
@inject('SettingsStore')
113+
@inject('SettingsStore', 'NodeInfoStore')
110114
@observer
111115
export default class Seed extends React.PureComponent<SeedProps, SeedState> {
112116
state = {
113117
understood: false,
114118
showModal: false,
115-
isDeleteModalVisible: false
119+
isDeleteModalVisible: false,
120+
isChannelExporting: false
116121
};
117122

118123
componentDidMount() {
@@ -188,6 +193,41 @@ export default class Seed extends React.PureComponent<SeedProps, SeedState> {
188193
);
189194
};
190195

196+
handleExportChannels = () => {
197+
Alert.alert(
198+
localeString('views.Tools.migration.export.title'),
199+
200+
`${localeString('views.Tools.migration.export.text1')}\n\n` +
201+
`⚠️ ${localeString('views.Tools.migration.export.text2')}`,
202+
[
203+
{
204+
text: localeString('general.cancel'),
205+
style: 'cancel'
206+
},
207+
{
208+
text: localeString('views.Tools.migration.export.confirm'),
209+
style: 'default',
210+
onPress: async () => {
211+
const { SettingsStore, NodeInfoStore } = this.props;
212+
const { isSqlite }: any = SettingsStore;
213+
const isTestnet = NodeInfoStore!.nodeInfo.isTestNet;
214+
const lndDir = () =>
215+
this.props.SettingsStore.lndDir || 'lnd';
216+
console.log(lndDir(), isTestnet, isSqlite);
217+
218+
await exportChannelDb(
219+
lndDir(),
220+
isTestnet,
221+
isSqlite,
222+
(loading) =>
223+
this.setState({ isChannelExporting: loading })
224+
);
225+
}
226+
}
227+
]
228+
);
229+
};
230+
191231
render() {
192232
const { navigation, SettingsStore, route } = this.props;
193233
const { understood, showModal } = this.state;
@@ -257,6 +297,20 @@ export default class Seed extends React.PureComponent<SeedProps, SeedState> {
257297
);
258298
};
259299

300+
const ExportChannelsButton = () => (
301+
<TouchableOpacity
302+
onPress={this.handleExportChannels}
303+
style={{ marginLeft: 14 }}
304+
>
305+
<Icon
306+
name="upload"
307+
type="feather"
308+
color={themeColor('text')}
309+
size={24}
310+
/>
311+
</TouchableOpacity>
312+
);
313+
260314
return (
261315
<Screen>
262316
{this.renderDeleteModal()}
@@ -282,7 +336,15 @@ export default class Seed extends React.PureComponent<SeedProps, SeedState> {
282336
<></>
283337
)}
284338
<DangerouslyCopySeed />
285-
{isRefundRescueKey ? <></> : <QRExport />}
339+
340+
{isRefundRescueKey ? (
341+
<></>
342+
) : (
343+
<>
344+
<QRExport />
345+
<ExportChannelsButton />
346+
</>
347+
)}
286348
</Row>
287349
) : undefined
288350
}

0 commit comments

Comments
 (0)