Skip to content

Commit 2c22c01

Browse files
committed
Merge branch 'start_competition_update'
# Conflicts: # src/indexer/indexer.service.ts
2 parents 5c6b268 + d4536e3 commit 2c22c01

File tree

7 files changed

+155
-30
lines changed

7 files changed

+155
-30
lines changed

package-lock.json

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"dotenv": "^16.4.5",
4545
"ethers": "^6.13.2",
4646
"moment": "^2.30.1",
47+
"moment-timezone": "^0.5.46",
4748
"nest-web3": "^1.1.3",
4849
"pg": "^8.12.0",
4950
"prom-client": "^14.2.0",

src/app.service.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export class AppService {
4242
}
4343

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

62+
if(competitionId) {
63+
query.where('competition.competitionId = :competitionId', { competitionId })
64+
}
65+
6266
if(typeof isWinner !== 'undefined') {
6367
query.andWhere({ isWinner })
6468
}

src/config/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,6 @@ export default () => ({
3838
GOOGLE_CLOUD_CONFIG: getGoogleCloudConfig(),
3939
SERVICE_PRIVATE_KEY: process.env.SERVICE_PRIVATE_KEY || '',
4040
ADMIN_API_KEY: process.env.ADMIN_API_KEY || '',
41+
COMPETITION_DAYS_INTERVAL: parseInt(process.env.COMPETITION_DAYS_INTERVAL || '7'),
42+
COMPETITION_COLLATERAL_THRESHOLD: parseInt(process.env.COMPETITION_COLLATERAL_THRESHOLD || '420000'), // in ONE tokens
4143
});

src/dto/token.dto.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,23 @@ export class GetTokensDto {
1818
@ApiProperty({ type: Boolean, required: false })
1919
isWinner?: boolean;
2020

21+
@ApiProperty({ type: Number, required: false })
22+
@IsOptional()
23+
competitionId?: number;
24+
2125
@ApiProperty({ type: Number, required: false, default: '100' })
2226
// @Transform((limit) => limit.value.toNumber())
2327
@Type(() => String)
2428
@IsString()
25-
limit: number;
29+
@IsOptional()
30+
limit?: number;
2631

2732
@ApiProperty({ type: Number, required: false, default: '0' })
2833
// @Transform((offset) => offset.value.toNumber())
2934
@Type(() => String)
3035
@IsString()
31-
offset: number;
36+
@IsOptional()
37+
offset?: number;
3238

3339
@ApiProperty({ enum: SortField, required: false })
3440
@IsOptional()

src/indexer/indexer.service.ts

Lines changed: 125 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,14 @@ import {UserService} from "../user/user.service";
1717
import {DataSource, EntityManager} from "typeorm";
1818
import * as TokenFactoryABI from "../abi/TokenFactory.json";
1919
import {AppService} from "../app.service";
20-
import {ZeroAddress} from "ethers";
20+
import {parseUnits, ZeroAddress} from "ethers";
2121
import Decimal from "decimal.js";
22-
import {Cron, CronExpression} from "@nestjs/schedule";
22+
import * as moment from "moment-timezone";
23+
import {Moment} from "moment";
24+
import {getRandomNumberFromInterval} from "../utils";
25+
import {Cron, CronExpression, SchedulerRegistry} from "@nestjs/schedule";
26+
27+
const CompetitionScheduleCheckJob = 'competition_schedule_check'
2328

2429
@Injectable()
2530
export class IndexerService {
@@ -35,6 +40,7 @@ export class IndexerService {
3540
private userService: UserService,
3641
private appService: AppService,
3742
private dataSource: DataSource,
43+
private schedulerRegistry: SchedulerRegistry
3844
) {
3945
const rpcUrl = configService.get('RPC_URL')
4046
const contractAddress = configService.get('TOKEN_FACTORY_ADDRESS')
@@ -622,7 +628,123 @@ export class IndexerService {
622628
return sendTxn.transactionHash.toString()
623629
}
624630

625-
private async startNewCompetition() {
631+
@Cron(CronExpression.EVERY_MINUTE, {
632+
name: CompetitionScheduleCheckJob
633+
})
634+
async scheduleNextCompetition() {
635+
const schedulerJob = this.schedulerRegistry.getCronJob(CompetitionScheduleCheckJob)
636+
if(schedulerJob) {
637+
schedulerJob.stop()
638+
}
639+
640+
const daysInterval = this.configService.get<number>('COMPETITION_DAYS_INTERVAL')
641+
const timeZone = 'America/Los_Angeles'
642+
let nextCompetitionDate: Moment
643+
// Competition starts every 7 day at a random time within one hour around midnight
644+
645+
try {
646+
const [prevCompetition] = await this.appService.getCompetitions({ limit: 1 })
647+
if(prevCompetition) {
648+
const { timestampStart, isCompleted } = prevCompetition
649+
650+
const lastCompetitionDeltaMs = moment().diff(moment(timestampStart * 1000))
651+
// Interval was exceeded
652+
const isIntervalExceeded = lastCompetitionDeltaMs > daysInterval * 24 * 60 * 60 * 1000
653+
654+
if(isCompleted || isIntervalExceeded) {
655+
// Start new competition tomorrow at 00:00
656+
nextCompetitionDate = moment()
657+
.tz(timeZone)
658+
.add(1, 'days')
659+
.startOf('day')
660+
} else {
661+
// Start new competition in 7 days at 00:00
662+
nextCompetitionDate = moment(timestampStart * 1000)
663+
.tz(timeZone)
664+
.add(daysInterval, 'days')
665+
.startOf('day')
666+
}
667+
} else {
668+
this.logger.error(`Previous competition not found in database. New competition will be created.`)
669+
// Start new competition tomorrow at 00:00
670+
nextCompetitionDate = moment()
671+
.tz(timeZone)
672+
.add(1, 'days')
673+
.startOf('day')
674+
}
675+
676+
// nextCompetitionDate = moment().add(60, 'seconds')
677+
678+
if(nextCompetitionDate.diff(moment(), 'minutes') < 1) {
679+
// Random is important otherwise they just make a new token 1 second before ending, and pumping it with a lot of ONE
680+
const randomMinutesNumber = getRandomNumberFromInterval(1, 59)
681+
nextCompetitionDate = nextCompetitionDate.add(randomMinutesNumber, 'minutes')
682+
683+
this.logger.log(`Next competition scheduled at ${
684+
nextCompetitionDate.format('YYYY-MM-DD HH:mm:ss')
685+
}, ${timeZone} timezone`)
686+
await this.sleep(nextCompetitionDate.diff(moment(), 'milliseconds'))
687+
await this.initiateNewCompetition()
688+
}
689+
} catch (e) {
690+
this.logger.error(`Failed to schedule next competition start:`, e)
691+
} finally {
692+
if(schedulerJob) {
693+
schedulerJob.start()
694+
}
695+
}
696+
}
697+
698+
async initiateNewCompetition() {
699+
const attemptsCount = 3
700+
const tokenCollateralThreshold = BigInt(parseUnits(
701+
this.configService.get<number>('COMPETITION_COLLATERAL_THRESHOLD').toString(), 18
702+
))
703+
704+
for(let i = 0; i < attemptsCount; i++) {
705+
try {
706+
let isCollateralThresholdReached = false
707+
const competitionId = await this.getCompetitionId()
708+
this.logger.log(`Current competition id=${competitionId}`)
709+
const tokens = await this.appService.getTokens({
710+
competitionId: Number(competitionId),
711+
limit: 10000
712+
})
713+
714+
this.logger.log(`Checking tokens (count=${tokens.length}) for minimum collateral=${tokenCollateralThreshold} wei...`)
715+
for(const token of tokens) {
716+
const collateral = await this.tokenFactoryContract.methods
717+
.collateralById(competitionId, token.address)
718+
.call() as bigint
719+
720+
if(collateral >= tokenCollateralThreshold) {
721+
isCollateralThresholdReached = true
722+
this.logger.log(`Token address=${token} received ${collateral} wei in collateral`)
723+
break;
724+
}
725+
}
726+
727+
if(isCollateralThresholdReached) {
728+
this.logger.log(`Initiate new competition...`)
729+
const newCompetitionTxHash = await this.callStartNewCompetitionTx()
730+
this.logger.log(`New competition txHash: ${newCompetitionTxHash}`)
731+
await this.sleep(5000)
732+
const newCompetitionId = await this.getCompetitionId()
733+
this.logger.log(`Started new competition id=${newCompetitionId}; calling token winner...`)
734+
const setWinnerHash = await this.setWinnerByCompetitionId(competitionId)
735+
this.logger.log(`setWinnerByCompetitionId called, txnHash=${setWinnerHash}`)
736+
} else {
737+
this.logger.log(`No tokens reached minimum collateral=${tokenCollateralThreshold} wei. Waiting for the next iteration.`)
738+
}
739+
break;
740+
} catch (e) {
741+
this.logger.warn(`Failed to send setWinner transaction, attempt: ${(i + 1)} / ${attemptsCount}:`, e)
742+
await this.sleep(10000)
743+
}
744+
}
745+
}
746+
747+
private async callStartNewCompetitionTx() {
626748
const gasFees = await this.tokenFactoryContract.methods
627749
.startNewCompetition()
628750
.estimateGas({ from: this.accountAddress });
@@ -651,26 +773,4 @@ export class IndexerService {
651773
.currentCompetitionId()
652774
.call() as bigint
653775
}
654-
655-
// @Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT, {
656-
// timeZone: 'America/Los_Angeles'
657-
// })
658-
// async callSetWinner() {
659-
// for(let i = 0; i < 3; i++) {
660-
// try {
661-
// const currentCompetitionId = await this.getCompetitionId()
662-
// this.logger.log(`Current competition id=${currentCompetitionId}`)
663-
// await this.startNewCompetition()
664-
// await this.sleep(4000)
665-
// const newCompetitionId = await this.getCompetitionId()
666-
// this.logger.log(`Started new competition id=${newCompetitionId}`)
667-
// const setWinnerHash = await this.setWinnerByCompetitionId(currentCompetitionId)
668-
// this.logger.log(`New setWinner is called, txnHash=${setWinnerHash}`)
669-
// break;
670-
// } catch (e) {
671-
// this.logger.warn(`Failed to send setWinner transaction, attempt: ${(i + 1)} / 3:`, e)
672-
// await this.sleep(4000)
673-
// }
674-
// }
675-
// }
676776
}

src/utils/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
const randomIntFromInterval = (min: number, max: number) => { // min and max included
1+
export const getRandomNumberFromInterval = (min: number, max: number) => { // min and max included
22
return Math.floor(Math.random() * (max - min + 1) + min);
33
}
44

55
export const generateNonce = () => {
6-
return randomIntFromInterval(1, 1_000_000_000)
6+
return getRandomNumberFromInterval(1, 1_000_000_000)
77
}

0 commit comments

Comments
 (0)