UNPKG

edge-core-js

Version:

Edge account & wallet management library

411 lines (303 loc) 10.6 kB
function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } 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 { buildReducer, filterReducer, memoizeReducer } from 'redux-keto' import { compare } from '../../util/compare' import { verifyData } from '../../util/crypto/verify' import { decryptAllWalletInfos, findFirstKey, makeAccountType } from '../login/keys' import { makeLoginTree, searchTree } from '../login/login' import { maybeFindCurrencyPluginId } from '../plugins/plugins-selectors' export const initialCustomTokens = {} const blankSessionKey = { loginId: new Uint8Array(), loginKey: new Uint8Array() } const accountInner = buildReducer({ accountWalletInfo: memoizeReducer( (next) => next.self.activeAppId, (next) => next.self.allWalletInfosFull, (appId, walletInfos) => { const type = makeAccountType(appId) const accountWalletInfo = findFirstKey(walletInfos, type) if (accountWalletInfo == null) { throw new Error(`Cannot find a "${type}" repo`) } return accountWalletInfo } ), accountWalletInfos: memoizeReducer( (next) => next.self.activeAppId, (next) => next.self.allWalletInfosFull, (appId, walletInfos) => { // Wallets created in Edge that then log into Airbitz or BitcoinPay // might end up with wallets stored in the wrong account repo. // This code attempts to locate those repos. const walletTypes = [makeAccountType(appId)] if (appId === '') walletTypes.push('account:repo:co.airbitz.wallet', '') return walletInfos.filter(info => walletTypes.includes(info.type)) } ), allWalletInfosFull: memoizeReducer( (next) => next.self.stashTree, (next) => next.self.login, (next) => next.self.legacyWalletInfos, (next) => next.self.walletStates, ( stashTree, appSessionKey, legacyWalletInfos, walletStates ) => decryptAllWalletInfos( stashTree, appSessionKey, legacyWalletInfos, walletStates ) ), allWalletInfosClean: memoizeReducer( (next) => next.self.allWalletInfosFull, (walletInfos) => walletInfos.map(info => ({ ...info, keys: {} })) ), currencyWalletErrors(state = {}, action, next, prev) { const { activeWalletIds } = next.self const walletStates = next.root.currency.wallets let dirty = activeWalletIds !== _optionalChain([prev, 'access', _ => _.self, 'optionalAccess', _2 => _2.activeWalletIds]) const out = {} for (const id of activeWalletIds) { const failure = walletStates[id].engineFailure if (failure != null) out[id] = failure if (out[id] !== state[id]) dirty = true } return dirty ? out : state }, currencyWalletIds: memoizeReducer( (next) => next.self.walletInfos, (next) => next.root.plugins.currency, (walletInfos, plugins) => Object.keys(walletInfos) .filter(walletId => { const info = walletInfos[walletId] const pluginId = maybeFindCurrencyPluginId(plugins, info.type) return !info.deleted && pluginId != null }) .sort((walletId1, walletId2) => { const info1 = walletInfos[walletId1] const info2 = walletInfos[walletId2] return info1.sortIndex - info2.sortIndex }) ), activeWalletIds: memoizeReducer( (next) => next.self.walletInfos, (next) => next.self.currencyWalletIds, (next) => next.self.keysLoaded, (walletInfos, ids, keysLoaded) => keysLoaded ? ids.filter(id => !walletInfos[id].archived) : [] ), archivedWalletIds: memoizeReducer( (next) => next.self.walletInfos, (next) => next.self.currencyWalletIds, (next) => next.self.keysLoaded, (walletInfos, ids, keysLoaded) => keysLoaded ? ids.filter(id => walletInfos[id].archived) : [] ), hiddenWalletIds: memoizeReducer( (next) => next.self.walletInfos, (next) => next.self.currencyWalletIds, (next) => next.self.keysLoaded, (walletInfos, ids, keysLoaded) => keysLoaded ? ids.filter(id => walletInfos[id].hidden) : [] ), keysLoaded(state = false, action) { return action.type === 'ACCOUNT_KEYS_LOADED' ? true : state }, legacyWalletInfos(state = [], action) { return action.type === 'ACCOUNT_KEYS_LOADED' ? action.payload.legacyWalletInfos : state }, walletInfos: memoizeReducer( (next) => next.self.allWalletInfosFull, (walletInfos) => { const out = {} for (const info of walletInfos) { out[info.id] = info } return out } ), walletStates(state = {}, action) { return action.type === 'ACCOUNT_CHANGED_WALLET_STATES' || action.type === 'ACCOUNT_KEYS_LOADED' ? action.payload.walletStates : state }, pauseWallets(state = false, action) { return action.type === 'LOGIN' ? action.payload.pauseWallets : state }, activeAppId: (state = '', action) => { return action.type === 'LOGIN' ? action.payload.appId : state }, loadFailure(state = null, action) { if (action.type === 'ACCOUNT_LOAD_FAILED') { const { error } = action.payload if (error instanceof Error) return error return new Error(String(error)) } return state }, login: memoizeReducer( (next) => next.self.activeAppId, (next) => next.self.loginTree, (appId, loginTree) => { const out = searchTree(loginTree, login => login.appId === appId) if (out == null) { throw new Error(`Internal error: cannot find login for ${appId}`) } return out } ), loginTree: memoizeReducer( (next) => next.self.stashTree, (next) => next.self.sessionKey, (stashTree, sessionKey) => { const loginTree = makeLoginTree(stashTree, sessionKey) return loginTree } ), loginType(state = 'newAccount', action) { return action.type === 'LOGIN' ? action.payload.loginType : state }, rootLoginId(state = new Uint8Array(0), action) { return action.type === 'LOGIN' ? action.payload.rootLoginId : state }, sessionKey(state = blankSessionKey, action) { return action.type === 'LOGIN' ? action.payload.sessionKey : state }, stashTree: memoizeReducer( (next) => next.self.rootLoginId, (next) => next.root.login.stashes, (rootLoginId, stashes) => { for (const stash of stashes) { if (verifyData(stash.loginId, rootLoginId)) return stash } throw new Error('There is no stash') } ), allTokens(state = {}, action, next, prev) { const { builtinTokens, customTokens } = next.self // Roll our own `memoizeReducer` implementation, // so we can minimize our diff as much as possible: if ( prev.self == null || builtinTokens !== prev.self.builtinTokens || customTokens !== prev.self.customTokens ) { const out = { ...state } for (const pluginId of Object.keys(next.root.plugins.currency)) { if ( prev.self == null || builtinTokens[pluginId] !== prev.self.builtinTokens[pluginId] || customTokens[pluginId] !== prev.self.customTokens[pluginId] ) { out[pluginId] = { ...customTokens[pluginId], ...builtinTokens[pluginId] } } } return out } return state }, builtinTokens(state = {}, action) { switch (action.type) { case 'ACCOUNT_BUILTIN_TOKENS_LOADED': { const { pluginId, tokens } = action.payload return { ...state, [pluginId]: tokens } } } return state }, customTokens( state = initialCustomTokens, action ) { switch (action.type) { case 'ACCOUNT_CUSTOM_TOKENS_LOADED': { const { customTokens } = action.payload return customTokens } case 'ACCOUNT_CUSTOM_TOKEN_ADDED': { const { pluginId, tokenId, token } = action.payload const oldList = _nullishCoalesce(state[pluginId], () => ( {})) // Has anything changed? if (compare(oldList[tokenId], token)) return state const newList = { ...oldList, [tokenId]: token } return { ...state, [pluginId]: newList } } case 'ACCOUNT_CUSTOM_TOKEN_REMOVED': { const { pluginId, tokenId } = action.payload const oldList = _nullishCoalesce(state[pluginId], () => ( {})) // Has anything changed? if (oldList[tokenId] == null) return state const { [tokenId]: unused, ...newList } = oldList return { ...state, [pluginId]: newList } } } return state }, alwaysEnabledTokenIds(state = {}, action) { switch (action.type) { case 'ACCOUNT_ALWAYS_ENABLED_TOKENS_CHANGED': { const { pluginId, tokenIds } = action.payload return { ...state, [pluginId]: tokenIds } } } return state }, swapSettings(state = {}, action) { switch (action.type) { case 'ACCOUNT_PLUGIN_SETTINGS_LOADED': return action.payload.swapSettings case 'ACCOUNT_SWAP_SETTINGS_CHANGED': { const { pluginId, swapSettings } = action.payload const out = { ...state } out[pluginId] = swapSettings return out } } return state }, userSettings(state = {}, action) { switch (action.type) { case 'ACCOUNT_PLUGIN_SETTINGS_CHANGED': { const { pluginId, userSettings } = action.payload const out = { ...state } out[pluginId] = userSettings return out } case 'ACCOUNT_PLUGIN_SETTINGS_LOADED': return action.payload.userSettings } return state } }) export const accountReducer = filterReducer (accountInner, (action, next) => { if ( /^ACCOUNT_/.test(action.type) && 'payload' in action && typeof action.payload === 'object' && 'accountId' in action.payload && action.payload.accountId === next.id ) { return action } if (action.type === 'LOGIN' && next.root.lastAccountId === next.id) { return action } return { type: 'UPDATE_NEXT' } })