diff --git a/__tests__/backups.ts b/__tests__/backups.ts deleted file mode 100644 index 9bffc5a78..000000000 --- a/__tests__/backups.ts +++ /dev/null @@ -1,320 +0,0 @@ -import SDK from '@synonymdev/slashtags-sdk'; -import RAM from 'random-access-memory'; -import { stringToBytes } from '@synonymdev/react-native-lnurl/dist/utils/helpers'; - -import '../src/utils/i18n'; -import { - EBackupCategories, - fetchBackup, - uploadBackup, -} from '../src/utils/backup/backpack'; -import { bytesToString } from '../src/utils/converters'; -import store from '../src/store'; -import { - addMetaTxTag, - updatePendingInvoice, - addMetaTxSlashtagsUrl, - resetMetadataState, -} from '../src/store/slices/metadata'; -import { - performBlocktankRestore, - performLdkActivityRestore, - performMetadataRestore, - performRemoteBackup, - performSettingsRestore, - performWidgetsRestore, -} from '../src/store/utils/backup'; -import { - dispatch, - getActivityStore, - getBlocktankStore, - getMetaDataStore, - getSettingsStore, - getWidgetsStore, -} from '../src/store/helpers'; -import { - updateSettings, - resetSettingsState, -} from '../src/store/slices/settings'; -import { - resetWidgetsState, - setFeedWidget, - updateWidgets, -} from '../src/store/slices/widgets'; -import { - addActivityItem, - resetActivityState, -} from '../src/store/slices/activity'; -import { EActivityType } from '../src/store/types/activity'; -import { EPaymentType } from '../src/store/types/wallet'; -import { - addPaidBlocktankOrder, - resetBlocktankState, -} from '../src/store/slices/blocktank'; -import { defaultOrderResponse } from '../src/store/shapes/blocktank'; -import { updateBlocktankOrder } from '../src/store/slices/blocktank'; -import { EAvailableNetwork } from '../src/utils/networks'; - -jest.setTimeout(30000); - -describe('Remote backups', () => { - let sdk, slashtag; - beforeAll(async () => { - sdk = new SDK({ - primaryKey: new Uint8Array(32), //For testing, so we don't fill up server with junk after each test - storage: RAM, - relay: 'wss://dht-relay.synonym.to', - }); - await sdk.ready(); - slashtag = sdk.slashtag(); - }); - - afterAll(async () => { - await sdk.close(); - }); - - it('Backups up and restores a blob', async () => { - const message = 'Back me up plz'; - const category = EBackupCategories.jest; - - const uploadRes = await uploadBackup( - slashtag, - stringToBytes(message), - category, - EAvailableNetwork.bitcoinRegtest, - ); - - if (uploadRes.isErr()) { - throw uploadRes.error; - } - - const timestamp = uploadRes.value; - - const fetchRes = await fetchBackup( - slashtag, - timestamp, - category, - EAvailableNetwork.bitcoinRegtest, - ); - - if (fetchRes.isErr()) { - throw fetchRes.error; - } - const jsonString = bytesToString(fetchRes.value.content); - expect(jsonString).toEqual(message); - }); - - it('Backups and restores metadata', async () => { - dispatch(addMetaTxTag({ txId: 'txid1', tag: 'tag' })); - dispatch( - updatePendingInvoice({ - id: 'id123', - tags: ['futuretag'], - address: 'address', - payReq: 'lightningInvoice', - }), - ); - dispatch(addMetaTxSlashtagsUrl({ txId: 'txid2', url: 'slashtag' })); - - const backup = getMetaDataStore(); - - const uploadRes = await performRemoteBackup({ - slashtag, - isSyncedKey: 'remoteMetadataBackupSynced', - syncRequiredKey: 'remoteMetadataBackupSyncRequired', - syncCompletedKey: 'remoteMetadataBackupLastSync', - backupCategory: EBackupCategories.metadata, - backup, - }); - - if (uploadRes.isErr()) { - throw uploadRes.error; - } - - dispatch(resetMetadataState()); - expect(store.getState().metadata.tags).toMatchObject({}); - - const restoreRes = await performMetadataRestore({ - slashtag, - }); - - if (restoreRes.isErr()) { - throw restoreRes.error; - } - - expect(restoreRes.value.backupExists).toEqual(true); - expect(store.getState().metadata).toEqual(backup); - expect(store.getState().backup.remoteMetadataBackupSynced).toEqual(true); - }); - - it('Backups and restores settings', async () => { - dispatch( - updateSettings({ - selectedCurrency: 'GBP', - enableOfflinePayments: false, - }), - ); - - const backup = getSettingsStore(); - - const uploadRes = await performRemoteBackup({ - slashtag, - isSyncedKey: 'remoteSettingsBackupSynced', - syncRequiredKey: 'remoteSettingsBackupSyncRequired', - syncCompletedKey: 'remoteSettingsBackupLastSync', - backupCategory: EBackupCategories.settings, - backup, - }); - - if (uploadRes.isErr()) { - throw uploadRes.error; - } - - dispatch(resetSettingsState()); - expect(store.getState().settings.selectedCurrency).toEqual('USD'); - - const restoreRes = await performSettingsRestore({ - slashtag, - }); - - if (restoreRes.isErr()) { - throw restoreRes.error; - } - - expect(restoreRes.value.backupExists).toEqual(true); - expect(store.getState().settings).toEqual(backup); - expect(store.getState().backup.remoteSettingsBackupSynced).toEqual(true); - }); - - it('Backups and restores widgets', async () => { - dispatch( - setFeedWidget({ - url: 'url', - type: 'type', - fields: [ - { - name: 'name', - main: 'main', - files: {}, - }, - ], - }), - ); - dispatch(updateWidgets({ onboardedWidgets: true })); - - const backup = getWidgetsStore(); - - const uploadRes = await performRemoteBackup({ - slashtag, - isSyncedKey: 'remoteWidgetsBackupSynced', - syncRequiredKey: 'remoteWidgetsBackupSyncRequired', - syncCompletedKey: 'remoteWidgetsBackupLastSync', - backupCategory: EBackupCategories.widgets, - backup, - }); - - if (uploadRes.isErr()) { - throw uploadRes.error; - } - - dispatch(resetWidgetsState()); - expect(store.getState().widgets.widgets).toMatchObject({}); - - const restoreRes = await performWidgetsRestore({ - slashtag, - }); - - if (restoreRes.isErr()) { - throw restoreRes.error; - } - - expect(restoreRes.value.backupExists).toEqual(true); - expect(store.getState().widgets).toEqual(backup); - expect(store.getState().backup.remoteWidgetsBackupSynced).toEqual(true); - }); - - it('Backups and restores LDK Activity', async () => { - dispatch( - addActivityItem({ - id: 'id', - activityType: EActivityType.lightning, - txType: EPaymentType.received, - message: '', - address: 'invoice', - confirmed: true, - value: 1, - timestamp: new Date().getTime(), - }), - ); - - const backup = getActivityStore().items.filter( - (a) => a.activityType === EActivityType.lightning, - ); - - const uploadRes = await performRemoteBackup({ - slashtag, - isSyncedKey: 'remoteLdkActivityBackupSynced', - syncRequiredKey: 'remoteLdkActivityBackupSyncRequired', - syncCompletedKey: 'remoteLdkActivityBackupLastSync', - backupCategory: EBackupCategories.ldkActivity, - backup, - }); - - if (uploadRes.isErr()) { - throw uploadRes.error; - } - - dispatch(resetActivityState()); - expect(store.getState().activity.items.length).toEqual(0); - - const restoreRes = await performLdkActivityRestore({ - slashtag, - }); - - if (restoreRes.isErr()) { - throw restoreRes.error; - } - - expect(restoreRes.value.backupExists).toEqual(true); - expect(store.getState().activity.items).toEqual(backup); - expect(store.getState().backup.remoteLdkActivityBackupSynced).toEqual(true); - }); - - it('Backups and restores Blocktank orders', async () => { - dispatch(addPaidBlocktankOrder({ orderId: 'id', txid: 'txid' })); - dispatch(updateBlocktankOrder(defaultOrderResponse)); - - const { orders, paidOrders } = getBlocktankStore(); - const backup = { orders, paidOrders }; - - const uploadRes = await performRemoteBackup({ - slashtag, - isSyncedKey: 'remoteBlocktankBackupSynced', - syncRequiredKey: 'remoteBlocktankBackupSyncRequired', - syncCompletedKey: 'remoteBlocktankBackupLastSync', - backupCategory: EBackupCategories.blocktank, - backup, - }); - - if (uploadRes.isErr()) { - throw uploadRes.error; - } - - dispatch(resetBlocktankState()); - expect(store.getState().blocktank.orders.length).toEqual(0); - expect(store.getState().blocktank.paidOrders).toMatchObject({}); - - const restoreRes = await performBlocktankRestore({ - slashtag, - }); - - if (restoreRes.isErr()) { - throw restoreRes.error; - } - - expect(restoreRes.value.backupExists).toEqual(true); - expect(store.getState().blocktank.orders).toEqual(backup.orders); - expect(store.getState().blocktank.paidOrders).toEqual(backup.paidOrders); - expect(store.getState().backup.remoteBlocktankBackupSynced).toEqual(true); - }); -}); diff --git a/ios/Podfile.lock b/ios/Podfile.lock index f33821404..124ccfdf3 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -392,7 +392,7 @@ PODS: - React-Core - react-native-keep-awake (1.2.2): - React-Core - - react-native-ldk (0.0.127): + - react-native-ldk (0.0.129): - React - react-native-mmkv (2.11.0): - MMKV (>= 1.2.13) @@ -905,7 +905,7 @@ SPEC CHECKSUMS: react-native-flipper: 9c1957af24b76493ba74f46d000a5c1d485e7731 react-native-image-picker: 2e2e82aba9b6a91a7c78f7d9afde341a2659c7b8 react-native-keep-awake: ad1d67f617756b139536977a0bf06b27cec0714a - react-native-ldk: 4ab3d26d5e1356313c572814289cc516dc18dd88 + react-native-ldk: 30a80ced71007307cfceb8a79e4996788ab413ea react-native-mmkv: e97c0c79403fb94577e5d902ab1ebd42b0715b43 react-native-netinfo: 5ddbf20865bcffab6b43d0e4e1fd8b3896beb898 react-native-quick-base64: a5dbe4528f1453e662fcf7351029500b8b63e7bb diff --git a/package.json b/package.json index 5bb6e075a..ac26570db 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "@synonymdev/blocktank-client": "0.0.50", "@synonymdev/blocktank-lsp-http-client": "0.13.1", "@synonymdev/feeds": "2.1.1", - "@synonymdev/react-native-ldk": "0.0.127", + "@synonymdev/react-native-ldk": "0.0.129", "@synonymdev/react-native-lnurl": "0.0.7", "@synonymdev/result": "0.0.2", "@synonymdev/slashtags-auth": "1.0.0-alpha.6", diff --git a/src/assets/icons/wallet.ts b/src/assets/icons/wallet.ts index 8dbf94d8e..dc6b13028 100644 --- a/src/assets/icons/wallet.ts +++ b/src/assets/icons/wallet.ts @@ -182,9 +182,6 @@ export const usersIcon = (color = 'white'): string => export const userIcon = (color = 'white'): string => ``; -export const userRectangleIcon = (color = 'white'): string => - ``; - export const speedFastIcon = (color = 'white'): string => ``; diff --git a/src/screens/Recovery/Lightning.tsx b/src/screens/Recovery/Lightning.tsx index 1f3985e8d..7edbc77ee 100644 --- a/src/screens/Recovery/Lightning.tsx +++ b/src/screens/Recovery/Lightning.tsx @@ -17,7 +17,7 @@ import { useSelectedSlashtag } from '../../hooks/slashtags'; import { SlashtagsProvider } from '../../components/SlashtagsProvider'; import { SlashtagsProvider2 } from '../../components/SlashtagsProvider2'; import { - EBackupCategories, + EBackupCategoriesOld, fetchBackup, listBackups, } from '../../utils/backup/backpack'; @@ -70,7 +70,7 @@ const LightningWithSlashtags = ({ const listLdkBackups = async (): Promise => { const res = await listBackups( slashtag.slashtag, - EBackupCategories.ldkComplete, + EBackupCategoriesOld.ldkComplete, __DEV__ ? selectedNetwork : EAvailableNetwork.bitcoin, ); @@ -116,7 +116,7 @@ const LightningWithSlashtags = ({ const res = await fetchBackup( slashtag.slashtag, timestamp, - EBackupCategories.ldkComplete, + EBackupCategoriesOld.ldkComplete, selectedNetwork, ); diff --git a/src/screens/Settings/AppStatus/index.tsx b/src/screens/Settings/AppStatus/index.tsx index d1584bb8b..65777d734 100644 --- a/src/screens/Settings/AppStatus/index.tsx +++ b/src/screens/Settings/AppStatus/index.tsx @@ -1,32 +1,35 @@ -import { SettingsScreenProps } from '../../../navigation/types'; import React, { memo, ReactElement, useEffect, useMemo, useState } from 'react'; -import { ScrollView, View as ThemedView } from '../../../styles/components'; -import SettingsView from '../SettingsView'; -import { Caption13M, Text01M } from '../../../styles/text'; -import { StyleSheet, View } from 'react-native'; import { useTranslation } from 'react-i18next'; -import { - BitcoinSlantedIcon, - BroadcastIcon, - CloudCheckIcon, - GlobeSimpleIcon, - LightningHollow, -} from '../../../styles/icons'; -import { IColors } from '../../../styles/colors'; +import { StyleSheet, View } from 'react-native'; + import { useAppSelector } from '../../../hooks/redux'; +import { SettingsScreenProps } from '../../../navigation/types'; +import { backupSelector } from '../../../store/reselect/backup'; +import { blocktankPaidOrdersFullSelector } from '../../../store/reselect/blocktank'; +import { + openChannelsSelector, + pendingChannelsSelector, +} from '../../../store/reselect/lightning'; import { isConnectedToElectrumSelector, isLDKReadySelector, isOnlineSelector, } from '../../../store/reselect/ui'; +import { TBackupItem } from '../../../store/types/backup'; +import { EBackupCategories } from '../../../store/utils/backup'; +import { IColors } from '../../../styles/colors'; +import { ScrollView, View as ThemedView } from '../../../styles/components'; import { - openChannelsSelector, - pendingChannelsSelector, -} from '../../../store/reselect/lightning'; -import { blocktankPaidOrdersFullSelector } from '../../../store/reselect/blocktank'; -import { backupSelector } from '../../../store/reselect/backup'; -import { i18nTime } from '../../../utils/i18n'; + BitcoinSlantedIcon, + BroadcastIcon, + CloudCheckIcon, + GlobeSimpleIcon, + LightningHollow, +} from '../../../styles/icons'; +import { Caption13M, Text01M } from '../../../styles/text'; import { FAILED_BACKUP_CHECK_TIME } from '../../../utils/backup/backups-subscriber'; +import { i18nTime } from '../../../utils/i18n'; +import SettingsView from '../SettingsView'; type TStatusItem = | 'internet' @@ -128,19 +131,15 @@ const AppStatus = ({}: SettingsScreenProps<'AppStatus'>): ReactElement => { }, []); const isBackupSyncOk = useMemo(() => { - const isSyncOk = (key: number | undefined): boolean => { - // undefined = no sync required = ok - return key ? now - key < FAILED_BACKUP_CHECK_TIME : true; + const isSyncOk = (b: TBackupItem): boolean => { + return ( + b.synced > b.required || now - b.required < FAILED_BACKUP_CHECK_TIME + ); }; - return ( - isSyncOk(backup.remoteLdkBackupLastSyncRequired) && - isSyncOk(backup.remoteLdkActivityBackupSyncRequired) && - isSyncOk(backup.remoteBlocktankBackupSyncRequired) && - isSyncOk(backup.remoteSettingsBackupSyncRequired) && - isSyncOk(backup.remoteMetadataBackupSyncRequired) && - isSyncOk(backup.remoteWidgetsBackupSyncRequired) - ); + return Object.values(EBackupCategories).every((key) => { + return isSyncOk(backup[key]); + }); }, [backup, now]); const fullBackupState: { state: TItemState; subtitle?: string } = @@ -148,14 +147,9 @@ const AppStatus = ({}: SettingsScreenProps<'AppStatus'>): ReactElement => { if (!isBackupSyncOk) { return { state: 'error' }; } - const syncTimes = [ - backup.remoteLdkBackupLastSync, - backup.remoteLdkActivityBackupLastSync, - backup.remoteBlocktankBackupLastSync, - backup.remoteSettingsBackupLastSync, - backup.remoteMetadataBackupLastSync, - backup.remoteWidgetsBackupLastSync, - ].filter((i) => i !== undefined) as Array; + const syncTimes = Object.values(EBackupCategories).map((key) => { + return backup[key].synced; + }); const max = Math.max(...syncTimes); let subtitle = tTime('dateTime', { v: new Date(max), diff --git a/src/screens/Settings/Backup/Metadata.tsx b/src/screens/Settings/Backup/Metadata.tsx index 9af554e2f..e32d13935 100644 --- a/src/screens/Settings/Backup/Metadata.tsx +++ b/src/screens/Settings/Backup/Metadata.tsx @@ -12,6 +12,7 @@ import { useAppDispatch, useAppSelector } from '../../../hooks/redux'; import { closeSheet } from '../../../store/slices/ui'; import { backupSelector } from '../../../store/reselect/backup'; import { i18nTime } from '../../../utils/i18n'; +import { EBackupCategories } from '../../../store/utils/backup'; const imageSrc = require('../../../assets/illustrations/tag.png'); @@ -21,16 +22,11 @@ const Metadata = (): ReactElement => { const dispatch = useAppDispatch(); const backup = useAppSelector(backupSelector); - const arr = [ - backup.remoteLdkBackupLastSync, - backup.remoteSettingsBackupLastSync, - backup.remoteWidgetsBackupLastSync, - backup.remoteMetadataBackupLastSync, - backup.remoteLdkActivityBackupLastSync, - backup.remoteBlocktankBackupLastSync, - ].filter((i) => i !== undefined) as Array; - - const max = Math.max(...arr); + const max = Math.max( + ...Object.values(EBackupCategories).map((key) => { + return backup[key].synced; + }), + ); const handleButtonPress = (): void => { dispatch(closeSheet('backupNavigation')); diff --git a/src/screens/Settings/BackupSettings/index.tsx b/src/screens/Settings/BackupSettings/index.tsx index a6463a6fd..6a9e716d5 100644 --- a/src/screens/Settings/BackupSettings/index.tsx +++ b/src/screens/Settings/BackupSettings/index.tsx @@ -1,19 +1,21 @@ -import React, { ReactElement, ReactNode, memo, useMemo, useState } from 'react'; -import { StyleSheet, View } from 'react-native'; +import React, { ReactElement, ReactNode, memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; +import { StyleSheet, View } from 'react-native'; import { EItemType, IListData } from '../../../components/List'; -import SettingsView from '../SettingsView'; import { useAppDispatch, useAppSelector } from '../../../hooks/redux'; -import { showBottomSheet } from '../../../store/utils/ui'; import { SettingsScreenProps } from '../../../navigation/types'; -import { Caption13M, Caption13Up, Text01M } from '../../../styles/text'; +import { backupSelector } from '../../../store/reselect/backup'; +import { lightningBackupSelector } from '../../../store/reselect/lightning'; +import { forceBackup } from '../../../store/slices/backup'; +import { TBackupItem } from '../../../store/types/backup'; +import { EBackupCategories } from '../../../store/utils/backup'; +import { showBottomSheet } from '../../../store/utils/ui'; import { ScrollView, - TouchableOpacity, View as ThemedView, + TouchableOpacity, } from '../../../styles/components'; -import { backupSelector } from '../../../store/reselect/backup'; import { ArrowClockwise, LightningHollow, @@ -21,41 +23,45 @@ import { RectanglesTwo, SettingsIcon, TagIcon, - TransferIcon, UsersIcon, - UserRectangleIcon, } from '../../../styles/icons'; -import { FAILED_BACKUP_CHECK_TIME } from '../../../utils/backup/backups-subscriber'; -import { updateBackup } from '../../../store/slices/backup'; +import { Caption13M, Caption13Up, Text01M } from '../../../styles/text'; +import { IThemeColors } from '../../../styles/themes'; import { i18nTime } from '../../../utils/i18n'; +import SettingsView from '../SettingsView'; const Status = ({ Icon, title, - isSyncedKey, - lastSync, - syncRequired, + status, + category, + disableRetry, }: { Icon: React.FunctionComponent; title: ReactNode; - isSyncedKey?: string; - lastSync?: number; - syncRequired?: number; + status: TBackupItem; + category?: EBackupCategories; + disableRetry?: boolean; }): ReactElement => { const { t } = useTranslation('settings'); const { t: tTime } = useTranslation('intl', { i18n: i18nTime }); const dispatch = useAppDispatch(); - const [hideRetry, setHideRetry] = useState(false); - const failed = - syncRequired && - new Date().getTime() - syncRequired > FAILED_BACKUP_CHECK_TIME; + let subtitle: string; + let iconColor: keyof IThemeColors; + let iconBackground: keyof IThemeColors; + let showRetry = false; - let subtitle; - if (failed) { - subtitle = t('backup.status_failed', { + if (status.running) { + iconColor = 'yellow'; + iconBackground = 'yellow16'; + subtitle = 'Running'; + } else if (status.synced >= status.required) { + iconColor = 'green'; + iconBackground = 'green16'; + subtitle = t('backup.status_success', { time: tTime('dateTime', { - v: new Date(syncRequired), + v: new Date(status.synced), formatParams: { v: { year: 'numeric', @@ -67,10 +73,13 @@ const Status = ({ }, }), }); - } else if (lastSync) { - subtitle = t('backup.status_success', { + } else { + iconColor = 'red'; + iconBackground = 'red16'; + showRetry = true; + subtitle = t('backup.status_failed', { time: tTime('dateTime', { - v: new Date(lastSync), + v: new Date(status.synced), formatParams: { v: { year: 'numeric', @@ -82,30 +91,27 @@ const Status = ({ }, }), }); - } else { - subtitle = t('backup.status_empty'); } const retry = (): void => { - if (isSyncedKey === undefined) { + if (!category) { return; } - setHideRetry(true); - dispatch(updateBackup({ [isSyncedKey]: false })); + dispatch(forceBackup({ category })); }; return ( - - + + {title} {subtitle} - {failed && isSyncedKey && !hideRetry && ( + {!disableRetry && showRetry && ( @@ -114,70 +120,94 @@ const Status = ({ ); }; +type TBackupCategory = { + Icon: React.FunctionComponent; + title: string; + category?: EBackupCategories; + status: TBackupItem; + disableRetry?: boolean; +}; + const BackupSettings = ({ navigation, }: SettingsScreenProps<'BackupSettings'>): ReactElement => { const { t } = useTranslation('settings'); const pin = useAppSelector((state) => state.settings.pin); const backup = useAppSelector(backupSelector); + const lightningBackup = useAppSelector(lightningBackupSelector); - const categories = [ - { - Icon: LightningHollow, - title: t('backup.category_connections'), - isSyncedKey: 'remoteLdkBackupSynced', - lastSync: backup.remoteLdkBackupLastSync, - syncRequired: backup.remoteLdkBackupLastSyncRequired, - }, + // find lightning latest backup item to show + const lightning = useMemo(() => { + const channels = Object.entries(lightningBackup).filter(([key]) => + key.startsWith('channel_'), + ); + if (channels.length === 0) { + return; + } + return channels.reduce((acc, [, value]) => { + return value.lastQueued > acc.lastQueued ? value : acc; + }, channels[0][1]); + }, [lightningBackup]); + + const categories: Array = [ { Icon: NoteIcon, title: t('backup.category_connection_receipts'), - isSyncedKey: 'remoteBlocktankBackupSynced', - lastSync: backup.remoteBlocktankBackupLastSync, - syncRequired: backup.remoteBlocktankBackupSyncRequired, - }, - { - Icon: TransferIcon, - title: t('backup.category_transaction_log'), - isSyncedKey: 'remoteLdkActivityBackupSynced', - lastSync: backup.remoteLdkActivityBackupLastSync, - syncRequired: backup.remoteLdkActivityBackupSyncRequired, + category: EBackupCategories.blocktank, + status: backup[EBackupCategories.blocktank], }, + // { + // Icon: TransferIcon, + // title: t('backup.category_transaction_log'), + // isSyncedKey: 'remoteLdkActivityBackupSynced', + // lastSync: backup.remoteLdkActivityBackupLastSync, + // syncRequired: backup.remoteLdkActivityBackupSyncRequired, + // }, { Icon: SettingsIcon, title: t('backup.category_settings'), - isSyncedKey: 'remoteSettingsBackupSynced', - lastSync: backup.remoteSettingsBackupLastSync, - syncRequired: backup.remoteSettingsBackupSyncRequired, + category: EBackupCategories.settings, + status: backup[EBackupCategories.settings], }, { Icon: RectanglesTwo, title: t('backup.category_widgets'), - isSyncedKey: 'remoteWidgetsBackupSynced', - lastSync: backup.remoteWidgetsBackupLastSync, - syncRequired: backup.remoteWidgetsBackupSyncRequired, + category: EBackupCategories.widgets, + status: backup[EBackupCategories.widgets], }, { Icon: TagIcon, title: t('backup.category_tags'), - isSyncedKey: 'remoteMetadataBackupSynced', - lastSync: backup.remoteMetadataBackupLastSync, - syncRequired: backup.remoteMetadataBackupSyncRequired, - }, - { - Icon: UserRectangleIcon, - title: t('backup.category_profile'), - lastSync: backup.hyperProfileSeedCheckSuccess, - syncRequired: backup.hyperProfileCheckRequested, + category: EBackupCategories.metadata, + status: backup[EBackupCategories.metadata], }, + // { + // Icon: UserRectangleIcon, + // title: t('backup.category_profile'), + // lastSync: backup.hyperProfileSeedCheckSuccess, + // syncRequired: backup.hyperProfileCheckRequested, + // }, { Icon: UsersIcon, title: t('backup.category_contacts'), - lastSync: backup.hyperContactsCheckSuccess, - syncRequired: backup.hyperContactsCheckRequested, + category: EBackupCategories.slashtags, + status: backup[EBackupCategories.slashtags], }, ]; + if (lightning) { + categories.unshift({ + Icon: LightningHollow, + title: t('backup.category_connections'), + status: { + running: false, + synced: lightning.lastPersisted ?? 0, + required: lightning.lastQueued, + }, + disableRetry: true, + }); + } + const settingsListData: IListData[] = useMemo( () => [ { diff --git a/src/store/migrations/index.ts b/src/store/migrations/index.ts index cf0d1b540..7e5c73ed2 100644 --- a/src/store/migrations/index.ts +++ b/src/store/migrations/index.ts @@ -1,22 +1,22 @@ // Add migrations for every persisted store version change +import { EAddressType } from 'beignet'; +import { defaultAddressContent } from 'beignet/src/shapes/wallet'; import { PersistedState } from 'redux-persist'; -import { initialActivityState } from '../slices/activity'; +import { __WEB_RELAY__ } from '../../constants/env'; +import { getDefaultSettings } from '../../screens/Widgets/WidgetEdit'; +import { EAvailableNetwork } from '../../utils/networks'; +import { initialBackupState } from '../shapes/backup'; import { defaultBlocktankInfoShape } from '../shapes/blocktank'; import { initialTodosState } from '../shapes/todos'; import { defaultViewControllers } from '../shapes/ui'; -import { initialChecksState } from '../slices/checks'; -import { initialBackupState } from '../shapes/backup'; -import { initialWidgetsState } from '../slices/widgets'; import { getDefaultWalletStoreShape, getNetworkContent, } from '../shapes/wallet'; -import { getDefaultSettings } from '../../screens/Widgets/WidgetEdit'; -import { __WEB_RELAY__ } from '../../constants/env'; -import { EAvailableNetwork } from '../../utils/networks'; -import { defaultAddressContent } from 'beignet/src/shapes/wallet'; -import { EAddressType } from 'beignet'; +import { initialActivityState } from '../slices/activity'; +import { initialChecksState } from '../slices/checks'; +import { initialWidgetsState } from '../slices/widgets'; const migrations = { 0: (state): PersistedState => { @@ -432,6 +432,28 @@ const migrations = { }, }; }, + 35: (state): PersistedState => { + const newNodes = { ...state.lightning.nodes }; + // Loop through all nodes + for (const walletName in newNodes) { + newNodes[walletName] = { + ...newNodes[walletName], + backup: getNetworkContent({}), + }; + } + + return { + ...state, + backup: { + ...initialBackupState, + ...state.backup, + }, + lightning: { + ...state.lightning, + nodes: newNodes, + }, + }; + }, }; export default migrations; diff --git a/src/store/reselect/lightning.ts b/src/store/reselect/lightning.ts index 422eaf2e7..58eaf77a9 100644 --- a/src/store/reselect/lightning.ts +++ b/src/store/reselect/lightning.ts @@ -219,3 +219,11 @@ export const lightningBalanceSelector = createSelector( }; }, ); + +export const lightningBackupSelector = createSelector( + [lightningState, selectedWalletSelector, selectedNetworkSelector], + (lightning, selectedWallet, selectedNetwork) => { + const node = lightning.nodes[selectedWallet]; + return node?.backup[selectedNetwork] ?? {}; + }, +); diff --git a/src/store/shapes/backup.ts b/src/store/shapes/backup.ts index 6f7ba41d5..a64e03b46 100644 --- a/src/store/shapes/backup.ts +++ b/src/store/shapes/backup.ts @@ -1,36 +1,16 @@ -import { TBackupState } from '../types/backup'; +import { TBackupItem, TBackupState } from '../types/backup'; +import { EBackupCategories } from '../utils/backup'; -export const initialBackupState: TBackupState = { - remoteBackupsEnabled: false, - remoteLdkBackupSynced: false, - remoteLdkBackupLastSyncRequired: undefined, - remoteSettingsBackupSynced: false, - remoteSettingsBackupLastSync: undefined, - remoteSettingsBackupSyncRequired: undefined, - remoteWidgetsBackupSynced: false, - remoteWidgetsBackupLastSync: undefined, - remoteWidgetsBackupSyncRequired: undefined, - remoteMetadataBackupSynced: false, - remoteMetadataBackupLastSync: undefined, - remoteMetadataBackupSyncRequired: undefined, - remoteLdkActivityBackupSynced: false, - remoteLdkActivityBackupLastSync: undefined, - remoteLdkActivityBackupSyncRequired: undefined, - remoteBlocktankBackupSynced: false, - remoteBlocktankBackupLastSync: undefined, - remoteBlocktankBackupSyncRequired: undefined, - remoteSlashtagsBackupSynced: false, - remoteSlashtagsBackupLastSync: undefined, - remoteSlashtagsBackupSyncRequired: undefined, - - hyperProfileSeedCheckSuccess: undefined, - hyperProfileCheckRequested: undefined, - hyperContactsCheckSuccess: undefined, - hyperContactsCheckRequested: undefined, - - iCloudBackupsEnabled: false, - iCloudLdkBackupsSynced: false, +const item: TBackupItem = { + required: Date.now() - 1000, + synced: Date.now(), + running: false, +}; - gDriveBackupsEnabled: false, - gDriveLdkBackupsSynced: false, +export const initialBackupState: TBackupState = { + [EBackupCategories.widgets]: { ...item }, + [EBackupCategories.settings]: { ...item }, + [EBackupCategories.metadata]: { ...item }, + [EBackupCategories.blocktank]: { ...item }, + [EBackupCategories.slashtags]: { ...item }, }; diff --git a/src/store/shapes/lightning.ts b/src/store/shapes/lightning.ts index bd23617a7..ab9239ad5 100644 --- a/src/store/shapes/lightning.ts +++ b/src/store/shapes/lightning.ts @@ -8,6 +8,7 @@ export const defaultLightningShape: TNode = { openChannelIds: getNetworkContent([]), peers: getNetworkContent([]), claimableBalances: getNetworkContent([]), + backup: getNetworkContent({}), }; export const initialLightningState: TLightningState = { diff --git a/src/store/slices/backup.ts b/src/store/slices/backup.ts index f4752a509..a9dfda563 100644 --- a/src/store/slices/backup.ts +++ b/src/store/slices/backup.ts @@ -1,11 +1,8 @@ import { PayloadAction, createSlice } from '@reduxjs/toolkit'; -import { updateActivityItems } from './activity'; -import { addPaidBlocktankOrder, updateBlocktankOrder } from './blocktank'; -import { updateSettings } from './settings'; -import { addContact, addContacts, deleteContact } from './slashtags'; -import { setFeedWidget } from './widgets'; + import { initialBackupState } from '../shapes/backup'; -import { TBackupState } from '../types/backup'; +import { EBackupCategories } from '../utils/backup'; +import { addPaidBlocktankOrder, updateBlocktankOrder } from './blocktank'; import { addLastUsedTag, addMetaTxSlashtagsUrl, @@ -18,68 +15,49 @@ import { updateMetaTxTags, updatePendingInvoice, } from './metadata'; -import { EActivityType } from '../types/activity'; +import { updateSettings } from './settings'; +import { addContact, addContacts, deleteContact } from './slashtags'; +import { setFeedWidget } from './widgets'; export const backupSlice = createSlice({ name: 'backup', initialState: initialBackupState, reducers: { - updateBackup: (state, action: PayloadAction>) => { - state = Object.assign(state, action.payload); + resetBackupState: () => initialBackupState, + backupStart: (state, action: PayloadAction<{ category: string }>) => { + const { category } = action.payload; + state[category].running = true; }, - startBackupSeederCheck: (state) => { - state.hyperProfileCheckRequested = - state.hyperProfileCheckRequested ?? new Date().getTime(); - state.hyperContactsCheckRequested = - state.hyperContactsCheckRequested ?? new Date().getTime(); + backupSuccess: (state, action: PayloadAction<{ category: string }>) => { + const { category } = action.payload; + state[category].running = false; + state[category].synced = Date.now(); }, - endBackupSeederCheck: ( - state, - action: PayloadAction<{ - profile: boolean; - contacts: boolean; - }>, - ) => { - state.hyperProfileCheckRequested = action.payload.profile - ? undefined - : state.hyperProfileCheckRequested; - state.hyperContactsCheckRequested = action.payload.contacts - ? undefined - : state.hyperContactsCheckRequested; - state.hyperProfileSeedCheckSuccess = action.payload.profile - ? new Date().getTime() - : state.hyperProfileSeedCheckSuccess; - state.hyperContactsCheckSuccess = action.payload.contacts - ? new Date().getTime() - : state.hyperContactsCheckSuccess; + backupError: (state, action: PayloadAction<{ category: string }>) => { + const { category } = action.payload; + state[category].running = false; + }, + forceBackup: (state, action: PayloadAction<{ category: string }>) => { + const { category } = action.payload; + state[category].required = Date.now(); + state[category].running = true; }, - resetBackupState: () => initialBackupState, }, extraReducers: (builder) => { const blocktankReducer = (state): void => { - state.remoteBlocktankBackupSynced = false; - state.remoteBlocktankBackupSyncRequired = - state.remoteBlocktankBackupSyncRequired ?? new Date().getTime(); + state[EBackupCategories.blocktank].required = Date.now(); }; const metadataReducer = (state): void => { - state.remoteMetadataBackupSynced = false; - state.remoteMetadataBackupSyncRequired = - state.remoteMetadataBackupSyncRequired ?? new Date().getTime(); + state[EBackupCategories.metadata].required = Date.now(); }; const settingsReducer = (state): void => { - state.remoteSettingsBackupSynced = false; - state.remoteSettingsBackupSyncRequired = - state.remoteSettingsBackupSyncRequired ?? new Date().getTime(); + state[EBackupCategories.settings].required = Date.now(); }; const slashtagsReducer = (state): void => { - state.remoteSlashtagsBackupSynced = false; - state.remoteSlashtagsBackupSyncRequired = - state.remoteSlashtagsBackupSyncRequired ?? new Date().getTime(); + state[EBackupCategories.slashtags].required = Date.now(); }; const widgetsReducer = (state): void => { - state.remoteWidgetsBackupSynced = false; - state.remoteWidgetsBackupSyncRequired = - state.remoteWidgetsBackupSyncRequired ?? new Date().getTime(); + state[EBackupCategories.widgets].required = Date.now(); }; builder @@ -99,28 +77,29 @@ export const backupSlice = createSlice({ .addCase(addContact, slashtagsReducer) .addCase(addContacts, slashtagsReducer) .addCase(deleteContact, slashtagsReducer) - .addCase(setFeedWidget, widgetsReducer) - .addCase(updateActivityItems, (state, action) => { - // we only listen for LN activity here - const hasLnActivity = action.payload.some( - (item) => item.activityType === EActivityType.lightning, - ); - if (hasLnActivity) { - state.remoteLdkActivityBackupSynced = false; - state.remoteLdkActivityBackupSyncRequired = - state.remoteLdkActivityBackupSyncRequired ?? new Date().getTime(); - } - }); + .addCase(setFeedWidget, widgetsReducer); + // .addCase(updateActivityItems, (state, action) => { + // // we only listen for LN activity here + // const hasLnActivity = action.payload.some( + // (item) => item.activityType === EActivityType.lightning, + // ); + // if (hasLnActivity) { + // state.remoteLdkActivityBackupSynced = false; + // state.remoteLdkActivityBackupSyncRequired = + // state.remoteLdkActivityBackupSyncRequired ?? new Date().getTime(); + // } + // }); }, }); const { actions, reducer } = backupSlice; export const { - updateBackup, - startBackupSeederCheck, - endBackupSeederCheck, resetBackupState, + backupStart, + backupSuccess, + backupError, + forceBackup, } = actions; export default reducer; diff --git a/src/store/slices/lightning.ts b/src/store/slices/lightning.ts index 2c234cfb0..c951a0f40 100644 --- a/src/store/slices/lightning.ts +++ b/src/store/slices/lightning.ts @@ -1,5 +1,8 @@ import { PayloadAction, createSlice } from '@reduxjs/toolkit'; -import { TClaimableBalance } from '@synonymdev/react-native-ldk'; +import { + TBackupStateUpdate, + TClaimableBalance, +} from '@synonymdev/react-native-ldk'; import { initialLightningState } from '../shapes/lightning'; import { EAvailableNetwork } from '../../utils/networks'; @@ -87,6 +90,17 @@ export const lightningSlice = createSlice({ state.nodes[selectedWallet].claimableBalances[selectedNetwork] = claimableBalances; }, + updateBackupState: ( + state, + action: PayloadAction<{ + backup: TBackupStateUpdate; + selectedWallet: TWalletName; + selectedNetwork: EAvailableNetwork; + }>, + ) => { + const { backup, selectedWallet, selectedNetwork } = action.payload; + state.nodes[selectedWallet].backup[selectedNetwork] = backup; + }, updateLdkAccountVersion: ( state, action: PayloadAction, @@ -106,6 +120,7 @@ export const { saveLightningPeer, removeLightningPeer, updateClaimableBalances, + updateBackupState, updateLdkAccountVersion, resetLightningState, } = actions; diff --git a/src/store/types/backup.ts b/src/store/types/backup.ts index a4271a7fe..0dddfb7f3 100644 --- a/src/store/types/backup.ts +++ b/src/store/types/backup.ts @@ -1,47 +1,14 @@ import { ENetworks, TAccount } from '@synonymdev/react-native-ldk'; +import { EBackupCategories } from '../utils/backup'; -export type TBackupState = { - //Backpack - remoteBackupsEnabled: boolean; - remoteLdkBackupSynced: boolean; - remoteLdkBackupLastSync?: number; - remoteLdkBackupLastSyncRequired?: number; - remoteSettingsBackupSynced: boolean; - remoteSettingsBackupLastSync?: number; - remoteSettingsBackupSyncRequired?: number; - remoteWidgetsBackupSynced: boolean; - remoteWidgetsBackupLastSync?: number; - remoteWidgetsBackupSyncRequired?: number; - remoteMetadataBackupSynced: boolean; - remoteMetadataBackupLastSync?: number; - remoteMetadataBackupSyncRequired?: number; - remoteLdkActivityBackupSynced: boolean; - remoteLdkActivityBackupLastSync?: number; - remoteLdkActivityBackupSyncRequired?: number; - remoteBlocktankBackupSynced: boolean; - remoteBlocktankBackupLastSync?: number; - remoteBlocktankBackupSyncRequired?: number; - remoteSlashtagsBackupSynced: boolean; - remoteSlashtagsBackupLastSync?: number; - remoteSlashtagsBackupSyncRequired?: number; - - //Hyperdrives - hyperProfileSeedCheckSuccess?: number; - hyperProfileCheckRequested?: number; - hyperContactsCheckSuccess?: number; - hyperContactsCheckRequested?: number; - - //iCloud - iCloudBackupsEnabled: boolean; - iCloudLdkBackupsSynced: boolean; - iCloudLdkBackupLastSync?: number; - //TODO transactions, slashtags, metadata, etc. +export type TBackupItem = { + running: boolean; + required: number; // timestamp of last time this backup was required + synced: number; // timestamp of last time this backup was synced +}; - //Google Drive - gDriveBackupsEnabled: boolean; - gDriveLdkBackupsSynced: boolean; - gDriveLdkBackupLastSync?: number; - //TODO transactions, slashtags, metadata, etc. +export type TBackupState = { + [key in EBackupCategories]: TBackupItem; }; export declare type TAccountBackup = { @@ -50,3 +17,9 @@ export declare type TAccountBackup = { network: ENetworks; data: T; }; + +export type TBackupMetadata = { + category: EBackupCategories; + timestamp: number; + version: number; +}; diff --git a/src/store/types/lightning.ts b/src/store/types/lightning.ts index 989211611..e27a0d0f2 100644 --- a/src/store/types/lightning.ts +++ b/src/store/types/lightning.ts @@ -1,4 +1,5 @@ import { + TBackupStateUpdate, TChannel, TClaimableBalance, TCreatePaymentReq, @@ -27,6 +28,7 @@ export type TNode = { info: IWalletItem<{}>; peers: IWalletItem; claimableBalances: IWalletItem; + backup: IWalletItem; }; export type TNodes = { diff --git a/src/store/utils/backup.ts b/src/store/utils/backup.ts index 55709c50a..c9a667205 100644 --- a/src/store/utils/backup.ts +++ b/src/store/utils/backup.ts @@ -1,188 +1,65 @@ +import lm, { + ENetworks, + ldk, + TBackupServerDetails, + TLdkData, +} from '@synonymdev/react-native-ldk'; import { err, ok, Result } from '@synonymdev/result'; -import lm, { ldk, ENetworks, TLdkData } from '@synonymdev/react-native-ldk'; -import { getBackupStore, dispatch } from '../helpers'; import { - EBackupCategories, + __BACKUPS_SERVER_HOST__, + __BACKUPS_SERVER_PUBKEY__, +} from '../../constants/env'; +import { Slashtag } from '../../hooks/slashtags'; +import { + EBackupCategoriesOld, fetchBackup, listBackups, - uploadBackup, } from '../../utils/backup/backpack'; -import { bytesToString, stringToBytes } from '../../utils/converters'; -import { Slashtag } from '../../hooks/slashtags'; +import { bytesToString } from '../../utils/converters'; +import { isObjPartialMatch } from '../../utils/helpers'; import { checkAccountVersion, - exportBackup, getLdkAccount, setAccount, setLdkStoragePath, } from '../../utils/lightning'; import { EAvailableNetwork } from '../../utils/networks'; import { getSelectedNetwork } from '../../utils/wallet'; -import { TBackupState, TAccountBackup } from '../types/backup'; -import { isObjPartialMatch } from '../../utils/helpers'; -import { getDefaultSettingsShape } from '../shapes/settings'; -import { addActivityItems, TActivity } from '../slices/activity'; -import { initialMetadataState, updateMetadata } from '../slices/metadata'; -import { updateSettings, TSettings } from '../slices/settings'; import { - updateWidgets, - initialWidgetsState, - TWidgetsState, -} from '../slices/widgets'; + dispatch, + getBlocktankStore, + getMetaDataStore, + getSettingsStore, + getSlashtagsStore, + getStore, + getWidgetsStore, +} from '../helpers'; +import { getDefaultSettingsShape } from '../shapes/settings'; +import { backupError, backupStart, backupSuccess } from '../slices/backup'; import { updateBlocktank } from '../slices/blocktank'; +import { initialMetadataState, updateMetadata } from '../slices/metadata'; +import { TSettings, updateSettings } from '../slices/settings'; import { addContacts } from '../slices/slashtags'; -import { EActivityType } from '../types/activity'; +import { updateWidgets } from '../slices/widgets'; +import { TAccountBackup, TBackupMetadata } from '../types/backup'; import { IBlocktank } from '../types/blocktank'; import { TMetadataState } from '../types/metadata'; -import { checkBackup } from '../../utils/slashtags'; -import { showToast } from '../../utils/notifications'; -import { FAILED_BACKUP_CHECK_TIME } from '../../utils/backup/backups-subscriber'; -import i18n from '../../utils/i18n'; import { TSlashtagsState } from '../types/slashtags'; -import { - __BACKUPS_SERVER_HOST__, - __BACKUPS_SERVER_PUBKEY__, -} from '../../constants/env'; -import { - endBackupSeederCheck, - startBackupSeederCheck, - updateBackup, -} from '../slices/backup'; - -/** - * Triggers a full remote backup - * @return {Promise>} - */ -export const performFullBackup = async ( - slashtag: Slashtag, -): Promise> => { - const ldkRemoteRes = await performRemoteLdkBackup(slashtag); - //TODO perform other backup types - - //TODO(slashtags): Send all drives (public + contacts) to the seeding server. - //TODO check results of each time and return errors if any - - if (ldkRemoteRes.isErr()) { - return err(ldkRemoteRes.error); - } - - return ok('Backup success'); -}; - -export const performRemoteLdkBackup = async ( - slashtag: Slashtag, - backup?: TAccountBackup, -): Promise> => { - dispatch(updateBackup({ remoteLdkBackupSynced: false })); - - let ldkBackup: TAccountBackup; - //Automated backup events pass the latest state through - if (backup) { - ldkBackup = backup; - } else { - const res = await exportBackup(); - if (res.isErr()) { - return err(res.error); - } - - ldkBackup = res.value; - } - - //Translate LDK type to our wallet type - let network = EAvailableNetwork.bitcoin; - switch (ldkBackup.network) { - case ENetworks.regtest: { - network = EAvailableNetwork.bitcoinRegtest; - break; - } - case ENetworks.testnet: { - network = EAvailableNetwork.bitcoinTestnet; - break; - } - case ENetworks.mainnet: { - network = EAvailableNetwork.bitcoin; - break; - } - } - - const res = await uploadBackup( - slashtag, - stringToBytes(JSON.stringify(backup)), - EBackupCategories.ldkComplete, - network, - ); - - if (res.isErr()) { - return err(res.error); - } - - dispatch( - updateBackup({ - remoteLdkBackupSynced: true, - remoteLdkBackupLastSync: new Date().getTime(), - remoteLdkBackupLastSyncRequired: undefined, - }), - ); - - return ok('Backup success'); -}; - -export const performRemoteBackup = async ({ - slashtag, - isSyncedKey, - syncRequiredKey, - syncCompletedKey, - backupCategory, - backup, - selectedNetwork, -}: { - slashtag: Slashtag; - isSyncedKey: keyof TBackupState; - syncRequiredKey: keyof TBackupState; - syncCompletedKey: keyof TBackupState; - backupCategory: EBackupCategories; - backup?: T; - selectedNetwork?: EAvailableNetwork; -}): Promise> => { - //Automated backup events pass the latest state through - if (!backup) { - return ok('Nothing to backup.'); - } - - if (!selectedNetwork) { - selectedNetwork = getSelectedNetwork(); - } - - const backupJson = JSON.stringify(backup); - const bytes = stringToBytes(backupJson); - - const res = await uploadBackup( - slashtag, - bytes, - backupCategory, - selectedNetwork, - ); - - if (res.isErr()) { - return err(res.error); - } - - dispatch( - updateBackup({ - [isSyncedKey]: true, - [syncRequiredKey]: undefined, - [syncCompletedKey]: new Date().getTime(), - }), - ); - - return ok('Backup success'); -}; +export enum EBackupCategories { + settings = 'bitkit_settings', + widgets = 'bitkit_widgets', + metadata = 'bitkit_metadata', + blocktank = 'bitkit_blocktank_orders', + slashtags = 'bitkit_slashtags_contacts', +} export const performLdkRestore = async ({ + backupServerDetails, selectedNetwork, }: { + backupServerDetails: TBackupServerDetails; selectedNetwork?: EAvailableNetwork; }): Promise> => { if (!selectedNetwork) { @@ -213,10 +90,6 @@ export const performLdkRestore = async ({ break; } - const backupServerDetails = { - host: __BACKUPS_SERVER_HOST__, - serverPubKey: __BACKUPS_SERVER_PUBKEY__, - }; const backupSetupRes = await ldk.backupSetup({ seed: lightningAccount.value.seed, network, @@ -269,7 +142,7 @@ export const performLdkRestoreDeprecated = async ({ } const res = await listBackups( slashtag, - EBackupCategories.ldkComplete, + EBackupCategoriesOld.ldkComplete, selectedNetwork, ); if (res.isErr()) { @@ -284,7 +157,7 @@ export const performLdkRestoreDeprecated = async ({ const fetchRes = await fetchBackup( slashtag, res.value[0].timestamp, - EBackupCategories.ldkComplete, + EBackupCategoriesOld.ldkComplete, selectedNetwork, ); if (fetchRes.isErr()) { @@ -312,419 +185,313 @@ export const performLdkRestoreDeprecated = async ({ return ok({ backupExists: true }); }; -/** - * Retrieves the backup data for the provided backupCategory. - * @param {Slashtag} slashtag - * @param {EBackupCategories} backupCategory - * @param {EAvailableNetwork} [selectedNetwork] - * @returns {Promise>} - */ -export const getBackup = async ({ - slashtag, - backupCategory, - selectedNetwork, -}: { - slashtag: Slashtag; - backupCategory: EBackupCategories; - selectedNetwork?: EAvailableNetwork; -}): Promise> => { - if (!selectedNetwork) { - selectedNetwork = getSelectedNetwork(); - } - const res = await listBackups(slashtag, backupCategory, selectedNetwork); - if (res.isErr()) { - return err(res.error); - } - - // No backup exists for the provided slashtag. - if (res.value.length === 0) { - return ok(undefined); - } - - const fetchRes = await fetchBackup( - slashtag, - res.value[0].timestamp, - backupCategory, - selectedNetwork, - ); - if (fetchRes.isErr()) { - return err(fetchRes.error); - } - - let jsonString = bytesToString(fetchRes.value.content); +export const performFullRestoreFromLatestBackup = async ( + slashtag: Slashtag, +): Promise> => { + try { + const backupServerDetails = { + host: __BACKUPS_SERVER_HOST__, + serverPubKey: __BACKUPS_SERVER_PUBKEY__, + }; - if ( - backupCategory === EBackupCategories.ldkActivity || - backupCategory === EBackupCategories.metadata - ) { - // Remove previously incorrectly encoded emojis from the backup - // eslint-disable-next-line no-control-regex - jsonString = jsonString.replace(/([\u0000-\u001F])/g, ''); - } + // ldk restore should be performed for all networks + for (const network of Object.values(EAvailableNetwork)) { + const ldkBackupRes = await performLdkRestore({ + backupServerDetails, + selectedNetwork: network, + }); + if (ldkBackupRes.isErr()) { + return err(ldkBackupRes.error.message); + } - const backup: T = JSON.parse(jsonString); + //No backup found on new server, try deprecated backup server + if (!ldkBackupRes.value.backupExists) { + const ldkBackupDeprecatedRes = await performLdkRestoreDeprecated({ + slashtag, + selectedNetwork: network, + }); - // Restore success - return ok(backup); -}; + if (ldkBackupDeprecatedRes.isErr()) { + return err(ldkBackupDeprecatedRes.error.message); + } + } + } -export const performSettingsRestore = async ({ - slashtag, - selectedNetwork, -}: { - slashtag: Slashtag; - selectedNetwork?: EAvailableNetwork; -}): Promise> => { - if (!selectedNetwork) { - selectedNetwork = getSelectedNetwork(); - } + // reset backup settings once again before restoring all other backups + const selectedNetwork = getSelectedNetwork(); + let network: ENetworks; + switch (selectedNetwork) { + case 'bitcoin': + network = ENetworks.mainnet; + break; + case 'bitcoinTestnet': + network = ENetworks.testnet; + break; + default: + network = ENetworks.regtest; + break; + } + const version = await checkAccountVersion(); + const lightningAccount = await getLdkAccount({ selectedNetwork, version }); + if (lightningAccount.isErr()) { + return err(lightningAccount.error); + } + const backupSetupRes = await ldk.backupSetup({ + seed: lightningAccount.value.seed, + network, + details: backupServerDetails, + }); - const backupRes = await getBackup({ - slashtag, - backupCategory: EBackupCategories.settings, - selectedNetwork, - }); - if (backupRes.isErr()) { - return err(backupRes.error.message); - } + if (backupSetupRes.isErr()) { + return err(backupSetupRes.error); + } - const backup = backupRes.value; - if (!backup) { - return ok({ backupExists: false }); - } + const backups = [ + ['settings', performSettingsRestore], + ['widgets', performWidgetsRestore], + ['metadata', performMetadataRestore], + ['blocktank', performBlocktankRestore], + ['slashtags', performSlashtagsRestore], + ] as const; + + for (const [name, func] of backups) { + const res = await func(); + if (res.isErr()) { + // Since this backup feature is not critical and mostly for user convenience + // there's no reason to throw an error here. + console.log(`Error restoring ${name}`, res.error.message); + } + } - const expectedBackupShape = getDefaultSettingsShape(); - //If the keys in the backup object are not found in the reference object assume the backup does not exist. - if (!isObjPartialMatch(backup, expectedBackupShape)) { - return ok({ backupExists: false }); + // Restore success + return ok({ backupExists: true }); + } catch (e) { + console.log(e); + return err(e); } - - dispatch( - updateSettings({ - ...expectedBackupShape, - ...backup, - biometrics: false, - pin: false, - pinForPayments: false, - pinOnLaunch: true, - }), - ); - dispatch(updateBackup({ remoteSettingsBackupSynced: true })); - - // Restore success - return ok({ backupExists: true }); }; -export const performWidgetsRestore = async ({ - slashtag, - selectedNetwork, -}: { - slashtag: Slashtag; - selectedNetwork?: EAvailableNetwork; -}): Promise> => { - if (!selectedNetwork) { - selectedNetwork = getSelectedNetwork(); - } - const backupRes = await getBackup({ - slashtag, - backupCategory: EBackupCategories.widgets, - selectedNetwork, - }); - if (backupRes.isErr()) { - return err(backupRes.error.message); - } - - const backup = backupRes.value; - if (!backup) { - return ok({ backupExists: false }); - } - - const expectedBackupShape = initialWidgetsState; - //If the keys in the backup object are not found in the reference object assume the backup does not exist. - if (!isObjPartialMatch(backup, expectedBackupShape, ['widgets'])) { - return ok({ backupExists: false }); - } - - dispatch( - updateWidgets({ - ...expectedBackupShape, - ...backup, - onboardedWidgets: true, - }), - ); - dispatch(updateBackup({ remoteWidgetsBackupSynced: true })); +export const performBackup = async ( + category: EBackupCategories, +): Promise> => { + try { + let data: {}; + switch (category) { + case EBackupCategories.settings: + data = getSettingsStore(); + break; + case EBackupCategories.widgets: + data = getWidgetsStore(); + break; + case EBackupCategories.metadata: + data = getMetaDataStore(); + break; + case EBackupCategories.blocktank: + const { paidOrders, orders } = getBlocktankStore(); + data = { paidOrders, orders }; + break; + case EBackupCategories.slashtags: + const { contacts } = getSlashtagsStore(); + data = { contacts }; + break; + } - // Restore success - return ok({ backupExists: true }); -}; + const metadata: TBackupMetadata = { + category, + timestamp: Date.now(), + version: getStore()._persist.version, + }; -export const performMetadataRestore = async ({ - slashtag, - selectedNetwork, -}: { - slashtag: Slashtag; - selectedNetwork?: EAvailableNetwork; -}): Promise> => { - if (!selectedNetwork) { - selectedNetwork = getSelectedNetwork(); - } + const content = JSON.stringify({ data, metadata }); - const backupRes = await getBackup({ - slashtag, - backupCategory: EBackupCategories.metadata, - selectedNetwork, - }); - if (backupRes.isErr()) { - return err(backupRes.error.message); + dispatch(backupStart({ category })); + const backupRes = await ldk.backupFile(category, content); + if (backupRes.isErr()) { + throw backupRes.error; + } + dispatch(backupSuccess({ category })); + return ok(`Backup ${category} success`); + } catch (e) { + console.log(`Backup ${category} error`, e.message); + dispatch(backupError({ category })); + return err(e); } +}; - const backup = backupRes.value; - - if (!backup) { - return ok({ backupExists: false }); - } +/** + * Retrieves the backup data for the provided backupCategory. + * @param {EBackupCategories} category + * @returns {Promise>} + */ +const getBackup = async ( + category: EBackupCategories, +): Promise> => { + try { + const fetchRes = await ldk.fetchBackupFile(category); + if (fetchRes.isErr()) { + return err(fetchRes.error); + } - const expectedBackupShape = initialMetadataState; - //If the keys in the backup object are not found in the reference object assume the backup does not exist. - if ( - !isObjPartialMatch(backup, expectedBackupShape, ['tags', 'slashTagsUrls']) - ) { - return ok({ backupExists: false }); + const content = JSON.parse(fetchRes.value); + return ok(content); + } catch (e) { + console.log(`GetBackup ${category} error`, e.message); + return err(e); } - - dispatch(updateMetadata({ ...expectedBackupShape, ...backup })); - dispatch(updateBackup({ remoteMetadataBackupSynced: true })); - - // Restore success - return ok({ backupExists: true }); }; -export const performLdkActivityRestore = async ({ - slashtag, - selectedNetwork, -}: { - slashtag: Slashtag; - selectedNetwork?: EAvailableNetwork; -}): Promise> => { - if (!selectedNetwork) { - selectedNetwork = getSelectedNetwork(); - } - - const backupRes = await getBackup({ - slashtag, - backupCategory: EBackupCategories.ldkActivity, - selectedNetwork, - }); - if (backupRes.isErr()) { - return err(backupRes.error.message); - } +const performSettingsRestore = async (): Promise< + Result<{ backupExists: boolean }> +> => { + try { + const backupRes = await getBackup(EBackupCategories.settings); + if (backupRes.isErr()) { + return err(backupRes.error.message); + } - const backup = backupRes.value; + const backup = backupRes.value.data; + const expectedBackupShape = getDefaultSettingsShape(); + //If the keys in the backup object are not found in the reference object assume the backup does not exist. + if (!isObjPartialMatch(backup, expectedBackupShape)) { + return ok({ backupExists: false }); + } - if (!backup) { - return ok({ backupExists: false }); - } + dispatch( + updateSettings({ + ...expectedBackupShape, + ...backup, + biometrics: false, + pin: false, + pinForPayments: false, + pinOnLaunch: true, + }), + ); + dispatch(backupSuccess({ category: EBackupCategories.settings })); - if ( - !( - Array.isArray(backup) && - backup.every((i) => i.activityType === EActivityType.lightning) - ) - ) { - return ok({ backupExists: false }); + // Restore success + return ok({ backupExists: true }); + } catch (e) { + console.log(`Restore ${EBackupCategories.settings} error`, e.message); + return err(e); } - - dispatch(addActivityItems(backup)); - dispatch(updateBackup({ remoteLdkActivityBackupSynced: true })); - - // Restore success - return ok({ backupExists: true }); }; -export const performBlocktankRestore = async ({ - slashtag, - selectedNetwork, -}: { - slashtag: Slashtag; - selectedNetwork?: EAvailableNetwork; -}): Promise> => { - if (!selectedNetwork) { - selectedNetwork = getSelectedNetwork(); - } - - const backupRes = await getBackup>({ - slashtag, - backupCategory: EBackupCategories.blocktank, - selectedNetwork, - }); - if (backupRes.isErr()) { - return err(backupRes.error.message); - } +const performWidgetsRestore = async (): Promise< + Result<{ backupExists: boolean }> +> => { + try { + const backupRes = await getBackup(EBackupCategories.widgets); + if (backupRes.isErr()) { + return err(backupRes.error.message); + } - const backup = backupRes.value; + const backup = backupRes.value.data; + const expectedBackupShape = getDefaultSettingsShape(); + //If the keys in the backup object are not found in the reference object assume the backup does not exist. + if (!isObjPartialMatch(backup, expectedBackupShape, ['widgets'])) { + return ok({ backupExists: false }); + } - if (!backup) { - return ok({ backupExists: false }); - } + dispatch( + updateWidgets({ + ...expectedBackupShape, + ...backup, + onboardedWidgets: true, + }), + ); + dispatch(backupSuccess({ category: EBackupCategories.widgets })); - if (!('orders' in backup && 'paidOrders' in backup)) { - return ok({ backupExists: false }); + // Restore success + return ok({ backupExists: true }); + } catch (e) { + console.log(`Restore ${EBackupCategories.settings} error`, e.message); + return err(e); } - - dispatch(updateBlocktank(backup)); - dispatch(updateBackup({ remoteBlocktankBackupSynced: true })); - - // Restore success - return ok({ backupExists: true }); }; -export const performSlashtagsRestore = async ({ - slashtag, - selectedNetwork, -}: { - slashtag: Slashtag; - selectedNetwork?: EAvailableNetwork; -}): Promise> => { - if (!selectedNetwork) { - selectedNetwork = getSelectedNetwork(); - } - - const backupRes = await getBackup>({ - slashtag, - backupCategory: EBackupCategories.slashtags, - selectedNetwork, - }); - if (backupRes.isErr()) { - return err(backupRes.error.message); - } +const performMetadataRestore = async (): Promise< + Result<{ backupExists: boolean }> +> => { + try { + const backupRes = await getBackup( + EBackupCategories.metadata, + ); + if (backupRes.isErr()) { + return err(backupRes.error.message); + } - const backup = backupRes.value; + const backup = backupRes.value.data; + const expectedBackupShape = initialMetadataState; + //If the keys in the backup object are not found in the reference object assume the backup does not exist. + if ( + !isObjPartialMatch(backup, expectedBackupShape, ['tags', 'slashTagsUrls']) + ) { + return ok({ backupExists: false }); + } - if (!backup) { - return ok({ backupExists: false }); - } + dispatch(updateMetadata({ ...expectedBackupShape, ...backup })); + dispatch(backupSuccess({ category: EBackupCategories.metadata })); - if (!('contacts' in backup)) { - return ok({ backupExists: false }); + // Restore success + return ok({ backupExists: true }); + } catch (e) { + console.log(`Restore ${EBackupCategories.settings} error`, e.message); + return err(e); } - - dispatch(addContacts(backup.contacts!)); - dispatch(updateBackup({ remoteSlashtagsBackupSynced: true })); - - // Restore success - return ok({ backupExists: true }); }; -export const performFullRestoreFromLatestBackup = async ( - slashtag: Slashtag, -): Promise> => { +const performBlocktankRestore = async (): Promise< + Result<{ backupExists: boolean }> +> => { try { - // ldk restore should be performed for all networks - for (const network of Object.values(EAvailableNetwork)) { - const ldkBackupRes = await performLdkRestore({ - selectedNetwork: network, - }); - if (ldkBackupRes.isErr()) { - return err(ldkBackupRes.error.message); - } - - //No backup found on new server, try deprecated backup server - if (!ldkBackupRes.value.backupExists) { - const ldkBackupDeprecatedRes = await performLdkRestoreDeprecated({ - slashtag, - selectedNetwork: network, - }); - - if (ldkBackupDeprecatedRes.isErr()) { - return err(ldkBackupDeprecatedRes.error.message); - } - } + const backupRes = await getBackup>( + EBackupCategories.blocktank, + ); + if (backupRes.isErr()) { + return err(backupRes.error.message); } - const selectedNetwork = getSelectedNetwork(); - - const settingsBackupRes = await performSettingsRestore({ - slashtag, - selectedNetwork, - }); - if (settingsBackupRes.isErr()) { - //Since this backup feature is not critical and mostly for user convenience there's no reason to throw an error here. - console.log('Error backing up settings', settingsBackupRes.error.message); + const backup = backupRes.value.data; + //If the keys in the backup object are not found in the reference object assume the backup does not exist. + if (!('orders' in backup && 'paidOrders' in backup)) { + return ok({ backupExists: false }); } - const widgetsBackupRes = await performWidgetsRestore({ - slashtag, - selectedNetwork, - }); - if (widgetsBackupRes.isErr()) { - //Since this backup feature is not critical and mostly for user convenience there's no reason to throw an error here. - console.log('Error backing up widgets', widgetsBackupRes.error.message); - } + dispatch(updateBlocktank(backup)); + dispatch(backupSuccess({ category: EBackupCategories.blocktank })); - const metadataBackupRes = await performMetadataRestore({ - slashtag, - selectedNetwork, - }); - if (metadataBackupRes.isErr()) { - //Since this backup feature is not critical and mostly for user convenience there's no reason to throw an error here. - console.log('Error backing up metadata', metadataBackupRes.error.message); - } + // Restore success + return ok({ backupExists: true }); + } catch (e) { + console.log(`Restore ${EBackupCategories.settings} error`, e.message); + return err(e); + } +}; - const ldkActivityRes = await performLdkActivityRestore({ - slashtag, - selectedNetwork, - }); - if (ldkActivityRes.isErr()) { - //Since this backup feature is not critical and mostly for user convenience there's no reason to throw an error here. - console.log('Error backing up ldkActivity', ldkActivityRes.error.message); +const performSlashtagsRestore = async (): Promise< + Result<{ backupExists: boolean }> +> => { + try { + const backupRes = await getBackup>( + EBackupCategories.slashtags, + ); + if (backupRes.isErr()) { + return err(backupRes.error.message); } - const btBackupRes = await performBlocktankRestore({ - slashtag, - selectedNetwork, - }); - if (btBackupRes.isErr()) { - //Since this backup feature is not critical and mostly for user convenience there's no reason to throw an error here. - console.log('Error backing up blocktank', btBackupRes.error.message); + const backup = backupRes.value.data; + //If the keys in the backup object are not found in the reference object assume the backup does not exist. + if (!('contacts' in backup)) { + return ok({ backupExists: false }); } - const slashBackupRes = await performSlashtagsRestore({ - slashtag, - selectedNetwork, - }); - if (slashBackupRes.isErr()) { - //Since this backup feature is not critical and mostly for user convenience there's no reason to throw an error here. - console.log('Error backing up contacts', slashBackupRes.error.message); - } + dispatch(addContacts(backup.contacts!)); + dispatch(backupSuccess({ category: EBackupCategories.slashtags })); // Restore success return ok({ backupExists: true }); } catch (e) { - console.log(e); + console.log(`Restore ${EBackupCategories.settings} error`, e.message); return err(e); } }; - -export const checkProfileAndContactsBackup = async ( - slashtag: Slashtag, -): Promise => { - dispatch(startBackupSeederCheck()); - const payload = await checkBackup(slashtag); - dispatch(endBackupSeederCheck(payload)); - - // now check if backup is too old and show warning if it is - const now = new Date().getTime(); - const backup = getBackupStore(); - if ( - (backup.hyperProfileCheckRequested && - now - backup.hyperProfileCheckRequested > FAILED_BACKUP_CHECK_TIME) || - (backup.hyperContactsCheckRequested && - now - backup.hyperContactsCheckRequested > FAILED_BACKUP_CHECK_TIME) - ) { - showToast({ - type: 'error', - title: i18n.t('settings:backup.failed_title'), - description: i18n.t('settings:backup.failed_message'), - }); - } -}; diff --git a/src/styles/icons.ts b/src/styles/icons.ts index 4d525fc7d..df38cf5ec 100644 --- a/src/styles/icons.ts +++ b/src/styles/icons.ts @@ -27,7 +27,6 @@ import { clipboardTextIcon, usersIcon, userIcon, - userRectangleIcon, speedFastIcon, speedNormalIcon, speedSlowIcon, @@ -282,16 +281,6 @@ export const UserIcon = styled(SvgXml).attrs((props) => ({ color: props.color ? props.theme.colors[props.color] : 'white', })); -export const UserRectangleIcon = styled(SvgXml).attrs((props) => ({ - xml: userRectangleIcon( - props.color ? props.theme.colors[props.color] : 'white', - ), - height: props.height ?? '32px', - width: props.width ?? '32px', -}))((props) => ({ - color: props.color ? props.theme.colors[props.color] : 'white', -})); - export const SpeedFastIcon = styled(SvgXml).attrs((props) => ({ xml: speedFastIcon(props.color ? props.theme.colors[props.color] : 'white'), height: props.height ?? '32px', diff --git a/src/utils/backup/backpack.ts b/src/utils/backup/backpack.ts index 9524184d1..b3884b6de 100644 --- a/src/utils/backup/backpack.ts +++ b/src/utils/backup/backpack.ts @@ -2,7 +2,6 @@ import BackupProtocol from 'backpack-client/src/backup-protocol.js'; import { ok, err, Result } from '@synonymdev/result'; import { Slashtag } from '@synonymdev/slashtags-sdk'; -import { name as appName, version as appVersion } from '../../../package.json'; import { EAvailableNetwork } from '../networks'; import { __BACKUPS_SERVER_SLASHTAG__, @@ -10,11 +9,11 @@ import { } from '../../constants/env'; const categoryWithNetwork = ( - category: EBackupCategories, + category: EBackupCategoriesOld, network: EAvailableNetwork, ): string => `${category}.${network}`.toLowerCase(); -export enum EBackupCategories { +export enum EBackupCategoriesOld { jest = 'bitkit.jest', transactions = 'bitkit.transactions', ldkComplete = 'bitkit.ldk.complete', @@ -47,50 +46,6 @@ const backupsFactory = async (slashtag: Slashtag): Promise => { return backupsInstances[key]; }; -/** - * Uploads a backup to the server - * @param {Slashtag} slashtag - * @param {Uint8Array} content - * @param {EBackupCategories} category - * @param {EAvailableNetwork} network - * @returns {Promise>} - */ -export const uploadBackup = async ( - slashtag: Slashtag, - content: Uint8Array, - category: EBackupCategories, - network: EAvailableNetwork, -): Promise> => { - try { - const backups = await backupsFactory(slashtag); - - const encryptedContent = backups.encrypt(content, slashtag.key); - - // Prepare some data to back up - const data = { - appName, - appVersion, - category: categoryWithNetwork(category, network), - content: encryptedContent, - }; - - const { error, results, success } = await backups.backupData( - __BACKUPS_SERVER_SLASHTAG__, - data, - ); - - if (!success) { - return err(error); - } - - const { timestamp } = results; - - return ok(timestamp); - } catch (e) { - return err(e); - } -}; - type TFetchResult = { appName: string; appVersion: string; @@ -103,14 +58,14 @@ type TFetchResult = { * Fetches a backup from the server * @param {Slashtag} slashtag * @param {number} timestamp - * @param {EBackupCategories} category + * @param {EBackupCategoriesOld} category * @param {EAvailableNetwork} network * @returns {Promise>} */ export const fetchBackup = async ( slashtag: Slashtag, timestamp: number, - category: EBackupCategories, + category: EBackupCategoriesOld, network: EAvailableNetwork, ): Promise> => { try { @@ -139,13 +94,13 @@ export const fetchBackup = async ( /** * Returns list of backups in order of newest to oldest * @param {Slashtag} slashtag - * @param {EBackupCategories} category + * @param {EBackupCategoriesOld} category * @param {EAvailableNetwork} network * @returns {Promise>} */ export const listBackups = async ( slashtag: Slashtag, - category: EBackupCategories, + category: EBackupCategoriesOld, network: EAvailableNetwork, ): Promise> => { try { diff --git a/src/utils/backup/backups-subscriber.tsx b/src/utils/backup/backups-subscriber.tsx index e13c5a18d..e14e9c03c 100644 --- a/src/utils/backup/backups-subscriber.tsx +++ b/src/utils/backup/backups-subscriber.tsx @@ -1,28 +1,12 @@ import React, { ReactElement, useEffect, useMemo, useState } from 'react'; -import lm from '@synonymdev/react-native-ldk'; -import { useAppSelector } from '../../hooks/redux'; import { useTranslation } from 'react-i18next'; -import { - checkProfileAndContactsBackup, - performRemoteBackup, - performRemoteLdkBackup, -} from '../../store/utils/backup'; -import { __DISABLE_SLASHTAGS__ } from '../../constants/env'; -import { useSelectedSlashtag } from '../../hooks/slashtags'; -import { backupSelector } from '../../store/reselect/backup'; -import { selectedNetworkSelector } from '../../store/reselect/wallet'; -import { EBackupCategories } from './backpack'; +import { __DISABLE_SLASHTAGS__, __E2E__ } from '../../constants/env'; import { useDebouncedEffect } from '../../hooks/helpers'; -import { settingsSelector } from '../../store/reselect/settings'; -import { metadataState } from '../../store/reselect/metadata'; -import { widgetsState } from '../../store/reselect/widgets'; -import { activityItemsState } from '../../store/reselect/activity'; -import { EActivityType } from '../../store/types/activity'; -import { blocktankSelector } from '../../store/reselect/blocktank'; -import { slashtagsSelector } from '../../store/reselect/slashtags'; +import { useAppSelector } from '../../hooks/redux'; +import { backupSelector } from '../../store/reselect/backup'; +import { EBackupCategories, performBackup } from '../../store/utils/backup'; import { showToast } from '../notifications'; -import { __E2E__ } from '../../constants/env'; const BACKUP_DEBOUNCE = 5000; // 5 seconds const BACKUP_CHECK_INTERVAL = 60 * 1000; // 1 minute @@ -31,174 +15,63 @@ const FAILED_BACKUP_NOTIFICATION_INTERVAL = 10 * 60 * 1000; // 10 minutes const EnabledSlashtag = (): ReactElement => { const { t } = useTranslation('settings'); - const selectedNetwork = useAppSelector(selectedNetworkSelector); - const { slashtag } = useSelectedSlashtag(); const backup = useAppSelector(backupSelector); - const settings = useAppSelector(settingsSelector); - const metadata = useAppSelector(metadataState); - const widgets = useAppSelector(widgetsState); - const activity = useAppSelector(activityItemsState); - const blocktank = useAppSelector(blocktankSelector); - const slashtags = useAppSelector(slashtagsSelector); const [now, setNow] = useState(new Date().getTime()); - useEffect(() => { - const sub = lm.subscribeToBackups((res) => { - performRemoteLdkBackup( - slashtag, - res.isOk() ? res.value : undefined, - ).catch((e) => { - console.error('LDK backup error', e); - }); - }); - - return () => lm.unsubscribeFromBackups(sub); - }, [slashtag]); + const backupSettings = backup[EBackupCategories.settings]; + const backupWidgets = backup[EBackupCategories.widgets]; + const backupMetadata = backup[EBackupCategories.metadata]; + const backupBlocktank = backup[EBackupCategories.blocktank]; + const backupSlashtags = backup[EBackupCategories.slashtags]; - // Attempts to backup settings anytime remoteSettingsBackupSynced is set to false. useDebouncedEffect( () => { - if (backup.remoteSettingsBackupSynced) { + if (backupSettings.synced > backupSettings.required) { return; } - performRemoteBackup({ - slashtag, - isSyncedKey: 'remoteSettingsBackupSynced', - syncRequiredKey: 'remoteSettingsBackupSyncRequired', - syncCompletedKey: 'remoteSettingsBackupLastSync', - backupCategory: EBackupCategories.settings, - selectedNetwork, - backup: settings, - }).then(); + performBackup(EBackupCategories.settings); }, - [backup.remoteSettingsBackupSynced, slashtag, settings, selectedNetwork], + [backupSettings.synced, backupSettings.required], BACKUP_DEBOUNCE, ); - - // Attempts to backup widgets anytime remoteWidgetsBackupSynced is set to false. useDebouncedEffect( () => { - if (backup.remoteWidgetsBackupSynced) { + if (backupWidgets.synced > backupWidgets.required) { return; } - performRemoteBackup({ - slashtag, - isSyncedKey: 'remoteWidgetsBackupSynced', - syncRequiredKey: 'remoteWidgetsBackupSyncRequired', - syncCompletedKey: 'remoteWidgetsBackupLastSync', - backupCategory: EBackupCategories.widgets, - selectedNetwork, - backup: widgets, - }).then(); + performBackup(EBackupCategories.widgets); }, - [backup.remoteWidgetsBackupSynced, slashtag, widgets, selectedNetwork], + [backupWidgets.synced, backupWidgets.required], BACKUP_DEBOUNCE, ); - - // Attempts to backup metadata anytime remoteMetadataBackupSynced is set to false. useDebouncedEffect( () => { - if (backup.remoteMetadataBackupSynced) { + if (backupMetadata.synced > backupMetadata.required) { return; } - performRemoteBackup({ - slashtag, - isSyncedKey: 'remoteMetadataBackupSynced', - syncRequiredKey: 'remoteMetadataBackupSyncRequired', - syncCompletedKey: 'remoteMetadataBackupLastSync', - backupCategory: EBackupCategories.metadata, - selectedNetwork, - backup: metadata, - }).then(); - }, - [backup.remoteMetadataBackupSynced, slashtag, metadata, selectedNetwork], - BACKUP_DEBOUNCE, - ); - - // Attempts to backup ldkActivity anytime remoteLdkActivityBackupSynced is set to false. - useDebouncedEffect( - () => { - if (backup.remoteLdkActivityBackupSynced) { - return; - } - - const ldkActivity = activity.filter( - (a) => a.activityType === EActivityType.lightning, - ); - - performRemoteBackup({ - slashtag, - isSyncedKey: 'remoteLdkActivityBackupSynced', - syncRequiredKey: 'remoteLdkActivityBackupSyncRequired', - syncCompletedKey: 'remoteLdkActivityBackupLastSync', - backupCategory: EBackupCategories.ldkActivity, - selectedNetwork, - backup: ldkActivity, - }).then(); + performBackup(EBackupCategories.metadata); }, - [backup.remoteLdkActivityBackupSynced, slashtag, activity, selectedNetwork], + [backupMetadata.synced, backupMetadata.required], BACKUP_DEBOUNCE, ); - - // Attempts to backup blocktank anytime remoteBlocktankBackupSynced is set to false. useDebouncedEffect( () => { - if (backup.remoteBlocktankBackupSynced) { + if (backupBlocktank.synced > backupBlocktank.required) { return; } - - const back = { - orders: blocktank.orders, - paidOrders: blocktank.paidOrders, - }; - - performRemoteBackup({ - slashtag, - isSyncedKey: 'remoteBlocktankBackupSynced', - syncRequiredKey: 'remoteBlocktankBackupSyncRequired', - syncCompletedKey: 'remoteBlocktankBackupLastSync', - backupCategory: EBackupCategories.blocktank, - selectedNetwork, - backup: back, - }).then(); + performBackup(EBackupCategories.blocktank); }, - [ - backup.remoteBlocktankBackupSynced, - slashtag, - blocktank.orders, - blocktank.paidOrders, - selectedNetwork, - ], + [backupBlocktank.synced, backupBlocktank.required], BACKUP_DEBOUNCE, ); - - // Attempts to backup contacts anytime remoteSlashtagsBackupSynced is set to false. useDebouncedEffect( () => { - if (backup.remoteSlashtagsBackupSynced) { + if (backupSlashtags.synced > backupSlashtags.required) { return; } - - const back = { - contacts: slashtags.contacts, - }; - - performRemoteBackup({ - slashtag, - isSyncedKey: 'remoteSlashtagsBackupSynced', - syncRequiredKey: 'remoteSlashtagsBackupSyncRequired', - syncCompletedKey: 'remoteSlashtagsBackupLastSync', - backupCategory: EBackupCategories.slashtags, - selectedNetwork, - backup: back, - }).then(); + performBackup(EBackupCategories.slashtags); }, - [ - backup.remoteSlashtagsBackupSynced, - slashtag, - slashtags.contacts, - selectedNetwork, - ], + [backupSlashtags.synced, backupSlashtags.required], BACKUP_DEBOUNCE, ); @@ -207,32 +80,13 @@ const EnabledSlashtag = (): ReactElement => { return false; } - if ( - (backup.remoteSettingsBackupSyncRequired && - now - backup.remoteSettingsBackupSyncRequired > - FAILED_BACKUP_CHECK_TIME) || - (backup.remoteWidgetsBackupSyncRequired && - now - backup.remoteWidgetsBackupSyncRequired > - FAILED_BACKUP_CHECK_TIME) || - (backup.remoteMetadataBackupSyncRequired && - now - backup.remoteMetadataBackupSyncRequired > - FAILED_BACKUP_CHECK_TIME) || - (backup.remoteLdkBackupLastSyncRequired && - now - backup.remoteLdkBackupLastSyncRequired > - FAILED_BACKUP_CHECK_TIME) || - (backup.remoteBlocktankBackupSyncRequired && - now - backup.remoteBlocktankBackupSyncRequired > - FAILED_BACKUP_CHECK_TIME) || - (backup.remoteSlashtagsBackupSyncRequired && - now - backup.remoteSlashtagsBackupSyncRequired > - FAILED_BACKUP_CHECK_TIME) || - (backup.remoteLdkActivityBackupSyncRequired && - now - backup.remoteLdkActivityBackupSyncRequired > - FAILED_BACKUP_CHECK_TIME) - ) { - return true; - } - return false; + // find if there are any backup categories that have been failing for more than 30 minutes + return Object.values(EBackupCategories).some((key) => { + return ( + backup[key].synced < backup[key].required && + now - backup[key].required > FAILED_BACKUP_CHECK_TIME + ); + }); }, [backup, now]); useEffect(() => { @@ -263,20 +117,6 @@ const EnabledSlashtag = (): ReactElement => { }; }, [t, shouldShowBackupWarning]); - useEffect(() => { - if (__E2E__) { - return; - } - - const timer = setInterval(() => { - checkProfileAndContactsBackup(slashtag); - }, BACKUP_CHECK_INTERVAL); - - return (): void => { - clearInterval(timer); - }; - }, [slashtag]); - return <>; }; diff --git a/src/utils/lightning/index.ts b/src/utils/lightning/index.ts index 02fbe97bd..a5c45aed3 100644 --- a/src/utils/lightning/index.ts +++ b/src/utils/lightning/index.ts @@ -10,7 +10,6 @@ import lm, { EEventTypes, ENetworks, TAccount, - TAccountBackup, TChannel, TChannelManagerClaim, TChannelManagerPaymentSent, @@ -24,6 +23,7 @@ import lm, { TTransactionData, TTransactionPosition, TGetFees, + TBackupStateUpdate, } from '@synonymdev/react-native-ldk'; import { @@ -51,6 +51,7 @@ import { } from '../../store/helpers'; import { defaultHeader } from '../../store/shapes/wallet'; import { + updateBackupState, updateLdkAccountVersion, updateLightningNodeId, } from '../../store/slices/lightning'; @@ -114,6 +115,7 @@ export const FALLBACK_BLOCKTANK_PEERS: IWalletItem = { let paymentSubscription: EmitterSubscription | undefined; let onChannelSubscription: EmitterSubscription | undefined; let onSpendableOutputsSubscription: EmitterSubscription | undefined; +let onBackupStateUpdate: EmitterSubscription | undefined; /** * Wipes LDK data from storage @@ -277,6 +279,18 @@ export const setupLdk = async ({ return err(storageRes.error); } const rapidGossipSyncUrl = getStore().settings.rapidGossipSyncUrl; + const backupRes = await ldk.backupSetup({ + network, + seed: account.value.seed, + details: { + host: __BACKUPS_SERVER_HOST__, + serverPubKey: __BACKUPS_SERVER_PUBKEY__, + }, + }); + if (backupRes.isErr()) { + return err(backupRes.error); + } + const lmStart = await lm.start({ account: account.value, getFees, @@ -300,10 +314,6 @@ export const setupLdk = async ({ manually_accept_inbound_channels: true, }, trustedZeroConfPeers: __TRUSTED_ZERO_CONF_PEERS__, - backupServerDetails: { - host: __BACKUPS_SERVER_HOST__, - serverPubKey: __BACKUPS_SERVER_PUBKEY__, - }, rapidGossipSyncUrl, skipParamCheck: true, //Switch off for debugging LDK networking issues }); @@ -467,12 +477,34 @@ export const subscribeToLightningPayments = ({ () => {}, ); } + if (!onBackupStateUpdate) { + onBackupStateUpdate = ldk.onEvent( + EEventTypes.backup_state_update, + (res: TBackupStateUpdate) => { + if (!selectedWallet) { + selectedWallet = getSelectedWallet(); + } + if (!selectedNetwork) { + selectedNetwork = getSelectedNetwork(); + } + + dispatch( + updateBackupState({ + backup: res, + selectedWallet, + selectedNetwork, + }), + ); + }, + ); + } }; export const unsubscribeFromLightningSubscriptions = (): void => { paymentSubscription?.remove(); onChannelSubscription?.remove(); onSpendableOutputsSubscription?.remove(); + onBackupStateUpdate?.remove(); }; let isRefreshing = false; @@ -980,27 +1012,6 @@ export const getSha256 = (str: string): string => { return hash.toString('hex'); }; -/** - * Exports complete backup string for current LDK account. - * @param account - * @returns {Promise>} - */ -export const exportBackup = async ( - account?: TAccount, -): Promise> => { - if (!account) { - const res = await getLdkAccount(); - if (res.isErr()) { - return err(res.error); - } - - account = res.value; - } - return await lm.backupAccount({ - account, - }); -}; - /** * Returns last known header information from storage. * @returns {Promise} diff --git a/yarn.lock b/yarn.lock index eff7b153e..e9dac4c24 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4222,10 +4222,10 @@ dependencies: b4a "^1.5.3" -"@synonymdev/react-native-ldk@0.0.127": - version "0.0.127" - resolved "https://registry.yarnpkg.com/@synonymdev/react-native-ldk/-/react-native-ldk-0.0.127.tgz#01dbf45319b9003adba111934a3723345a3a461c" - integrity sha512-CmPCq6C8Km55Zg35uM8wfFwY4GtqVQLHOJ1ppmEB36auGvCa0mdfOi7zUaRBw9E4c3W5fnOnAzeReju5WFyR5Q== +"@synonymdev/react-native-ldk@0.0.129": + version "0.0.129" + resolved "https://registry.yarnpkg.com/@synonymdev/react-native-ldk/-/react-native-ldk-0.0.129.tgz#77b30fbf268afe01e753ba8ff54ef3966ac96365" + integrity sha512-xpAHIBiphmbe8niuIcRCZ+XVrdV/leBJOmpN9W5eHNMIbT9SYEHS7Tgyzh+vzQ+xM9/AEfKmSWEgA+MkVFqpNg== dependencies: bech32 "^2.0.0" bitcoinjs-lib "^6.0.2"