288 lines
9.8 KiB
TypeScript
288 lines
9.8 KiB
TypeScript
import { action, observable, runInAction } from 'mobx';
|
|
import ReactNativeBlobUtil from 'react-native-blob-util';
|
|
import * as CryptoJS from 'crypto-js';
|
|
|
|
import NodeInfoStore from './NodeInfoStore';
|
|
import SettingsStore from './SettingsStore';
|
|
|
|
import lndMobile from '../lndmobile/LndMobileInjection';
|
|
const { channel } = lndMobile;
|
|
import {
|
|
exportAllChannelBackups,
|
|
restoreChannelBackups
|
|
} from '../lndmobile/channel';
|
|
|
|
import BackendUtils from '../utils/BackendUtils';
|
|
import { LndMobileEventEmitter } from '../utils/LndMobileUtils';
|
|
import Base64Utils from '../utils/Base64Utils';
|
|
import { errorToUserFriendly } from '../utils/ErrorUtils';
|
|
|
|
import Storage from '../storage';
|
|
|
|
export const BACKUPS_HOST = 'https://backups.lnolymp.us';
|
|
|
|
export const LEGACY_LAST_CHANNEL_BACKUP_STATUS = 'LAST_CHANNEL_BACKUP_STATUS';
|
|
export const LEGACY_LAST_CHANNEL_BACKUP_TIME = 'LAST_CHANNEL_BACKUP_TIME';
|
|
|
|
export const LAST_CHANNEL_BACKUP_STATUS = 'zeus-last-channel-backup-status';
|
|
export const LAST_CHANNEL_BACKUP_TIME = 'zeus-last-channel-backup-time';
|
|
|
|
export default class ChannelBackupStore {
|
|
@observable public channelEventsSubscription: any;
|
|
@observable public backups: Array<any> = [];
|
|
@observable public loading: boolean = false;
|
|
@observable public error: boolean = false;
|
|
@observable public error_msg: string;
|
|
|
|
nodeInfoStore: NodeInfoStore;
|
|
settingsStore: SettingsStore;
|
|
|
|
constructor(nodeInfoStore: NodeInfoStore, settingsStore: SettingsStore) {
|
|
this.nodeInfoStore = nodeInfoStore;
|
|
this.settingsStore = settingsStore;
|
|
}
|
|
|
|
@action
|
|
public reset = () => {
|
|
this.channelEventsSubscription = null;
|
|
this.error_msg = '';
|
|
this.error = false;
|
|
this.loading = false;
|
|
};
|
|
|
|
private logBackupStatus = async (status: string) => {
|
|
await Storage.setItem(LAST_CHANNEL_BACKUP_STATUS, status);
|
|
await Storage.setItem(LAST_CHANNEL_BACKUP_TIME, `${new Date()}`);
|
|
};
|
|
|
|
public backupChannels = async () => {
|
|
const backup = await exportAllChannelBackups();
|
|
if (!backup?.multi_chan_backup?.multi_chan_backup) {
|
|
throw new Error();
|
|
}
|
|
const multi = backup.multi_chan_backup.multi_chan_backup;
|
|
const multiString = Base64Utils.bytesToBase64(multi);
|
|
|
|
const encryptedBackup = CryptoJS.AES.encrypt(
|
|
multiString,
|
|
this.settingsStore.seedPhrase.toString()
|
|
).toString();
|
|
|
|
try {
|
|
const authResponse = await ReactNativeBlobUtil.fetch(
|
|
'POST',
|
|
`${BACKUPS_HOST}/api/auth`,
|
|
{ 'Content-Type': 'application/json' },
|
|
JSON.stringify({
|
|
pubkey: this.nodeInfoStore.nodeInfo.identity_pubkey
|
|
})
|
|
);
|
|
|
|
if (authResponse.info().status !== 200) {
|
|
this.logBackupStatus('ERROR');
|
|
throw new Error();
|
|
}
|
|
|
|
const { verification } = authResponse.json();
|
|
|
|
const messageSignData = await BackendUtils.signMessage(
|
|
verification
|
|
);
|
|
const signature =
|
|
messageSignData.zbase || messageSignData.signature;
|
|
const backupResponse = await ReactNativeBlobUtil.fetch(
|
|
'POST',
|
|
`${BACKUPS_HOST}/api/backup`,
|
|
{ 'Content-Type': 'application/json' },
|
|
JSON.stringify({
|
|
pubkey: this.nodeInfoStore.nodeInfo.identity_pubkey,
|
|
message: verification,
|
|
signature,
|
|
backup: encryptedBackup
|
|
})
|
|
);
|
|
if (
|
|
backupResponse.info().status === 200 &&
|
|
backupResponse.json().success
|
|
) {
|
|
// log success timestamp
|
|
this.logBackupStatus('SUCCESS');
|
|
return true;
|
|
}
|
|
|
|
this.logBackupStatus('ERROR');
|
|
throw new Error();
|
|
} catch {
|
|
this.logBackupStatus('ERROR');
|
|
throw new Error();
|
|
}
|
|
};
|
|
|
|
public recoverStaticChannelBackup = async () => {
|
|
const authResponse = await ReactNativeBlobUtil.fetch(
|
|
'POST',
|
|
`${BACKUPS_HOST}/api/auth`,
|
|
{ 'Content-Type': 'application/json' },
|
|
JSON.stringify({
|
|
pubkey: this.nodeInfoStore.nodeInfo.identity_pubkey
|
|
})
|
|
);
|
|
const authResponseStatus = authResponse.info().status;
|
|
if (authResponseStatus !== 200) {
|
|
throw new Error(`Auth response status code: ${authResponseStatus}`);
|
|
}
|
|
const { verification } = authResponse.json();
|
|
|
|
const messageSignData = await BackendUtils.signMessage(verification);
|
|
const signature = messageSignData.zbase || messageSignData.signature;
|
|
const recoverResponse = await ReactNativeBlobUtil.fetch(
|
|
'POST',
|
|
`${BACKUPS_HOST}/api/recover`,
|
|
{ 'Content-Type': 'application/json' },
|
|
JSON.stringify({
|
|
pubkey: this.nodeInfoStore.nodeInfo.identity_pubkey,
|
|
message: verification,
|
|
signature
|
|
})
|
|
);
|
|
const data = recoverResponse.json();
|
|
const { backup, created_at, success } = data;
|
|
if (recoverResponse.info().status === 200 && success && backup) {
|
|
await this.triggerRecovery(backup);
|
|
return { backup, created_at };
|
|
} else {
|
|
throw new Error(data.error);
|
|
}
|
|
};
|
|
|
|
@action
|
|
public triggerRecovery = async (backup: string): Promise<void> => {
|
|
this.error_msg = '';
|
|
this.loading = true;
|
|
|
|
try {
|
|
const decryptedBytes = CryptoJS.AES.decrypt(
|
|
backup,
|
|
this.settingsStore.seedPhrase.toString()
|
|
);
|
|
const decryptedString = decryptedBytes.toString(CryptoJS.enc.Utf8);
|
|
|
|
await restoreChannelBackups(decryptedString);
|
|
|
|
runInAction(() => {
|
|
this.error_msg = '';
|
|
this.loading = false;
|
|
});
|
|
} catch (err: any) {
|
|
const errorMsg = errorToUserFriendly(err);
|
|
runInAction(() => {
|
|
this.error_msg = errorMsg;
|
|
this.loading = false;
|
|
});
|
|
|
|
throw new Error(this.error_msg);
|
|
}
|
|
};
|
|
|
|
@action
|
|
public advancedRecoveryList = async () => {
|
|
this.loading = true;
|
|
this.error = false;
|
|
this.backups = [];
|
|
try {
|
|
const authResponse = await ReactNativeBlobUtil.fetch(
|
|
'POST',
|
|
`${BACKUPS_HOST}/api/auth`,
|
|
{ 'Content-Type': 'application/json' },
|
|
JSON.stringify({
|
|
pubkey: this.nodeInfoStore.nodeInfo.identity_pubkey
|
|
})
|
|
);
|
|
const authResponseStatus = authResponse.info().status;
|
|
if (authResponseStatus !== 200) {
|
|
throw new Error(
|
|
`Auth response status code: ${authResponseStatus}`
|
|
);
|
|
}
|
|
const { verification } = authResponse.json();
|
|
|
|
const messageSignData = await BackendUtils.signMessage(
|
|
verification
|
|
);
|
|
const signature =
|
|
messageSignData.zbase || messageSignData.signature;
|
|
const recoverResponse = await ReactNativeBlobUtil.fetch(
|
|
'POST',
|
|
`${BACKUPS_HOST}/api/recoverAdvanced`,
|
|
{ 'Content-Type': 'application/json' },
|
|
JSON.stringify({
|
|
pubkey: this.nodeInfoStore.nodeInfo.identity_pubkey,
|
|
message: verification,
|
|
signature
|
|
})
|
|
);
|
|
const data = recoverResponse.json();
|
|
const { backups } = data;
|
|
if (recoverResponse.info().status === 200 && backups) {
|
|
runInAction(() => {
|
|
this.backups = backups;
|
|
this.loading = false;
|
|
});
|
|
return backups;
|
|
}
|
|
|
|
throw new Error(data.error);
|
|
} catch (error) {
|
|
runInAction(() => {
|
|
this.error = true;
|
|
this.loading = false;
|
|
});
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
public initSubscribeChannelEvents = async () => {
|
|
// Check if latest channel backup status is success
|
|
// or if it's over three days ago and trigger backup
|
|
const status = await Storage.getItem(LAST_CHANNEL_BACKUP_STATUS);
|
|
const time = await Storage.getItem(LAST_CHANNEL_BACKUP_TIME);
|
|
if (
|
|
(status && status === 'ERROR') ||
|
|
(time && this.isOlderThanThreeDays(time)) ||
|
|
(!time && !status)
|
|
) {
|
|
this.backupChannels();
|
|
}
|
|
if (this.channelEventsSubscription?.remove)
|
|
this.channelEventsSubscription.remove();
|
|
this.channelEventsSubscription = LndMobileEventEmitter.addListener(
|
|
'SubscribeChannelEvents',
|
|
async (event: any) => {
|
|
try {
|
|
if (!event?.data) return;
|
|
|
|
const channelEvent = channel.decodeChannelEvent(event.data);
|
|
if (
|
|
channelEvent.open_channel ||
|
|
channelEvent.closed_channel
|
|
) {
|
|
this.backupChannels();
|
|
}
|
|
} catch (error: any) {
|
|
console.error(
|
|
'subscribe channel backups error: ' + error.message
|
|
);
|
|
}
|
|
}
|
|
);
|
|
|
|
await channel.subscribeChannelEvents();
|
|
};
|
|
|
|
private isOlderThanThreeDays(time: string) {
|
|
const ONE_HOUR = 60 * 60 * 1000; /* ms */
|
|
const THREE_DAYS = 3 * 24 * ONE_HOUR;
|
|
return Number(new Date()) - Number(new Date(time)) > THREE_DAYS;
|
|
}
|
|
}
|