edge-core-js
Version:
Edge account & wallet management library
275 lines (223 loc) • 8.6 kB
JavaScript
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]
})
)