From cdc43b6bf0778aa3e49840a9e0af6ecf8ba32020 Mon Sep 17 00:00:00 2001 From: Jason Date: Tue, 8 Aug 2023 10:30:24 +0200 Subject: [PATCH] feat: recovery lightning backups from backup history --- src/screens/Recovery/Lightning.tsx | 297 +++++++++++++++++++++ src/screens/Recovery/Recovery.tsx | 73 +---- src/screens/Recovery/RecoveryNavigator.tsx | 3 + src/store/actions/backup.ts | 2 +- src/utils/backup/backups-subscriber.tsx | 4 +- src/utils/i18n/locales/en/security.json | 3 +- 6 files changed, 307 insertions(+), 75 deletions(-) create mode 100644 src/screens/Recovery/Lightning.tsx diff --git a/src/screens/Recovery/Lightning.tsx b/src/screens/Recovery/Lightning.tsx new file mode 100644 index 000000000..140a10d4c --- /dev/null +++ b/src/screens/Recovery/Lightning.tsx @@ -0,0 +1,297 @@ +import React, { ReactElement, useEffect, useState } from 'react'; +import { ActivityIndicator, StyleSheet, View } from 'react-native'; +import { useTranslation } from 'react-i18next'; + +import lm, { ldk, TLdkData } from '@synonymdev/react-native-ldk'; +import { View as ThemedView } from '../../styles/components'; +import List, { EItemType, IListData, ItemData } from '../../components/List'; + +import NavigationHeader from '../../components/NavigationHeader'; +import SafeAreaInset from '../../components/SafeAreaInset'; +import Button from '../../components/Button'; +import { RecoveryStackScreenProps } from '../../navigation/types'; +import { useSelectedSlashtag } from '../../hooks/slashtags'; +import { SlashtagsProvider } from '../../components/SlashtagsProvider'; +import { + EBackupCategories, + fetchBackup, + listBackups, +} from '../../utils/backup/backpack'; +import { EAvailableNetworks } from '../../utils/networks'; +import Dialog from '../../components/Dialog'; +import { startWalletServices } from '../../utils/startup'; +import { showToast } from '../../utils/notifications'; +import RNExitApp from 'react-native-exit-app'; +import { selectedNetworkSelector } from '../../store/reselect/wallet'; +import { useSelector } from 'react-redux'; +import { bytesToString } from '../../utils/converters'; +import { setLdkStoragePath } from '../../utils/lightning'; +import { TAccountBackup } from '../../store/types/backup'; +const Lightning = ( + props: RecoveryStackScreenProps<'Lightning'>, +): ReactElement => { + return ( + + + + ); +}; + +const LightningWithSlashtags = ({ + navigation, +}: RecoveryStackScreenProps<'Lightning'>): ReactElement => { + const { t } = useTranslation('security'); + const slashtag = useSelectedSlashtag(); + const [history, setHistory] = useState({ + title: 'Loading backups...', + data: [], + }); + const [showConfirmRecoveryDialog, setShowConfirmRecoveryDialog] = + useState(false); + const [isFetchingBackup, setIsFetchingBackup] = useState(false); + const [backup, setBackup] = useState | null>(null); + const [isRecoveringChannels, setIsRecoveringChannels] = useState(false); + const [recoveredSats, setRecoveredSats] = useState(0); + const [showLdkRecoverySuccessDialog, setShowLdkRecoverySuccessDialog] = + useState(false); + const selectedNetwork = useSelector(selectedNetworkSelector); + + //On mount + useEffect(() => { + if (!slashtag || history.data.length > 0) { + return; + } + + const listLdkBackups = async (): Promise => { + const res = await listBackups( + slashtag.slashtag, + EBackupCategories.ldkComplete, + EAvailableNetworks.bitcoin, + ); + + if (res.isErr()) { + console.error(res.error); + showToast({ + type: 'error', + title: t('lightning_recovery_error'), + description: res.error.message, + }); + return; + } + + const data: ItemData[] = res.value.map(({ timestamp }) => { + return { + title: `${new Date(timestamp).toLocaleString()}`, + enabled: true, + type: EItemType.button, + onPress: async (): Promise => + confirmRestoreFromBackup(timestamp), + }; + }); + + setHistory({ + data: data, + }); + }; + + listLdkBackups().catch((e) => console.log(e)); + }); + + const onBack = (): void => { + console.warn(JSON.stringify(navigation)); + navigation.goBack(); + }; + + const confirmRestoreFromBackup = async (timestamp: number): Promise => { + if (isFetchingBackup) { + return; + } + + setIsFetchingBackup(true); + const res = await fetchBackup( + slashtag.slashtag, + timestamp, + EBackupCategories.ldkComplete, + selectedNetwork, + ); + + if (res.isErr()) { + console.log(res.error); + setIsFetchingBackup(false); + showToast({ + type: 'error', + title: t('lightning_recovery_error'), + description: res.error.message, + }); + return; + } + + const bytesToStringRes = bytesToString(res.value.content); + if (bytesToStringRes.isErr()) { + console.log(bytesToStringRes.error); + setIsFetchingBackup(false); + showToast({ + type: 'error', + title: t('lightning_recovery_error'), + description: bytesToStringRes.error.message, + }); + return; + } + + setBackup(JSON.parse(bytesToStringRes.value)); + setIsFetchingBackup(false); + setShowConfirmRecoveryDialog(true); + }; + + const onShowLdkRecoveryConfirmed = async (): Promise => { + if (!backup) { + return; + } + + if (Object.keys(backup.data.channel_monitors).length === 0) { + showToast({ + type: 'error', + title: t('lightning_recovery_error'), + description: t('lightning_recovery_no_channels'), + }); + return; + } + + setShowConfirmRecoveryDialog(false); + setIsRecoveringChannels(true); + + await ldk.stop(); + + const storageRes = await setLdkStoragePath(); + if (storageRes.isErr()) { + console.error(storageRes.error); + setIsRecoveringChannels(false); + showToast({ + type: 'error', + title: t('lightning_recovery_error'), + description: storageRes.error.message, + }); + return; + } + + const importRes = await lm.importAccount({ + backup, + }); + if (importRes.isErr()) { + console.error(importRes.error); + setIsRecoveringChannels(false); + showToast({ + type: 'error', + title: t('lightning_recovery_error'), + description: importRes.error.message, + }); + return; + } + + const setupRes = await startWalletServices({ + onchain: false, + lightning: true, + restore: false, + staleBackupRecoveryMode: true, + }); + + if (setupRes.isErr()) { + showToast({ + type: 'error', + title: t('lightning_recovery_error'), + description: setupRes.error.message, + }); + setIsRecoveringChannels(false); + return; + } + + const balances = await ldk.claimableBalances(false); + if (balances.isErr()) { + showToast({ + type: 'error', + title: t('lightning_recovery_error'), + description: balances.error.message, + }); + setIsRecoveringChannels(false); + return; + } + + await ldk.stop(); + + let sats = 0; + balances.value.forEach((balance) => { + sats += balance.claimable_amount_satoshis; + }); + + setRecoveredSats(sats); + setShowLdkRecoverySuccessDialog(true); + setIsRecoveringChannels(false); + }; + + const onCloseApp = (): void => { + RNExitApp.exitApp(); + }; + + return ( + + + + + {isRecoveringChannels ? ( + + ) : ( + + )} + +