diff --git a/src/app.controller.ts b/src/app.controller.ts index 29af1e7..1b82680 100644 --- a/src/app.controller.ts +++ b/src/app.controller.ts @@ -15,7 +15,7 @@ import { ConfigService } from '@nestjs/config'; import {SkipThrottle} from "@nestjs/throttler"; import {AddCommentDto, GetCommentsDto} from "./dto/comment.dto"; import {AppService} from "./app.service"; -import {GetTokenBalancesDto, GetTokensDto} from "./dto/token.dto"; +import {GetTokenBalancesDto, GetTokensDto, GetTokenWinnersDto} from "./dto/token.dto"; import {GetTradesDto} from "./dto/trade.dto"; import {AddUserDto} from "./dto/user.dto"; import {UserService} from "./user/user.service"; @@ -55,6 +55,11 @@ export class AppController { return this.appService.getTokenBalances(dto) } + @Get('/token/winners') + getWinners(@Query() dto: GetTokenWinnersDto) { + return this.appService.getTokenWinners(dto) + } + @Get('/comments') getComments(@Query() dto: GetCommentsDto) { return this.appService.getComments(dto) diff --git a/src/app.service.ts b/src/app.service.ts index e3710a9..72a9126 100644 --- a/src/app.service.ts +++ b/src/app.service.ts @@ -1,8 +1,8 @@ import {Injectable, Logger} from '@nestjs/common'; import {Between, DataSource} from "typeorm"; -import {Comment, Token, TokenBalance, UserAccount} from "./entities"; +import {Comment, Token, TokenBalance, TokenWinner, UserAccount} from "./entities"; import {AddCommentDto, GetCommentsDto} from "./dto/comment.dto"; -import {GetTokenBalancesDto, GetTokensDto} from "./dto/token.dto"; +import {GetTokenBalancesDto, GetTokensDto, GetTokenWinnersDto} from "./dto/token.dto"; import {Trade} from "./entities"; import {GetTradesDto} from "./dto/trade.dto"; import {UserService} from "./user/user.service"; @@ -54,7 +54,6 @@ export class AppService { async getTokenBalances(dto: GetTokenBalancesDto){ const { offset = 0, limit = 100 } = dto - return this.dataSource.manager.find(TokenBalance, { where: { token: { @@ -69,6 +68,16 @@ export class AppService { }) } + async getTokenWinners(dto: GetTokenWinnersDto) { + return await this.dataSource.manager.find(TokenWinner, { + order: { + timestamp: 'desc' + }, + take: dto.limit, + skip: dto.offset, + }) + } + async getTrades(dto: GetTradesDto){ return await this.dataSource.manager.find(Trade, { where: { diff --git a/src/dto/token.dto.ts b/src/dto/token.dto.ts index f05fc32..0e31b78 100644 --- a/src/dto/token.dto.ts +++ b/src/dto/token.dto.ts @@ -39,3 +39,17 @@ export class GetTokenBalancesDto { @IsString() offset: number; } + +export class GetTokenWinnersDto { + @ApiProperty({ type: Number, required: false, default: '100' }) + // @Transform((limit) => limit.value.toNumber()) + @Type(() => String) + @IsString() + limit: number; + + @ApiProperty({ type: Number, required: false, default: '0' }) + // @Transform((offset) => offset.value.toNumber()) + @Type(() => String) + @IsString() + offset: number; +} diff --git a/src/entities/index.ts b/src/entities/index.ts index 859fa6f..98389f8 100644 --- a/src/entities/index.ts +++ b/src/entities/index.ts @@ -4,6 +4,7 @@ import { IndexerState } from './indexer.state.entity'; import { Trade } from './trade.entity'; import { Comment } from './comment.entity'; import { TokenBalance } from './token.balances.entity'; +import { TokenWinner } from './token.winner.entity'; const entities = [ UserAccount, @@ -11,8 +12,9 @@ const entities = [ IndexerState, Trade, Comment, - TokenBalance + TokenBalance, + TokenWinner ]; -export { UserAccount, Token, IndexerState, Trade, Comment, TokenBalance }; +export { UserAccount, Token, IndexerState, Trade, Comment, TokenBalance, TokenWinner }; export default entities; diff --git a/src/entities/token.winner.entity.ts b/src/entities/token.winner.entity.ts new file mode 100644 index 0000000..dc93d91 --- /dev/null +++ b/src/entities/token.winner.entity.ts @@ -0,0 +1,37 @@ +import { + Column, + CreateDateColumn, + Entity, + PrimaryGeneratedColumn, + ManyToOne +} from 'typeorm'; +import { ApiProperty } from '@nestjs/swagger'; +import {Token} from "./token.entity"; + +@Entity({ name: 'token_winners' }) +export class TokenWinner { + @ApiProperty() + @PrimaryGeneratedColumn('uuid') + id: string; + + @ManyToOne(() => Token, { + eager: true + }) + token: Token + + @ApiProperty() + @Column({ type: 'decimal', default: 0 }) + timestamp: string; + + @ApiProperty() + @Column() + txnHash: string; + + @ApiProperty() + @Column({ type: 'integer' }) + blockNumber: number; + + @ApiProperty() + @CreateDateColumn({ name: 'createdAt' }) + createdAt: Date; +} diff --git a/src/indexer/indexer.service.ts b/src/indexer/indexer.service.ts index 771f73f..4472c93 100644 --- a/src/indexer/indexer.service.ts +++ b/src/indexer/indexer.service.ts @@ -3,7 +3,7 @@ import {Contract, ContractAbi, EventLog, Web3} from "web3"; import {TokenMetadata, TradeEventLog, TradeType} from "../types"; import axios from "axios"; import process from "process"; -import {IndexerState, Token, TokenBalance, Trade} from "../entities"; +import {IndexerState, Token, TokenBalance, TokenWinner, Trade} from "../entities"; import {ConfigService} from "@nestjs/config"; import {UserService} from "../user/user.service"; import {DataSource} from "typeorm"; @@ -83,6 +83,42 @@ export class IndexerService { } } + private async processSetWinnerEvents(events: EventLog[]) { + for(const event of events) { + const txnHash = event.transactionHash.toLowerCase() + const blockNumber = Number(event.blockNumber) + const values = event.returnValues + const winnerAddress = (values['winner'] as string).toLowerCase() + const timestamp = String(values['timestamp'] as bigint) + + const existedWinner = await this.dataSource.manager.findOne(TokenWinner, { + where: { + token: { + address: winnerAddress + }, + timestamp + } + }) + + if(!existedWinner) { + const token = await this.appService.getTokenByAddress(winnerAddress) + if(!token) { + this.logger.error(`Winner token entry not found in database, winnerAddress=${winnerAddress} , exit`) + process.exit(1) + } + await this.dataSource.manager.insert(TokenWinner, { + token, + timestamp, + txnHash, + blockNumber + }) + this.logger.log(`Added new token winner=${winnerAddress}, timestamp=${timestamp}`) + } else { + this.logger.warn(`Token winner=${winnerAddress}, timestamp=${timestamp} already exists, skip`) + } + } + } + private async processTradeEvents(events: TradeEventLog[]) { for(const event of events) { const { type, data } = event @@ -208,6 +244,10 @@ export class IndexerService { } if(toBlock - fromBlock >= 1) { + const setWinnerEvents = await this.tokenFactoryContract.getPastEvents('allEvents', { + fromBlock, toBlock, topics: [ this.web3.utils.sha3('SetWinner(address,uint256)')], + }) as EventLog[]; + const tokenCreatedEvents = await this.tokenFactoryContract.getPastEvents('allEvents', { fromBlock, toBlock, @@ -289,9 +329,10 @@ export class IndexerService { this.logger.log(`Create token: address=${tokenAddress}, name=${name}, symbol=${symbol}, uri=${uri}, creator=${creatorAddress}, txnHash=${txnHash}`); } + await this.processSetWinnerEvents(setWinnerEvents) await this.processTradeEvents(tradeEvents) - this.logger.log(`[${fromBlock}-${toBlock}] (${((toBlock - fromBlock + 1))} blocks), new tokens=${tokenCreatedEvents.length}, trade=${[...buyEvents, ...sellEvents].length} (buy=${buyEvents.length}, sell=${sellEvents.length})`) + this.logger.log(`[${fromBlock}-${toBlock}] (${((toBlock - fromBlock + 1))} blocks), new tokens=${tokenCreatedEvents.length}, trade=${[...buyEvents, ...sellEvents].length} (buy=${buyEvents.length}, sell=${sellEvents.length}), setWinner=${setWinnerEvents.length}`) } else { // Wait for blockchain toBlock = fromBlockParam - 1