diff --git a/src/common/services/caching/entities/cache.info.ts b/src/common/services/caching/entities/cache.info.ts index 22213e6e6..39a9ec15d 100644 --- a/src/common/services/caching/entities/cache.info.ts +++ b/src/common/services/caching/entities/cache.info.ts @@ -99,9 +99,19 @@ export class CacheInfo { ttl: 5 * TimeConstants.oneMinute, }; + static AllDexTokens: CacheInfo = { + key: 'allDexTokens', + ttl: 10 * TimeConstants.oneMinute, + }; + + static AllApiTokens: CacheInfo = { + key: 'allApiTokens', + ttl: 10 * TimeConstants.oneMinute, + }; + static AllTokens: CacheInfo = { key: 'allTokens', - ttl: 10 * TimeConstants.oneMinute, + ttl: TimeConstants.oneMinute, }; static EgldToken: CacheInfo = { diff --git a/src/common/services/elrond-communication/elrond-api.service.ts b/src/common/services/elrond-communication/elrond-api.service.ts index 1f979e07d..6d7fe118f 100644 --- a/src/common/services/elrond-communication/elrond-api.service.ts +++ b/src/common/services/elrond-communication/elrond-api.service.ts @@ -616,41 +616,20 @@ export class ElrondApiService { ); } - async getAllMexTokens(): Promise { + async getAllDexTokens(): Promise { const allTokens = await this.doGetGeneric( - this.getAllMexTokens.name, + this.getAllDexTokens.name, 'mex/tokens?size=10000', ); - return allTokens.map((t) => Token.fromElrondApiToken(t)); + return allTokens.map((t) => Token.fromElrondApiDexToken(t)); } - async getAllMexTokensWithDecimals(): Promise { - const batchSize = constants.getTokensFromApiBatchSize; - const tokens: Token[] = await this.getAllMexTokens(); - - const tokenChunks = BatchUtils.splitArrayIntoChunks(tokens, batchSize); - for (const tokenChunk of tokenChunks) { - const identifiersParam = tokenChunk.map((t) => t.identifier).join(','); - const tokensWithDecimals = await this.doGetGeneric( - this.getAllMexTokensWithDecimals.name, - `tokens?identifiers=${identifiersParam}&fields=identifier,decimals&size=${tokenChunk.length}`, - ); - - if (!tokensWithDecimals) { - continue; - } - - for (const tokenWithDecimals of tokensWithDecimals) { - const token = tokens.find( - (t) => t.identifier === tokenWithDecimals.identifier, - ); - if (token) { - token.decimals = tokenWithDecimals.decimals; - } - } - } - - return tokens; + async getAllTokens(): Promise { + const allTokens = await this.doGetGeneric( + this.getAllTokens.name, + 'tokens?size=10000&fields=identifier,name,ticker,decimals', + ); + return allTokens.map((t) => Token.fromElrondApiToken(t)); } async getEgldPriceFromEconomics(): Promise { diff --git a/src/common/services/elrond-communication/elrond-tools.service.ts b/src/common/services/elrond-communication/elrond-tools.service.ts index f953ee729..536c7680f 100644 --- a/src/common/services/elrond-communication/elrond-tools.service.ts +++ b/src/common/services/elrond-communication/elrond-tools.service.ts @@ -25,7 +25,9 @@ export class ElrondToolsService { }); } - async getEgldHistoricalPrice(isoDateOnly: string): Promise { + async getEgldHistoricalPrice( + isoDateOnly: string, + ): Promise { return await this.getTokenPriceByTimestamp( elrondConfig.wegld, elrondConfig.usdc, @@ -37,12 +39,15 @@ export class ElrondToolsService { token: string, isoDateOnly: string, cachedEgldPriceUsd?: string, - ): Promise { + ): Promise { const priceInEgld = await this.getTokenPriceByTimestamp( token, elrondConfig.wegld, isoDateOnly, ); + if (!priceInEgld) { + return; + } const egldPriceUsd = cachedEgldPriceUsd ?? (await this.getEgldHistoricalPrice(isoDateOnly)); return new BigNumber(priceInEgld).multipliedBy(egldPriceUsd).toFixed(); @@ -68,20 +73,21 @@ export class ElrondToolsService { firstToken: string, secondToken: string, isoDateOnly: string, - ): Promise { + ): Promise { const query = this.getTokenPriceByTimestampQuery( firstToken, secondToken, isoDateOnly, ); const res = await this.doPost(this.getTokenPriceByTimestamp.name, query); - return res.data.trading.pair.price[0].last.toFixed(20); + return res?.data?.trading?.pair?.price?.[0]?.last?.toFixed(20) ?? undefined; } private async getConfig(): Promise { const accessTokenInfo = await this.nativeAuthSigner.getToken(); return { authorization: `Bearer ${accessTokenInfo.token}`, + timeout: 500, }; } diff --git a/src/common/services/elrond-communication/models/Token.model.ts b/src/common/services/elrond-communication/models/Token.model.ts index ed49f6d47..20b0d49bd 100644 --- a/src/common/services/elrond-communication/models/Token.model.ts +++ b/src/common/services/elrond-communication/models/Token.model.ts @@ -19,13 +19,21 @@ export class Token { Object.assign(this, init); } - static fromElrondApiToken(token: any): Token { + static fromElrondApiDexToken(token: any): Token { return new Token({ identifier: token.id, symbol: token.symbol, name: token.name, priceUsd: token.price, - decimals: token.decimals ?? undefined, + }); + } + + static fromElrondApiToken(token: any): Token { + return new Token({ + identifier: token.identifier, + symbol: token.ticker, + name: token.name, + decimals: token.decimals, }); } } diff --git a/src/crons/cache.warmer/tokens.warmer.service.ts b/src/crons/cache.warmer/tokens.warmer.service.ts index ef41e64a3..4a2c6a1c2 100644 --- a/src/crons/cache.warmer/tokens.warmer.service.ts +++ b/src/crons/cache.warmer/tokens.warmer.service.ts @@ -21,13 +21,12 @@ export class TokensWarmerService { ); } - @Cron(CronExpression.EVERY_MINUTE) + @Cron(CronExpression.EVERY_10_MINUTES) async updateTokens() { await Locker.lock( - 'MEX Tokens invalidations', + 'Tokens invalidations', async () => { - const tokens = - await this.elrondApiService.getAllMexTokensWithDecimals(); + const tokens = await this.elrondApiService.getAllTokens(); await this.invalidateKey( CacheInfo.AllTokens.key, tokens, @@ -38,6 +37,22 @@ export class TokensWarmerService { ); } + @Cron(CronExpression.EVERY_MINUTE) + async updateDexTokens() { + await Locker.lock( + 'DEX Tokens invalidations', + async () => { + const tokens = await this.elrondApiService.getAllDexTokens(); + await this.invalidateKey( + CacheInfo.AllDexTokens.key, + tokens, + CacheInfo.AllDexTokens.ttl, + ); + }, + true, + ); + } + @Cron(CronExpression.EVERY_MINUTE) async updateEgldTokens() { await Locker.lock( diff --git a/src/modules/asset-history/services/assets-history.nft-events.service.ts b/src/modules/asset-history/services/assets-history.nft-events.service.ts index 92c65da70..84ff45200 100644 --- a/src/modules/asset-history/services/assets-history.nft-events.service.ts +++ b/src/modules/asset-history/services/assets-history.nft-events.service.ts @@ -69,6 +69,10 @@ export class AssetsHistoryNftEventService { }); } } + case NftEventEnum.ESDTNFTBurn: + case NftEventEnum.ESDTNFTUpdateAttributes: { + break; + } default: { return this.mapNftEventLog(nonce, transferEvent.identifier, mainEvent); } diff --git a/src/modules/assets/assets-getter.service.ts b/src/modules/assets/assets-getter.service.ts index 4908b78fa..8d670b2f6 100644 --- a/src/modules/assets/assets-getter.service.ts +++ b/src/modules/assets/assets-getter.service.ts @@ -337,6 +337,15 @@ export class AssetsGetterService { offset: number, sortByRank?: Sort, ): Promise> { + for (let i = 0; i < traits.length; i++) { + const multipleAttributesPerTraitFilter = traits.find( + (t) => t.name === traits[i].name && t.value !== traits[i].value, + ); + if (multipleAttributesPerTraitFilter) { + return new CollectionType({ items: [], count: 0 }); + } + } + const [nfts, count] = await this.nftTraitsService.getCollectionNftsByTraitsAndRanks( collection, diff --git a/src/modules/assets/content.validation.service.ts b/src/modules/assets/content.validation.service.ts index 05bc1aa99..1fa24d510 100644 --- a/src/modules/assets/content.validation.service.ts +++ b/src/modules/assets/content.validation.service.ts @@ -7,7 +7,9 @@ import { VerifyContentService } from './verify-content.service'; export class ContentValidation { private status: boolean = true; private verifyContentService: VerifyContentService; - constructor(private logger: Logger) { + private readonly logger: Logger; + constructor() { + this.logger = new Logger(ContentValidation.name); this.verifyContentService = new VerifyContentService(); } diff --git a/src/modules/auctions/auctions-getter.service.ts b/src/modules/auctions/auctions-getter.service.ts index 2f93aff20..5b78ccd9a 100644 --- a/src/modules/auctions/auctions-getter.service.ts +++ b/src/modules/auctions/auctions-getter.service.ts @@ -681,20 +681,14 @@ export class AuctionsGetterService { marketplaceKey?: string, collectionIdentifier?: string, ): Promise { - const [currentPaymentTokenIds, allMexTokens, egldToken, lkmexToken] = - await Promise.all([ - this.persistenceService.getCurrentPaymentTokenIdsWithCounts( - marketplaceKey, - collectionIdentifier, - ), - this.usdPriceService.getCachedMexTokensWithDecimals(), - this.usdPriceService.getToken(elrondConfig.egld), - this.usdPriceService.getToken(elrondConfig.lkmex), - ]); - - const allTokens: Token[] = allMexTokens - .concat(egldToken) - .concat(lkmexToken); + const [currentPaymentTokenIds, allTokens] = await Promise.all([ + this.persistenceService.getCurrentPaymentTokenIdsWithCounts( + marketplaceKey, + collectionIdentifier, + ), + this.usdPriceService.getAllCachedTokens(), + ]); + let mappedTokens = []; for (const payment of currentPaymentTokenIds) { const token = allTokens.find( diff --git a/src/modules/nftCollections/collection-nfts.redis-handler.ts b/src/modules/nftCollections/collection-nfts.redis-handler.ts index 99f8f3cb1..b25059904 100644 --- a/src/modules/nftCollections/collection-nfts.redis-handler.ts +++ b/src/modules/nftCollections/collection-nfts.redis-handler.ts @@ -72,7 +72,7 @@ export class CollectionsNftsRedisHandler extends BaseCollectionsAssetsRedisHandl let nftsGroupByCollection: { [key: string]: any[] } = {}; for (const nfts of nftsResponse) { - nftsGroupByCollection[nfts[0]?.collection] = nfts; + nftsGroupByCollection[nfts?.[0]?.collection] = nfts; } return nftsGroupByCollection; } diff --git a/src/modules/usdPrice/usd-price.module.ts b/src/modules/usdPrice/usd-price.module.ts index 9fc0417fa..5f91cef64 100644 --- a/src/modules/usdPrice/usd-price.module.ts +++ b/src/modules/usdPrice/usd-price.module.ts @@ -3,9 +3,15 @@ import { ElrondCommunicationModule } from 'src/common/services/elrond-communicat import { UsdPriceService } from './usd-price.service'; import { UsdPriceRedisHandler } from './usd-price.redis-handler'; import { UsdPriceResolver } from './usd-price.resolver'; +import { UsdTokenPriceResolver } from './usd-token-price.resolver'; @Module({ - providers: [UsdPriceResolver, UsdPriceRedisHandler, UsdPriceService], + providers: [ + UsdPriceResolver, + UsdTokenPriceResolver, + UsdPriceRedisHandler, + UsdPriceService, + ], imports: [ElrondCommunicationModule], exports: [UsdPriceService], }) diff --git a/src/modules/usdPrice/usd-price.service.ts b/src/modules/usdPrice/usd-price.service.ts index 38e01a829..6b2c77bde 100644 --- a/src/modules/usdPrice/usd-price.service.ts +++ b/src/modules/usdPrice/usd-price.service.ts @@ -24,121 +24,163 @@ export class UsdPriceService { ); } - public async getCachedMexTokensWithDecimals(): Promise { + async getUsdAmountDenom( + token: string, + amount: string, + timestamp?: number, + ): Promise { + if (amount === '0') { + return amount; + } + + if (token === elrondConfig.egld || token === elrondConfig.wegld) { + return computeUsdAmount( + await this.getEgldPrice(timestamp), + amount, + elrondConfig.decimals, + ); + } + + const tokenPriceUsd = await this.getEsdtPriceUsd(token, timestamp); + if (!tokenPriceUsd) { + return; + } + const tokenData = await this.getToken(token); + return computeUsdAmount(tokenPriceUsd, amount, tokenData.decimals); + } + + public async getAllCachedTokens(): Promise { return await this.cacheService.getOrSetCache( this.persistentRedisClient, CacheInfo.AllTokens.key, - async () => await this.elrondApiService.getAllMexTokensWithDecimals(), + async () => await this.setAllCachedTokens(), CacheInfo.AllTokens.ttl, - TimeConstants.oneMinute, ); } - private async getCachedTokenData( - tokenId: string, - ): Promise { - const mexTokens = await this.getCachedMexTokensWithDecimals(); - const token = mexTokens.find((t) => t.identifier === tokenId); + public async getToken(tokenId: string): Promise { + if (tokenId === elrondConfig.egld) { + return new Token({ + identifier: elrondConfig.egld, + symbol: elrondConfig.egld, + name: elrondConfig.egld, + decimals: elrondConfig.decimals, + priceUsd: await this.getEgldPrice(), + }); + } + + const tokens = await this.getAllCachedTokens(); + const token = tokens.find((token) => token.identifier === tokenId); if (token) { return token; } + return await this.cacheService.getOrSetCache( this.persistentRedisClient, `token_${tokenId}`, async () => await this.elrondApiService.getTokenData(tokenId), CacheInfo.AllTokens.ttl, - TimeConstants.oneMinute, ); } - private async getCachedEgldPrice(): Promise { + async getTokenPriceUsd(token: string): Promise { + if (token === elrondConfig.egld || token === elrondConfig.wegld) { + return await this.getEgldPrice(); + } + return await this.getEsdtPriceUsd(token); + } + + private async getEsdtPriceUsd( + tokenId: string, + timestamp?: number, + ): Promise { + if (!timestamp || DateUtils.isTimestampToday(timestamp)) { + const dexTokens = await this.getCachedDexTokens(); + const token = dexTokens.find((token) => token.identifier === tokenId); + return token?.priceUsd; + } + + return await this.getTokenHistoricalPriceByEgld(tokenId, timestamp); + } + + private async getTokenHistoricalPriceByEgld( + token: string, + timestamp: number, + ): Promise { + const isoDateOnly = DateUtils.timestampToIsoStringWithoutTime(timestamp); + const egldPriceUsd = await this.getEgldHistoricalPrice(timestamp); + const cacheKey = this.getTokenHistoricalPriceCacheKey(token, isoDateOnly); return await this.cacheService.getOrSetCache( this.persistentRedisClient, - CacheInfo.EgldToken.key, - async () => await this.elrondApiService.getEgldPriceFromEconomics(), - CacheInfo.EgldToken.ttl, - TimeConstants.oneMinute, + cacheKey, + async () => + await this.elrondToolsService.getTokenHistoricalPriceByEgld( + token, + isoDateOnly, + egldPriceUsd, + ), + DateUtils.isTimestampToday(timestamp) + ? TimeConstants.oneDay + : CacheInfo.TokenHistoricalPrice.ttl, ); } - async getToken(tokenId: string): Promise { - switch (tokenId) { - case elrondConfig.egld: { - const egldPriceUsd: string = await this.getCachedEgldPrice(); - return new Token({ - identifier: elrondConfig.egld, - symbol: elrondConfig.egld, - name: elrondConfig.egld, - decimals: elrondConfig.decimals, - priceUsd: egldPriceUsd, - }); - } - case elrondConfig.lkmex: { - const mexToken = await this.getCachedTokenData(elrondConfig.mex); - return new Token({ - identifier: tokenId, - name: 'LockedMEX', - symbol: 'LKMEX', - decimals: elrondConfig.decimals, - priceUsd: mexToken?.priceUsd ?? null, - }); - } - default: { - let token: Token = await this.getCachedTokenData(tokenId); - return token - ? token - : new Token({ - identifier: tokenId, - }); - } - } + private async setAllCachedTokens(): Promise { + let [apiTokens, dexTokens, egldPriceUSD] = await Promise.all([ + this.getCachedApiTokens(), + this.getCachedDexTokens(), + this.getEgldPrice(), + ]); + dexTokens.map((dexToken) => { + apiTokens.find( + (apiToken) => apiToken.identifier === dexToken.identifier, + ).priceUsd = dexToken.priceUsd; + }); + const egldToken: Token = new Token({ + identifier: elrondConfig.egld, + symbol: elrondConfig.egld, + name: elrondConfig.egld, + decimals: elrondConfig.decimals, + priceUsd: egldPriceUSD, + }); + return apiTokens.concat([egldToken]); } - async getUsdAmount(tokenId: string, amount: string): Promise { - const token: Token = await this.getToken(tokenId); - return computeUsdAmount(token.priceUsd, amount, token.decimals); + private async getCachedDexTokens(): Promise { + return await this.cacheService.getOrSetCache( + this.persistentRedisClient, + CacheInfo.AllDexTokens.key, + async () => await this.elrondApiService.getAllDexTokens(), + CacheInfo.AllDexTokens.ttl, + ); } - async getUsdAmountDenom( - tokenId: string, - amount: string, - ): Promise { - const token: Token = await this.getToken(tokenId); - if (token && token.priceUsd && token.decimals) { - const usdAmount = computeUsdAmount( - token.priceUsd, - amount, - token.decimals, - ); - return usdAmount; - } - return null; + private async getCachedApiTokens(): Promise { + return await this.cacheService.getOrSetCache( + this.persistentRedisClient, + CacheInfo.AllApiTokens.key, + async () => await this.elrondApiService.getAllTokens(), + CacheInfo.AllApiTokens.ttl, + ); } - async getTokenPriceUsd( - token: string, - amount: string, - timestamp: number, - ): Promise { - if (token === elrondConfig.egld) { - return computeUsdAmount( - await this.getCachedEgldHistoricalPrice(timestamp), - amount, - elrondConfig.decimals, - ); + private async getEgldPrice(timestamp?: number): Promise { + if (!timestamp || DateUtils.isTimestampToday(timestamp)) { + return await this.getCurrentEgldPrice(); } + return await this.getEgldHistoricalPrice(timestamp); + } - const tokenPriceUsd = await this.getCachedTokenHistoricalPriceByEgld( - token, - timestamp, + private async getCurrentEgldPrice(): Promise { + return await this.cacheService.getOrSetCache( + this.persistentRedisClient, + CacheInfo.EgldToken.key, + async () => await this.elrondApiService.getEgldPriceFromEconomics(), + CacheInfo.EgldToken.ttl, ); - const tokenData = await this.getCachedTokenData(token); - return computeUsdAmount(tokenPriceUsd, amount, tokenData.decimals); } - private async getCachedEgldHistoricalPrice( - timestamp: number, - ): Promise { + private async getEgldHistoricalPrice(timestamp?: number): Promise { const isoDateOnly = DateUtils.timestampToIsoStringWithoutTime(timestamp); const cacheKey = this.getTokenHistoricalPriceCacheKey( elrondConfig.wegld, @@ -155,28 +197,6 @@ export class UsdPriceService { ); } - private async getCachedTokenHistoricalPriceByEgld( - token: string, - timestamp: number, - ): Promise { - const isoDateOnly = DateUtils.timestampToIsoStringWithoutTime(timestamp); - const egldPriceUsd = await this.getCachedEgldHistoricalPrice(timestamp); - const cacheKey = this.getTokenHistoricalPriceCacheKey(token, isoDateOnly); - return await this.cacheService.getOrSetCache( - this.persistentRedisClient, - cacheKey, - async () => - await this.elrondToolsService.getTokenHistoricalPriceByEgld( - token, - isoDateOnly, - egldPriceUsd, - ), - DateUtils.isTimestampToday(timestamp) - ? TimeConstants.oneDay - : CacheInfo.TokenHistoricalPrice.ttl, - ); - } - private getTokenHistoricalPriceCacheKey( token: string, isoDateOnly: string, diff --git a/src/modules/usdPrice/usd-token-price.resolver.ts b/src/modules/usdPrice/usd-token-price.resolver.ts new file mode 100644 index 000000000..6c453602e --- /dev/null +++ b/src/modules/usdPrice/usd-token-price.resolver.ts @@ -0,0 +1,19 @@ +import { Resolver, ResolveField, Parent } from '@nestjs/graphql'; +import { BaseResolver } from '../common/base.resolver'; +import { UsdPriceService } from './usd-price.service'; +import { Token } from 'src/common/services/elrond-communication/models/Token.model'; + +@Resolver(() => Token) +export class UsdTokenPriceResolver extends BaseResolver(Token) { + constructor(private readonly usdPriceService: UsdPriceService) { + super(); + } + + @ResolveField(() => String) + async priceUsd(@Parent() token: Token) { + return ( + token.priceUsd ?? + (await this.usdPriceService.getTokenPriceUsd(token.identifier)) + ); + } +} diff --git a/src/utils/date-utils.ts b/src/utils/date-utils.ts index 4be8b1573..471837bf5 100644 --- a/src/utils/date-utils.ts +++ b/src/utils/date-utils.ts @@ -1,5 +1,5 @@ export class DateUtils { - static getTimestamp(date: Date): number { + static getTimestamp(date: Date = new Date()): number { return new Date(date).getTime() / 1000; }