Skip to content

Commit

Permalink
Merge branch 'start_competition_update'
Browse files Browse the repository at this point in the history
# Conflicts:
#	src/indexer/indexer.service.ts
  • Loading branch information
ArtemKolodko committed Dec 16, 2024
2 parents 5c6b268 + d4536e3 commit 2c22c01
Show file tree
Hide file tree
Showing 7 changed files with 155 additions and 30 deletions.
12 changes: 12 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"dotenv": "^16.4.5",
"ethers": "^6.13.2",
"moment": "^2.30.1",
"moment-timezone": "^0.5.46",
"nest-web3": "^1.1.3",
"pg": "^8.12.0",
"prom-client": "^14.2.0",
Expand Down
6 changes: 5 additions & 1 deletion src/app.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export class AppService {
}

async getTokens(dto: GetTokensDto){
const { search, offset, limit, isWinner, sortingField, sortingOrder } = dto
const { search, offset, limit, isWinner, sortingField, sortingOrder, competitionId } = dto
const query = this.dataSource.getRepository(Token)
.createQueryBuilder('token')
.leftJoinAndSelect('token.user', 'user')
Expand All @@ -59,6 +59,10 @@ export class AppService {
.orWhere('LOWER(token.txnHash) = LOWER(:txnHash)', { txnHash: search })
}

if(competitionId) {
query.where('competition.competitionId = :competitionId', { competitionId })
}

if(typeof isWinner !== 'undefined') {
query.andWhere({ isWinner })
}
Expand Down
2 changes: 2 additions & 0 deletions src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,6 @@ export default () => ({
GOOGLE_CLOUD_CONFIG: getGoogleCloudConfig(),
SERVICE_PRIVATE_KEY: process.env.SERVICE_PRIVATE_KEY || '',
ADMIN_API_KEY: process.env.ADMIN_API_KEY || '',
COMPETITION_DAYS_INTERVAL: parseInt(process.env.COMPETITION_DAYS_INTERVAL || '7'),
COMPETITION_COLLATERAL_THRESHOLD: parseInt(process.env.COMPETITION_COLLATERAL_THRESHOLD || '420000'), // in ONE tokens
});
10 changes: 8 additions & 2 deletions src/dto/token.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,23 @@ export class GetTokensDto {
@ApiProperty({ type: Boolean, required: false })
isWinner?: boolean;

@ApiProperty({ type: Number, required: false })
@IsOptional()
competitionId?: number;

@ApiProperty({ type: Number, required: false, default: '100' })
// @Transform((limit) => limit.value.toNumber())
@Type(() => String)
@IsString()
limit: number;
@IsOptional()
limit?: number;

@ApiProperty({ type: Number, required: false, default: '0' })
// @Transform((offset) => offset.value.toNumber())
@Type(() => String)
@IsString()
offset: number;
@IsOptional()
offset?: number;

@ApiProperty({ enum: SortField, required: false })
@IsOptional()
Expand Down
150 changes: 125 additions & 25 deletions src/indexer/indexer.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,14 @@ import {UserService} from "../user/user.service";
import {DataSource, EntityManager} from "typeorm";
import * as TokenFactoryABI from "../abi/TokenFactory.json";
import {AppService} from "../app.service";
import {ZeroAddress} from "ethers";
import {parseUnits, ZeroAddress} from "ethers";
import Decimal from "decimal.js";
import {Cron, CronExpression} from "@nestjs/schedule";
import * as moment from "moment-timezone";
import {Moment} from "moment";
import {getRandomNumberFromInterval} from "../utils";
import {Cron, CronExpression, SchedulerRegistry} from "@nestjs/schedule";

const CompetitionScheduleCheckJob = 'competition_schedule_check'

@Injectable()
export class IndexerService {
Expand All @@ -35,6 +40,7 @@ export class IndexerService {
private userService: UserService,
private appService: AppService,
private dataSource: DataSource,
private schedulerRegistry: SchedulerRegistry
) {
const rpcUrl = configService.get('RPC_URL')
const contractAddress = configService.get('TOKEN_FACTORY_ADDRESS')
Expand Down Expand Up @@ -622,7 +628,123 @@ export class IndexerService {
return sendTxn.transactionHash.toString()
}

private async startNewCompetition() {
@Cron(CronExpression.EVERY_MINUTE, {
name: CompetitionScheduleCheckJob
})
async scheduleNextCompetition() {
const schedulerJob = this.schedulerRegistry.getCronJob(CompetitionScheduleCheckJob)
if(schedulerJob) {
schedulerJob.stop()
}

const daysInterval = this.configService.get<number>('COMPETITION_DAYS_INTERVAL')
const timeZone = 'America/Los_Angeles'
let nextCompetitionDate: Moment
// Competition starts every 7 day at a random time within one hour around midnight

try {
const [prevCompetition] = await this.appService.getCompetitions({ limit: 1 })
if(prevCompetition) {
const { timestampStart, isCompleted } = prevCompetition

const lastCompetitionDeltaMs = moment().diff(moment(timestampStart * 1000))
// Interval was exceeded
const isIntervalExceeded = lastCompetitionDeltaMs > daysInterval * 24 * 60 * 60 * 1000

if(isCompleted || isIntervalExceeded) {
// Start new competition tomorrow at 00:00
nextCompetitionDate = moment()
.tz(timeZone)
.add(1, 'days')
.startOf('day')
} else {
// Start new competition in 7 days at 00:00
nextCompetitionDate = moment(timestampStart * 1000)
.tz(timeZone)
.add(daysInterval, 'days')
.startOf('day')
}
} else {
this.logger.error(`Previous competition not found in database. New competition will be created.`)
// Start new competition tomorrow at 00:00
nextCompetitionDate = moment()
.tz(timeZone)
.add(1, 'days')
.startOf('day')
}

// nextCompetitionDate = moment().add(60, 'seconds')

if(nextCompetitionDate.diff(moment(), 'minutes') < 1) {
// Random is important otherwise they just make a new token 1 second before ending, and pumping it with a lot of ONE
const randomMinutesNumber = getRandomNumberFromInterval(1, 59)
nextCompetitionDate = nextCompetitionDate.add(randomMinutesNumber, 'minutes')

this.logger.log(`Next competition scheduled at ${
nextCompetitionDate.format('YYYY-MM-DD HH:mm:ss')
}, ${timeZone} timezone`)
await this.sleep(nextCompetitionDate.diff(moment(), 'milliseconds'))
await this.initiateNewCompetition()
}
} catch (e) {
this.logger.error(`Failed to schedule next competition start:`, e)
} finally {
if(schedulerJob) {
schedulerJob.start()
}
}
}

async initiateNewCompetition() {
const attemptsCount = 3
const tokenCollateralThreshold = BigInt(parseUnits(
this.configService.get<number>('COMPETITION_COLLATERAL_THRESHOLD').toString(), 18
))

for(let i = 0; i < attemptsCount; i++) {
try {
let isCollateralThresholdReached = false
const competitionId = await this.getCompetitionId()
this.logger.log(`Current competition id=${competitionId}`)
const tokens = await this.appService.getTokens({
competitionId: Number(competitionId),
limit: 10000
})

this.logger.log(`Checking tokens (count=${tokens.length}) for minimum collateral=${tokenCollateralThreshold} wei...`)
for(const token of tokens) {
const collateral = await this.tokenFactoryContract.methods
.collateralById(competitionId, token.address)
.call() as bigint

if(collateral >= tokenCollateralThreshold) {
isCollateralThresholdReached = true
this.logger.log(`Token address=${token} received ${collateral} wei in collateral`)
break;
}
}

if(isCollateralThresholdReached) {
this.logger.log(`Initiate new competition...`)
const newCompetitionTxHash = await this.callStartNewCompetitionTx()
this.logger.log(`New competition txHash: ${newCompetitionTxHash}`)
await this.sleep(5000)
const newCompetitionId = await this.getCompetitionId()
this.logger.log(`Started new competition id=${newCompetitionId}; calling token winner...`)
const setWinnerHash = await this.setWinnerByCompetitionId(competitionId)
this.logger.log(`setWinnerByCompetitionId called, txnHash=${setWinnerHash}`)
} else {
this.logger.log(`No tokens reached minimum collateral=${tokenCollateralThreshold} wei. Waiting for the next iteration.`)
}
break;
} catch (e) {
this.logger.warn(`Failed to send setWinner transaction, attempt: ${(i + 1)} / ${attemptsCount}:`, e)
await this.sleep(10000)
}
}
}

private async callStartNewCompetitionTx() {
const gasFees = await this.tokenFactoryContract.methods
.startNewCompetition()
.estimateGas({ from: this.accountAddress });
Expand Down Expand Up @@ -651,26 +773,4 @@ export class IndexerService {
.currentCompetitionId()
.call() as bigint
}

// @Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT, {
// timeZone: 'America/Los_Angeles'
// })
// async callSetWinner() {
// for(let i = 0; i < 3; i++) {
// try {
// const currentCompetitionId = await this.getCompetitionId()
// this.logger.log(`Current competition id=${currentCompetitionId}`)
// await this.startNewCompetition()
// await this.sleep(4000)
// const newCompetitionId = await this.getCompetitionId()
// this.logger.log(`Started new competition id=${newCompetitionId}`)
// const setWinnerHash = await this.setWinnerByCompetitionId(currentCompetitionId)
// this.logger.log(`New setWinner is called, txnHash=${setWinnerHash}`)
// break;
// } catch (e) {
// this.logger.warn(`Failed to send setWinner transaction, attempt: ${(i + 1)} / 3:`, e)
// await this.sleep(4000)
// }
// }
// }
}
4 changes: 2 additions & 2 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const randomIntFromInterval = (min: number, max: number) => { // min and max included
export const getRandomNumberFromInterval = (min: number, max: number) => { // min and max included
return Math.floor(Math.random() * (max - min + 1) + min);
}

export const generateNonce = () => {
return randomIntFromInterval(1, 1_000_000_000)
return getRandomNumberFromInterval(1, 1_000_000_000)
}

0 comments on commit 2c22c01

Please sign in to comment.