UNPKG

@yoroi/portfolio

Version:

The Portfolio package of Yoroi SDK

354 lines (351 loc) 10.3 kB
import { App, Portfolio } from '@yoroi/types'; import { hasEntryValue, hasValue, observerMaker, queueTaskMaker } from '@yoroi/common'; import { freeze } from 'immer'; import { filter } from 'rxjs'; import { sortTokenAmountsByInfo } from './helpers/sorting'; import { isEventTokenManagerSync } from './validators/token-manager-event-sync'; import { isFt } from './helpers/is-ft'; import { isNft } from './helpers/is-nft'; export const portfolioBalanceManagerMaker = function (_ref) { let { tokenManager, primaryTokenInfo, storage, sourceId } = _ref; let { observer = observerMaker(), queue = queueTaskMaker() } = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; let isHydrated = false; let secondaries = freeze(new Map()); let sortedBalances = freeze({ records: new Map(), all: [], nfts: [], fts: [] }); let primaryBreakdown = freeze({ lockedAsStorageCost: 0n, availableRewards: 0n, totalFromTxs: 0n }, true); let primaryBalance = freeze({ quantity: 0n, info: primaryTokenInfo }, true); let hasOnlyPrimary = true; let isEmpty = true; const sortBalances = balancesSorter({ primaryTokenInfo }); const updatePrimary = primaryUpdater({ primaryTokenInfo, storagePrimaryBreakdown: storage.primaryBreakdown }); const subscription = tokenManager.observable$.pipe(filter(() => isHydrated), filter(dtoEvent => isNotTriggeredBySelf(sourceId)(dtoEvent)), filter(dtoEvent => isEventTokenManagerSync(dtoEvent) && hasStaleTokenInfo(secondaries)(dtoEvent))).subscribe(() => { queue.enqueue(() => new Promise(resolve => { const asyncExecutor = async () => { secondaries = await refreshTokenInfos({ tokenManager, secondaries, sourceId }); resolve(); }; asyncExecutor(); })); }); const hydrate = () => { const cachedPrimaryBreakdown = freeze(storage.primaryBreakdown.read(), true); const lockedAsStorageCost = cachedPrimaryBreakdown?.lockedAsStorageCost ?? 0n; const totalFromTxs = cachedPrimaryBreakdown?.totalFromTxs ?? 0n; const availableRewards = cachedPrimaryBreakdown?.availableRewards ?? 0n; const balance = totalFromTxs + availableRewards; const newPrimaryBreakdown = freeze({ lockedAsStorageCost, availableRewards, totalFromTxs }); const newPrimaryBalance = freeze({ quantity: balance, info: primaryTokenInfo }, true); secondaries = freeze(new Map(storage.balances.all().filter(hasEntryValue)), true); const sorted = sortBalances({ balances: [...secondaries.values(), newPrimaryBalance] }); sortedBalances = splitByType(sorted); primaryBreakdown = newPrimaryBreakdown; primaryBalance = newPrimaryBalance; hasOnlyPrimary = sortedBalances.all.length === 1; isEmpty = hasOnlyPrimary && primaryBalance.quantity === 0n; isHydrated = true; observer.notify({ on: Portfolio.Event.ManagerOn.Hydrate, sourceId }); }; const refresh = () => syncBalances({ primaryStated: primaryBreakdown, secondaryBalances: secondaries }); const updatePrimaryStated = _ref2 => { let { lockedAsStorageCost, totalFromTxs } = _ref2; // state const { availableRewards } = primaryBreakdown; const newPrimaryBreakdown = { availableRewards, // args lockedAsStorageCost, totalFromTxs }; const newPrimaryBalance = updatePrimary(newPrimaryBreakdown); primaryBreakdown = newPrimaryBreakdown; primaryBalance = newPrimaryBalance; observer.notify({ on: Portfolio.Event.ManagerOn.Sync, sourceId, mode: 'primary-stated' }); }; const updatePrimaryDerived = _ref3 => { let { availableRewards } = _ref3; // state const { totalFromTxs, lockedAsStorageCost } = primaryBreakdown; const newPrimaryBreakdown = { lockedAsStorageCost, totalFromTxs, // args availableRewards }; const newPrimaryBalance = updatePrimary(newPrimaryBreakdown); primaryBreakdown = newPrimaryBreakdown; primaryBalance = newPrimaryBalance; observer.notify({ on: Portfolio.Event.ManagerOn.Sync, sourceId, mode: 'primary-derived' }); }; const syncBalances = _ref4 => { let { primaryStated, secondaryBalances } = _ref4; queue.enqueue(() => new Promise((resolve, reject) => { const asyncExecutor = async () => { const secondaryTokenIds = [...secondaryBalances.keys()]; const tokenInfos = await tokenManager.sync({ secondaryTokenIds, sourceId }); const newBalances = new Map(); secondaryBalances.forEach((_ref5, id) => { let { quantity } = _ref5; const cachedTokenInfo = tokenInfos.get(id); if (!cachedTokenInfo) return reject(new App.Errors.InvalidState('Missing token info in cache should never happen')); const secondaryBalance = { info: cachedTokenInfo.record, quantity }; newBalances.set(id, secondaryBalance); }); const { availableRewards } = primaryBreakdown; const { totalFromTxs, lockedAsStorageCost } = primaryStated; const newPrimaryBreakdown = { totalFromTxs, lockedAsStorageCost, availableRewards }; // persist storage.balances.clear(); storage.balances.save([...newBalances.entries()]); const newPrimaryBalance = updatePrimary(newPrimaryBreakdown); // update state secondaries = freeze(newBalances, true); const sorted = sortBalances({ balances: [...secondaries.values(), newPrimaryBalance] }); sortedBalances = splitByType(sorted); primaryBreakdown = newPrimaryBreakdown; primaryBalance = newPrimaryBalance; hasOnlyPrimary = sortedBalances.all.length === 1; isEmpty = hasOnlyPrimary && primaryBalance.quantity === 0n; observer.notify({ on: Portfolio.Event.ManagerOn.Sync, sourceId, mode: 'all' }); resolve(); }; asyncExecutor(); })); }; const getPrimaryBreakdown = () => primaryBreakdown; const getPrimaryBalance = () => primaryBalance; const getHasOnlyPrimary = () => hasOnlyPrimary; const getBalances = () => sortedBalances; const getIsEmpty = () => isEmpty; const destroy = () => { observer.destroy(); queue.destroy(); tokenManager.unsubscribe(subscription); }; const clear = () => { queue.enqueue(() => new Promise(resolve => { const asyncExecutor = async () => { storage.balances.clear(); storage.primaryBreakdown.clear(); secondaries = freeze(new Map(), true); primaryBreakdown = freeze({ lockedAsStorageCost: 0n, availableRewards: 0n, totalFromTxs: 0n }, true); primaryBalance = freeze({ quantity: 0n, info: primaryTokenInfo }, true); sortedBalances = freeze({ records: new Map(), all: [], nfts: [], fts: [] }, true); observer.notify({ on: Portfolio.Event.ManagerOn.Clear, sourceId }); resolve(); }; asyncExecutor(); })); }; return freeze({ hydrate, refresh, syncBalances, updatePrimaryDerived, updatePrimaryStated, subscribe: observer.subscribe, unsubscribe: observer.unsubscribe, observable$: observer.observable, getPrimaryBreakdown, getPrimaryBalance, getHasOnlyPrimary, getBalances, getIsEmpty, destroy, clear }, true); }; const balancesSorter = _ref6 => { let { primaryTokenInfo } = _ref6; return _ref7 => { let { balances } = _ref7; return freeze(sortTokenAmountsByInfo({ amounts: balances.filter(hasValue), primaryTokenInfo }), true); }; }; const isNotTriggeredBySelf = sourceId => dtoEvent => dtoEvent.sourceId !== sourceId; const hasStaleTokenInfo = secondaries => dtoEvent => dtoEvent.ids.some(id => secondaries.has(id)); const refreshTokenInfos = async _ref8 => { let { tokenManager, secondaries, sourceId } = _ref8; const tokenInfos = await tokenManager.sync({ secondaryTokenIds: [...secondaries.keys()], sourceId }); const newBalances = [...secondaries.values()].filter(hasValue).map(balance => { const newBalance = { ...balance, info: tokenInfos.get(balance.info.id)?.record ?? balance.info }; return [newBalance.info.id, newBalance]; }); return freeze(new Map(newBalances), true); }; const splitByType = sortedBalances => { return freeze({ records: new Map(sortedBalances.map(_ref9 => { let { info, quantity } = _ref9; return [info.id, { info, quantity }]; })), all: sortedBalances, fts: sortedBalances.filter(_ref10 => { let { info } = _ref10; return isFt(info); }), nfts: sortedBalances.filter(_ref11 => { let { info } = _ref11; return isNft(info); }) }, true); }; const primaryUpdater = _ref12 => { let { storagePrimaryBreakdown, primaryTokenInfo } = _ref12; return newPrimaryBreakdown => { storagePrimaryBreakdown.clear(); storagePrimaryBreakdown.save(newPrimaryBreakdown); const { availableRewards, totalFromTxs } = newPrimaryBreakdown; return freeze({ info: primaryTokenInfo, quantity: availableRewards + totalFromTxs }, true); }; }; // TODO list // - [ ] Allow users to specify custom properties // - [ ] Allow users to create baskets for tokens // - [ ] Allow users to add tokens to baskets // - [ ] Allow users to remove tokens from baskets // - [ ] Allow users to delete baskets // - [ ] - getBaskets + Baskets event // - [ ] Allow users to mark tokens as favorite // - [ ] Allow users to mark to auto-garbage tokens // - [ ] Allow users to ban tokens (mark as scam) // - [ ] - override token info + discovery + Event //# sourceMappingURL=balance-manager.js.map