Files
app.zeusln.zeus/stores/ChannelBackupStore.ts

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;
}
}