import * as ACTIONS from 'constants/action_types'; import { Lbryio } from 'lbryinc'; import { SETTINGS, Lbry, doWalletEncrypt, doWalletDecrypt } from 'lbry-redux'; import { selectGetSyncIsPending, selectSetSyncIsPending, selectSyncIsLocked } from 'redux/selectors/sync'; import { makeSelectClientSetting } from 'redux/selectors/settings'; import { getSavedPassword } from 'util/saved-passwords'; import { doAnalyticsTagSync, doHandleSyncComplete } from 'redux/actions/app'; import { selectUserVerifiedEmail } from 'redux/selectors/user'; let syncTimer = null; const SYNC_INTERVAL = 1000 * 60 * 5; // 5 minutes const NO_WALLET_ERROR = 'no wallet found for this user'; const BAD_PASSWORD_ERROR_NAME = 'InvalidPasswordError'; export function doSetDefaultAccount(success, failure) { return (dispatch) => { dispatch({ type: ACTIONS.SET_DEFAULT_ACCOUNT, }); Lbry.account_list() .then((accountList) => { const { lbc_mainnet: accounts } = accountList; let defaultId; for (let i = 0; i < accounts.length; ++i) { if (accounts[i].satoshis > 0) { defaultId = accounts[i].id; break; } } // In a case where there's no balance on either account // assume the second (which is created after sync) as default if (!defaultId && accounts.length > 1) { defaultId = accounts[1].id; } // Set the default account if (defaultId) { Lbry.account_set({ account_id: defaultId, default: true }) .then(() => { if (success) { success(); } }) .catch((err) => { if (failure) { failure(err); } }); } else if (failure) { // no default account to set failure('Could not set a default account'); // fail } }) .catch((err) => { if (failure) { failure(err); } }); }; } export function doSetSync(oldHash, newHash, data) { return (dispatch) => { dispatch({ type: ACTIONS.SET_SYNC_STARTED, }); return Lbryio.call('sync', 'set', { old_hash: oldHash, new_hash: newHash, data }, 'post') .then((response) => { if (!response.hash) { throw Error('No hash returned for sync/set.'); } return dispatch({ type: ACTIONS.SET_SYNC_COMPLETED, data: { syncHash: response.hash }, }); }) .catch((error) => { dispatch({ type: ACTIONS.SET_SYNC_FAILED, data: { error }, }); }); }; } export const doGetSyncDesktop = (cb?, password) => (dispatch, getState) => { const state = getState(); const syncEnabled = makeSelectClientSetting(SETTINGS.ENABLE_SYNC)(state); const getSyncPending = selectGetSyncIsPending(state); const setSyncPending = selectSetSyncIsPending(state); const syncLocked = selectSyncIsLocked(state); return getSavedPassword().then((savedPassword) => { const passwordArgument = password || password === '' ? password : savedPassword === null ? '' : savedPassword; if (syncEnabled && !getSyncPending && !setSyncPending && !syncLocked) { return dispatch(doGetSync(passwordArgument, cb)); } }); }; export function doSyncLoop(noInterval) { return (dispatch, getState) => { if (!noInterval && syncTimer) clearInterval(syncTimer); const state = getState(); const hasVerifiedEmail = selectUserVerifiedEmail(state); const syncEnabled = makeSelectClientSetting(SETTINGS.ENABLE_SYNC)(state); const syncLocked = selectSyncIsLocked(state); if (hasVerifiedEmail && syncEnabled && !syncLocked) { dispatch(doGetSyncDesktop((error, hasNewData) => dispatch(doHandleSyncComplete(error, hasNewData)))); dispatch(doAnalyticsTagSync()); if (!noInterval) { syncTimer = setInterval(() => { const state = getState(); const syncEnabled = makeSelectClientSetting(SETTINGS.ENABLE_SYNC)(state); if (syncEnabled) { dispatch(doGetSyncDesktop((error, hasNewData) => dispatch(doHandleSyncComplete(error, hasNewData)))); dispatch(doAnalyticsTagSync()); } }, SYNC_INTERVAL); } } }; } export function doSyncUnsubscribe() { return (dispatch) => { if (syncTimer) { clearInterval(syncTimer); } }; } export function doGetSync(passedPassword, callback) { const password = passedPassword === null || passedPassword === undefined ? '' : passedPassword; function handleCallback(error, hasNewData) { if (callback) { if (typeof callback !== 'function') { throw new Error('Second argument passed to "doGetSync" must be a function'); } callback(error, hasNewData); } } return (dispatch) => { dispatch({ type: ACTIONS.GET_SYNC_STARTED, }); const data = {}; Lbry.wallet_status() .then((status) => { if (status.is_locked) { return Lbry.wallet_unlock({ password }); } // Wallet is already unlocked return true; }) .then((isUnlocked) => { if (isUnlocked) { return Lbry.sync_hash(); } data.unlockFailed = true; throw new Error(); }) .then((hash) => Lbryio.call('sync', 'get', { hash }, 'post')) .then((response) => { const syncHash = response.hash; data.syncHash = syncHash; data.syncData = response.data; data.changed = response.changed; data.hasSyncedWallet = true; if (response.changed) { return Lbry.sync_apply({ password, data: response.data, blocking: true }); } }) .then((response) => { if (!response) { dispatch({ type: ACTIONS.GET_SYNC_COMPLETED, data }); handleCallback(null, data.changed); return; } const { hash: walletHash, data: walletData } = response; if (walletHash !== data.syncHash) { // different local hash, need to synchronise dispatch(doSetSync(data.syncHash, walletHash, walletData)); } dispatch({ type: ACTIONS.GET_SYNC_COMPLETED, data }); handleCallback(null, data.changed); }) .catch((syncAttemptError) => { const badPasswordError = syncAttemptError && syncAttemptError.data && syncAttemptError.data.name === BAD_PASSWORD_ERROR_NAME; if (data.unlockFailed) { dispatch({ type: ACTIONS.GET_SYNC_FAILED, data: { error: syncAttemptError } }); if (badPasswordError) { dispatch({ type: ACTIONS.SYNC_APPLY_BAD_PASSWORD }); } handleCallback(syncAttemptError); } else if (data.hasSyncedWallet) { const error = (syncAttemptError && syncAttemptError.message) || 'Error getting synced wallet'; dispatch({ type: ACTIONS.GET_SYNC_FAILED, data: { error, }, }); if (badPasswordError) { dispatch({ type: ACTIONS.SYNC_APPLY_BAD_PASSWORD }); } handleCallback(error); } else { const noWalletError = syncAttemptError && syncAttemptError.message === NO_WALLET_ERROR; dispatch({ type: ACTIONS.GET_SYNC_COMPLETED, data: { hasSyncedWallet: false, syncHash: null, // If there was some unknown error, bail fatalError: !noWalletError, }, }); // user doesn't have a synced wallet // call sync_apply to get data to sync // first time sync. use any string for old hash if (noWalletError) { Lbry.sync_apply({ password }) .then(({ hash: walletHash, data: syncApplyData }) => { dispatch(doSetSync('', walletHash, syncApplyData, password)); handleCallback(); }) .catch((syncApplyError) => { handleCallback(syncApplyError); }); } } }); }; } export function doSyncApply(syncHash, syncData, password) { return (dispatch) => { dispatch({ type: ACTIONS.SYNC_APPLY_STARTED, }); Lbry.sync_apply({ password, data: syncData }) .then(({ hash: walletHash, data: walletData }) => { dispatch({ type: ACTIONS.SYNC_APPLY_COMPLETED, }); if (walletHash !== syncHash) { // different local hash, need to synchronise dispatch(doSetSync(syncHash, walletHash, walletData)); } }) .catch(() => { dispatch({ type: ACTIONS.SYNC_APPLY_FAILED, data: { error: 'Invalid password specified. Please enter the password for your previously synchronised wallet.', }, }); }); }; } export function doCheckSync() { return (dispatch) => { dispatch({ type: ACTIONS.GET_SYNC_STARTED, }); Lbry.sync_hash().then((hash) => { Lbryio.call('sync', 'get', { hash }, 'post') .then((response) => { const data = { hasSyncedWallet: true, syncHash: response.hash, syncData: response.data, hashChanged: response.changed, }; dispatch({ type: ACTIONS.GET_SYNC_COMPLETED, data }); }) .catch(() => { // user doesn't have a synced wallet dispatch({ type: ACTIONS.GET_SYNC_COMPLETED, data: { hasSyncedWallet: false, syncHash: null }, }); }); }); }; } export function doResetSync() { return (dispatch) => new Promise((resolve) => { dispatch({ type: ACTIONS.SYNC_RESET }); resolve(); }); } export function doSyncEncryptAndDecrypt(oldPassword, newPassword, encrypt) { return (dispatch) => { const data = {}; return Lbry.sync_hash() .then((hash) => Lbryio.call('sync', 'get', { hash }, 'post')) .then((syncGetResponse) => { data.oldHash = syncGetResponse.hash; return Lbry.sync_apply({ password: oldPassword, data: syncGetResponse.data }); }) .then(() => { if (encrypt) { dispatch(doWalletEncrypt(newPassword)); } else { dispatch(doWalletDecrypt()); } }) .then(() => Lbry.sync_apply({ password: newPassword })) .then((syncApplyResponse) => { if (syncApplyResponse.hash !== data.oldHash) { return dispatch(doSetSync(data.oldHash, syncApplyResponse.hash, syncApplyResponse.data)); } }) .catch(console.error); // eslint-disable-line }; } export function doSetSyncLock(lock) { return { type: ACTIONS.SET_SYNC_LOCK, data: lock, }; } export function doSetPrefsReady() { return { type: ACTIONS.SET_PREFS_READY, data: true, }; }