UNPKG

edge-core-js

Version:

Edge account & wallet management library

275 lines (223 loc) 8.6 kB
function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }import { combinePixies, filterPixie, mapPixie, stopUpdates, } from 'redux-pixies' import { close, update } from 'yaob' import { asMaybeOtpError, } from '../../types/types' import { makePeriodicTask } from '../../util/periodic-task' import { snooze } from '../../util/snooze' import { syncLogin } from '../login/login' import { waitForPlugins } from '../plugins/plugins-selectors' import { toApiInput } from '../root-pixie' import { addStorageWallet, SYNC_INTERVAL, syncStorageWallet } from '../storage/storage-actions' import { makeAccountApi } from './account-api' import { loadAllWalletStates, reloadPluginSettings } from './account-files' import { initialCustomTokens } from './account-reducer' import { loadBuiltinTokens, loadCustomTokens, saveCustomTokens } from './custom-tokens' const accountPixie = combinePixies({ accountApi(input) { return { destroy() { // The Pixie library stops updating props after destruction, // so we are stuck seeing the logged-in state. Fix that: const hack = input.props hack.state = { accounts: {} } const { accountOutput } = input.props if (accountOutput == null) return const { accountApi } = accountOutput if (accountApi == null) return update(accountApi) close(accountApi) close(accountApi.dataStore) const { currencyConfig, swapConfig } = accountApi for (const pluginId of Object.keys(currencyConfig)) { close(currencyConfig[pluginId]) } for (const pluginId of Object.keys(swapConfig)) { close(swapConfig[pluginId]) } }, async update() { const ai = toApiInput(input) const { accountId, accountState, log } = input.props const { accountWalletInfos } = accountState async function loadAllFiles() { await Promise.all([ loadAllWalletStates(ai, accountId), loadCustomTokens(ai, accountId), reloadPluginSettings(ai, accountId) ]) } try { // Wait for the currency plugins (should already be loaded by now): await waitForPlugins(ai) await loadBuiltinTokens(ai, accountId) log.warn('Login: currency plugins exist') // Start the repo: await Promise.all( accountWalletInfos.map(info => addStorageWallet(ai, info)) ) log.warn('Login: synced account repos') await loadAllFiles() log.warn('Login: loaded files') // Create the API object: input.onOutput(makeAccountApi(ai, accountId)) log.warn('Login: complete') } catch (error) { input.props.dispatch({ type: 'ACCOUNT_LOAD_FAILED', payload: { accountId, error } }) } return await stopUpdates } } }, // Starts & stops the sync timer for this account: syncTimer: filterPixie( (input) => { async function doDataSync() { const ai = toApiInput(input) const { accountId, accountState } = input.props const { accountWalletInfos } = accountState if (input.props.state.accounts[accountId] == null) return const changeLists = await Promise.all( accountWalletInfos.map(info => syncStorageWallet(ai, info.id)) ) const changes = [] for (const list of changeLists) changes.push(...list) if (changes.length > 0) { await Promise.all([ reloadPluginSettings(ai, accountId), loadAllWalletStates(ai, accountId) ]) } } async function doLoginSync() { const ai = toApiInput(input) const { accountId } = input.props if (input.props.state.accounts[accountId] == null) return const { sessionKey } = input.props.state.accounts[accountId] await syncLogin(ai, sessionKey) } // We don't report sync failures, since that could be annoying: const dataTask = makePeriodicTask(doDataSync, SYNC_INTERVAL) const loginTask = makePeriodicTask(doLoginSync, SYNC_INTERVAL, { onError(error) { // Only send OTP errors to the GUI: const otpError = asMaybeOtpError(error) if (otpError != null) input.props.onError(otpError) } }) return { update() { if (_optionalChain([input, 'access', _ => _.props, 'access', _2 => _2.accountOutput, 'optionalAccess', _3 => _3.accountApi]) == null) return // Start once the EdgeAccount API exists: dataTask.start({ wait: SYNC_INTERVAL * (1 + Math.random()) }) loginTask.start({ wait: SYNC_INTERVAL * (1 + Math.random()) }) }, destroy() { dataTask.stop() loginTask.stop() } } }, props => (props.state.paused ? undefined : props) ), /** * Watches for changes to the token state, and writes those to disk. * * The pixie system ensures that multiple `update` calls will not occur * at once. This way, if the GUI makes dozens of calls to `addCustomToken`, * we will consolidate those down to a single write to disk. */ tokenSaver(input) { let lastTokens = initialCustomTokens return async function update() { const { accountId, accountState } = input.props const { customTokens } = accountState if (customTokens !== lastTokens && lastTokens !== initialCustomTokens) { await saveCustomTokens(toApiInput(input), accountId).catch(error => input.props.onError(error) ) await snooze(100) // Rate limiting } lastTokens = customTokens } }, watcher(input) { let lastState // let lastWallets return () => { const { accountState, accountOutput } = input.props if (accountState == null || accountOutput == null) return const { accountApi } = accountOutput // TODO: Remove this once update detection is reliable: if (accountApi != null) update(accountApi) // General account state: if (lastState !== accountState) { lastState = accountState if (accountApi != null) { // TODO: Put this back once we solve the race condition: // update(accountApi) const { currencyConfig, swapConfig } = accountApi for (const pluginId of Object.keys(currencyConfig)) { update(currencyConfig[pluginId]) } for (const pluginId of Object.keys(swapConfig)) { update(swapConfig[pluginId]) } } } // Wallet list: // TODO: Why don't we always detect `currencyWallets` updates? // if (lastWallets !== input.props.output.currency.wallets) { // lastWallets = input.props.output.currency.wallets // if (accountOutput.accountApi != null) update(accountOutput.accountApi) // } } }, currencyWallets(input) { let lastActiveWalletIds return () => { const { accountOutput, accountState } = input.props const { activeWalletIds } = accountState let dirty = lastActiveWalletIds !== activeWalletIds lastActiveWalletIds = activeWalletIds let lastOut = {} if (_optionalChain([accountOutput, 'optionalAccess', _4 => _4.currencyWallets]) != null) { lastOut = accountOutput.currencyWallets } const out = {} const { wallets } = input.props.output.currency for (const walletId of activeWalletIds) { const api = _optionalChain([wallets, 'access', _5 => _5[walletId], 'optionalAccess', _6 => _6.walletApi]) if (api !== lastOut[walletId]) dirty = true if (api != null) out[walletId] = api } if (dirty) input.onOutput(out) } } }) export const accounts = mapPixie( accountPixie, (props) => props.state.accountIds, (props, accountId) => ({ ...props, accountId, accountState: props.state.accounts[accountId], accountOutput: props.output.accounts[accountId] }) )