diff --git a/src/modules/fees-collector/services/fees-collector.setter.service.ts b/src/modules/fees-collector/services/fees-collector.setter.service.ts index fe62a6a39..686003b81 100644 --- a/src/modules/fees-collector/services/fees-collector.setter.service.ts +++ b/src/modules/fees-collector/services/fees-collector.setter.service.ts @@ -47,4 +47,26 @@ export class FeesCollectorSetterService extends GenericSetterService { CacheTtlInfo.ContractInfo.localTtl, ); } + + async allTokens(tokens: string[]): Promise { + return this.setData( + this.getCacheKey('allTokens'), + tokens, + CacheTtlInfo.ContractInfo.remoteTtl, + CacheTtlInfo.ContractInfo.localTtl, + ); + } + + async accumulatedFeesUntilNow( + scAddress: string, + week: number, + value: string, + ): Promise { + return this.setData( + this.getCacheKey('accumulatedFeesUntilNow', scAddress, week), + value, + CacheTtlInfo.ContractBalance.remoteTtl, + CacheTtlInfo.ContractBalance.localTtl, + ); + } } diff --git a/src/modules/staking/services/staking.compute.service.ts b/src/modules/staking/services/staking.compute.service.ts index e296048ae..918521353 100644 --- a/src/modules/staking/services/staking.compute.service.ts +++ b/src/modules/staking/services/staking.compute.service.ts @@ -141,17 +141,24 @@ export class StakingComputeService { async computeExtraRewardsBounded( stakeAddress: string, blockDifferenceBig: BigNumber, + ): Promise { + const extraRewardsAPRBoundedPerBlock = + await this.computeExtraRewardsAPRBoundedPerBlock(stakeAddress); + + return extraRewardsAPRBoundedPerBlock.multipliedBy(blockDifferenceBig); + } + + async computeExtraRewardsAPRBoundedPerBlock( + stakeAddress: string, ): Promise { const [farmTokenSupply, annualPercentageRewards] = await Promise.all([ this.stakingAbi.farmTokenSupply(stakeAddress), this.stakingAbi.annualPercentageRewards(stakeAddress), ]); - const extraRewardsAPRBoundedPerBlock = new BigNumber(farmTokenSupply) + return new BigNumber(farmTokenSupply) .multipliedBy(annualPercentageRewards) .dividedBy(constantsConfig.MAX_PERCENT) .dividedBy(constantsConfig.BLOCKS_IN_YEAR); - - return extraRewardsAPRBoundedPerBlock.multipliedBy(blockDifferenceBig); } async farmingTokenPriceUSD(stakeAddress: string): Promise { @@ -325,10 +332,18 @@ export class StakingComputeService { // 10 blocks per minute * 60 minutes per hour * 24 hours per day const blocksInDay = 10 * 60 * 24; + const extraRewardsAPRBoundedPerBlock = + await this.computeExtraRewardsAPRBoundedPerBlock(stakeAddress); + + const perBlockRewards = BigNumber.min( + extraRewardsAPRBoundedPerBlock, + perBlockRewardAmount, + ); + return parseFloat( new BigNumber(rewardsCapacity) .minus(accumulatedRewards) - .dividedBy(perBlockRewardAmount) + .dividedBy(perBlockRewards) .dividedBy(blocksInDay) .toFixed(2), ); diff --git a/src/modules/staking/services/staking.setter.service.ts b/src/modules/staking/services/staking.setter.service.ts index 407ccdd51..57a33a683 100644 --- a/src/modules/staking/services/staking.setter.service.ts +++ b/src/modules/staking/services/staking.setter.service.ts @@ -5,6 +5,7 @@ import { CacheTtlInfo } from 'src/services/caching/cache.ttl.info'; import { GenericSetterService } from 'src/services/generics/generic.setter.service'; import { Logger } from 'winston'; import { EsdtTokenPayment } from '@multiversx/sdk-exchange'; +import { BoostedYieldsFactors } from 'src/modules/farm/models/farm.v2.model'; @Injectable() export class StakingSetterService extends GenericSetterService { @@ -178,6 +179,18 @@ export class StakingSetterService extends GenericSetterService { ); } + async setBoostedYieldsFactors( + stakeAddress: string, + value: BoostedYieldsFactors, + ): Promise { + return await this.setData( + this.getCacheKey('boostedYieldsFactors', stakeAddress), + value, + CacheTtlInfo.ContractState.remoteTtl, + CacheTtlInfo.ContractState.localTtl, + ); + } + async setUserTotalStakePosition( stakeAddress: string, userAddress: string, diff --git a/src/services/cache.warmer.module.ts b/src/services/cache.warmer.module.ts index ee0c89535..33f6b870d 100644 --- a/src/services/cache.warmer.module.ts +++ b/src/services/cache.warmer.module.ts @@ -39,6 +39,10 @@ import { TokensCacheWarmerService } from './crons/tokens.cache.warmer.service'; import { FarmModuleV2 } from 'src/modules/farm/v2/farm.v2.module'; import { EscrowCacheWarmerService } from './crons/escrow.cache.warmer.service'; import { EscrowModule } from 'src/modules/escrow/escrow.module'; +import { FeesCollectorCacheWarmerService } from './crons/fees.collector.cache.warmer.service'; +import { FeesCollectorModule } from 'src/modules/fees-collector/fees-collector.module'; +import { WeekTimekeepingModule } from 'src/submodules/week-timekeeping/week-timekeeping.module'; +import { WeeklyRewardsSplittingModule } from 'src/submodules/weekly-rewards-splitting/weekly-rewards-splitting.module'; @Module({ imports: [ @@ -66,6 +70,9 @@ import { EscrowModule } from 'src/modules/escrow/escrow.module'; GovernanceModule, DynamicModuleUtils.getCacheModule(), EscrowModule, + FeesCollectorModule, + WeekTimekeepingModule, + WeeklyRewardsSplittingModule, ], controllers: [], providers: [ @@ -85,6 +92,7 @@ import { EscrowModule } from 'src/modules/escrow/escrow.module'; ElasticService, TokensCacheWarmerService, EscrowCacheWarmerService, + FeesCollectorCacheWarmerService, ], }) export class CacheWarmerModule {} diff --git a/src/services/crons/fees.collector.cache.warmer.service.ts b/src/services/crons/fees.collector.cache.warmer.service.ts new file mode 100644 index 000000000..ef2c3e0f2 --- /dev/null +++ b/src/services/crons/fees.collector.cache.warmer.service.ts @@ -0,0 +1,157 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { PUB_SUB } from '../redis.pubSub.module'; +import { RedisPubSub } from 'graphql-redis-subscriptions'; +import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; +import { Logger } from 'winston'; +import { Cron, CronExpression } from '@nestjs/schedule'; +import { PerformanceProfiler } from '@multiversx/sdk-nestjs-monitoring'; +import { FeesCollectorAbiService } from 'src/modules/fees-collector/services/fees-collector.abi.service'; +import { WeekTimekeepingAbiService } from 'src/submodules/week-timekeeping/services/week-timekeeping.abi.service'; +import { constantsConfig, scAddress } from 'src/config'; +import { FeesCollectorSetterService } from 'src/modules/fees-collector/services/fees-collector.setter.service'; +import { WeeklyRewardsSplittingAbiService } from 'src/submodules/weekly-rewards-splitting/services/weekly-rewards-splitting.abi.service'; +import { FeesCollectorComputeService } from 'src/modules/fees-collector/services/fees-collector.compute.service'; +import { WeeklyRewardsSplittingSetterService } from 'src/submodules/weekly-rewards-splitting/services/weekly.rewarrds.splitting.setter.service'; +import { Lock } from '@multiversx/sdk-nestjs-common'; + +@Injectable() +export class FeesCollectorCacheWarmerService { + constructor( + @Inject(PUB_SUB) private pubSub: RedisPubSub, + @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger, + private readonly feesCollectorAbi: FeesCollectorAbiService, + private readonly feesCollectorCompute: FeesCollectorComputeService, + private readonly feesCollectorSetter: FeesCollectorSetterService, + private readonly weekTimekeepingAbi: WeekTimekeepingAbiService, + private readonly weeklyRewardsSplittingAbi: WeeklyRewardsSplittingAbiService, + private readonly weeklyRewardsSplittingSetter: WeeklyRewardsSplittingSetterService, + ) {} + + @Cron(CronExpression.EVERY_MINUTE) + @Lock({ name: 'cacheFeesCollector', verbose: true }) + async cacheFeesCollector(): Promise { + this.logger.info('Start refresh fees collector data', { + context: 'CacheFeesCollector', + }); + + const profiler = new PerformanceProfiler(); + + const [allTokens, currentWeek] = await Promise.all([ + this.feesCollectorAbi.allTokens(), + this.weekTimekeepingAbi.currentWeek(scAddress.feesCollector), + ]); + + const accumulatedFeesUntilNow = + await this.feesCollectorCompute.computeAccumulatedFeesUntilNow( + scAddress.feesCollector, + currentWeek, + ); + + const cachedKeys = await Promise.all([ + this.feesCollectorSetter.allTokens(allTokens), + this.feesCollectorSetter.accumulatedFeesUntilNow( + scAddress.feesCollector, + currentWeek, + accumulatedFeesUntilNow, + ), + ]); + + const tokensAccumulatedFeesCacheKeys = + await this.cacheTokensAccumulatedFees(allTokens, currentWeek); + + const claimWeeksCacheKeys = await this.cacheClaimWeeksData( + currentWeek, + scAddress.feesCollector, + ); + + cachedKeys.push(...tokensAccumulatedFeesCacheKeys); + cachedKeys.push(...claimWeeksCacheKeys); + + await this.deleteCacheKeys(cachedKeys); + + profiler.stop(); + this.logger.info( + `Finish refresh fees collector data in ${profiler.duration}`, + { + context: 'CacheFeesCollector', + }, + ); + } + + private async cacheTokensAccumulatedFees( + allTokens: string[], + week: number, + ): Promise { + const cachedKeys = []; + for (const token of allTokens) { + const accumulatedFees = + await this.feesCollectorAbi.getAccumulatedFeesRaw(week, token); + + const cacheKey = await this.feesCollectorSetter.accumulatedFees( + week, + token, + accumulatedFees, + ); + + cachedKeys.push(cacheKey); + } + + return cachedKeys; + } + + private async cacheClaimWeeksData( + currentWeek: number, + feesCollectorScAddress: string, + ): Promise { + const cachedKeys = []; + const startWeek = currentWeek - constantsConfig.USER_MAX_CLAIM_WEEKS; + + for (let week = startWeek; week <= currentWeek; week++) { + if (week < 1) { + continue; + } + + const totalEnergyForWeek = + await this.weeklyRewardsSplittingAbi.totalEnergyForWeekRaw( + feesCollectorScAddress, + week, + ); + const totalRewardsForWeek = + await this.weeklyRewardsSplittingAbi.totalRewardsForWeekRaw( + feesCollectorScAddress, + week, + ); + const totalLockedTokensForWeek = + await this.weeklyRewardsSplittingAbi.totalLockedTokensForWeekRaw( + feesCollectorScAddress, + week, + ); + + const keys = await Promise.all([ + this.weeklyRewardsSplittingSetter.totalEnergyForWeek( + feesCollectorScAddress, + week, + totalEnergyForWeek, + ), + this.weeklyRewardsSplittingSetter.totalRewardsForWeek( + feesCollectorScAddress, + week, + totalRewardsForWeek, + ), + this.weeklyRewardsSplittingSetter.totalLockedTokensForWeek( + feesCollectorScAddress, + week, + totalLockedTokensForWeek, + ), + ]); + + cachedKeys.push(...keys); + } + + return cachedKeys; + } + + private async deleteCacheKeys(invalidatedKeys: string[]) { + await this.pubSub.publish('deleteCacheKeys', invalidatedKeys); + } +} diff --git a/src/services/crons/staking.cache.warmer.service.ts b/src/services/crons/staking.cache.warmer.service.ts index 0a74e227a..0565433f2 100644 --- a/src/services/crons/staking.cache.warmer.service.ts +++ b/src/services/crons/staking.cache.warmer.service.ts @@ -55,12 +55,14 @@ export class StakingCacheWarmerService { minUnboundEpochs, divisionSafetyConstant, state, + boostedYieldsFactors, apr, ] = await Promise.all([ this.stakingAbi.getAnnualPercentageRewardsRaw(address), this.stakingAbi.getMinUnbondEpochsRaw(address), this.stakingAbi.getDivisionSafetyConstantRaw(address), this.stakingAbi.getStateRaw(address), + this.stakingAbi.getBoostedYieldsFactorsRaw(address), this.stakeCompute.computeStakeFarmAPR(address), ]); @@ -78,6 +80,10 @@ export class StakingCacheWarmerService { divisionSafetyConstant, ), this.stakeSetterService.setState(address, state), + this.stakeSetterService.setBoostedYieldsFactors( + address, + boostedYieldsFactors, + ), this.stakeSetterService.setStakeFarmAPR(address, apr), ]);