edge-core-js
Version:
Edge account & wallet management library
411 lines (303 loc) • 10.6 kB
JavaScript
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' }
})