Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Develop to main #85

Merged
merged 14 commits into from
Oct 4, 2023
Merged
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"@fastify/static": "^6.6.0",
"@lido-nestjs/consensus": "^1.5.0",
"@lido-nestjs/constants": "^5.0.0",
"@lido-nestjs/contracts": "^9.0.0",
"@lido-nestjs/contracts": "^9.1.0",
"@lido-nestjs/decorators": "^1.0.0",
"@lido-nestjs/execution": "^1.9.3",
"@lido-nestjs/fetch": "^1.4.0",
Expand Down
2 changes: 2 additions & 0 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { ExecutionProviderModule } from 'common/execution-provider';
import { ContractsModule } from 'common/contracts';
import { AppService } from './app.service';
import { HTTPModule } from '../http';
import { EventsModule } from '../events';

@Module({
imports: [
Expand All @@ -21,6 +22,7 @@ import { HTTPModule } from '../http';
PrometheusModule,
ConfigModule,
JobsModule,
EventsModule,
ContractsModule,
],
providers: [{ provide: APP_INTERCEPTOR, useClass: SentryInterceptor }, AppService],
Expand Down
4 changes: 4 additions & 0 deletions src/common/config/env.validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ export class EnvironmentVariables {
@IsString()
JOB_INTERVAL_QUEUE_INFO = CronExpression.EVERY_10_MINUTES;

@IsOptional()
@IsString()
JOB_INTERVAL_CONTRACT_CONFIG = CronExpression.EVERY_10_HOURS;

@IsArray()
@ArrayMinSize(1)
@Transform(({ value }) => value.split(','))
Expand Down
17 changes: 14 additions & 3 deletions src/common/contracts/contracts.module.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,26 @@
import { WithdrawalQueueContractModule } from '@lido-nestjs/contracts';
import {
WithdrawalQueueContractModule,
LidoContractModule,
OracleReportSanityCheckerModule,
AccountingOracleHashConsensusModule,
} from '@lido-nestjs/contracts';
import { Global, Module } from '@nestjs/common';
import { ExecutionProvider } from 'common/execution-provider';

@Global()
@Module({
imports: [
WithdrawalQueueContractModule.forRootAsync({
WithdrawalQueueContractModule,
LidoContractModule,
OracleReportSanityCheckerModule,
AccountingOracleHashConsensusModule,
].map((module) =>
module.forRootAsync({
async useFactory(provider: ExecutionProvider) {
return { provider };
},
inject: [ExecutionProvider],
}),
],
),
})
export class ContractsModule {}
1 change: 1 addition & 0 deletions src/common/genesis-time/genesis-time.constants.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export const SECONDS_PER_SLOT = 12;
export const SLOTS_PER_EPOCH = 32;
export const EPOCH_PER_FRAME = 225;
3 changes: 2 additions & 1 deletion src/common/genesis-time/genesis-time.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import { Module } from '@nestjs/common';
import { ConsensusProviderModule } from 'common/consensus-provider';
import { LoggerModule } from 'common/logger';
import { GenesisTimeService } from './genesis-time.service';
import { ContractConfigStorageModule } from '../../storage';

@Module({
imports: [LoggerModule, ConsensusProviderModule],
imports: [LoggerModule, ConsensusProviderModule, ContractConfigStorageModule],
providers: [GenesisTimeService],
exports: [GenesisTimeService],
})
Expand Down
8 changes: 7 additions & 1 deletion src/common/genesis-time/genesis-time.service.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { Inject, Injectable, LoggerService, OnModuleInit } from '@nestjs/common';
import { LOGGER_PROVIDER } from '@lido-nestjs/logger';
import { ConsensusProviderService } from 'common/consensus-provider';
import { SECONDS_PER_SLOT, SLOTS_PER_EPOCH } from './genesis-time.constants';
import { EPOCH_PER_FRAME, SECONDS_PER_SLOT, SLOTS_PER_EPOCH } from './genesis-time.constants';
import { ContractConfigStorageService } from '../../storage';

@Injectable()
export class GenesisTimeService implements OnModuleInit {
constructor(
@Inject(LOGGER_PROVIDER) protected readonly logger: LoggerService,
protected readonly consensusService: ConsensusProviderService,
protected readonly contractConfig: ContractConfigStorageService,
) {}

public async onModuleInit(): Promise<void> {
Expand Down Expand Up @@ -57,5 +59,9 @@ export class GenesisTimeService implements OnModuleInit {
return Math.floor((currentTime - genesisTime) / SECONDS_PER_SLOT / SLOTS_PER_EPOCH);
}

public getFrameOfEpoch(epoch: number) {
return Math.floor((epoch - this.contractConfig.getInitialEpoch()) / EPOCH_PER_FRAME);
}

protected genesisTime = -1;
}
10 changes: 10 additions & 0 deletions src/events/events.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';

import { EventsService } from './events.service';
import { RewardsModule } from './rewards';

@Module({
imports: [RewardsModule],
providers: [EventsService],
})
export class EventsModule {}
23 changes: 23 additions & 0 deletions src/events/events.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
import { LOGGER_PROVIDER, LoggerService } from 'common/logger';
import { RewardsService } from './rewards';

@Injectable()
export class EventsService implements OnModuleInit {
constructor(
@Inject(LOGGER_PROVIDER) protected readonly logger: LoggerService,
protected readonly rewardsService: RewardsService,
) {}

public async onModuleInit(): Promise<void> {
// Do not wait for initialization to avoid blocking the main process
this.initialize();
}

/**
* Initializes event listeners
*/
protected async initialize(): Promise<void> {
await Promise.all([this.rewardsService.initialize()]);
}
}
2 changes: 2 additions & 0 deletions src/events/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './events.module';
export * from './events.service';
3 changes: 3 additions & 0 deletions src/events/rewards/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './rewards.service';
export * from './rewards.module';
export * from './rewards.constants';
6 changes: 6 additions & 0 deletions src/events/rewards/rewards.constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const LIDO_ETH_DESTRIBUTED_EVENT =
'event ETHDistributed(uint256 indexed reportTimestamp, uint256 preCLBalance, uint256 postCLBalance, uint256 withdrawalsWithdrawn, uint256 executionLayerRewardsWithdrawn, uint256 postBufferedEther)';
export const LIDO_EL_REWARDS_RECEIVED_EVENT = 'event ELRewardsReceived(uint256 amount)';
export const LIDO_WITHDRAWALS_RECEIVED_EVENT = 'event WithdrawalsReceived(uint256 amount)';
export const LIDO_TOKEN_REBASED_EVENT =
'event TokenRebased(uint256 indexed reportTimestamp, uint256 timeElapsed, uint256 preTotalShares, uint256 preTotalEther, uint256 postTotalShares, uint256 postTotalEther, uint256 sharesMintedAsFees)';
11 changes: 11 additions & 0 deletions src/events/rewards/rewards.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { RewardsService } from './rewards.service';
import { JobModule } from '../../common/job';
import { RewardsStorageModule } from '../../storage';

@Module({
imports: [JobModule, RewardsStorageModule],
providers: [RewardsService],
exports: [RewardsService],
})
export class RewardsModule {}
153 changes: 153 additions & 0 deletions src/events/rewards/rewards.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { Inject, Injectable } from '@nestjs/common';
import { SECONDS_PER_SLOT } from '../../common/genesis-time';
import { SimpleFallbackJsonRpcBatchProvider } from '@lido-nestjs/execution';
import { Lido, LIDO_CONTRACT_TOKEN } from '@lido-nestjs/contracts';
import { Interface } from 'ethers';
import {
LIDO_EL_REWARDS_RECEIVED_EVENT,
LIDO_ETH_DESTRIBUTED_EVENT,
LIDO_TOKEN_REBASED_EVENT,
LIDO_WITHDRAWALS_RECEIVED_EVENT,
} from './rewards.constants';
import { BigNumber } from '@ethersproject/bignumber';
import { LOGGER_PROVIDER, LoggerService } from '../../common/logger';
import { ConfigService } from '../../common/config';
import { RewardsStorageService } from '../../storage';

@Injectable()
export class RewardsService {
constructor(
@Inject(LOGGER_PROVIDER) protected readonly logger: LoggerService,
@Inject(LIDO_CONTRACT_TOKEN) protected readonly contractLido: Lido,
protected readonly rewardsStorage: RewardsStorageService,
protected readonly configService: ConfigService,
protected readonly provider: SimpleFallbackJsonRpcBatchProvider,
) {}

/**
* Initializes the job
*/
public async initialize(): Promise<void> {
await this.updateRewards();

// getting total rewards per frame starts from TokenRebased event because it contains
// how much time is gone from last report (can be more the 1-day in rare critical situation)
const tokenRebased = this.contractLido.filters.TokenRebased();
this.provider.on(tokenRebased, () => {
this.logger.debug('event TokenRebased triggered');
this.updateRewards();
});
this.logger.log('Service initialized', { service: 'rewards event' });
}

protected async updateRewards(): Promise<void> {
const rewardsPerFrame = await this.getLastTotalRewardsPerFrame();
if (rewardsPerFrame) {
this.rewardsStorage.setRewardsPerFrame(rewardsPerFrame);
}
}

public async getLastTotalRewardsPerFrame(): Promise<BigNumber | null> {
const framesFromLastReport = await this.getFramesFromLastReport();
if (framesFromLastReport === null) {
return null;
}

const { blockNumber, frames } = framesFromLastReport;

const { preCLBalance, postCLBalance } = await this.getEthDistributed(blockNumber);
const elRewards = (await this.getElRewards(blockNumber)) ?? BigNumber.from(0);
const withdrawal = (await this.getWithdrawalsReceived(blockNumber)) ?? BigNumber.from(0);

const clValidatorsBalanceDiff = postCLBalance.sub(preCLBalance);
const withdrawalsReceived = withdrawal ?? BigNumber.from(0);
const clRewards = clValidatorsBalanceDiff.add(withdrawalsReceived);

return clRewards.add(elRewards).div(frames);
}

protected async get48HoursAgoBlock() {
const currentBlock = await this.provider.getBlockNumber();
return currentBlock - Math.ceil((2 * 24 * 60 * 60) / SECONDS_PER_SLOT);
}

protected async getElRewards(fromBlock: number): Promise<BigNumber> {
const res = this.contractLido.filters.ELRewardsReceived();
const logs = await this.provider.getLogs({
topics: res.topics,
toBlock: 'latest',
fromBlock,
address: res.address,
});
const lastLog = logs[logs.length - 1];
const parser = new Interface([LIDO_EL_REWARDS_RECEIVED_EVENT]);
const parsedData = parser.parseLog(lastLog);
return BigNumber.from(parsedData.args.getValue('amount'));
}

protected async getEthDistributed(fromBlock: number): Promise<{
preCLBalance: BigNumber;
postCLBalance: BigNumber;
}> {
const res = this.contractLido.filters.ETHDistributed();
const logs = await this.provider.getLogs({
topics: res.topics,
toBlock: 'latest',
fromBlock,
address: res.address,
});

const lastLog = logs[logs.length - 1];
const parser = new Interface([LIDO_ETH_DESTRIBUTED_EVENT]);
const parsedData = parser.parseLog(lastLog);

const preCLBalance = BigNumber.from(parsedData.args.getValue('preCLBalance'));
const postCLBalance = BigNumber.from(parsedData.args.getValue('postCLBalance'));
return { preCLBalance, postCLBalance };
}

protected async getWithdrawalsReceived(fromBlock: number): Promise<BigNumber> {
const res = this.contractLido.filters.WithdrawalsReceived();
const logs = await this.provider.getLogs({
topics: res.topics,
toBlock: 'latest',
fromBlock,
address: res.address,
});

const lastLog = logs[logs.length - 1];
const parser = new Interface([LIDO_WITHDRAWALS_RECEIVED_EVENT]);
const parsedData = parser.parseLog(lastLog);

return BigNumber.from(parsedData.args.getValue('amount'));
}

// reports can be skipped, so we need timeElapsed (time from last report)
protected async getFramesFromLastReport(): Promise<{
blockNumber: number;
frames: BigNumber;
} | null> {
const last48HoursAgoBlock = await this.get48HoursAgoBlock();

const res = this.contractLido.filters.TokenRebased();
const logs = await this.provider.getLogs({
topics: res.topics,
toBlock: 'latest',
fromBlock: last48HoursAgoBlock,
address: res.address,
});

if (logs.length === 0) {
return null;
}

const lastLog = logs[logs.length - 1];
const parser = new Interface([LIDO_TOKEN_REBASED_EVENT]);
const parsedData = parser.parseLog(lastLog);

return {
blockNumber: lastLog.blockNumber,
frames: BigNumber.from(parsedData.args.getValue('timeElapsed')).div(24 * 60 * 60),
};
}
}
3 changes: 1 addition & 2 deletions src/http/http.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,10 @@ import { METRICS_URL } from 'common/prometheus';
import { SWAGGER_URL } from './common/swagger';
import { ThrottlerModule, ThrottlerBehindProxyGuard } from './common/throttler';
import { LoggerMiddleware, MetricsMiddleware } from './common/middleware';
import { CacheModule } from './common/cache';
import { CacheModule, CacheControlHeadersInterceptor } from './common/cache';
import { RequestTimeModule } from './request-time';
import { NFTModule } from './nft';
import { EstimateModule } from './estimate';
import { CacheControlHeadersInterceptor } from './common/cache/cache-control-headers.interceptor';

@Module({
imports: [RequestTimeModule, NFTModule, EstimateModule, CacheModule, ThrottlerModule],
Expand Down
1 change: 0 additions & 1 deletion src/http/nft/nft.svg.utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { formatUnits } from 'ethers';

import { glyphNumbers, simpleNumbers, crystallMap } from './assets/nft.parts';
import { SVG_ID_LENGTH } from './nft.constants';

Expand Down
1 change: 0 additions & 1 deletion src/http/nft/nft.utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { formatUnits } from 'ethers';

import { MAX_AMOUNT_IN_ETH, MIN_AMOUNT_IN_WEI } from './nft.constants';

export const convertFromWei = (amountInWei: string, prefix?: string): string => {
Expand Down
39 changes: 39 additions & 0 deletions src/http/request-time/dto/request-time-v2.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { ApiProperty } from '@nestjs/swagger';

export class RequestTimeV2Dto {
@ApiProperty({
example: 5,
description: 'Maximum waiting ms',
})
ms: number;

@ApiProperty({
example: 0,
description: 'Queue ETH last update timestamp',
})
stethLastUpdate: number;

@ApiProperty({
example: 0,
description: 'Validators last update timestamp',
})
validatorsLastUpdate: number;

@ApiProperty({
example: 10,
description: 'Queue requests count',
})
requests: number;

@ApiProperty({
example: 10,
description: 'Withdrawal At',
})
withdrawalAt: string;

@ApiProperty({
example: 0,
description: 'Queue steth amount',
})
steth: string;
}
2 changes: 2 additions & 0 deletions src/http/request-time/request-time.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ export const MAX_EFFECTIVE_BALANCE = parseEther('32'); // ETH
export const MAX_WITHDRAWALS_PER_PAYLOAD = 2 ** 4;
// const MIN_VALIDATOR_WITHDRAWABILITY_DELAY = 256;
export const MAX_VALID_NUMBER = Number.MAX_SAFE_INTEGER;

export const GAP_AFTER_REPORT = 30 * 60 * 1000; // 30 mins
Loading