@@ -17,9 +17,14 @@ import {UserService} from "../user/user.service";
1717import { DataSource , EntityManager } from "typeorm" ;
1818import * as TokenFactoryABI from "../abi/TokenFactory.json" ;
1919import { AppService } from "../app.service" ;
20- import { ZeroAddress } from "ethers" ;
20+ import { parseUnits , ZeroAddress } from "ethers" ;
2121import 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 ( )
2530export 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}
0 commit comments