Skip to content

Commit c9ac8fb

Browse files
committed
Refactor project structure
1 parent 7a4f90a commit c9ac8fb

File tree

6 files changed

+304
-256
lines changed

6 files changed

+304
-256
lines changed

src/app.controller.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,11 @@ export class AppController {
9494
return user
9595
}
9696

97+
@Get('/user/:address/tokens/created')
98+
async getUserTokensCreated(@Param('address') address: string) {
99+
return await this.userService.getTokensCreated(address)
100+
}
101+
97102
@Post('/uploadImage')
98103
@UseInterceptors(FileInterceptor('file'))
99104
async uploadImage(@UploadedFile() uploadedFile: Express.Multer.File, @Headers() headers) {

src/app.module.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {APP_GUARD} from "@nestjs/core";
1111
import {ScheduleModule} from "@nestjs/schedule";
1212
import { UserService } from './user/user.service';
1313
import { GcloudService } from './gcloud/gcloud.service';
14+
import { IndexerService } from './indexer/indexer.service';
1415

1516
@Module({
1617
imports: [
@@ -39,7 +40,8 @@ import { GcloudService } from './gcloud/gcloud.service';
3940
useClass: ThrottlerGuard
4041
},
4142
UserService,
42-
GcloudService
43+
GcloudService,
44+
IndexerService
4345
],
4446
})
4547
export class AppModule {}

src/app.service.ts

Lines changed: 2 additions & 255 deletions
Original file line numberDiff line numberDiff line change
@@ -1,273 +1,20 @@
11
import {Injectable, Logger} from '@nestjs/common';
2-
import {ConfigService} from "@nestjs/config";
3-
import {Contract, ContractAbi, EventLog, Web3} from "web3";
4-
import * as TokenFactoryABI from './abi/TokenFactory.json'
52
import {Between, DataSource} from "typeorm";
6-
import {Comment, IndexerState, Token, UserAccount} from "./entities";
3+
import {Comment, Token} from "./entities";
74
import {AddCommentDto, GetCommentsDto} from "./dto/comment.dto";
85
import {GetTokensDto} from "./dto/token.dto";
9-
import * as process from "node:process";
106
import {Trade} from "./entities";
11-
import {Cron, CronExpression} from "@nestjs/schedule";
127
import * as moment from "moment";
138
import {GetTradesDto} from "./dto/trade.dto";
149
import {UserService} from "./user/user.service";
15-
import axios from "axios";
16-
import {TokenMetadata, TradeEventLog, TradeType} from "./types";
1710

1811
@Injectable()
1912
export class AppService {
2013
private readonly logger = new Logger(AppService.name);
21-
private readonly web3: Web3
22-
private readonly tokenFactoryContract: Contract<ContractAbi>
23-
private readonly blocksIndexingRange = 1000
2414
constructor(
25-
private configService: ConfigService,
2615
private userService: UserService,
2716
private dataSource: DataSource,
28-
) {
29-
const rpcUrl = configService.get('RPC_URL')
30-
const contractAddress = configService.get('PUMP_FUN_CONTRACT_ADDRESS')
31-
const initialBlockNumber = configService.get('PUMP_FUN_INITIAL_BLOCK_NUMBER')
32-
33-
if(!contractAddress) {
34-
this.logger.error(`[PUMP_FUN_CONTRACT_ADDRESS] is missing but required, exit`)
35-
process.exit(1)
36-
}
37-
38-
if(!initialBlockNumber) {
39-
this.logger.error(`[PUMP_FUN_INITIAL_BLOCK_NUMBER] is missing but required, exit`)
40-
process.exit(1)
41-
}
42-
43-
this.logger.log(`Starting app service, RPC_URL=${
44-
rpcUrl
45-
}, PUMP_FUN_CONTRACT_ADDRESS=${
46-
contractAddress
47-
}, PUMP_FUN_INITIAL_BLOCK_NUMBER=${
48-
initialBlockNumber
49-
}`)
50-
51-
this.web3 = new Web3(rpcUrl);
52-
this.tokenFactoryContract = new this.web3.eth.Contract(TokenFactoryABI, contractAddress);
53-
this.bootstrap().then(
54-
() => {
55-
this.eventsTrackingLoop()
56-
}
57-
)
58-
this.logger.log(`App service started`)
59-
}
60-
61-
private async bootstrap() {
62-
try {
63-
const indexerState = await this.dataSource.manager.findOne(IndexerState, {
64-
where: {}
65-
})
66-
if(!indexerState) {
67-
const blockNumber = +this.configService.get<number>('PUMP_FUN_INITIAL_BLOCK_NUMBER')
68-
if(!blockNumber) {
69-
this.logger.error('[PUMP_FUN_INITIAL_BLOCK_NUMBER] is empty but required, exit')
70-
process.exit(1)
71-
}
72-
await this.dataSource.manager.insert(IndexerState, {
73-
blockNumber
74-
})
75-
this.logger.log(`Set initial blockNumber=${blockNumber}`)
76-
}
77-
78-
const bootstrapUsers = [
79-
'0x98f0c3d42b8dafb1f73d8f105344c6a4434a0109',
80-
'0x2AB4eF5E937CcC03a9c0eAfC7C00836774B149E0'
81-
]
82-
for(const userAddress of bootstrapUsers) {
83-
if(!(await this.userService.getUserByAddress(userAddress))) {
84-
await this.userService.addNewUser({ address: userAddress })
85-
}
86-
}
87-
} catch (e) {
88-
this.logger.error(`Failed to bootstrap, exit`, e)
89-
process.exit(1)
90-
}
91-
}
92-
93-
private async processTradeEvents(events: TradeEventLog[]) {
94-
for(const event of events) {
95-
const { data, type } = event
96-
const txnHash = data.transactionHash.toLowerCase()
97-
const blockNumber = Number(data.blockNumber)
98-
const values = data.returnValues
99-
const tokenAddress = (values['token'] as string).toLowerCase()
100-
const amountIn = String(values['amount0In'] as bigint)
101-
const amountOut = String(values['amount0Out'] as bigint)
102-
const fee = String(values['fee'] as bigint)
103-
const timestamp = Number(values['timestamp'] as bigint)
104-
105-
const txn = await this.web3.eth.getTransaction(txnHash)
106-
const userAddress = txn.from.toLowerCase()
107-
const user = await this.userService.getUserByAddress(userAddress)
108-
if(!user) {
109-
this.logger.error(`Trade event: failed to get user by address="${userAddress}", event tx hash="${data.transactionHash}", exit`)
110-
process.exit(1)
111-
}
112-
113-
const token = await this.getTokenByAddress(tokenAddress)
114-
if(!token) {
115-
this.logger.error(`Trade event: failed to get token by address="${tokenAddress}", event tx hash="${data.transactionHash}", exit`)
116-
process.exit(1)
117-
}
118-
119-
try {
120-
await this.dataSource.manager.insert(Trade, {
121-
type,
122-
txnHash,
123-
blockNumber,
124-
user,
125-
token,
126-
amountIn,
127-
amountOut,
128-
fee,
129-
timestamp
130-
});
131-
this.logger.log(`Trade [${type}]: userAddress=${userAddress}, token=${tokenAddress}, amountIn=${amountIn}, amountOut=${amountOut}, fee=${fee}, timestamp=${timestamp}, txnHash=${txnHash}`)
132-
} catch (e) {
133-
this.logger.error(`Failed to process trade [${type}]: userAddress=${userAddress}, token=${tokenAddress} txnHash=${txnHash}`, e)
134-
throw new Error(e);
135-
}
136-
}
137-
}
138-
139-
private async getLatestIndexedBlockNumber() {
140-
const indexerState = await this.dataSource.manager.findOne(IndexerState, {
141-
where: {},
142-
})
143-
if(indexerState) {
144-
return indexerState.blockNumber
145-
}
146-
return 0
147-
}
148-
149-
private async updateLastIndexerBlockNumber(blockNumber: number) {
150-
const stateRepository = this.dataSource.manager.getRepository(IndexerState)
151-
const indexerState = await stateRepository.findOne({
152-
where: {}
153-
})
154-
indexerState.blockNumber = blockNumber
155-
await stateRepository.save(indexerState)
156-
}
157-
158-
async eventsTrackingLoop() {
159-
const lastIndexedBlockNumber = await this.getLatestIndexedBlockNumber()
160-
const fromBlockParam = lastIndexedBlockNumber + 1
161-
162-
let fromBlock = fromBlockParam
163-
let toBlock = fromBlock
164-
try {
165-
const blockchainBlockNumber = +(String(await this.web3.eth.getBlockNumber()))
166-
toBlock = fromBlock + this.blocksIndexingRange - 1
167-
if(toBlock > blockchainBlockNumber) {
168-
toBlock = blockchainBlockNumber
169-
}
170-
171-
if(toBlock - fromBlock >= 1) {
172-
const tokenCreatedEvents = await this.tokenFactoryContract.getPastEvents('allEvents', {
173-
fromBlock,
174-
toBlock,
175-
topics: [
176-
this.web3.utils.sha3('TokenCreated(address,string,string,string,address,uint256)'),
177-
],
178-
}) as EventLog[];
179-
180-
const buyEvents = await this.tokenFactoryContract.getPastEvents('allEvents', {
181-
fromBlock,
182-
toBlock,
183-
topics: [
184-
this.web3.utils.sha3('TokenBuy(address,uint256,uint256,uint256,uint256)'),
185-
],
186-
}) as EventLog[];
187-
188-
const sellEvents = await this.tokenFactoryContract.getPastEvents('allEvents', {
189-
fromBlock,
190-
toBlock,
191-
topics: [
192-
this.web3.utils.sha3('TokenSell(address,uint256,uint256,uint256,uint256)'),
193-
],
194-
}) as EventLog[];
195-
196-
const tradeEvents: TradeEventLog[] = [...buyEvents].map(data => {
197-
return {
198-
type: TradeType.buy,
199-
data
200-
}
201-
}).concat([...sellEvents].map(data => {
202-
return {
203-
type: TradeType.sell,
204-
data
205-
}
206-
})).sort((a, b) => {
207-
return +(a.data.returnValues.timestamp.toString()) - +(b.data.returnValues.timestamp.toString())
208-
})
209-
210-
for(const tokenCreated of tokenCreatedEvents) {
211-
const txnHash = tokenCreated.transactionHash.toLowerCase()
212-
const values = tokenCreated.returnValues
213-
const tokenAddress = (values['token'] as string).toLowerCase()
214-
const name = values['name'] as string
215-
const symbol = values['symbol'] as string
216-
const uri = values['uri'] as string
217-
const creatorAddress = (values['creator'] as string).toLowerCase()
218-
const timestamp = Number(values['timestamp'] as bigint)
219-
220-
let uriData = null
221-
try {
222-
const { data } = await axios.get<TokenMetadata>(uri)
223-
uriData = data
224-
} catch (e) {
225-
this.logger.error(`Failed to get token uri data, uri=${uri}, tokenAddress=${tokenAddress}`, e)
226-
}
227-
228-
const user = await this.dataSource.manager.findOne(UserAccount, {
229-
where: {
230-
address: creatorAddress
231-
}
232-
})
233-
234-
await this.dataSource.manager.insert(Token, {
235-
txnHash,
236-
address: tokenAddress,
237-
blockNumber: Number(tokenCreated.blockNumber),
238-
name,
239-
symbol,
240-
timestamp,
241-
user,
242-
uri,
243-
uriData,
244-
});
245-
this.logger.log(`Create token: address=${tokenAddress}, name=${name}, symbol=${symbol}, uri=${uri}, creator=${creatorAddress}, txnHash=${txnHash}`);
246-
}
247-
248-
await this.processTradeEvents(tradeEvents)
249-
250-
this.logger.log(`[${fromBlock}-${toBlock}] (${((toBlock - fromBlock + 1))} blocks), new tokens=${tokenCreatedEvents.length}, trade=${[...buyEvents, ...sellEvents].length} (buy=${buyEvents.length}, sell=${sellEvents.length})`)
251-
} else {
252-
// Wait for blockchain
253-
toBlock = fromBlockParam - 1
254-
await new Promise(resolve => setTimeout(resolve, 5 * 1000));
255-
}
256-
} catch (e) {
257-
toBlock = fromBlockParam - 1
258-
this.logger.error(`[${fromBlock} - ${toBlock}] Failed to index blocks range:`, e)
259-
await new Promise(resolve => setTimeout(resolve, 30 * 1000));
260-
}
261-
262-
try {
263-
await this.updateLastIndexerBlockNumber(toBlock)
264-
} catch (e) {
265-
this.logger.error(`Failed to update last blockNumber=${toBlock}, exit`, e)
266-
process.exit(1)
267-
}
268-
269-
this.eventsTrackingLoop()
270-
}
17+
) {}
27118

27219
async getComments(dto: GetCommentsDto){
27320
return await this.dataSource.manager.find(Comment, {
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Test, TestingModule } from '@nestjs/testing';
2+
import { IndexerService } from './indexer.service';
3+
4+
describe('IndexerService', () => {
5+
let service: IndexerService;
6+
7+
beforeEach(async () => {
8+
const module: TestingModule = await Test.createTestingModule({
9+
providers: [IndexerService],
10+
}).compile();
11+
12+
service = module.get<IndexerService>(IndexerService);
13+
});
14+
15+
it('should be defined', () => {
16+
expect(service).toBeDefined();
17+
});
18+
});

0 commit comments

Comments
 (0)