@yoroi/portfolio
Version:
The Portfolio package of Yoroi SDK
354 lines (351 loc) • 10.3 kB
JavaScript
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