diff --git a/App.tsx b/App.tsx
index 135edb775..a340aaef4 100644
--- a/App.tsx
+++ b/App.tsx
@@ -139,6 +139,7 @@ import EditFee from './views/EditFee';
import Seed from './views/Settings/Seed';
import SeedRecovery from './views/Settings/SeedRecovery';
import Sync from './views/Sync';
+import SyncRecovery from './views/SyncRecovery';
import LspExplanationFees from './views/Explanations/LspExplanationFees';
import LspExplanationRouting from './views/Explanations/LspExplanationRouting';
import LspExplanationWrappedInvoices from './views/Explanations/LspExplanationWrappedInvoices';
@@ -603,6 +604,10 @@ export default class App extends React.PureComponent {
name="Sync"
component={Sync}
/>
+
await subscribeCustomMessages();
getMyNodeInfo = async () => await getInfo();
getNetworkInfo = async () => await getNetworkInfo();
+ getRecoveryInfo = async () => await getRecoveryInfo();
getInvoices = async () => await listInvoices();
createInvoice = async (data: any) =>
await addInvoice({
diff --git a/locales/en.json b/locales/en.json
index 6edf4cc0d..f9f578dde 100644
--- a/locales/en.json
+++ b/locales/en.json
@@ -305,6 +305,9 @@
"views.OpenChannel.removeAdditionalChannel": "Remove additional channel",
"views.Wallet.BalancePane.sync.title": "Finishing sync",
"views.Wallet.BalancePane.sync.text": "Hang on tight! You will be ready to use Zeus soon.",
+ "views.Wallet.BalancePane.recovery.title": "Recovery mode",
+ "views.Wallet.BalancePane.recovery.text": "Please leave ZEUS open until the process completes.",
+ "views.Wallet.BalancePane.recovery.textAlt": "Leave ZEUS open until completion.",
"views.Wallet.BalancePane.backup.title": "Back up your funds",
"views.Wallet.BalancePane.backup.text": "Create a backup to never lose access to your bitcoin.",
"views.Wallet.BalancePane.backup.action": "Start backup ->",
@@ -952,6 +955,7 @@
"views.Sync.tip": "Tip",
"views.Sync.numBlocksUntilSynced": "Number of blocks until synced",
"views.LSPS1.pubkeyAndHostNotFound": "Node pubkey and host are not set",
+ "views.SyncRecovery.title": "Recovering wallet",
"views.LSPS1.timeoutError": "Did not receive response from server",
"views.LSPS1.channelExpiryBlocks": "Channel Expiry Blocks",
"views.LSPS1.maxChannelExpiryBlocks": "Max channel Expiry Blocks",
diff --git a/stores/SyncStore.ts b/stores/SyncStore.ts
index d751cb725..f62da1b0f 100644
--- a/stores/SyncStore.ts
+++ b/stores/SyncStore.ts
@@ -10,6 +10,8 @@ import SettingsStore from './SettingsStore';
export default class SyncStore {
@observable public isSyncing: boolean = false;
+ @observable public isRecovering: boolean = false;
+ @observable public recoveryProgress: number | null;
@observable public isInExpressGraphSync: boolean = false;
@observable public bestBlockHeight: number;
@observable public currentBlockHeight: number;
@@ -26,6 +28,7 @@ export default class SyncStore {
@action
public reset = () => {
this.isSyncing = false;
+ this.isRecovering = false;
this.isInExpressGraphSync = false;
this.error = false;
};
@@ -137,4 +140,42 @@ export default class SyncStore {
if (queryMempool) this.getBestBlockHeight();
}
};
+
+ @action
+ public checkRecoveryStatus = () => {
+ BackendUtils.getRecoveryInfo().then((data: any) => {
+ if (data.recovery_mode && !data.recovery_finished) {
+ this.startRecovering();
+ }
+ });
+ };
+
+ @action
+ public getRecoveryStatus = async () => {
+ await BackendUtils.getRecoveryInfo().then((data: any) => {
+ if (data.recovery_mode) {
+ if (data.progress) {
+ this.recoveryProgress = data.progress;
+ }
+ if (data.recovery_finished) {
+ this.isRecovering = false;
+ this.recoveryProgress = null;
+ }
+ } else {
+ this.isRecovering = false;
+ this.recoveryProgress = null;
+ }
+ return data;
+ });
+ };
+
+ @action
+ public startRecovering = async () => {
+ this.isRecovering = true;
+
+ while (this.isRecovering) {
+ await sleep(2000);
+ await this.getRecoveryStatus();
+ }
+ };
}
diff --git a/utils/BackendUtils.ts b/utils/BackendUtils.ts
index cd9f217f0..dae5ea476 100644
--- a/utils/BackendUtils.ts
+++ b/utils/BackendUtils.ts
@@ -75,6 +75,7 @@ class BackendUtils {
this.call('subscribeCustomMessages', args);
getMyNodeInfo = (...args: any[]) => this.call('getMyNodeInfo', args);
getNetworkInfo = (...args: any[]) => this.call('getNetworkInfo', args);
+ getRecoveryInfo = (...args: any[]) => this.call('getRecoveryInfo', args);
getInvoices = (...args: any[]) => this.call('getInvoices', args);
createInvoice = (...args: any[]) => this.call('createInvoice', args);
getPayments = (...args: any[]) => this.call('getPayments', args);
diff --git a/views/SyncRecovery.tsx b/views/SyncRecovery.tsx
new file mode 100644
index 000000000..6ef6b5590
--- /dev/null
+++ b/views/SyncRecovery.tsx
@@ -0,0 +1,92 @@
+import React from 'react';
+import { Dimensions, View } from 'react-native';
+import { inject, observer } from 'mobx-react';
+import CircularProgress from 'react-native-circular-progress-indicator';
+import { StackNavigationProp } from '@react-navigation/stack';
+
+import Button from '../components/Button';
+import Screen from '../components/Screen';
+import Header from '../components/Header';
+
+import SyncStore from '../stores/SyncStore';
+
+import { localeString } from '../utils/LocaleUtils';
+import { themeColor } from '../utils/ThemeUtils';
+
+interface SyncRecoveryProps {
+ navigation: StackNavigationProp;
+ SyncStore: SyncStore;
+}
+
+@inject('SyncStore')
+@observer
+export default class SyncRecovery extends React.PureComponent<
+ SyncRecoveryProps,
+ {}
+> {
+ render() {
+ const { navigation, SyncStore } = this.props;
+ const { recoveryProgress } = SyncStore;
+
+ const { width } = Dimensions.get('window');
+
+ return (
+
+
+
+
+ {
+ 'worklet';
+ return value.toFixed && value.toFixed(1); // 1 decimal place
+ }}
+ valueSuffix="%"
+ />
+
+
+
+
+
+ );
+ }
+}
diff --git a/views/Wallet/BalancePane.tsx b/views/Wallet/BalancePane.tsx
index 342f2642b..50c2ea146 100644
--- a/views/Wallet/BalancePane.tsx
+++ b/views/Wallet/BalancePane.tsx
@@ -18,7 +18,7 @@ import NodeInfoStore from '../../stores/NodeInfoStore';
import SettingsStore from '../../stores/SettingsStore';
import SyncStore from '../../stores/SyncStore';
-import { version, playStore } from '../../package.json';
+import { version } from '../../package.json';
import LockIcon from '../../assets/images/SVG/Lock.svg';
@@ -69,7 +69,13 @@ export default class BalancePane extends React.PureComponent<
pendingOpenBalance
} = BalanceStore;
const { implementation } = SettingsStore;
- const { currentBlockHeight, bestBlockHeight, isSyncing } = SyncStore;
+ const {
+ currentBlockHeight,
+ bestBlockHeight,
+ recoveryProgress,
+ isSyncing,
+ isRecovering
+ } = SyncStore;
const pendingUnconfirmedBalance = new BigNumber(pendingOpenBalance)
.plus(unconfirmedBlockchainBalance)
@@ -162,6 +168,103 @@ export default class BalancePane extends React.PureComponent<
marginBottom: 20
}}
>
+ {isRecovering && recoveryProgress !== 1 && (
+ {
+ if (recoveryProgress) {
+ navigation.navigate('SyncRecovery');
+ }
+ }}
+ >
+
+
+ {`${localeString(
+ 'views.Wallet.BalancePane.recovery.title'
+ )}${
+ !recoveryProgress
+ ? ` - ${localeString(
+ 'views.Wallet.BalancePane.recovery.textAlt'
+ ).replace('Zeus', 'ZEUS')}`
+ : ''
+ }`}
+
+ {recoveryProgress && (
+
+ {localeString(
+ 'views.Wallet.BalancePane.recovery.text'
+ ).replace('Zeus', 'ZEUS')}
+
+ )}
+ {recoveryProgress && (
+
+
+
+ {`${Math.floor(
+ recoveryProgress * 100
+ ).toString()}%`}
+
+
+ )}
+
+
+ )}
{isSyncing && (
navigation.navigate('Sync')}
@@ -379,7 +482,7 @@ export default class BalancePane extends React.PureComponent<
marginBottom: -40
}}
>
- {playStore ? `v${version}-play` : `v${version}`}
+ {`v${version}`}
);
diff --git a/views/Wallet/Wallet.tsx b/views/Wallet/Wallet.tsx
index d3cb6f3be..b7e707ecc 100644
--- a/views/Wallet/Wallet.tsx
+++ b/views/Wallet/Wallet.tsx
@@ -23,6 +23,7 @@ import { inject, observer } from 'mobx-react';
import RNRestart from 'react-native-restart';
import { StackNavigationProp } from '@react-navigation/stack';
import SystemNavigationBar from 'react-native-system-navigation-bar';
+import EncryptedStorage from 'react-native-encrypted-storage';
import ChannelsPane from '../Channels/ChannelsPane';
import BalancePane from './BalancePane';
@@ -387,6 +388,8 @@ export default class Wallet extends React.Component {
embeddedLndNetwork === 'Testnet'
);
}
+ if (implementation === 'embedded-lnd')
+ SyncStore.checkRecoveryStatus();
await NodeInfoStore.getNodeInfo();
NodeInfoStore.getNetworkInfo();
if (BackendUtils.supportsAccounts()) UTXOsStore.listAccounts();
@@ -399,6 +402,15 @@ export default class Wallet extends React.Component {
});
}
if (recovery) {
+ const isBackedUp = await EncryptedStorage.getItem(
+ 'backup-complete'
+ );
+ if (!isBackedUp) {
+ await EncryptedStorage.setItem(
+ 'backup-complete',
+ JSON.stringify(true)
+ );
+ }
if (isSyncing) return;
try {
await ChannelBackupStore.recoverStaticChannelBackup();