Skip to content
This repository has been archived by the owner on Jan 24, 2024. It is now read-only.

Commit

Permalink
feat(position-presenter): Add concept of position presenter (#1220)
Browse files Browse the repository at this point in the history
  • Loading branch information
JForsaken committed Aug 19, 2022
1 parent 2bd4326 commit ce5e530
Show file tree
Hide file tree
Showing 18 changed files with 263 additions and 122 deletions.
7 changes: 4 additions & 3 deletions src/app-toolkit/app-toolkit.service.ts
Expand Up @@ -8,10 +8,10 @@ import { ContractFactory } from '~contract';
import { MulticallService } from '~multicall/multicall.service';
import { NetworkProviderService } from '~network-provider/network-provider.service';
import { DefaultDataProps } from '~position/display.interface';
import { PositionFetcherTemplateRegistry } from '~position/position-fetcher.template-registry';
import { PositionKeyService } from '~position/position-key.service';
import { AppTokenPosition, ContractPosition, NonFungibleToken } from '~position/position.interface';
import { AppGroupsDefinition, PositionService } from '~position/position.service';
import { AppTokenSelectorService } from '~position/selectors/app-token-selector.service';
import { CreateTokenDependencySelectorOptions } from '~position/selectors/token-dependency-selector.interface';
import { TokenDependencySelectorService } from '~position/selectors/token-dependency-selector.service';
import { BaseToken } from '~position/token.interface';
Expand All @@ -30,9 +30,10 @@ export class AppToolkit implements IAppToolkit {
@Inject(AppService) private readonly appService: AppService,
@Inject(NetworkProviderService) private readonly networkProviderService: NetworkProviderService,
@Inject(PositionService) private readonly positionService: PositionService,
@Inject(PositionKeyService) private readonly positionKeyService: PositionKeyService,
@Inject(PositionFetcherTemplateRegistry)
@Inject(PositionKeyService)
private readonly positionKeyService: PositionKeyService,
@Inject(PriceSelectorService) private readonly priceSelectorService: PriceSelectorService,
@Inject(AppTokenSelectorService) private readonly appTokenSelectorService: AppTokenSelectorService,
@Inject(TokenDependencySelectorService)
private readonly tokenDependencySelectorService: TokenDependencySelectorService,
@Inject(MulticallService) private readonly multicallService: MulticallService,
Expand Down
7 changes: 7 additions & 0 deletions src/app-toolkit/decorators/balance-product-meta.decorator.ts
@@ -0,0 +1,7 @@
import { applyDecorators, SetMetadata } from '@nestjs/common';

export const BALANCE_PRODUCT_META_SELECTOR = 'BALANCE_PRODUCT_META_SELECTOR';

export const BalanceProductMeta = (groupSelector: string) => {
return applyDecorators(SetMetadata(BALANCE_PRODUCT_META_SELECTOR, groupSelector));
};
7 changes: 0 additions & 7 deletions src/app-toolkit/decorators/group-meta.decorator.ts

This file was deleted.

4 changes: 2 additions & 2 deletions src/app-toolkit/decorators/index.ts
Expand Up @@ -5,7 +5,7 @@ import { PositionFetcher } from '~position/position-fetcher.decorator';

import { BalanceFetcher } from './balance-fetcher.decorator';
import { BalancePresenter } from './balance-presenter.decorator';
import { GroupMeta } from './group-meta.decorator';
import { BalanceProductMeta } from './balance-product-meta.decorator';

export const Register = {
AppDefinition,
Expand All @@ -16,5 +16,5 @@ export const Register = {
TokenPositionFetcher: PositionFetcher(ContractType.APP_TOKEN),
ContractPositionBalanceFetcher: PositionBalanceFetcher(ContractType.POSITION),
TokenPositionBalanceFetcher: PositionBalanceFetcher(ContractType.APP_TOKEN),
GroupMeta: GroupMeta,
BalanceProductMeta,
};
Expand Up @@ -12,6 +12,7 @@ import { ContractType } from '~position/contract.interface';
import { ContractPositionBalance } from '~position/position-balance.interface';
import { isClaimable, isSupplied } from '~position/position.utils';
import { Network } from '~types/network.interface';

import { NetworkId } from '../helpers/constants';
import { GoodGhostingGameConfigFetcherHelper } from '../helpers/good-ghosting.game.config-fetcher';

Expand Down
Expand Up @@ -3,9 +3,9 @@ import axios from 'axios';

import { SingleStakingFarmDefinition } from '~app-toolkit';
import { CacheOnInterval } from '~cache/cache-on-interval.decorator';
import { NetworkId, getGameVersionType, RewardType, transformRewardArrayToObject } from '../helpers/constants';

import GOOD_GHOSTING_DEFINITION from '../good-ghosting.definition';
import { NetworkId, getGameVersionType, RewardType, transformRewardArrayToObject } from '../helpers/constants';

import { GamesResponse, PlayerBalance, PlayerResponse, BASE_API_URL } from './constants';

Expand Down
2 changes: 1 addition & 1 deletion src/apps/yearn/yearn.module.ts
Expand Up @@ -21,9 +21,9 @@ import { YearnAppDefinition, YEARN_DEFINITION } from './yearn.definition';
// Helpers
YearnVaultTokenDefinitionsResolver,
// Ethereum
EthereumYearnGovernanceContractPositionFetcher,
EthereumYearnV1VaultTokenFetcher,
EthereumYearnV2VaultTokenFetcher,
EthereumYearnGovernanceContractPositionFetcher,
EthereumYearnYieldTokenFetcher,
// Fantom
FantomYearnV2VaultTokenFetcher,
Expand Down
68 changes: 37 additions & 31 deletions src/balance/balance-presentation.service.ts
Expand Up @@ -3,12 +3,14 @@ import { get, groupBy } from 'lodash';

import { presentBalanceFetcherResponse } from '~app-toolkit/helpers/presentation/balance-fetcher-response.present';
import { AppTokenPositionBalance, ContractPositionBalance } from '~position/position-balance.interface';
import { PositionFetcherRegistry } from '~position/position-fetcher.registry';
import { PositionPresenterRegistry } from '~position/position-presenter.registry';
import { PositionGroup } from '~position/template/position-presenter.template';
import { Network } from '~types';

import { TokenBalanceResponse } from './balance-fetcher.interface';
import { BalancePresenterRegistry } from './balance-presenter.registry';
import { DefaultBalancePresenterFactory } from './default.balance-presenter.factory';
import { DefaultPositionPresenterFactory } from './default.position-presenter.factory';

export type PresentParams = {
appId: string;
Expand All @@ -20,50 +22,64 @@ export type PresentParams = {
@Injectable()
export class BalancePresentationService {
constructor(
@Inject(PositionFetcherRegistry) private readonly positionFetcherRegistry: PositionFetcherRegistry,
@Inject(PositionPresenterRegistry) private readonly positionPresenterRegistry: PositionPresenterRegistry,
@Inject(BalancePresenterRegistry) private readonly balancePresenterRegistry: BalancePresenterRegistry,
@Inject(DefaultBalancePresenterFactory)
private readonly defaultBalancePresenterFactory: DefaultBalancePresenterFactory,
@Inject(DefaultPositionPresenterFactory)
private readonly defaultPositionPresenterFactory: DefaultPositionPresenterFactory,
) {}

private groupBalancesByGroupLabel(
private groupBalancesByPositionGroup(
balances: (AppTokenPositionBalance | ContractPositionBalance)[],
groupLabel: string,
positionGroup: PositionGroup,
) {
const getDynamicLabel = (label: string) => {
const matches = label.match(/{{(.*)}}/);
if (!matches) return null;
return matches[1].trim();
};

const dynamicLabel = getDynamicLabel(groupLabel);
if (!dynamicLabel) return { [groupLabel]: balances };
const dynamicLabel = getDynamicLabel(positionGroup.label);
if (!dynamicLabel)
return { [positionGroup.label]: balances.filter(({ groupId }) => positionGroup.groupIds.includes(groupId)) };
else {
return groupBy(balances, balance => get(balance, dynamicLabel));
}
}

private async groupMetaProcessor(
appId: string,
network: Network,
balances: (AppTokenPositionBalance | ContractPositionBalance)[],
) {
const groupMetaResolvers = this.balancePresenterRegistry.getMetaResolvers(appId, network);
const hasMissingGroupLabel = balances.some(({ groupLabel }) => !groupLabel);
if (hasMissingGroupLabel) return null;
async present({ appId, address, network, balances }: PresentParams): Promise<TokenBalanceResponse> {
const presenter =
this.balancePresenterRegistry.get(appId, network) ??
this.defaultBalancePresenterFactory.build({ appId, network });

return presenter.present(address, balances);
}

async presentTemplates({ appId, network, balances }: PresentParams): Promise<TokenBalanceResponse> {
// Use default presenter when no custom presenter
const customPresenter = this.positionPresenterRegistry.get(appId, network);
const defaultPresenter = this.defaultPositionPresenterFactory.build({ appId, network });
if (!customPresenter) return defaultPresenter.presentBalances(balances);

// Group balances by group label specified in the template `this.groupLabel`
const groupedBalances = groupBy(balances, balance => balance.groupLabel);
const filteredBalances = balances.filter(
({ groupId }) => !customPresenter.excludedGroupIdsFromBalances.includes(groupId),
);

// When balance product meta resolvers, use default presenter with position groups from either the custom or default presenter
const positionGroups = customPresenter.positionGroups ?? defaultPresenter.getBalanceProductGroups();
const balanceProductMetaResolvers = this.positionPresenterRegistry.getBalanceProductMetaResolvers(appId, network);
if (!balanceProductMetaResolvers) return defaultPresenter.presentBalances(filteredBalances, positionGroups);

// Try to resolve balance product metas, grouping balances by group selector specified in the custom presenter
const presentedBalances = await Promise.all(
Object.entries(groupedBalances).map(async ([groupLabel, balances]) => {
// For each group label, compute group label (might be using a selector), and group by the results
const balancesByGroupLabel = this.groupBalancesByGroupLabel(balances, groupLabel);
positionGroups.map(positionGroup => {
const groupedBalances = this.groupBalancesByPositionGroup(filteredBalances, positionGroup);

return Promise.all(
// For each computed label group, run the meta resolve if exists
Object.entries(balancesByGroupLabel).map(async ([computedGroupLabel, balances]) => {
const groupMetaResolver = groupMetaResolvers?.get(groupLabel);
Object.entries(groupedBalances).map(async ([computedGroupLabel, balances]) => {
const groupMetaResolver = balanceProductMetaResolvers?.get(positionGroup.label);
if (!groupMetaResolver) return { label: computedGroupLabel, assets: balances };
else {
const meta = await groupMetaResolver(balances);
Expand All @@ -76,14 +92,4 @@ export class BalancePresentationService {

return presentBalanceFetcherResponse(presentedBalances.flat());
}

async present({ appId, address, network, balances }: PresentParams): Promise<TokenBalanceResponse> {
const presenter =
this.balancePresenterRegistry.get(appId, network) ??
this.defaultBalancePresenterFactory.build({ appId, network });

let presentedBalances: TokenBalanceResponse | null = await this.groupMetaProcessor(appId, network, balances);
presentedBalances ??= await presenter.present(address, balances);
return presentedBalances;
}
}
6 changes: 0 additions & 6 deletions src/balance/balance-presenter.interface.ts
@@ -1,13 +1,7 @@
import { MetadataItemWithLabel } from '~balance/balance-fetcher.interface';
import { PositionBalance } from '~position/position-balance.interface';
import { ContractPositionBalance, TokenBalance } from '~position/position-balance.interface';

import { TokenBalanceResponse } from './balance-fetcher.interface';

export interface BalancePresenter {
present(address: string, balances: PositionBalance[]): Promise<TokenBalanceResponse>;
}

export type ReadonlyBalances = ReadonlyArray<Readonly<Balance>>;
export type GroupMeta = MetadataItemWithLabel[];
export type Balance = TokenBalance | ContractPositionBalance;
40 changes: 3 additions & 37 deletions src/balance/balance-presenter.registry.ts
@@ -1,60 +1,26 @@
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
import { DiscoveryService, MetadataScanner, Reflector } from '@nestjs/core';
import { DiscoveryService } from '@nestjs/core';

import { BALANCE_PRESENTER_APP, BALANCE_PRESENTER_NETWORK } from '~app-toolkit/decorators/balance-presenter.decorator';
import { BALANCE_PRESENTER_GROUP_LABEL } from '~app-toolkit/decorators/group-meta.decorator';
import { Network } from '~types/network.interface';
import { buildRegistry } from '~utils/build-registry';

import { BalancePresenter, GroupMeta, ReadonlyBalances } from './balance-presenter.interface';
import { BalancePresenter } from './balance-presenter.interface';

@Injectable()
export class BalancePresenterRegistry implements OnModuleInit {
private registry = new Map<Network, Map<string, BalancePresenter>>();
private groupMetaResolverRegistry = new Map<
Network,
Map<string, Map<string, (balances: ReadonlyBalances) => Promise<GroupMeta>>>
>();

constructor(
@Inject(DiscoveryService) private readonly discoveryService: DiscoveryService,
@Inject(MetadataScanner) private readonly metadataScanner: MetadataScanner,
@Inject(Reflector) private readonly reflector: Reflector,
) {}
constructor(@Inject(DiscoveryService) private readonly discoveryService: DiscoveryService) {}

onModuleInit() {
this.registry = buildRegistry<[Network, string], BalancePresenter>(this.discoveryService, [
BALANCE_PRESENTER_NETWORK,
BALANCE_PRESENTER_APP,
]);

// Build group meta registry
this.registry.forEach((r, network) => {
r.forEach((presenter, appId) => {
this.metadataScanner.scanFromPrototype(presenter, Object.getPrototypeOf(presenter), (methodName: string) => {
this.registerGroupMeta(presenter, methodName, { network, appId });
});
});
});
}

private registerGroupMeta(instance: any, methodName: string, ctx: { network: Network; appId: string }) {
const methodRef = instance[methodName];
const groupLabel = this.reflector.get(BALANCE_PRESENTER_GROUP_LABEL, methodRef);
if (!groupLabel) return;

if (!this.groupMetaResolverRegistry.get(ctx.network)) this.groupMetaResolverRegistry.set(ctx.network, new Map());
if (!this.groupMetaResolverRegistry.get(ctx.network)?.get(ctx.appId))
this.groupMetaResolverRegistry.get(ctx.network)?.set(ctx.appId, new Map());

this.groupMetaResolverRegistry.get(ctx.network)?.get(ctx.appId)?.set(groupLabel, methodRef);
}

get(appId: string, network: Network) {
return this.registry.get(network)?.get(appId) ?? null;
}

getMetaResolvers(appId: string, network: Network) {
return this.groupMetaResolverRegistry.get(network)?.get(appId) ?? null;
}
}
2 changes: 2 additions & 0 deletions src/balance/balance.module.ts
Expand Up @@ -13,6 +13,7 @@ import { BalanceController } from './balance.controller';
import { BalanceService } from './balance.service';
import { DefaultBalancePresenterFactory } from './default.balance-presenter.factory';
import { DefaultContractPositionBalanceFetcherFactory } from './default.contract-position-balance-fetcher.factory';
import { DefaultPositionPresenterFactory } from './default.position-presenter.factory';
import { DefaultTokenBalanceFetcherFactory } from './default.token-balance-fetcher.factory';

@Module({
Expand All @@ -24,6 +25,7 @@ import { DefaultTokenBalanceFetcherFactory } from './default.token-balance-fetch
BalanceService,
DefaultBalancePresenterFactory,
DefaultContractPositionBalanceFetcherFactory,
DefaultPositionPresenterFactory,
DefaultTokenBalanceFetcherFactory,
],
controllers: [BalanceController],
Expand Down

0 comments on commit ce5e530

Please sign in to comment.