UNPKG

edge-core-js

Version:

Edge account & wallet management library

556 lines (391 loc) 13 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 { lt } from 'biggystring' import { buildReducer, filterReducer, memoizeReducer } from 'redux-keto' import { compare } from '../../../util/compare' import { findCurrencyPluginId } from '../../plugins/plugins-selectors' import { uniqueStrings } from './enabled-tokens' /** Maps from txid hash to file creation date & path. */ // Used for detectedTokenIds & enabledTokenIds: export const initialTokenIds = [] const currencyWalletInner = buildReducer ({ accountId(state, action, next) { if (state != null) return state for (const accountId of Object.keys(next.root.accounts)) { const account = next.root.accounts[accountId] for (const walletId of Object.keys(account.walletInfos)) { if (walletId === next.id) return accountId } } throw new Error(`Cannot find account for walletId ${next.id}`) }, allEnabledTokenIds: memoizeReducer( (next) => next.root.accounts[next.self.accountId].alwaysEnabledTokenIds[ next.self.pluginId ], (next) => next.self.enabledTokenIds, (alwaysEnabledTokenIds = [], enabledTokenIds = []) => uniqueStrings([...alwaysEnabledTokenIds, ...enabledTokenIds]) ), pluginId: memoizeReducer( next => next.root.login.walletInfos[next.id].type, next => next.root.plugins.currency, (walletType, plugins) => { return findCurrencyPluginId(plugins, walletType) } ), paused(state, action, next) { return state == null ? next.root.accounts[next.self.accountId].pauseWallets : action.type === 'CURRENCY_WALLET_CHANGED_PAUSED' ? action.payload.paused : state }, currencyInfo(state, action, next) { if (state != null) return state const { pluginId } = next.self return next.root.plugins.currency[pluginId].currencyInfo }, detectedTokenIds: sortStringsReducer( (state = initialTokenIds, action) => { if (action.type === 'CURRENCY_WALLET_LOADED_TOKEN_FILE') { return action.payload.detectedTokenIds } else if (action.type === 'CURRENCY_ENGINE_DETECTED_TOKENS') { const { detectedTokenIds } = action.payload return uniqueStrings([...state, ...detectedTokenIds]) } else if (action.type === 'CURRENCY_ENGINE_CLEARED') { return [] } return state } ), enabledTokenIds: sortStringsReducer( (state = initialTokenIds, action) => { if (action.type === 'CURRENCY_WALLET_LOADED_TOKEN_FILE') { return action.payload.enabledTokenIds } else if (action.type === 'CURRENCY_WALLET_ENABLED_TOKENS_CHANGED') { return action.payload.enabledTokenIds } else if (action.type === 'CURRENCY_ENGINE_DETECTED_TOKENS') { const { enablingTokenIds } = action.payload return uniqueStrings([...state, ...enablingTokenIds]) } return state } ), changeServiceSubscriptions(state = [], action) { if (action.type === 'CURRENCY_ENGINE_UPDATE_CHANGE_SERVICE_SUBSCRIPTIONS') { const filteredState = state.filter(subscription => { return !action.payload.subscriptions.some( sub => sub.address === subscription.address ) }) return [...filteredState, ...action.payload.subscriptions] } return state }, tokenFileDirty(state = false, action, next, prev) { switch (action.type) { case 'CURRENCY_WALLET_LOADED_TOKEN_FILE': case 'CURRENCY_WALLET_SAVED_TOKEN_FILE': // The file has been synced to disk, so it's not dirty: return false case 'CURRENCY_ENGINE_CLEARED': case 'CURRENCY_ENGINE_DETECTED_TOKENS': case 'CURRENCY_WALLET_ENABLED_TOKENS_CHANGED': // These actions might update the file, so check for diffs: return ( next.self.detectedTokenIds !== prev.self.detectedTokenIds || next.self.enabledTokenIds !== prev.self.enabledTokenIds ) default: return state } }, tokenFileLoaded(state = false, action) { switch (action.type) { case 'CURRENCY_WALLET_LOADED_TOKEN_FILE': return true case 'CURRENCY_ENGINE_CLEARED': return false default: return state } }, engineFailure(state = null, action) { if (action.type === 'CURRENCY_ENGINE_FAILED') { const { error } = action.payload if (error instanceof Error) return error return new Error(String(error)) } return state }, engineStarted(state = false, action) { return action.type === 'CURRENCY_ENGINE_STARTED' ? true : action.type === 'CURRENCY_ENGINE_STOPPED' ? false : state }, fiat(state = '', action) { return action.type === 'CURRENCY_WALLET_FIAT_CHANGED' ? action.payload.fiatCurrencyCode : state }, fiatLoaded(state = false, action) { return action.type === 'CURRENCY_WALLET_FIAT_CHANGED' ? true : state }, files(state = {}, action) { switch (action.type) { case 'CURRENCY_WALLET_FILE_CHANGED': { const { json, txidHash } = action.payload const out = { ...state } out[txidHash] = json return out } case 'CURRENCY_WALLET_FILES_LOADED': { const { files } = action.payload return { ...state, ...files } } } return state }, fileNames(state = {}, action) { switch (action.type) { case 'CURRENCY_WALLET_FILE_NAMES_LOADED': { const { txFileNames } = action.payload return { ...state, ...txFileNames } } case 'CURRENCY_WALLET_FILE_CHANGED': { const { fileName, creationDate, txidHash } = action.payload if ( state[txidHash] == null || creationDate < state[txidHash].creationDate ) { state[txidHash] = { creationDate, fileName } } return state } } return state }, syncRatio(state = 0, action) { switch (action.type) { case 'CURRENCY_ENGINE_CHANGED_SYNC_RATIO': { return action.payload.ratio } case 'CURRENCY_ENGINE_CLEARED': { return 0 } } return state }, balanceMap(state = new Map(), action) { if (action.type === 'CURRENCY_ENGINE_CHANGED_BALANCE') { const { balance, tokenId } = action.payload const out = new Map(state) out.set(tokenId, balance) return out } return state }, balances: memoizeReducer( next => next.self.balanceMap, next => next.self.currencyInfo, next => next.root.accounts[next.self.accountId].allTokens[next.self.pluginId], (balanceMap, currencyInfo, allTokens) => { const out = {} for (const tokenId of balanceMap.keys()) { const balance = balanceMap.get(tokenId) const { currencyCode } = tokenId == null ? currencyInfo : allTokens[tokenId] if (balance != null) out[currencyCode] = balance } return out } ), height(state = 0, action) { return action.type === 'CURRENCY_ENGINE_CHANGED_HEIGHT' ? action.payload.height : state }, name(state = null, action) { return action.type === 'CURRENCY_WALLET_NAME_CHANGED' ? action.payload.name : state }, nameLoaded(state = false, action) { return action.type === 'CURRENCY_WALLET_NAME_CHANGED' ? true : state }, seenTxCheckpoint(state = null, action) { return action.type === 'CURRENCY_ENGINE_SEEN_TX_CHECKPOINT_CHANGED' ? action.payload.checkpoint : state }, sortedTxidHashes: memoizeReducer( next => next.self.txidHashes, txidHashes => Object.keys(txidHashes).sort((txidHash1, txidHash2) => { if (txidHashes[txidHash1].date > txidHashes[txidHash2].date) return -1 if (txidHashes[txidHash1].date < txidHashes[txidHash2].date) return 1 return 0 }) ), stakingStatus(state = { stakedAmounts: [] }, action) { return action.type === 'CURRENCY_ENGINE_CHANGED_STAKING' ? action.payload.stakingStatus : state }, txidHashes(state = {}, action) { switch (action.type) { case 'CURRENCY_ENGINE_CHANGED_TXS': { return mergeTxidHashes(state, action.payload.txidHashes) } case 'CURRENCY_WALLET_FILE_NAMES_LOADED': { const { txFileNames } = action.payload const newTxidHashes = {} for (const txidHash of Object.keys(txFileNames)) { newTxidHashes[txidHash] = { date: txFileNames[txidHash].creationDate } } return mergeTxidHashes(state, newTxidHashes) } } return state }, txs(state = {}, action, next) { switch (action.type) { case 'CHANGE_MERGE_TX': { const { tx } = action.payload const out = { ...state } out[tx.txid] = tx return out } case 'CURRENCY_ENGINE_CHANGED_TXS': { const { txs } = action.payload const out = { ...state } for (const tx of txs) { out[tx.txid] = mergeTx(tx, out[tx.txid]) } return out } case 'CURRENCY_ENGINE_CLEARED': return {} } return state }, unactivatedTokenIds(state = [], action) { switch (action.type) { case 'CURRENCY_ENGINE_CHANGED_UNACTIVATED_TOKEN_IDS': { return action.payload.unactivatedTokenIds } case 'CURRENCY_ENGINE_CLEARED': { return [] } } return state }, gotTxs(state = new Set(), action) { switch (action.type) { case 'CURRENCY_ENGINE_GOT_TXS': { const { tokenId } = action.payload const out = new Set(state) out.add(tokenId) return out } case 'CURRENCY_ENGINE_CLEARED': return new Set() default: return state } }, walletInfo(state, action, next) { return next.root.login.walletInfos[next.id] }, publicWalletInfo(state = null, action) { return action.type === 'CURRENCY_WALLET_PUBLIC_INFO' ? action.payload.walletInfo : state } }) function mergeTxidHashes(a, b) { const out = { ...a } for (const hash of Object.keys(b)) { const oldItem = out[hash] const newItem = b[hash] out[hash] = oldItem == null ? newItem : { date: Math.min(newItem.date, oldItem.date), txid: _nullishCoalesce(newItem.txid, () => ( oldItem.txid)) } } return out } export const currencyWalletReducer = filterReducer (currencyWalletInner, (action, next) => { return /^CURRENCY_/.test(action.type) && 'payload' in action && typeof action.payload === 'object' && 'walletId' in action.payload && action.payload.walletId === next.id ? action : { type: 'UPDATE_NEXT' } }) /** * Merges a new incoming transaction with an existing transaction. */ export function mergeTx( tx, oldTx ) { const { confirmations = 'unconfirmed', isSend = lt(tx.nativeAmount, '0'), tokenId = null } = tx const out = { blockHeight: tx.blockHeight, chainAction: tx.chainAction, chainAssetAction: new Map(_nullishCoalesce(_optionalChain([oldTx, 'optionalAccess', _ => _.chainAssetAction]), () => ( []))), confirmations, date: tx.date, isSend, memos: tx.memos, nativeAmount: new Map(_nullishCoalesce(_optionalChain([oldTx, 'optionalAccess', _2 => _2.nativeAmount]), () => ( []))), networkFee: new Map(_nullishCoalesce(_optionalChain([oldTx, 'optionalAccess', _3 => _3.networkFee]), () => ( []))), otherParams: tx.otherParams, ourReceiveAddresses: tx.ourReceiveAddresses, signedTx: tx.signedTx, txid: tx.txid } out.nativeAmount.set(tokenId, tx.nativeAmount) out.networkFee.set(tokenId, _nullishCoalesce(tx.networkFee, () => ( '0'))) if (tx.feeRateUsed != null) { out.feeRateUsed = tx.feeRateUsed } if (tx.parentNetworkFee != null) { out.networkFee.set(null, String(tx.parentNetworkFee)) } if (tx.chainAssetAction != null) { out.chainAssetAction.set(tokenId, tx.chainAssetAction) } return out } function sortStringsReducer(reducer) { return (state, action) => { const out = reducer(state, action) if (out === state) return state out.sort((a, b) => (a === b ? 0 : a > b ? 1 : -1)) if (state == null || !compare(out, state)) return out return state } }