From 183717b4a17652be8405bf037992c49d5a871c4f Mon Sep 17 00:00:00 2001 From: hschiau Date: Wed, 11 Dec 2024 19:53:07 +0200 Subject: [PATCH 1/2] MEX-527: add token memory store service - add service that implements the memory store interface (can be used seamless by the plugin) - add `@Expose` decorator to all args fields for both the `tokens` and `filteredTokens` queries so that they work with the `plainToInstance` function in class-transformer - add utils for calculating token price/volume/trade_count changes; use util in token compute service as well --- .../memory-store/entities/global.state.ts | 23 + .../memory-store/memory.store.module.ts | 13 +- .../services/token.memory.store.service.ts | 565 ++++++++++++++++++ .../tokens/models/tokens.filter.args.ts | 26 +- .../tokens/services/token.compute.service.ts | 49 +- src/utils/token.utils.ts | 64 ++ 6 files changed, 698 insertions(+), 42 deletions(-) create mode 100644 src/modules/memory-store/services/token.memory.store.service.ts create mode 100644 src/utils/token.utils.ts diff --git a/src/modules/memory-store/entities/global.state.ts b/src/modules/memory-store/entities/global.state.ts index f0f26aa2f..c5331a97a 100644 --- a/src/modules/memory-store/entities/global.state.ts +++ b/src/modules/memory-store/entities/global.state.ts @@ -29,6 +29,29 @@ export class GlobalStateSingleton { public getPairsArray(): PairModel[] { return Object.values(this.pairsState); } + + public getTokensArray(): EsdtToken[] { + return Object.values(this.tokensState); + } + + public getPairsTokens(enabledSwaps: boolean): EsdtToken[] { + const pairAddresses = enabledSwaps + ? Object.values(this.pairsState) + .filter((pair) => pair.state === 'Active') + .map((pair) => pair.address) + : Object.values(this.pairsState).map((pair) => pair.address); + + let tokenIDs = []; + pairAddresses.forEach((address) => { + tokenIDs.push( + this.pairsEsdtTokens[address].firstTokenID, + this.pairsEsdtTokens[address].secondTokenID, + ); + }); + + tokenIDs = [...new Set(tokenIDs)]; + return tokenIDs.map((tokenID) => this.tokensState[tokenID]); + } } export const GlobalState = new GlobalStateSingleton(); diff --git a/src/modules/memory-store/memory.store.module.ts b/src/modules/memory-store/memory.store.module.ts index e5cdbe5c0..0217a1725 100644 --- a/src/modules/memory-store/memory.store.module.ts +++ b/src/modules/memory-store/memory.store.module.ts @@ -1,9 +1,18 @@ import { Module } from '@nestjs/common'; import { MemoryStoreFactoryService } from './services/memory.store.factory.service'; import { PairMemoryStoreService } from './services/pair.memory.store.service'; +import { TokenMemoryStoreService } from './services/token.memory.store.service'; @Module({ - providers: [MemoryStoreFactoryService, PairMemoryStoreService], - exports: [MemoryStoreFactoryService, PairMemoryStoreService], + providers: [ + MemoryStoreFactoryService, + PairMemoryStoreService, + TokenMemoryStoreService, + ], + exports: [ + MemoryStoreFactoryService, + PairMemoryStoreService, + TokenMemoryStoreService, + ], }) export class MemoryStoreModule {} diff --git a/src/modules/memory-store/services/token.memory.store.service.ts b/src/modules/memory-store/services/token.memory.store.service.ts new file mode 100644 index 000000000..182b5a47b --- /dev/null +++ b/src/modules/memory-store/services/token.memory.store.service.ts @@ -0,0 +1,565 @@ +import { Injectable } from '@nestjs/common'; +import { IMemoryStoreService } from './interfaces'; +import { EsdtToken } from 'src/modules/tokens/models/esdtToken.model'; +import { TokensResponse } from 'src/modules/tokens/models/tokens.response'; +import { GlobalState, GlobalStateInitStatus } from '../entities/global.state'; +import { QueryField } from '../entities/query.field.type'; +import { plainToInstance } from 'class-transformer'; +import { + TokensFilter, + TokensFiltersArgs, + TokenSortingArgs, + TokensSortableFields, +} from 'src/modules/tokens/models/tokens.filter.args'; +import { PaginationArgs } from 'src/modules/dex.model'; +import ConnectionArgs, { + getPagingParameters, +} from 'src/modules/common/filters/connection.args'; +import { createModelFromFields } from '../utils/graphql.utils'; +import PageResponse from 'src/modules/common/page.response'; +import { SortingOrder } from 'src/modules/common/page.data'; +import { Connection } from 'graphql-relay'; +import BigNumber from 'bignumber.js'; +import { + calculateTokenPriceChange24h, + calculateTokenPriceChange7d, + calculateTokenTradeChange24h, + calculateTokenVolumeUSDChange24h, +} from 'src/utils/token.utils'; + +@Injectable() +export class TokenMemoryStoreService extends IMemoryStoreService< + EsdtToken, + TokensResponse +> { + static typenameMappings: Record> = { + EsdtToken: { + assets: 'AssetsModel', + roles: 'RolesModel', + }, + AssetsModel: { + social: 'SocialModel', + }, + }; + + static targetedQueries: Record< + string, + { + isFiltered: boolean; + missingFields: QueryField[]; + identifierField: string; + } + > = { + tokens: { + isFiltered: false, + identifierField: 'identifier', + missingFields: [], + }, + filteredTokens: { + isFiltered: true, + identifierField: 'identifier', + missingFields: [], + }, + }; + + isReady(): boolean { + // TODO: implement separate readiness status per store + return GlobalState.initStatus === GlobalStateInitStatus.DONE; + } + + getAllData(): EsdtToken[] { + return GlobalState.getTokensArray(); + } + + getQueryResponse( + queryName: string, + queryArguments: Record, + requestedFields: QueryField[], + ): TokensResponse | EsdtToken[] { + if (!TokenMemoryStoreService.targetedQueries[queryName]) { + throw new Error( + `Data for query '${queryName}' is not solvable from the memory store.`, + ); + } + + const isFilteredQuery = + TokenMemoryStoreService.targetedQueries[queryName].isFiltered; + + const pagination = this.getPaginationFromArgs( + queryArguments, + isFilteredQuery, + ); + const filters = this.getFiltersFromArgs( + queryArguments, + isFilteredQuery, + ); + const sorting = this.getSortingFromArgs(queryArguments); + + let tokens = GlobalState.getPairsTokens(filters.enabledSwaps); + tokens = this.filterTokens(tokens, filters, isFilteredQuery); + + if (!isFilteredQuery) { + return tokens.map((token) => + createModelFromFields( + token, + requestedFields, + 'EsdtToken', + this.getTypenameMapping(), + ), + ); + } + + if (sorting && sorting.sortField) { + tokens = this.sortTokens( + tokens, + sorting.sortField, + sorting.sortOrder, + ); + } + + const totalCount = tokens.length; + + return PageResponse.mapResponse( + tokens + .map((token) => + createModelFromFields( + token, + requestedFields, + 'EsdtToken', + this.getTypenameMapping(), + ), + ) + .slice(pagination.offset, pagination.offset + pagination.limit), + this.getConnectionFromArgs(queryArguments) ?? new ConnectionArgs(), + totalCount, + pagination.offset, + pagination.limit, + ); + } + + appendFieldsToQueryResponse( + queryName: string, + response: TokensResponse | EsdtToken[], + requestedFields: QueryField[], + ): TokensResponse | EsdtToken[] { + if (!TokenMemoryStoreService.targetedQueries[queryName]) { + throw new Error( + `Data for query '${queryName}' is not solvable from the memory store.`, + ); + } + const currentQuery = TokenMemoryStoreService.targetedQueries[queryName]; + + if (currentQuery.isFiltered) { + return this.appendFieldsToFilteredQueryResponse( + response, + queryName, + requestedFields, + ); + } + + const responseArray = response as EsdtToken[]; + + const identifierField = currentQuery.identifierField; + + const originalIdentifiers = responseArray.map( + (token) => token[identifierField], + ); + + const tokensFromStore = this.getTokensByIDs( + queryName, + requestedFields, + originalIdentifiers, + ); + + return responseArray.map((token, index) => { + return { + ...token, + ...tokensFromStore[index], + }; + }); + } + + getTypenameMapping(): Record> { + return TokenMemoryStoreService.typenameMappings; + } + + getTargetedQueries(): Record< + string, + { + isFiltered: boolean; + missingFields: QueryField[]; + identifierField: string; + } + > { + return TokenMemoryStoreService.targetedQueries; + } + + private appendFieldsToFilteredQueryResponse( + response: Record, + queryName: string, + requestedFields: QueryField[], + ): TokensResponse { + const identifierField = + TokenMemoryStoreService.targetedQueries[queryName].identifierField; + + const connectionResponse = response as Connection; + + const originalIdentifiers = connectionResponse.edges.map((edge) => { + return edge.node[identifierField]; + }); + + const tokensFromStore = this.getTokensByIDs( + queryName, + requestedFields, + originalIdentifiers, + ); + + connectionResponse.edges = connectionResponse.edges.map( + (edge, index) => { + edge.node = { + ...edge.node, + ...tokensFromStore[index], + }; + return edge; + }, + ); + + return connectionResponse as TokensResponse; + } + + private getTokensByIDs( + queryName: string, + requestedFields: QueryField[], + identifiers?: string[], + ): EsdtToken[] { + const identifierField = + TokenMemoryStoreService.targetedQueries[queryName].identifierField; + + let tokens = GlobalState.getTokensArray(); + + if (identifiers && identifiers.length > 0) { + tokens = tokens.filter((token) => + identifiers.includes(token[identifierField]), + ); + } + + return tokens.map((token) => + createModelFromFields( + token, + requestedFields, + 'EsdtToken', + this.getTypenameMapping(), + ), + ); + } + + private getFiltersFromArgs( + queryArguments: Record, + isFilteredQuery: boolean, + ): TokensFiltersArgs | TokensFilter { + const filters = isFilteredQuery + ? plainToInstance(TokensFilter, queryArguments.filters, { + excludeExtraneousValues: true, + enableImplicitConversion: true, + exposeUnsetFields: false, + }) + : plainToInstance(TokensFiltersArgs, queryArguments, { + excludeExtraneousValues: true, + enableImplicitConversion: true, + exposeUnsetFields: false, + }); + + return filters; + } + + private getPaginationFromArgs( + queryArguments: Record, + isFilteredQuery: boolean, + ): PaginationArgs { + if (!isFilteredQuery) { + return plainToInstance(PaginationArgs, queryArguments, { + excludeExtraneousValues: true, + enableImplicitConversion: true, + exposeUnsetFields: false, + }); + } + + const connectionArgs = this.getConnectionFromArgs(queryArguments); + + return getPagingParameters(connectionArgs); + } + + private getConnectionFromArgs( + queryArguments: Record, + ): ConnectionArgs { + return plainToInstance(ConnectionArgs, queryArguments.pagination, { + excludeExtraneousValues: true, + enableImplicitConversion: true, + exposeUnsetFields: false, + }); + } + + private getSortingFromArgs( + queryArguments: Record, + ): TokenSortingArgs { + return plainToInstance(TokenSortingArgs, queryArguments.sorting, { + excludeExtraneousValues: true, + enableImplicitConversion: true, + exposeUnsetFields: false, + }); + } + + private filterTokens( + tokens: EsdtToken[], + filters: TokensFiltersArgs | TokensFilter, + isFilteredQuery: boolean, + ): EsdtToken[] { + tokens = this.filterByIdentifiers(tokens, filters); + tokens = this.filterByType(tokens, filters); + + if (!isFilteredQuery) { + return tokens; + } + + tokens = this.filterBySearchTerm(tokens, filters as TokensFilter); + tokens = this.filterByLiquidityUSD(tokens, filters as TokensFilter); + + return tokens; + } + + private filterByIdentifiers( + tokens: EsdtToken[], + filters: TokensFiltersArgs | TokensFilter, + ): EsdtToken[] { + if (!filters.identifiers || filters.identifiers.length === 0) { + return tokens; + } + + return tokens.filter((token) => + filters.identifiers.includes(token.identifier), + ); + } + + private filterByType( + tokens: EsdtToken[], + filters: TokensFiltersArgs | TokensFilter, + ): EsdtToken[] { + if (!filters.type) { + return tokens; + } + + return tokens.filter((token) => token.type === filters.type); + } + + private filterBySearchTerm( + tokens: EsdtToken[], + filters: TokensFilter, + ): EsdtToken[] { + if (!filters.searchToken || filters.searchToken.trim().length < 3) { + return tokens; + } + + const searchTerm = filters.searchToken.toUpperCase().trim(); + + return tokens.filter( + (token) => + token.name.toUpperCase().includes(searchTerm) || + token.identifier.toUpperCase().includes(searchTerm) || + token.ticker.toUpperCase().includes(searchTerm), + ); + } + + private filterByLiquidityUSD( + tokens: EsdtToken[], + filters: TokensFilter, + ): EsdtToken[] { + if (!filters.minLiquidity) { + return tokens; + } + + return tokens.filter((token) => { + const liquidity = new BigNumber(token.liquidityUSD); + return liquidity.gte(filters.minLiquidity); + }); + } + + private sortTokens( + tokens: EsdtToken[], + sortField: string, + sortOrder: SortingOrder, + ): EsdtToken[] { + switch (sortField) { + case TokensSortableFields.PRICE: + return tokens.sort((a, b) => { + if (sortOrder === SortingOrder.ASC) { + return new BigNumber(a.price).comparedTo(b.price); + } + return new BigNumber(b.price).comparedTo(a.price); + }); + case TokensSortableFields.PREVIOUS_24H_PRICE: + return tokens.sort((a, b) => { + if (sortOrder === SortingOrder.ASC) { + return new BigNumber(a.previous24hPrice).comparedTo( + b.previous24hPrice, + ); + } + return new BigNumber(b.previous24hPrice).comparedTo( + a.previous24hPrice, + ); + }); + case TokensSortableFields.PREVIOUS_7D_PRICE: + return tokens.sort((a, b) => { + if (sortOrder === SortingOrder.ASC) { + return new BigNumber(a.previous7dPrice).comparedTo( + b.previous7dPrice, + ); + } + return new BigNumber(b.previous7dPrice).comparedTo( + a.previous7dPrice, + ); + }); + case TokensSortableFields.PRICE_CHANGE_7D: + return tokens.sort((a, b) => { + const priceChangeA = calculateTokenPriceChange7d( + a.price, + a.previous7dPrice, + ); + const priceChangeB = calculateTokenPriceChange7d( + b.price, + b.previous7dPrice, + ); + + if (sortOrder === SortingOrder.ASC) { + return new BigNumber(priceChangeA).comparedTo( + priceChangeB, + ); + } + return new BigNumber(priceChangeB).comparedTo(priceChangeA); + }); + case TokensSortableFields.PRICE_CHANGE_24H: + return tokens.sort((a, b) => { + const priceChangeA = calculateTokenPriceChange24h( + a.price, + a.previous24hPrice, + ); + const priceChangeB = calculateTokenPriceChange24h( + b.price, + b.previous24hPrice, + ); + + if (sortOrder === SortingOrder.ASC) { + return new BigNumber(priceChangeA).comparedTo( + priceChangeB, + ); + } + return new BigNumber(priceChangeB).comparedTo(priceChangeA); + }); + case TokensSortableFields.VOLUME_CHANGE_24H: + return tokens.sort((a, b) => { + const volumeChangeA = calculateTokenVolumeUSDChange24h( + a.volumeUSD24h, + a.previous24hVolume, + ); + const volumeChangeB = calculateTokenVolumeUSDChange24h( + b.volumeUSD24h, + b.previous24hVolume, + ); + + if (sortOrder === SortingOrder.ASC) { + return new BigNumber(volumeChangeA).comparedTo( + volumeChangeB, + ); + } + return new BigNumber(volumeChangeB).comparedTo( + volumeChangeA, + ); + }); + case TokensSortableFields.TRADES_COUNT_CHANGE_24H: + return tokens.sort((a, b) => { + const tradeCountChangeA = calculateTokenTradeChange24h( + a.swapCount24h, + a.previous24hSwapCount, + ); + const tradeCountChangeB = calculateTokenTradeChange24h( + b.swapCount24h, + b.previous24hSwapCount, + ); + + if (sortOrder === SortingOrder.ASC) { + return new BigNumber(tradeCountChangeA).comparedTo( + tradeCountChangeB, + ); + } + return new BigNumber(tradeCountChangeB).comparedTo( + tradeCountChangeA, + ); + }); + case TokensSortableFields.CREATED_AT: + return tokens.sort((a, b) => { + if (sortOrder === SortingOrder.ASC) { + return new BigNumber(a.createdAt).comparedTo( + b.createdAt, + ); + } + return new BigNumber(b.createdAt).comparedTo(a.createdAt); + }); + case TokensSortableFields.LIQUIDITY: + return tokens.sort((a, b) => { + if (sortOrder === SortingOrder.ASC) { + return new BigNumber(a.liquidityUSD).comparedTo( + b.liquidityUSD, + ); + } + return new BigNumber(b.liquidityUSD).comparedTo( + a.liquidityUSD, + ); + }); + case TokensSortableFields.VOLUME: + return tokens.sort((a, b) => { + if (sortOrder === SortingOrder.ASC) { + return new BigNumber(a.volumeUSD24h).comparedTo( + b.volumeUSD24h, + ); + } + return new BigNumber(b.volumeUSD24h).comparedTo( + a.volumeUSD24h, + ); + }); + case TokensSortableFields.PREVIOUS_24H_VOLUME: + return tokens.sort((a, b) => { + if (sortOrder === SortingOrder.ASC) { + return new BigNumber(a.previous24hVolume).comparedTo( + b.previous24hVolume, + ); + } + return new BigNumber(b.previous24hVolume).comparedTo( + a.previous24hVolume, + ); + }); + case TokensSortableFields.TRADES_COUNT: + return tokens.sort((a, b) => { + if (sortOrder === SortingOrder.ASC) { + return new BigNumber(a.swapCount24h).comparedTo( + b.swapCount24h, + ); + } + return new BigNumber(b.swapCount24h).comparedTo( + a.swapCount24h, + ); + }); + case TokensSortableFields.TRENDING_SCORE: + return tokens.sort((a, b) => { + if (sortOrder === SortingOrder.ASC) { + return new BigNumber(a.trendingScore).comparedTo( + b.trendingScore, + ); + } + return new BigNumber(b.trendingScore).comparedTo( + a.trendingScore, + ); + }); + default: + return tokens; + } + } +} diff --git a/src/modules/tokens/models/tokens.filter.args.ts b/src/modules/tokens/models/tokens.filter.args.ts index 86f204ced..141361f01 100644 --- a/src/modules/tokens/models/tokens.filter.args.ts +++ b/src/modules/tokens/models/tokens.filter.args.ts @@ -1,5 +1,7 @@ import { ArgsType, Field, InputType, registerEnumType } from '@nestjs/graphql'; +import { Expose, Transform } from 'class-transformer'; import { SortingOrder } from 'src/modules/common/page.data'; +import { sortingOrderToString } from 'src/modules/router/models/filter.args'; export enum TokensSortableFields { PRICE = 'price', @@ -21,33 +23,51 @@ registerEnumType(TokensSortableFields, { name: 'TokensSortableFields' }); @ArgsType() export class TokensFiltersArgs { + @Expose() @Field(() => [String], { nullable: true }) identifiers: string; + @Expose() @Field({ nullable: true }) type: string; - @Field({ defaultValue: false }) - enabledSwaps: boolean; + @Expose() + @Field(() => Boolean, { defaultValue: false }) + enabledSwaps = false; } @InputType() export class TokensFilter { + @Expose() @Field(() => [String], { nullable: true }) identifiers?: string[]; + @Expose() @Field(() => String, { nullable: true }) type?: string; + @Expose() @Field(() => Boolean, { defaultValue: false }) - enabledSwaps: boolean; + enabledSwaps = false; + @Expose() @Field({ nullable: true }) searchToken?: string; + @Expose() @Field({ nullable: true }) minLiquidity: number; } +export function tokenSortableFieldToString( + value: TokensSortableFields, +): string { + return TokensSortableFields[value]; +} + @InputType() export class TokenSortingArgs { + @Expose() + @Transform(({ value }) => tokenSortableFieldToString(value)) @Field(() => TokensSortableFields, { nullable: true }) sortField?: TokensSortableFields; + @Expose() + @Transform(({ value }) => sortingOrderToString(value)) @Field(() => SortingOrder, { defaultValue: SortingOrder.ASC }) sortOrder: SortingOrder; } diff --git a/src/modules/tokens/services/token.compute.service.ts b/src/modules/tokens/services/token.compute.service.ts index 4fa8de569..9f347e7d7 100644 --- a/src/modules/tokens/services/token.compute.service.ts +++ b/src/modules/tokens/services/token.compute.service.ts @@ -29,6 +29,12 @@ import { CacheService } from '@multiversx/sdk-nestjs-cache'; import { TokenService } from './token.service'; import { computeValueUSD } from 'src/utils/token.converters'; import { getAllKeys } from 'src/utils/get.many.utils'; +import { + calculateTokenPriceChange24h, + calculateTokenPriceChange7d, + calculateTokenTradeChange24h, + calculateTokenVolumeUSDChange24h, +} from 'src/utils/token.utils'; @Injectable() export class TokenComputeService implements ITokenComputeService { @@ -337,14 +343,7 @@ export class TokenComputeService implements ITokenComputeService { this.tokenPrevious24hPrice(tokenID), ]); - const currentPriceBN = new BigNumber(currentPrice); - const previous24hPriceBN = new BigNumber(previous24hPrice); - - if (previous24hPriceBN.isZero() || previous24hPrice === undefined) { - return 0; - } - - return currentPriceBN.dividedBy(previous24hPriceBN).toNumber(); + return calculateTokenPriceChange24h(currentPrice, previous24hPrice); } async computeTokenPriceChange7d(tokenID: string): Promise { @@ -353,14 +352,7 @@ export class TokenComputeService implements ITokenComputeService { this.tokenPrevious7dPrice(tokenID), ]); - const currentPriceBN = new BigNumber(currentPrice); - const previous7dPriceBN = new BigNumber(previous7dPrice); - - if (previous7dPriceBN.isZero()) { - return 0; - } - - return currentPriceBN.dividedBy(previous7dPriceBN).toNumber(); + return calculateTokenPriceChange7d(currentPrice, previous7dPrice); } @ErrorLoggerAsync({ @@ -381,19 +373,10 @@ export class TokenComputeService implements ITokenComputeService { this.tokenPrevious24hVolumeUSD(tokenID), ]); - const currentVolumeBN = new BigNumber(currentVolume); - const previous24hVolumeBN = new BigNumber(previous24hVolume); - - if (currentVolumeBN.isZero()) { - return 0; - } - - const maxPrevious24hVolume = BigNumber.maximum( - previous24hVolumeBN, - constantsConfig.trendingScore.MIN_24H_VOLUME, + return calculateTokenVolumeUSDChange24h( + currentVolume, + previous24hVolume, ); - - return currentVolumeBN.dividedBy(maxPrevious24hVolume).toNumber(); } @ErrorLoggerAsync({ @@ -414,15 +397,7 @@ export class TokenComputeService implements ITokenComputeService { this.tokenPrevious24hSwapCount(tokenID), ]); - const currentSwapsBN = new BigNumber(currentSwaps); - const previous24hSwapsBN = new BigNumber(previous24hSwaps); - - const maxPrevious24hTradeCount = BigNumber.maximum( - previous24hSwapsBN, - constantsConfig.trendingScore.MIN_24H_TRADE_COUNT, - ); - - return currentSwapsBN.dividedBy(maxPrevious24hTradeCount).toNumber(); + return calculateTokenTradeChange24h(currentSwaps, previous24hSwaps); } @ErrorLoggerAsync({ diff --git a/src/utils/token.utils.ts b/src/utils/token.utils.ts new file mode 100644 index 000000000..91b358ca4 --- /dev/null +++ b/src/utils/token.utils.ts @@ -0,0 +1,64 @@ +import BigNumber from 'bignumber.js'; +import { constantsConfig } from 'src/config'; + +export function calculateTokenPriceChange24h( + currentPrice: string, + previous24hPrice: string, +): number { + const currentPriceBN = new BigNumber(currentPrice); + const previous24hPriceBN = new BigNumber(previous24hPrice); + + if (previous24hPriceBN.isZero() || previous24hPrice === undefined) { + return 0; + } + + return currentPriceBN.dividedBy(previous24hPriceBN).toNumber(); +} + +export function calculateTokenPriceChange7d( + currentPrice: string, + previous7dPrice: string, +): number { + const currentPriceBN = new BigNumber(currentPrice); + const previous7dPriceBN = new BigNumber(previous7dPrice); + + if (previous7dPriceBN.isZero()) { + return 0; + } + + return currentPriceBN.dividedBy(previous7dPriceBN).toNumber(); +} + +export function calculateTokenVolumeUSDChange24h( + currentVolume: string, + previous24hVolume: string, +): number { + const currentVolumeBN = new BigNumber(currentVolume); + const previous24hVolumeBN = new BigNumber(previous24hVolume); + + if (currentVolumeBN.isZero()) { + return 0; + } + + const maxPrevious24hVolume = BigNumber.maximum( + previous24hVolumeBN, + constantsConfig.trendingScore.MIN_24H_VOLUME, + ); + + return currentVolumeBN.dividedBy(maxPrevious24hVolume).toNumber(); +} + +export function calculateTokenTradeChange24h( + currentSwaps: number, + previous24hSwaps: number, +): number { + const currentSwapsBN = new BigNumber(currentSwaps); + const previous24hSwapsBN = new BigNumber(previous24hSwaps); + + const maxPrevious24hTradeCount = BigNumber.maximum( + previous24hSwapsBN, + constantsConfig.trendingScore.MIN_24H_TRADE_COUNT, + ); + + return currentSwapsBN.dividedBy(maxPrevious24hTradeCount).toNumber(); +} From 79714f27d1ea2c05636051ee68ddc21c161fdeb8 Mon Sep 17 00:00:00 2001 From: hschiau Date: Wed, 11 Dec 2024 19:53:39 +0200 Subject: [PATCH 2/2] MEX-527: add tokens memory store to factory service --- .../services/memory.store.factory.service.ts | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/modules/memory-store/services/memory.store.factory.service.ts b/src/modules/memory-store/services/memory.store.factory.service.ts index 0a72cab41..bea90c858 100644 --- a/src/modules/memory-store/services/memory.store.factory.service.ts +++ b/src/modules/memory-store/services/memory.store.factory.service.ts @@ -3,15 +3,24 @@ import { IMemoryStoreService } from './interfaces'; import { PairMemoryStoreService } from './pair.memory.store.service'; import { PairModel } from 'src/modules/pair/models/pair.model'; import { PairsResponse } from 'src/modules/pair/models/pairs.response'; +import { TokenMemoryStoreService } from './token.memory.store.service'; +import { EsdtToken } from 'src/modules/tokens/models/esdtToken.model'; +import { TokensResponse } from 'src/modules/tokens/models/tokens.response'; @Injectable() export class MemoryStoreFactoryService { private queryMapping: Record> = {}; - constructor(private readonly pairMemoryStore: PairMemoryStoreService) { + constructor( + private readonly pairMemoryStore: PairMemoryStoreService, + private readonly tokenMemoryStore: TokenMemoryStoreService, + ) { const pairQueries = Object.keys( this.pairMemoryStore.getTargetedQueries(), ); + const tokenQueries = Object.keys( + this.tokenMemoryStore.getTargetedQueries(), + ); for (const query of pairQueries) { this.queryMapping[query] = this @@ -20,10 +29,19 @@ export class MemoryStoreFactoryService { PairsResponse >; } + for (const query of tokenQueries) { + this.queryMapping[query] = this + .tokenMemoryStore as IMemoryStoreService< + EsdtToken[], + TokensResponse + >; + } } isReady(): boolean { - return this.pairMemoryStore.isReady(); + return ( + this.pairMemoryStore.isReady() && this.tokenMemoryStore.isReady() + ); } getTargetedQueryNames(): string[] {