diff --git a/proto/dex_state.proto b/proto/dex_state.proto index d7de2f957..93f5adecf 100644 --- a/proto/dex_state.proto +++ b/proto/dex_state.proto @@ -269,7 +269,7 @@ message GetFilteredStakingFarmsRequest { } message UpdateStakingFarmsRequest { - repeated StakingProxy stakingFarms = 1; + repeated StakingFarm stakingFarms = 1; google.protobuf.FieldMask updateMask = 2; } diff --git a/src/modules/auto-router/services/auto-router.service.ts b/src/modules/auto-router/services/auto-router.service.ts index a425d31e3..9623244b2 100644 --- a/src/modules/auto-router/services/auto-router.service.ts +++ b/src/modules/auto-router/services/auto-router.service.ts @@ -45,6 +45,7 @@ import { XoxnoQuoteModel, XoxnoPathModel, } from '../models/xoxno-aggregator.model'; +import { TokenService } from 'src/modules/tokens/services/token.service'; @Injectable() export class AutoRouterService { @@ -61,6 +62,7 @@ export class AutoRouterService { private readonly composeTasksAbi: ComposableTasksAbiService, private readonly pairsState: PairsStateService, private readonly tokensState: TokensStateService, + private readonly tokenService: TokenService, private readonly xoxnoAggregatorService: XoxnoAggregatorService, private readonly apiConfigService: ApiConfigService, @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger, @@ -1052,7 +1054,13 @@ export class AutoRouterService { quote.amountOut, ); const feeTokenID = await this.toWrappedIfEGLD([quote.feeToken]); - const feeToken = await this.tokensState.getTokens([feeTokenID[0]]); + const feeTokenFromState = await this.tokensState.getTokens([ + feeTokenID[0], + ]); + const feeToken = + feeTokenFromState.length > 0 && feeTokenFromState[0] + ? feeTokenFromState + : await this.tokenService.getAllTokensMetadata([feeTokenID[0]]); return new SmartSwapModel({ amountOut: quote.amountOut, @@ -1088,14 +1096,47 @@ export class AutoRouterService { paths.flatMap((p) => p.swaps.flatMap((s) => [s.from, s.to])), ), ]; - const esdtTokenIDs = await this.toWrappedIfEGLD(tokenIDs); - // Batch fetch all token metadata (cached) - const tokensMetadata = await this.tokensState.getTokens(esdtTokenIDs); - const tokenMap = new Map( - tokenIDs.map((id, i) => [id, tokensMetadata[i]]), + const wrappedEgldTokenID = await this.wrapAbi.wrappedEgldTokenID(); + const esdtTokenIDs = tokenIDs.map((t) => + t === mxConfig.EGLDIdentifier ? wrappedEgldTokenID : t, + ); + + const allStateTokenIdentifiers = new Set( + (await this.tokensState.getAllTokens(['identifier'])).map( + (t) => t.identifier, + ), + ); + const knownTokenIDs = esdtTokenIDs.filter((id) => + allStateTokenIdentifiers.has(id), + ); + const unknownTokenIDs = esdtTokenIDs.filter( + (id) => !allStateTokenIdentifiers.has(id), ); + const tokenMap = new Map(); + if (knownTokenIDs.length > 0) { + const stateTokens = await this.tokensState.getTokens(knownTokenIDs); + stateTokens.forEach((token) => { + if (token) tokenMap.set(token.identifier, token); + }); + } + + if (unknownTokenIDs.length > 0) { + const unknownTokensMetadata = + await this.tokenService.getAllTokensMetadata(unknownTokenIDs); + unknownTokensMetadata + .filter((token) => token != null) + .forEach((token) => tokenMap.set(token.identifier, token)); + } + + if (tokenIDs.includes(mxConfig.EGLDIdentifier)) { + tokenMap.set( + mxConfig.EGLDIdentifier, + tokenMap.get(wrappedEgldTokenID), + ); + } + return paths.map((path) => { const swaps = path.swaps; diff --git a/src/modules/state/entities/state.tasks.entities.ts b/src/modules/state/entities/state.tasks.entities.ts index 7215ba3ce..c27017d27 100644 --- a/src/modules/state/entities/state.tasks.entities.ts +++ b/src/modules/state/entities/state.tasks.entities.ts @@ -25,6 +25,8 @@ export enum StateTasks { REFRESH_STAKING_FARM = 'refreshStakingFarm', REFRESH_STAKING_FARMS = 'refreshAllStakingFarms', REFRESH_FEES_COLLECTOR = 'refreshFeesCollector', + REFRESH_TOKEN = 'refreshToken', + REFRESH_TOKENS = 'refreshAllTokens', } export const StateTaskPriority: Record = { @@ -35,8 +37,10 @@ export const StateTaskPriority: Record = { refreshAllFarms: 13, refreshAllStakingFarms: 13, refreshFeesCollector: 13, + refreshAllTokens: 13, refreshFarm: 15, refreshStakingFarm: 15, + refreshToken: 15, broadcastPriceUpdates: 30, indexLpToken: 100, updateSnapshot: 200, @@ -48,6 +52,7 @@ export const StateTasksWithArguments = [ StateTasks.INDEX_PAIR, StateTasks.REFRESH_FARM, StateTasks.REFRESH_STAKING_FARM, + StateTasks.REFRESH_TOKEN, ]; export const PENDING_PRICE_UPDATES_KEY = 'dexService.pendingPriceUpdates'; diff --git a/src/modules/state/mocks/tokens.state.service.mock.ts b/src/modules/state/mocks/tokens.state.service.mock.ts index f9d05cb8f..1a4965d76 100644 --- a/src/modules/state/mocks/tokens.state.service.mock.ts +++ b/src/modules/state/mocks/tokens.state.service.mock.ts @@ -14,6 +14,10 @@ export class TokensStateServiceMock { return tokenIDs.map((tokenID) => Tokens(tokenID)); } + async getAllTokens(fields: (keyof EsdtToken)[] = []): Promise { + return this.getTokens(MockedTokens, fields); + } + async getFilteredTokens( offset: number, limit: number, diff --git a/src/modules/state/services/state.sync.service.ts b/src/modules/state/services/state.sync.service.ts index bd140c17a..228a5dc0e 100644 --- a/src/modules/state/services/state.sync.service.ts +++ b/src/modules/state/services/state.sync.service.ts @@ -343,6 +343,12 @@ export class StateSyncService { return this.pairsSync.getPairReservesAndState(pair); } + async refreshTokenMetadata( + tokenID: string, + ): Promise | undefined> { + return this.tokensSync.refreshTokenMetadata(tokenID); + } + async getPairAnalytics(pair: PairModel): Promise> { return this.analyticsSync.getPairAnalytics(pair); } diff --git a/src/modules/state/services/state.tasks.service.ts b/src/modules/state/services/state.tasks.service.ts index 361fdc603..d1bebb956 100644 --- a/src/modules/state/services/state.tasks.service.ts +++ b/src/modules/state/services/state.tasks.service.ts @@ -36,6 +36,7 @@ const MAX_TASK_RETRIES = 5; const TASK_RETRY_TTL_SECONDS = 86400; const INDEX_LP_MAX_ATTEMPTS = 60; const PAIR_REFRESH_CONCURRENCY = 50; +const TOKEN_REFRESH_CONCURRENCY = 50; function getTaskRetryKey(task: TaskDto): string { const base = `${TASK_RETRY_COUNT_KEY_PREFIX}:${task.name}`; @@ -167,6 +168,12 @@ export class StateTasksService { case StateTasks.REFRESH_FEES_COLLECTOR: await this.refreshFeesCollector(); break; + case StateTasks.REFRESH_TOKEN: + await this.refreshToken(task.args[0]); + break; + case StateTasks.REFRESH_TOKENS: + await this.refreshTokens(); + break; default: break; } @@ -549,4 +556,53 @@ export class StateTasksService { await this.feesCollectorState.updateFeesCollector(feesCollectorUpdates); } + + async refreshToken(identifier: string): Promise { + const updates = await this.syncService.refreshTokenMetadata(identifier); + + if (!updates) { + throw new Error(`Token ${identifier} not found`); + } + + const tokenUpdates = new Map>(); + tokenUpdates.set(identifier, updates); + + const updateResult = await this.tokensState.updateTokens(tokenUpdates); + + this.logger.debug(`Refresh token ${identifier} task completed`, { + context: StateTasksService.name, + updateResult, + }); + } + + async refreshTokens(): Promise { + const tokens = await this.tokensState.getAllTokens(['identifier']); + + const tokenUpdates = new Map>(); + + const profiler = new PerformanceProfiler(); + + for (let i = 0; i < tokens.length; i += TOKEN_REFRESH_CONCURRENCY) { + const chunk = tokens.slice(i, i + TOKEN_REFRESH_CONCURRENCY); + const results = await Promise.all( + chunk.map((token) => + this.syncService.refreshTokenMetadata(token.identifier), + ), + ); + results.forEach((updates, idx) => { + if (updates) { + tokenUpdates.set(chunk[idx].identifier, updates); + } + }); + } + + profiler.stop('Finished syncing tokens metadata in', true); + + const updateResult = await this.tokensState.updateTokens(tokenUpdates); + + this.logger.debug(`Refresh all tokens metadata task completed`, { + context: StateTasksService.name, + updateResult, + }); + } } diff --git a/src/modules/state/services/sync/tokens.sync.service.ts b/src/modules/state/services/sync/tokens.sync.service.ts index 56f7c88b9..014f18648 100644 --- a/src/modules/state/services/sync/tokens.sync.service.ts +++ b/src/modules/state/services/sync/tokens.sync.service.ts @@ -49,6 +49,26 @@ export class TokensSyncService { return token as EsdtToken; } + async refreshTokenMetadata( + tokenID: string, + ): Promise | undefined> { + const tokenMetadata = await this.apiService.getToken(tokenID); + + if (tokenMetadata === undefined) { + return undefined; + } + + const token = this.getTokenFromMetadata(tokenMetadata); + + if (token.assets) { + token.assets.lockedAccounts = token.assets.lockedAccounts + ? Object.keys(token.assets.lockedAccounts) + : []; + } + + return token; + } + private getTokenFromMetadata(tokenMetadata: EsdtToken): Partial { const token: Partial = { identifier: tokenMetadata.identifier, diff --git a/src/modules/tokens/models/esdtToken.model.ts b/src/modules/tokens/models/esdtToken.model.ts index 53a3de972..e22bca19c 100644 --- a/src/modules/tokens/models/esdtToken.model.ts +++ b/src/modules/tokens/models/esdtToken.model.ts @@ -35,21 +35,21 @@ export class EsdtToken extends BaseEsdtToken implements IEsdtToken { @Field() derivedEGLD: string; price?: string; - @Field() + @Field({ nullable: true }) previous24hPrice?: string; - @Field() + @Field({ nullable: true }) previous7dPrice?: string; - @Field() + @Field({ nullable: true }) volumeUSD24h?: string; - @Field() + @Field({ nullable: true }) previous24hVolume?: string; - @Field() + @Field({ nullable: true }) liquidityUSD?: string; - @Field() + @Field({ nullable: true }) swapCount24h?: number; - @Field() + @Field({ nullable: true }) previous24hSwapCount?: number; - @Field() + @Field({ nullable: true }) trendingScore?: string; supply?: string; circulatingSupply?: string; @@ -75,7 +75,7 @@ export class EsdtToken extends BaseEsdtToken implements IEsdtToken { roles?: RolesModel[]; type?: string; balance?: string; - @Field() + @Field({ nullable: true }) createdAt?: string; pairAddress?: string; priceChange24h?: number;