diff --git a/src/common/graphql/graphql.service.ts b/src/common/graphql/graphql.service.ts index faebff87b..8044f7015 100644 --- a/src/common/graphql/graphql.service.ts +++ b/src/common/graphql/graphql.service.ts @@ -16,6 +16,9 @@ export class GraphQlService { const exchangeServiceUrl = this.apiConfigService.getExchangeServiceUrlMandatory(); const graphqlClient = new GraphQLClient(exchangeServiceUrl, { fetch: this.createFetchWithTimeout(60_000), + headers: { + 'origin': this.apiConfigService.getSelfUrl(), + }, }); try { diff --git a/src/endpoints/mex/entities/mex.pair.ts b/src/endpoints/mex/entities/mex.pair.ts index 0e7cc5d76..a9ecc12d4 100644 --- a/src/endpoints/mex/entities/mex.pair.ts +++ b/src/endpoints/mex/entities/mex.pair.ts @@ -68,12 +68,6 @@ export class MexPair { @ApiProperty({ type: String, example: 'jungledex' }) exchange: MexPairExchange | undefined; - @ApiProperty({ type: Boolean, nullable: true }) - hasFarms: boolean | undefined = undefined; - - @ApiProperty({ type: Boolean, nullable: true }) - hasDualFarms: boolean | undefined = undefined; - @ApiProperty({ type: Number, nullable: true }) tradesCount: number | undefined = undefined; @@ -82,4 +76,10 @@ export class MexPair { @ApiProperty({ type: Number, nullable: true }) deployedAt: number | undefined = undefined; + + @ApiProperty({ type: Boolean, nullable: true }) + hasFarms?: boolean; + + @ApiProperty({ type: Boolean, nullable: true }) + hasDualFarms?: boolean; } diff --git a/src/endpoints/mex/entities/mex.pairs..filter.ts b/src/endpoints/mex/entities/mex.pairs..filter.ts index 3021a4a3a..604682ccd 100644 --- a/src/endpoints/mex/entities/mex.pairs..filter.ts +++ b/src/endpoints/mex/entities/mex.pairs..filter.ts @@ -5,4 +5,5 @@ export class MexPairsFilter { Object.assign(this, init); } exchange?: MexPairExchange; + includeFarms?: boolean; } diff --git a/src/endpoints/mex/entities/mex.settings.ts b/src/endpoints/mex/entities/mex.settings.ts index 28dd58bcf..f7e730772 100644 --- a/src/endpoints/mex/entities/mex.settings.ts +++ b/src/endpoints/mex/entities/mex.settings.ts @@ -23,7 +23,7 @@ export class MexSettings { ...response.proxy.map((x: any) => x.address), ]; settings.pairContracts = [ - ...response.pairs.filter((x: any) => x.state === 'Active').map((x: any) => x.address), + ...response.pairs.map((x: any) => x.address), ...response.proxy.map((x: any) => x.address), ]; settings.wrapContracts = response.wrappingInfo.map((x: any) => x.address); @@ -39,11 +39,11 @@ export class MexSettings { settings.lockedAssetIdentifier = lockedAssetIdentifiers.find((identifier: string) => identifier.startsWith('LKMEX')); settings.lockedAssetIdentifierV2 = lockedAssetIdentifiers.find((identifier: string) => identifier.startsWith('XMEX')); - const mexEgldPairs = response.pairs.filter((x: any) => x.firstToken.name === 'WrappedEGLD' && x.secondToken.name === 'MEX'); - if (mexEgldPairs.length > 0) { - settings.wegldId = mexEgldPairs[0].firstToken.identifier; - settings.mexId = mexEgldPairs[0].secondToken.identifier; - } + const wrappedToken = response.wrappingInfo[0].wrappedToken.identifier; + const mexToken = response.simpleLockEnergy.baseAssetToken.identifier; + + settings.wegldId = wrappedToken; + settings.mexId = mexToken; return settings; } diff --git a/src/endpoints/mex/graphql/farms.query.ts b/src/endpoints/mex/graphql/farms.query.ts new file mode 100644 index 000000000..95b3f3c99 --- /dev/null +++ b/src/endpoints/mex/graphql/farms.query.ts @@ -0,0 +1,98 @@ +import { gql } from "graphql-request"; + +export const farmsQuery = gql` + query { + farms { + ... on FarmModelV1_2 { + version + address + farmToken { + collection + name + ticker + __typename + } + farmingToken { + name + identifier + decimals + __typename + } + farmedToken { + name + identifier + decimals + __typename + } + farmTokenPriceUSD + farmingTokenPriceUSD + farmedTokenPriceUSD + } + ... on FarmModelV1_3 { + version + address + farmToken { + collection + name + ticker + __typename + } + farmingToken { + name + identifier + decimals + __typename + } + farmedToken { + name + identifier + decimals + __typename + } + farmTokenPriceUSD + farmingTokenPriceUSD + farmedTokenPriceUSD + } + ... on FarmModelV2 { + version + address + farmToken { + collection + name + ticker + __typename + } + farmingToken { + name + identifier + decimals + __typename + } + farmedToken { + name + identifier + decimals + __typename + } + farmTokenPriceUSD + farmingTokenPriceUSD + farmedTokenPriceUSD + } + } + stakingFarms { + address + farmingToken { + name + identifier + decimals + __typename + } + farmToken { + name + collection + decimals + __typename + } + } + } + `; diff --git a/src/endpoints/mex/graphql/filtered.pairs.query.ts b/src/endpoints/mex/graphql/filtered.pairs.query.ts new file mode 100644 index 000000000..189688771 --- /dev/null +++ b/src/endpoints/mex/graphql/filtered.pairs.query.ts @@ -0,0 +1,51 @@ +import { gql } from "graphql-request"; + +export const filteredPairsQuery = (includeFarms: boolean = false) => { + const farmFields = includeFarms ? ` + hasFarms + hasDualFarms` : ''; + + return gql` + query filteredPairs($pagination: ConnectionArgs!, $filters: PairsFilter!) { + filteredPairs(pagination: $pagination, filters: $filters) { + edges { + cursor + node { + address + liquidityPoolToken { + identifier + name + __typename + } + liquidityPoolTokenPriceUSD + firstToken { + name + identifier + previous24hPrice + __typename + } + secondToken { + name + identifier + previous24hPrice + __typename + } + firstTokenPriceUSD + secondTokenPriceUSD + state + type + lockedValueUSD + volumeUSD24h + tradesCount + tradesCount24h + deployedAt + ${farmFields} + } + } + pageInfo { + hasNextPage + } + } + } + `; +}; diff --git a/src/endpoints/mex/graphql/pairs.count.query.ts b/src/endpoints/mex/graphql/pairs.count.query.ts new file mode 100644 index 000000000..50eb5efdd --- /dev/null +++ b/src/endpoints/mex/graphql/pairs.count.query.ts @@ -0,0 +1,8 @@ +import { gql } from "graphql-request"; + +export const pairCountQuery = gql` +query PairCount { + factory { + pairCount + } + }`; diff --git a/src/endpoints/mex/graphql/settings.query.ts b/src/endpoints/mex/graphql/settings.query.ts new file mode 100644 index 000000000..66401c089 --- /dev/null +++ b/src/endpoints/mex/graphql/settings.query.ts @@ -0,0 +1,60 @@ +import { gql } from "graphql-request"; + +export const settingsQuery = (pairLimitCount: number) => gql` +query { + filteredPairs(pagination: {first: ${pairLimitCount}}, filters: {state: ["Active"]}) { + edges { + node { + address + } + } + } + proxy { + address + lockedAssetTokens { + collection + } + } + farms { + ... on FarmModelV1_2 { + state + address + } + ... on FarmModelV1_3 { + state + address + } + ... on FarmModelV2 { + state + address + } + } + wrappingInfo { + address + wrappedToken { + identifier + } + } + distribution { + address + } + lockedAssetFactory { + address + } + stakingFarms { + state + address + } + stakingProxies { + address + } + factory { + address + } + simpleLockEnergy { + baseAssetToken { + identifier + } + } +} +`; diff --git a/src/endpoints/mex/graphql/staking.proxy.query.ts b/src/endpoints/mex/graphql/staking.proxy.query.ts new file mode 100644 index 000000000..d1afc2111 --- /dev/null +++ b/src/endpoints/mex/graphql/staking.proxy.query.ts @@ -0,0 +1,12 @@ +import { gql } from "graphql-request"; + +export const stakingProxyQuery = gql` +query StakingProxy { + stakingProxies { + address + dualYieldToken { + name + collection + } + } +}`; diff --git a/src/endpoints/mex/graphql/token.prices.hour.resolution.query.ts b/src/endpoints/mex/graphql/token.prices.hour.resolution.query.ts new file mode 100644 index 000000000..cf87057b3 --- /dev/null +++ b/src/endpoints/mex/graphql/token.prices.hour.resolution.query.ts @@ -0,0 +1,13 @@ +import { gql } from "graphql-request"; + +export const tokenPricesHourResolutionQuery = (tokenIdentifier: string) => gql` + query tokenPricesHourResolution { + values24h( + series: "${tokenIdentifier}", + metric: "priceUSD" + ) { + timestamp + value + } + } + `; diff --git a/src/endpoints/mex/graphql/tokens.query.ts b/src/endpoints/mex/graphql/tokens.query.ts new file mode 100644 index 000000000..4d69a9420 --- /dev/null +++ b/src/endpoints/mex/graphql/tokens.query.ts @@ -0,0 +1,9 @@ +import { gql } from "graphql-request"; + +export const tokensQuery = gql` + query tokens { + tokens { + identifier + type + } + }`; diff --git a/src/endpoints/mex/mex.controller.ts b/src/endpoints/mex/mex.controller.ts index 1d07276cd..988ef755d 100644 --- a/src/endpoints/mex/mex.controller.ts +++ b/src/endpoints/mex/mex.controller.ts @@ -11,7 +11,7 @@ import { MexTokenService } from "./mex.token.service"; import { MexFarmService } from './mex.farm.service'; import { MexFarm } from './entities/mex.farm'; import { QueryPagination } from 'src/common/entities/query.pagination'; -import { ParseIntPipe, ParseTokenPipe, ParseEnumPipe } from '@multiversx/sdk-nestjs-common'; +import { ParseIntPipe, ParseTokenPipe, ParseEnumPipe, ParseBoolPipe } from '@multiversx/sdk-nestjs-common'; import { MexPairExchange } from './entities/mex.pair.exchange'; import { MexPairsFilter } from './entities/mex.pairs..filter'; import { MexTokenChartsService } from './mex.token.charts.service'; @@ -31,10 +31,8 @@ export class MexController { @Get("/mex/settings") @ApiExcludeEndpoint() - @ApiResponse({ - status: 200, - description: 'The settings of the xExchange', - }) + @ApiResponse({ status: 200, description: 'The settings of the xExchange' }) + @ApiNotFoundResponse({ description: 'MEX settings not found' }) async getMexSettings(): Promise { const settings = await this.mexSettingsService.getSettings(); if (!settings) { @@ -56,12 +54,14 @@ export class MexController { @ApiQuery({ name: 'from', description: 'Number of items to skip for the result set', required: false }) @ApiQuery({ name: 'size', description: 'Number of items to retrieve', required: false }) @ApiQuery({ name: 'exchange', description: 'Filter by exchange', required: false, enum: MexPairExchange }) + @ApiQuery({ name: 'includeFarms', description: 'Include farms information in response', required: false, type: Boolean }) async getMexPairs( @Query('from', new DefaultValuePipe(0), ParseIntPipe) from: number, @Query("size", new DefaultValuePipe(25), ParseIntPipe) size: number, @Query('exchange', new ParseEnumPipe(MexPairExchange)) exchange?: MexPairExchange, + @Query('includeFarms', new DefaultValuePipe(false), ParseBoolPipe) includeFarms?: boolean, ): Promise { - const filter = new MexPairsFilter({ exchange }); + const filter = new MexPairsFilter({ exchange, includeFarms }); return await this.mexPairsService.getMexPairs(from, size, filter); } @@ -83,10 +83,12 @@ export class MexController { @Get("/mex/pairs/count") @ApiOperation({ summary: 'Maiar Exchange pairs count', description: 'Returns active liquidity pools count available on Maiar Exchange' }) @ApiQuery({ name: 'exchange', description: 'Filter by exchange', required: false, enum: MexPairExchange }) + @ApiQuery({ name: 'includeFarms', description: 'Include farms information in response', required: false, type: Boolean }) async getMexPairsCount( @Query('exchange', new ParseEnumPipe(MexPairExchange)) exchange?: MexPairExchange, + @Query('includeFarms', new DefaultValuePipe(false), ParseBoolPipe) includeFarms?: boolean, ): Promise { - const filter = new MexPairsFilter({ exchange }); + const filter = new MexPairsFilter({ exchange, includeFarms }); return await this.mexPairsService.getMexPairsCount(filter); } @@ -146,19 +148,25 @@ export class MexController { @Get("/mex/pairs/:baseId/:quoteId") @ApiOperation({ summary: 'xExchange pairs details', description: 'Returns liquidity pool details by providing a combination of two tokens' }) @ApiOkResponse({ type: MexPair }) + @ApiNotFoundResponse({ description: 'Pair not found' }) + @ApiQuery({ name: 'includeFarms', description: 'Include farms information in response', required: false, type: Boolean }) async getMexPair( @Param('baseId') baseId: string, @Param('quoteId') quoteId: string, + @Query('includeFarms', new DefaultValuePipe(false), ParseBoolPipe) includeFarms?: boolean, ): Promise { - const pair = await this.mexPairsService.getMexPair(baseId, quoteId); + const pair = await this.mexPairsService.getMexPair(baseId, quoteId, includeFarms); if (!pair) { - throw new NotFoundException(); + throw new NotFoundException('Pair not found'); } return pair; } @Get('mex/tokens/prices/hourly/:identifier') + @ApiOperation({ summary: 'xExchange token prices hourly', description: 'Returns token prices hourly' }) + @ApiOkResponse({ type: [MexTokenChart] }) + @ApiNotFoundResponse({ description: 'Price not available for given token identifier' }) async getTokenPricesHourResolution( @Param('identifier', ParseTokenPipe) identifier: string): Promise { const charts = await this.mexTokenChartsService.getTokenPricesHourResolution(identifier); @@ -170,6 +178,9 @@ export class MexController { } @Get('mex/tokens/prices/daily/:identifier') + @ApiOperation({ summary: 'xExchange token prices daily', description: 'Returns token prices daily' }) + @ApiOkResponse({ type: [MexTokenChart] }) + @ApiNotFoundResponse({ description: 'Price not available for given token identifier' }) async getTokenPricesDayResolution( @Param('identifier', ParseTokenPipe) identifier: string, @Query('after') after: string): Promise { diff --git a/src/endpoints/mex/mex.farm.service.ts b/src/endpoints/mex/mex.farm.service.ts index 0bce98f5e..4cebd7b1f 100644 --- a/src/endpoints/mex/mex.farm.service.ts +++ b/src/endpoints/mex/mex.farm.service.ts @@ -1,7 +1,6 @@ import { Constants } from "@multiversx/sdk-nestjs-common"; import { CacheService } from "@multiversx/sdk-nestjs-cache"; import { forwardRef, Inject, Injectable } from "@nestjs/common"; -import { gql } from "graphql-request"; import { QueryPagination } from "src/common/entities/query.pagination"; import { CacheInfo } from "src/utils/cache.info"; import { GraphQlService } from "src/common/graphql/graphql.service"; @@ -9,6 +8,8 @@ import { MexFarm } from "./entities/mex.farm"; import { MexTokenService } from "./mex.token.service"; import { MexStakingProxy } from "./entities/mex.staking.proxy"; import { ApiConfigService } from "src/common/api-config/api.config.service"; +import { farmsQuery } from "./graphql/farms.query"; +import { stakingProxyQuery } from "./graphql/staking.proxy.query"; @Injectable() export class MexFarmService { @@ -53,104 +54,7 @@ export class MexFarmService { } private async getAllMexFarmsRaw(): Promise { - const query = gql` - query { - farms { - ... on FarmModelV1_2 { - version - address - farmToken { - collection - name - ticker - __typename - } - farmingToken { - name - identifier - decimals - __typename - } - farmedToken { - name - identifier - decimals - __typename - } - farmTokenPriceUSD - farmingTokenPriceUSD - farmedTokenPriceUSD - } - ... on FarmModelV1_3 { - version - address - farmToken { - collection - name - ticker - __typename - } - farmingToken { - name - identifier - decimals - __typename - } - farmedToken { - name - identifier - decimals - __typename - } - farmTokenPriceUSD - farmingTokenPriceUSD - farmedTokenPriceUSD - } - ... on FarmModelV2 { - version - address - farmToken { - collection - name - ticker - __typename - } - farmingToken { - name - identifier - decimals - __typename - } - farmedToken { - name - identifier - decimals - __typename - } - farmTokenPriceUSD - farmingTokenPriceUSD - farmedTokenPriceUSD - } - } - stakingFarms { - address - farmingToken { - name - identifier - decimals - __typename - } - farmToken { - name - collection - decimals - __typename - } - } - } - `; - - const response: any = await this.graphQlService.getExchangeServiceData(query, {}); + const response: any = await this.graphQlService.getExchangeServiceData(farmsQuery, {}); if (!response) { return []; } @@ -178,18 +82,7 @@ export class MexFarmService { } private async getAllStakingProxiesRaw(): Promise { - const query = gql` - query StakingProxy { - stakingProxies { - address - dualYieldToken { - name - collection - } - } - }`; - - const response: any = await this.graphQlService.getExchangeServiceData(query, {}); + const response: any = await this.graphQlService.getExchangeServiceData(stakingProxyQuery, {}); if (!response) { return []; } diff --git a/src/endpoints/mex/mex.pair.service.ts b/src/endpoints/mex/mex.pair.service.ts index 353503ebb..a3ad3824c 100644 --- a/src/endpoints/mex/mex.pair.service.ts +++ b/src/endpoints/mex/mex.pair.service.ts @@ -1,7 +1,6 @@ import { Constants } from '@multiversx/sdk-nestjs-common'; import { CacheService } from '@multiversx/sdk-nestjs-cache'; import { BadRequestException, Injectable } from '@nestjs/common'; -import { gql } from 'graphql-request'; import { CacheInfo } from 'src/utils/cache.info'; import { GraphQlService } from 'src/common/graphql/graphql.service'; import { MexPair } from './entities/mex.pair'; @@ -13,6 +12,7 @@ import { ApiConfigService } from 'src/common/api-config/api.config.service'; import { MexPairExchange } from './entities/mex.pair.exchange'; import { MexPairsFilter } from './entities/mex.pairs..filter'; import { MexPairStatus } from './entities/mex.pair.status'; +import { filteredPairsQuery } from './graphql/filtered.pairs.query'; @Injectable() export class MexPairService { @@ -26,134 +26,87 @@ export class MexPairService { ) { } async refreshMexPairs(): Promise { - const pairs = await this.getAllMexPairsRaw(); + const pairs = await this.getAllMexPairsRaw(false); await this.cachingService.setRemote(CacheInfo.MexPairs.key, pairs, CacheInfo.MexPairs.ttl); await this.cachingService.setLocal(CacheInfo.MexPairs.key, pairs, Constants.oneSecond() * 30); } async getMexPairs(from: number, size: number, filter?: MexPairsFilter): Promise { - let allMexPairs = await this.getAllMexPairs(); + let allMexPairs = await this.getAllMexPairs(filter?.includeFarms ?? false); allMexPairs = this.applyFilters(allMexPairs, filter); return allMexPairs.slice(from, from + size); } - - async getMexPair(baseId: string, quoteId: string): Promise { - const allMexPairs = await this.getAllMexPairs(); + async getMexPair(baseId: string, quoteId: string, includeFarms: boolean = false): Promise { + const allMexPairs = await this.getAllMexPairs(includeFarms); return allMexPairs.find(pair => pair.baseId === baseId && pair.quoteId === quoteId); } - async getAllMexPairs(): Promise { + async getAllMexPairs(includeFarms: boolean = false): Promise { if (!this.apiConfigService.isExchangeEnabled()) { return []; } + const cacheKey = includeFarms ? CacheInfo.MexPairsWithFarms.key : CacheInfo.MexPairs.key; + const ttl = includeFarms ? CacheInfo.MexPairsWithFarms.ttl : CacheInfo.MexPairs.ttl; + return await this.cachingService.getOrSet( - CacheInfo.MexPairs.key, - async () => await this.getAllMexPairsRaw(), - CacheInfo.MexPairs.ttl, + cacheKey, + async () => await this.getAllMexPairsRaw(includeFarms), + ttl, Constants.oneSecond() * 30, ); } async getMexPairsCount(filter?: MexPairsFilter): Promise { - const mexPairs = await this.getAllMexPairs(); + const mexPairs = await this.getAllMexPairs(filter?.includeFarms ?? false); const filteredPairs = this.applyFilters(mexPairs, filter); return filteredPairs.length; } - async getAllMexPairsRaw(): Promise { + async getAllMexPairsRaw(includeFarms: boolean = false): Promise { try { const settings = await this.mexSettingService.getSettings(); if (!settings) { throw new BadRequestException('Could not fetch MEX settings'); } - const pairsLimit = gql` - query PairCount { - factory { - pairCount - } - }`; + const allPairs: MexPair[] = []; + let cursor: string | null = null; + let hasNextPage = true; - const pairsLimitResult: any = await this.graphQlService.getExchangeServiceData(pairsLimit); - const totalPairs = pairsLimitResult?.factory?.pairCount; + while (hasNextPage) { + const variables = { + pagination: { first: 25, after: cursor }, + filters: { state: [MexPairStatus.active] }, + }; - const variables = { - pagination: { first: totalPairs }, - filters: { state: MexPairStatus.active }, - }; + const query = filteredPairsQuery(includeFarms); + const result: any = await this.graphQlService.getExchangeServiceData(query, variables); - const query = gql` - query filteredPairs($pagination: ConnectionArgs!, $filters: PairsFilter!) { - filteredPairs(pagination: $pagination, filters: $filters) { - edges { - cursor - node { - address - liquidityPoolToken { - identifier - name - __typename - } - liquidityPoolTokenPriceUSD - firstToken { - name - identifier - decimals - previous24hPrice - __typename - } - secondToken { - name - identifier - decimals - previous24hPrice - __typename - } - firstTokenPrice - firstTokenPriceUSD - secondTokenPrice - secondTokenPriceUSD - info { - reserves0 - reserves1 - totalSupply - __typename - } - state - type - lockedValueUSD - volumeUSD24h - hasFarms - hasDualFarms - tradesCount - tradesCount24h - deployedAt - __typename - } - } - } + if (!result) { + break; } - `; - const result: any = await this.graphQlService.getExchangeServiceData(query, variables); - if (!result) { - return []; + const pairs = result.filteredPairs.edges.map((edge: any) => this.getPairInfo(edge.node, includeFarms)); + allPairs.push(...pairs.filter((pair: MexPair | undefined) => pair !== undefined)); + + hasNextPage = result.filteredPairs.pageInfo.hasNextPage; + cursor = result.filteredPairs.edges.length > 0 ? result.filteredPairs.edges[result.filteredPairs.edges.length - 1].cursor : null; } - return result.filteredPairs.edges - .map((edge: any) => this.getPairInfo(edge.node)); + return allPairs; } catch (error) { - this.logger.error('An error occurred while getting all mex pairs'); + this.logger.error('An error occurred while getting all mex pairs from the exchange'); this.logger.error(error); return []; } } - private getPairInfo(pair: any): MexPair | undefined { + + private getPairInfo(pair: any, includeFarms: boolean = false): MexPair | undefined { const firstTokenSymbol = pair.firstToken.identifier.split('-')[0]; const secondTokenSymbol = pair.secondToken.identifier.split('-')[0]; const state = this.getPairState(pair.state); @@ -178,13 +131,29 @@ export class MexPairService { exchange = MexPairExchange.unknown; } + const baseInfo = { + address: pair.address, + id: pair.liquidityPoolToken.identifier, + symbol: pair.liquidityPoolToken.identifier.split('-')[0], + name: pair.liquidityPoolToken.name, + price: Number(pair.liquidityPoolTokenPriceUSD), + totalValue: Number(pair.lockedValueUSD), + volume24h: Number(pair.volumeUSD24h), + tradesCount: Number(pair.tradesCount), + tradesCount24h: Number(pair.tradesCount24h), + deployedAt: Number(pair.deployedAt), + state, + type, + exchange, + ...(includeFarms && { + hasFarms: pair.hasFarms ?? false, + hasDualFarms: pair.hasDualFarms ?? false, + }), + }; + if ((firstTokenSymbol === 'WEGLD' && secondTokenSymbol === 'USDC') || secondTokenSymbol === 'WEGLD') { return { - address: pair.address, - id: pair.liquidityPoolToken.identifier, - symbol: pair.liquidityPoolToken.identifier.split('-')[0], - name: pair.liquidityPoolToken.name, - price: Number(pair.liquidityPoolTokenPriceUSD), + ...baseInfo, basePrevious24hPrice: Number(pair.firstToken.previous24hPrice), quotePrevious24hPrice: Number(pair.secondToken.previous24hPrice), baseId: pair.firstToken.identifier, @@ -195,25 +164,11 @@ export class MexPairService { quotePrice: Number(pair.secondTokenPriceUSD), quoteSymbol: secondTokenSymbol, quoteName: pair.secondToken.name, - totalValue: Number(pair.lockedValueUSD), - volume24h: Number(pair.volumeUSD24h), - hasFarms: pair.hasFarms, - hasDualFarms: pair.hasDualFarms, - tradesCount: Number(pair.tradesCount), - tradesCount24h: Number(pair.tradesCount24h), - deployedAt: Number(pair.deployedAt), - state, - type, - exchange, }; } return { - address: pair.address, - id: pair.liquidityPoolToken.identifier, - symbol: pair.liquidityPoolToken.identifier.split('-')[0], - name: pair.liquidityPoolToken.name, - price: Number(pair.liquidityPoolTokenPriceUSD), + ...baseInfo, basePrevious24hPrice: Number(pair.secondToken.previous24hPrice), quotePrevious24hPrice: Number(pair.firstToken.previous24hPrice), baseId: pair.secondToken.identifier, @@ -224,16 +179,6 @@ export class MexPairService { quotePrice: Number(pair.firstTokenPriceUSD), quoteSymbol: firstTokenSymbol, quoteName: pair.firstToken.name, - totalValue: Number(pair.lockedValueUSD), - volume24h: Number(pair.volumeUSD24h), - hasFarms: pair.hasFarms, - hasDualFarms: pair.hasDualFarms, - tradesCount: Number(pair.tradesCount), - tradesCount24h: Number(pair.tradesCount24h), - deployedAt: Number(pair.deployedAt), - state, - type, - exchange, }; } diff --git a/src/endpoints/mex/mex.settings.service.ts b/src/endpoints/mex/mex.settings.service.ts index 2770ef3f8..243c8a6f9 100644 --- a/src/endpoints/mex/mex.settings.service.ts +++ b/src/endpoints/mex/mex.settings.service.ts @@ -1,13 +1,14 @@ import { Constants } from "@multiversx/sdk-nestjs-common"; import { CacheService } from "@multiversx/sdk-nestjs-cache"; import { Injectable } from "@nestjs/common"; -import { gql } from "graphql-request"; import { CacheInfo } from "src/utils/cache.info"; import { GraphQlService } from "src/common/graphql/graphql.service"; import { TransactionMetadata } from "../transactions/transaction-action/entities/transaction.metadata"; import { TransactionMetadataTransfer } from "../transactions/transaction-action/entities/transaction.metadata.transfer"; import { MexSettings } from "./entities/mex.settings"; import { ApiConfigService } from "src/common/api-config/api.config.service"; +import { settingsQuery } from "./graphql/settings.query"; +import { pairCountQuery } from "./graphql/pairs.count.query"; @Injectable() export class MexSettingsService { @@ -87,83 +88,33 @@ export class MexSettingsService { } public async getSettingsRaw(): Promise { - const variables = { - offset: 0, - limit: 500, - }; - - const query = gql` - query ($offset: Int, $limit: Int) { - pairs(offset: $offset, limit: $limit) { - state - address - firstToken { - name - identifier - decimals - __typename - } - secondToken { - name - identifier - decimals - __typename - } - } - proxy { - address - lockedAssetTokens { - collection - __typename - } - } - farms { - ... on FarmModelV1_2 { - state - address - } - ... on FarmModelV1_3 { - state - address - } - ... on FarmModelV2 { - state - address - } - } - wrappingInfo { - address - shard - } - distribution { - address - } - lockedAssetFactory { - address - } - stakingFarms { - state - address - } - stakingProxies { - address - } - factory { - address - } - } - `; - - const response = await this.graphQlService.getExchangeServiceData(query, variables); + const pairLimitCount = await this.getPairLimitCount(); + const response = await this.graphQlService.getExchangeServiceData(settingsQuery(pairLimitCount)); if (!response) { return null; } - const settings = MexSettings.fromQueryResponse(response); + const transformedResponse = { + ...response, + pairs: response.filteredPairs.edges.map((edge: { node: { address: string } }) => ({ + address: edge.node.address, + })), + }; + + const settings = MexSettings.fromQueryResponse(transformedResponse); return settings; } getWegldId(): string | undefined { return this.wegldId; } + + private async getPairLimitCount(): Promise { + const response = await this.graphQlService.getExchangeServiceData(pairCountQuery); + if (!response) { + return 500; + } + + return response.factory.pairCount; + } } diff --git a/src/endpoints/mex/mex.token.charts.service.ts b/src/endpoints/mex/mex.token.charts.service.ts index 8d2e62b94..0b3dde374 100644 --- a/src/endpoints/mex/mex.token.charts.service.ts +++ b/src/endpoints/mex/mex.token.charts.service.ts @@ -6,6 +6,7 @@ import { MexTokenChart } from "./entities/mex.token.chart"; import { MexTokenService } from "./mex.token.service"; import { CacheService } from "@multiversx/sdk-nestjs-cache"; import { CacheInfo } from "src/utils/cache.info"; +import { tokenPricesHourResolutionQuery } from "./graphql/token.prices.hour.resolution.query"; @Injectable() export class MexTokenChartsService { @@ -31,19 +32,8 @@ export class MexTokenChartsService { return undefined; } - const query = gql` - query tokenPricesHourResolution { - values24h( - series: "${tokenIdentifier}", - metric: "priceUSD" - ) { - timestamp - value - } - } - `; - try { + const query = tokenPricesHourResolutionQuery(tokenIdentifier); const data = await this.graphQlService.getExchangeServiceData(query); return this.convertToMexTokenChart(data?.values24h) || []; } catch (error) { diff --git a/src/endpoints/mex/mex.token.service.ts b/src/endpoints/mex/mex.token.service.ts index c98b180c9..7907bf258 100644 --- a/src/endpoints/mex/mex.token.service.ts +++ b/src/endpoints/mex/mex.token.service.ts @@ -2,7 +2,6 @@ import { BadRequestException, forwardRef, Inject, Injectable } from "@nestjs/com import { CacheInfo } from "src/utils/cache.info"; import { MexToken } from "./entities/mex.token"; import { MexPairService } from "./mex.pair.service"; -import { MexPairState } from "./entities/mex.pair.state"; import { MexPair } from "./entities/mex.pair"; import { ApiConfigService } from "src/common/api-config/api.config.service"; import { MexFarmService } from "./mex.farm.service"; @@ -13,7 +12,7 @@ import { OriginLogger } from "@multiversx/sdk-nestjs-common"; import { QueryPagination } from "src/common/entities/query.pagination"; import { MexTokenType } from "./entities/mex.token.type"; import { GraphQlService } from "src/common/graphql/graphql.service"; -import { gql } from "graphql-request"; +import { tokensQuery } from "./graphql/tokens.query"; @Injectable() export class MexTokenService { @@ -172,10 +171,9 @@ export class MexTokenService { private async getAllMexTokensRaw(): Promise { const pairs = await this.mexPairService.getAllMexPairs(); - const filteredPairs = pairs.filter(x => x.state === MexPairState.active); const mexTokens: MexToken[] = []; - for (const pair of filteredPairs) { + for (const pair of pairs) { if (pair.baseSymbol === 'WEGLD' && pair.quoteSymbol === "USDC") { const wegldToken = new MexToken(); wegldToken.id = pair.baseId; @@ -184,7 +182,7 @@ export class MexTokenService { wegldToken.price = pair.basePrice; wegldToken.previous24hPrice = pair.basePrevious24hPrice; wegldToken.previous24hVolume = pair.volume24h; - wegldToken.tradesCount = this.computeTradesCountForMexToken(wegldToken, filteredPairs); + wegldToken.tradesCount = this.computeTradesCountForMexToken(wegldToken, pairs); mexTokens.push(wegldToken); } @@ -193,7 +191,7 @@ export class MexTokenService { continue; } - mexToken.tradesCount = this.computeTradesCountForMexToken(mexToken, filteredPairs); + mexToken.tradesCount = this.computeTradesCountForMexToken(mexToken, pairs); mexTokens.push(mexToken); } @@ -261,16 +259,7 @@ export class MexTokenService { throw new BadRequestException('Could not fetch MEX tokens'); } - const query = gql` - query tokens { - tokens { - identifier - type - } - } - `; - - const result: any = await this.graphQlService.getExchangeServiceData(query); + const result: any = await this.graphQlService.getExchangeServiceData(tokensQuery); if (!result || !result.tokens) { return []; } diff --git a/src/endpoints/tokens/token.service.ts b/src/endpoints/tokens/token.service.ts index 3ceb5183a..3401800ae 100644 --- a/src/endpoints/tokens/token.service.ts +++ b/src/endpoints/tokens/token.service.ts @@ -117,7 +117,7 @@ export class TokenService { } async getTokens(queryPagination: QueryPagination, filter: TokenFilter): Promise { - const {from, size} = queryPagination; + const { from, size } = queryPagination; let tokens = await this.getFilteredTokens(filter); @@ -127,9 +127,9 @@ export class TokenService { this.applyTickerFromAssets(token); } - return tokens - .map(item => ApiUtils.mergeObjects(new TokenDetailed(), item)) - .filter(t => t.identifier !== this.egldIdentifierInMultiTransfer); + return tokens + .map(item => ApiUtils.mergeObjects(new TokenDetailed(), item)) + .filter(t => t.identifier !== this.egldIdentifierInMultiTransfer); } applyTickerFromAssets(token: Token) { @@ -360,11 +360,11 @@ export class TokenService { if (TokenUtils.isNft(identifier)) { const nftData = await this.gatewayService.getAddressNft(address, identifier); - tokenWithBalance = new TokenDetailedWithBalance({...token, ...nftData}); + tokenWithBalance = new TokenDetailedWithBalance({ ...token, ...nftData }); } else { const esdtData = await this.gatewayService.getAddressEsdt(address, identifier); - tokenWithBalance = new TokenDetailedWithBalance({...token, ...esdtData}); + tokenWithBalance = new TokenDetailedWithBalance({ ...token, ...esdtData }); } // eslint-disable-next-line require-await @@ -969,9 +969,9 @@ export class TokenService { private async getTotalTransactions(token: TokenDetailed): Promise<{ count: number, lastUpdatedAt: number } | undefined> { try { - const count = await this.transactionService.getTransactionCount(new TransactionFilter({tokens: [token.identifier, ...token.assets?.extraTokens ?? []]})); + const count = await this.transactionService.getTransactionCount(new TransactionFilter({ tokens: [token.identifier, ...token.assets?.extraTokens ?? []] })); - return {count, lastUpdatedAt: new Date().getTimeInSeconds()}; + return { count, lastUpdatedAt: new Date().getTimeInSeconds() }; } catch (error) { this.logger.error(`An unhandled error occurred when getting transaction count for token '${token.identifier}'`); this.logger.error(error); @@ -1017,11 +1017,10 @@ export class TokenService { private async applyMexLiquidity(tokens: TokenDetailed[]): Promise { try { - const pairs = await this.mexPairService.getAllMexPairs(); - const filteredPairs = pairs.filter(x => x.state === MexPairState.active); + const allPairs = await this.mexPairService.getAllMexPairs(); for (const token of tokens) { - const pairs = filteredPairs.filter(x => x.baseId === token.identifier || x.quoteId === token.identifier); + const pairs = allPairs.filter(x => x.baseId === token.identifier || x.quoteId === token.identifier); if (pairs.length > 0) { token.totalLiquidity = pairs.sum(x => x.totalValue / 2); token.totalVolume24h = pairs.sum(x => x.volume24h ?? 0); diff --git a/src/test/unit/controllers/mex.controller.spec.ts b/src/test/unit/controllers/mex.controller.spec.ts index 4ea1c1029..ab913746d 100644 --- a/src/test/unit/controllers/mex.controller.spec.ts +++ b/src/test/unit/controllers/mex.controller.spec.ts @@ -11,6 +11,7 @@ import request = require('supertest'); import { PublicAppModule } from "src/public.app.module"; import { QueryPagination } from "src/common/entities/query.pagination"; import { MexPairExchange } from "src/endpoints/mex/entities/mex.pair.exchange"; +import { MexPairsFilter } from 'src/endpoints/mex/entities/mex.pairs..filter'; describe('MexController', () => { let app: INestApplication; @@ -68,7 +69,7 @@ describe('MexController', () => { .expect(200); expect(mexPairServiceMocks.getMexPairs).toHaveBeenCalledWith( - 0, 25, { "exchange": undefined } + 0, 25, new MexPairsFilter({ exchange: undefined, includeFarms: false }) ); }); @@ -81,7 +82,7 @@ describe('MexController', () => { .expect(200); expect(mexPairServiceMocks.getMexPairs).toHaveBeenCalledWith( - 0, 5, { "exchange": undefined } + 0, 5, new MexPairsFilter({ exchange: undefined, includeFarms: false }) ); }); @@ -94,7 +95,7 @@ describe('MexController', () => { .expect(200); expect(mexPairServiceMocks.getMexPairs).toHaveBeenCalledWith( - 0, 5, { "exchange": MexPairExchange.xexchange } + 0, 5, new MexPairsFilter({ exchange: MexPairExchange.xexchange, includeFarms: false }) ); }); @@ -107,7 +108,7 @@ describe('MexController', () => { .expect(200); expect(mexPairServiceMocks.getMexPairs).toHaveBeenCalledWith( - 0, 5, { "exchange": MexPairExchange.unknown } + 0, 5, new MexPairsFilter({ exchange: MexPairExchange.unknown, includeFarms: false }) ); }); @@ -121,7 +122,7 @@ describe('MexController', () => { }); expect(mexPairServiceMocks.getMexPairsCount).toHaveBeenCalledWith( - { "exchange": undefined } + new MexPairsFilter({ exchange: undefined, includeFarms: false }) ); }); @@ -135,7 +136,7 @@ describe('MexController', () => { }); expect(mexPairServiceMocks.getMexPairsCount).toHaveBeenCalledWith( - { "exchange": MexPairExchange.xexchange } + new MexPairsFilter({ exchange: MexPairExchange.xexchange, includeFarms: false }) ); }); @@ -149,7 +150,7 @@ describe('MexController', () => { }); expect(mexPairServiceMocks.getMexPairsCount).toHaveBeenCalledWith( - { "exchange": MexPairExchange.unknown } + new MexPairsFilter({ exchange: MexPairExchange.unknown, includeFarms: false }) ); }); @@ -162,7 +163,44 @@ describe('MexController', () => { .get(`${path}/pairs/${baseId}/${quoteId}`) .expect(200); - expect(mexPairServiceMocks.getMexPair).toHaveBeenCalledWith(baseId, quoteId); + expect(mexPairServiceMocks.getMexPair).toHaveBeenCalledWith(baseId, quoteId, false); + }); + + it('should return mex pairs with farms information', async () => { + mexPairServiceMocks.getMexPairs.mockReturnValue([]); + await request(app.getHttpServer()) + .get(`${path}/pairs?includeFarms=true`) + .expect(200); + + expect(mexPairServiceMocks.getMexPairs).toHaveBeenCalledWith( + 0, 25, new MexPairsFilter({ exchange: undefined, includeFarms: true }) + ); + }); + + it('should return mex pair with farms information', async () => { + mexPairServiceMocks.getMexPair.mockReturnValue({}); + const baseId = 'MEX-455c57'; + const quoteId = 'WEGLD-bd4d79'; + + await request(app.getHttpServer()) + .get(`${path}/pairs/${baseId}/${quoteId}?includeFarms=true`) + .expect(200); + + expect(mexPairServiceMocks.getMexPair).toHaveBeenCalledWith(baseId, quoteId, true); + }); + + it('should return total mex pairs count with farms information', async () => { + mexPairServiceMocks.getMexPairsCount.mockReturnValue(10); + await request(app.getHttpServer()) + .get(`${path}/pairs/count?includeFarms=true`) + .expect(200) + .expect(response => { + expect(+response.text).toStrictEqual(10); + }); + + expect(mexPairServiceMocks.getMexPairsCount).toHaveBeenCalledWith( + new MexPairsFilter({ exchange: undefined, includeFarms: true }) + ); }); }); diff --git a/src/test/unit/services/graphql.service.spec.ts b/src/test/unit/services/graphql.service.spec.ts index 4f184aa02..25a61e43c 100644 --- a/src/test/unit/services/graphql.service.spec.ts +++ b/src/test/unit/services/graphql.service.spec.ts @@ -14,6 +14,7 @@ describe('GraphQlService', () => { mockApiConfigService = { getExchangeServiceUrlMandatory: jest.fn().mockReturnValue('https://graph.xexchange.com/graphql'), getMarketplaceServiceUrl: jest.fn().mockReturnValue('https://nfts-graph.multiversx.com/graphql'), + getSelfUrl: jest.fn().mockReturnValue('https://api.multiversx.com'), }; mockGraphQLClient = { diff --git a/src/utils/cache.info.ts b/src/utils/cache.info.ts index d95dcec43..a1c73440c 100644 --- a/src/utils/cache.info.ts +++ b/src/utils/cache.info.ts @@ -338,6 +338,11 @@ export class CacheInfo { ttl: Constants.oneMinute() * 10, }; + static MexPairsWithFarms: CacheInfo = { + key: 'mexPairsWithFarms', + ttl: Constants.oneMinute() * 10, + }; + static MexTokens: CacheInfo = { key: "mexTokens", ttl: Constants.oneMinute() * 10,