diff --git a/src/app.controller.ts b/src/app.controller.ts index 7494c6e..d31d0e6 100644 --- a/src/app.controller.ts +++ b/src/app.controller.ts @@ -86,8 +86,8 @@ export class AppController { } @Get('/user/:address') - async getUserByAddress(@Param('address') address: string) { - const user = await this.userService.getUserByAddress(address) + async getUserByAddress(@Param('address') userAddress: string) { + const user = await this.userService.getUserByAddress(userAddress) if(!user) { throw new NotFoundException('User not found') } @@ -95,8 +95,8 @@ export class AppController { } @Get('/user/:address/tokens/created') - async getUserTokensCreated(@Param('address') address: string) { - return await this.userService.getTokensCreated(address) + async getUserTokensCreated(@Param('address') userAddress: string) { + return await this.userService.getTokensCreated(userAddress) } @Post('/uploadImage') diff --git a/src/app.service.ts b/src/app.service.ts index a149c11..8ebbf04 100644 --- a/src/app.service.ts +++ b/src/app.service.ts @@ -1,10 +1,9 @@ import {Injectable, Logger} from '@nestjs/common'; import {Between, DataSource} from "typeorm"; -import {Comment, Token} from "./entities"; +import {Comment, Token, TokenBalance, UserAccount} from "./entities"; import {AddCommentDto, GetCommentsDto} from "./dto/comment.dto"; import {GetTokensDto} from "./dto/token.dto"; import {Trade} from "./entities"; -import * as moment from "moment"; import {GetTradesDto} from "./dto/trade.dto"; import {UserService} from "./user/user.service"; @@ -96,46 +95,23 @@ export class AppService { return identifiers[0].id } - // @Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT) - // async handleCron() { - // const totalAttempts = 3 - // for(let i = 0; i < totalAttempts; i++) { - // try { - // const winnerTokenId = await this.getDailyWinnerTokenId() - // this.logger.log(`Daily winner tokenId: ${winnerTokenId}`) - // break; - // } catch (e) { - // this.logger.error(`Failed to get daily winner, attempt: ${i+1}/${totalAttempts}`, e) - // } - // } - // } - - async getDailyWinnerTokenId(): Promise { - const dateStart = moment().subtract(1, 'days').startOf('day') - const dateEnd = moment().subtract(1, 'day').endOf('day') - - const tokensMap = new Map() - const tokens = await this.getTokens({ offset: 0, limit: 1000 }) - for(const token of tokens) { - const tokenSwaps = await this.dataSource.manager.find(Trade, { - where: { - token: { - id: token.id - }, - createdAt: Between(dateStart.toDate(), dateEnd.toDate()) + async getTokenHolder(tokenAddress: string, userAddress: string) { + return await this.dataSource.manager.findOne(TokenBalance, { + where: { + token: { + address: tokenAddress.toLowerCase() + }, + user: { + address: userAddress.toLowerCase() } - }) - const totalAmount = tokenSwaps.reduce((acc, item) => acc += BigInt(item.amountOut), 0n) - tokensMap.set(token.id, totalAmount) - } - const sortedMapArray = ([...tokensMap.entries()] - .sort(([aKey, aValue], [bKey, bValue]) => { - return aValue - bValue > 0 ? -1 : 1 - })); - if(sortedMapArray.length > 0) { - const [winnerTokenId] = sortedMapArray[0] - return winnerTokenId - } - return null + } + }) + } + + async createTokenHolder(token: Token, user: UserAccount) { + return await this.dataSource.manager.insert(TokenBalance, { + token, + user, + }) } } diff --git a/src/entities/index.ts b/src/entities/index.ts index 89f9a27..859fa6f 100644 --- a/src/entities/index.ts +++ b/src/entities/index.ts @@ -3,14 +3,16 @@ import { Token } from './token.entity'; import { IndexerState } from './indexer.state.entity'; import { Trade } from './trade.entity'; import { Comment } from './comment.entity'; +import { TokenBalance } from './token.balances.entity'; const entities = [ UserAccount, Token, IndexerState, Trade, - Comment + Comment, + TokenBalance ]; -export { UserAccount, Token, IndexerState, Trade, Comment }; +export { UserAccount, Token, IndexerState, Trade, Comment, TokenBalance }; export default entities; diff --git a/src/entities/token.balances.entity.ts b/src/entities/token.balances.entity.ts new file mode 100644 index 0000000..e1973d3 --- /dev/null +++ b/src/entities/token.balances.entity.ts @@ -0,0 +1,39 @@ +import { + Column, + CreateDateColumn, + Entity, + PrimaryGeneratedColumn, + ManyToOne, UpdateDateColumn +} from 'typeorm'; +import { ApiProperty } from '@nestjs/swagger'; +import {Token} from "./token.entity"; +import {UserAccount} from "./user-account.entity"; + +@Entity({ name: 'token_balances' }) +export class TokenBalance { + @ApiProperty() + @PrimaryGeneratedColumn('uuid') + id: string; + + @ManyToOne(() => UserAccount, { + eager: true + }) + user: UserAccount + + @ManyToOne(() => Token, { + eager: true + }) + token: Token + + @ApiProperty() + @Column({ type: 'decimal', default: 0 }) + balance: string; + + @ApiProperty() + @UpdateDateColumn({ name: 'updateAt' }) + updatedAt: Date; + + @ApiProperty() + @CreateDateColumn({ name: 'createdAt' }) + createdAt: Date; +} diff --git a/src/entities/token.entity.ts b/src/entities/token.entity.ts index 8bad81a..e288dcb 100644 --- a/src/entities/token.entity.ts +++ b/src/entities/token.entity.ts @@ -40,6 +40,10 @@ export class Token { @Column() symbol: string; + @ApiProperty() + @Column({ type: 'decimal', default: 0 }) + totalSupply: string; + @ApiProperty() @Column() uri: string; diff --git a/src/indexer/indexer.service.ts b/src/indexer/indexer.service.ts index ab29abe..670f193 100644 --- a/src/indexer/indexer.service.ts +++ b/src/indexer/indexer.service.ts @@ -2,8 +2,8 @@ import {Injectable, Logger} from '@nestjs/common'; import {Contract, ContractAbi, EventLog, Web3} from "web3"; import {TokenMetadata, TradeEventLog, TradeType} from "../types"; import axios from "axios"; -import process from "node:process"; -import {IndexerState, Token, Trade} from "../entities"; +import process from "process"; +import {IndexerState, Token, TokenBalance, Trade} from "../entities"; import {ConfigService} from "@nestjs/config"; import {UserService} from "../user/user.service"; import {DataSource} from "typeorm"; @@ -15,6 +15,7 @@ export class IndexerService { private readonly logger = new Logger(IndexerService.name); private readonly web3: Web3 private readonly tokenFactoryContract: Contract + private readonly tokenContract: Contract private readonly blocksIndexingRange = 1000 constructor( @@ -79,13 +80,13 @@ export class IndexerService { private async processTradeEvents(events: TradeEventLog[]) { for(const event of events) { - const { data, type } = event + const { type, data } = event const txnHash = data.transactionHash.toLowerCase() const blockNumber = Number(data.blockNumber) const values = data.returnValues const tokenAddress = (values['token'] as string).toLowerCase() - const amountIn = String(values['amount0In'] as bigint) - const amountOut = String(values['amount0Out'] as bigint) + const amountIn = values['amount0In'] as bigint + const amountOut = values['amount0Out'] as bigint const fee = String(values['fee'] as bigint) const timestamp = Number(values['timestamp'] as bigint) @@ -108,6 +109,47 @@ export class IndexerService { process.exit(1) } + const tokenRepository = this.dataSource.manager.getRepository(Token) + const tokenHoldersRepository = this.dataSource.manager.getRepository(TokenBalance) + + if(type === 'buy') { + try { + let holder = await this.appService.getTokenHolder(tokenAddress, userAddress) + if(!holder) { + await this.appService.createTokenHolder(token, user) + holder = await this.appService.getTokenHolder(tokenAddress, userAddress) + } + holder.balance = String(BigInt(holder.balance) + amountOut) + await tokenHoldersRepository.save(holder) + + token.totalSupply = String(BigInt(token.totalSupply) + amountOut) + await tokenRepository.save(token) + + this.logger.log(`Updated token balance [${type}]: userAddress=${userAddress}, balance=${holder.balance}, token total supply=${token.totalSupply}`) + } catch (e) { + this.logger.error(`Failed to process token holder balance [${type}]: tokenAddress=${tokenAddress}, userAddress=${userAddress}`, e) + throw new Error(e); + } + } else { + try { + let holder = await this.appService.getTokenHolder(tokenAddress, userAddress) + if(!holder) { + this.logger.log(`Failed to find token holder, exit`) + process.exit(1) + } + holder.balance = String(BigInt(holder.balance) - amountIn) + await tokenHoldersRepository.save(holder) + + token.totalSupply = String(BigInt(token.totalSupply) - amountIn) + await tokenRepository.save(token) + + this.logger.log(`Updated token balance [${type}]: userAddress=${userAddress}, balance=${holder.balance}, token total supply=${token.totalSupply}`) + } catch (e) { + this.logger.error(`Failed to process token holder balance [${type}]: tokenAddress=${tokenAddress}, userAddress=${userAddress}`, e) + throw new Error(e); + } + } + try { await this.dataSource.manager.insert(Trade, { type, @@ -115,8 +157,8 @@ export class IndexerService { blockNumber, user, token, - amountIn, - amountOut, + amountIn: String(amountIn), + amountOut: String(amountOut), fee, timestamp }); diff --git a/src/user/user.service.ts b/src/user/user.service.ts index 26c03b1..576b79b 100644 --- a/src/user/user.service.ts +++ b/src/user/user.service.ts @@ -26,11 +26,17 @@ export class UserService { }) } - async getTokensCreated(address: string) { + async getTokensCreated(userAddress: string) { return await this.dataSource.manager.find(Token, { + relations: ['user'], where: { - address: address.toLowerCase(), + user: { + address: userAddress.toLowerCase() + } }, + order: { + createdAt: 'desc' + } }) } }