diff --git a/packages/worker/src/balance/balance.service.spec.ts b/packages/worker/src/balance/balance.service.spec.ts index 2a4438ff02..5e7a9293f5 100644 --- a/packages/worker/src/balance/balance.service.spec.ts +++ b/packages/worker/src/balance/balance.service.spec.ts @@ -7,6 +7,7 @@ import { ConfigService } from "@nestjs/config"; import { Transfer } from "../transfer/interfaces/transfer.interface"; import { BlockchainService } from "../blockchain/blockchain.service"; import { BalanceRepository } from "../repositories"; +import { TokenType } from "../entities"; import { BalanceService } from "./"; describe("BalanceService", () => { @@ -77,8 +78,14 @@ describe("BalanceService", () => { const blockNumber2 = 15; beforeEach(() => { - balanceService.changedBalances.set(blockNumber, new Map>()); - balanceService.changedBalances.set(blockNumber2, new Map>()); + balanceService.changedBalances.set( + blockNumber, + new Map>() + ); + balanceService.changedBalances.set( + blockNumber2, + new Map>() + ); }); it("clears tracked balances for the specified block number", () => { @@ -95,18 +102,21 @@ describe("BalanceService", () => { from: "0x36615cf349d7f6344891b1e7ca7c72883f5dc049", to: "0x0000000000000000000000000000000000008001", blockNumber: 10, + tokenType: TokenType.ETH, }), mock({ tokenAddress: "0x000000000000000000000000000000000000800a", from: "0xd206eaf6819007535e893410cfa01885ce40e99a", to: "0x0000000000000000000000000000000000008001", blockNumber: 10, + tokenType: TokenType.ETH, }), mock({ tokenAddress: "0x2392e98fb47cf05773144db3ce8002fac4f39c84", from: "0x0000000000000000000000000000000000000000", to: "0x36615cf349d7f6344891b1e7ca7c72883f5dc049", blockNumber: 10, + tokenType: TokenType.ERC20, }), ]; @@ -127,24 +137,28 @@ describe("BalanceService", () => { from: "0x000000000000000000000000000000000000800a", to: "0x0000000000000000000000000000000000008001", blockNumber: 10, + tokenType: TokenType.ERC20, }), mock({ tokenAddress: "0x000000000000000000000000000000000000800a", from: "0x000000000000000000000000000000000000800a", to: "0x0000000000000000000000000000000000008001", blockNumber: 10, + tokenType: TokenType.ETH, }), mock({ tokenAddress: "0x000000000000000000000000000000000000800a", from: "0xd206eaf6819007535e893410cfa01885ce40e99a", to: "0x0000000000000000000000000000000000000000", blockNumber: 10, + tokenType: TokenType.ETH, }), mock({ tokenAddress: "0x2392e98fb47cf05773144db3ce8002fac4f39c84", from: "0xd206eaf6819007535e893410cfa01885ce40e99a", to: "0x0000000000000000000000000000000000000000", blockNumber: 10, + tokenType: TokenType.ERC20, }), ]; @@ -196,37 +210,55 @@ describe("BalanceService", () => { blockChangedBalances .get("0x0000000000000000000000000000000000008001") .get("0x2392e98fb47cf05773144db3ce8002fac4f39c84") - ).toBeUndefined(); + ).toEqual({ + balance: undefined, + tokenType: TokenType.ERC20, + }); expect( blockChangedBalances .get("0x000000000000000000000000000000000000800a") .get("0x2392e98fb47cf05773144db3ce8002fac4f39c84") - ).toBeUndefined(); + ).toEqual({ + balance: undefined, + tokenType: TokenType.ERC20, + }); expect( blockChangedBalances .get("0x0000000000000000000000000000000000008001") .get("0x000000000000000000000000000000000000800a") - ).toBeUndefined(); + ).toEqual({ + balance: undefined, + tokenType: TokenType.ETH, + }); expect( blockChangedBalances .get("0x000000000000000000000000000000000000800a") .get("0x000000000000000000000000000000000000800a") - ).toBeUndefined(); + ).toEqual({ + balance: undefined, + tokenType: TokenType.ETH, + }); expect( blockChangedBalances .get("0xd206eaf6819007535e893410cfa01885ce40e99a") .get("0x000000000000000000000000000000000000800a") - ).toBeUndefined(); + ).toEqual({ + balance: undefined, + tokenType: TokenType.ETH, + }); expect( blockChangedBalances .get("0xd206eaf6819007535e893410cfa01885ce40e99a") .get("0x2392e98fb47cf05773144db3ce8002fac4f39c84") - ).toBeUndefined(); + ).toEqual({ + balance: undefined, + tokenType: TokenType.ERC20, + }); }); it("tracks changed balance addresses for transfers", () => { @@ -246,7 +278,10 @@ describe("BalanceService", () => { blockChangedBalances .get("0x0000000000000000000000000000000000008001") .get("0x000000000000000000000000000000000000800a") - ).toBeUndefined(); + ).toEqual({ + balance: undefined, + tokenType: TokenType.ETH, + }); expect( blockChangedBalances .get("0x36615cf349d7f6344891b1e7ca7c72883f5dc049") @@ -256,7 +291,10 @@ describe("BalanceService", () => { blockChangedBalances .get("0x36615cf349d7f6344891b1e7ca7c72883f5dc049") .get("0x000000000000000000000000000000000000800a") - ).toBeUndefined(); + ).toEqual({ + balance: undefined, + tokenType: TokenType.ETH, + }); expect( blockChangedBalances .get("0x36615cf349d7f6344891b1e7ca7c72883f5dc049") @@ -266,7 +304,10 @@ describe("BalanceService", () => { blockChangedBalances .get("0x36615cf349d7f6344891b1e7ca7c72883f5dc049") .get("0x2392e98fb47cf05773144db3ce8002fac4f39c84") - ).toBeUndefined(); + ).toEqual({ + balance: undefined, + tokenType: TokenType.ERC20, + }); expect( blockChangedBalances .get("0xd206eaf6819007535e893410cfa01885ce40e99a") @@ -276,22 +317,27 @@ describe("BalanceService", () => { blockChangedBalances .get("0xd206eaf6819007535e893410cfa01885ce40e99a") .get("0x000000000000000000000000000000000000800a") - ).toBeUndefined(); + ).toEqual({ + balance: undefined, + tokenType: TokenType.ETH, + }); }); it("merge changed balances with existing changed balances for the block", () => { - const existingBlockBalances = new Map>(); + const existingBlockBalances = new Map>(); existingBlockBalances.set( "0x0000000000000000000000000000000000008007", - new Map([ - ["0x000000000000000000000000000000000000800a", undefined], - ["0x0000000000000000000000000000000000008123", undefined], + new Map([ + ["0x000000000000000000000000000000000000800a", { balance: undefined, tokenType: TokenType.ETH }], + ["0x0000000000000000000000000000000000008123", { balance: undefined, tokenType: TokenType.ERC20 }], ]) ); existingBlockBalances.set( "0x36615cf349d7f6344891b1e7ca7c72883f5dc049", - new Map([["0x000000000000000000000000000000000000800a", undefined]]) + new Map([ + ["0x000000000000000000000000000000000000800a", { balance: undefined, tokenType: TokenType.ETH }], + ]) ); balanceService.changedBalances.set(transfers[0].blockNumber, existingBlockBalances); @@ -310,7 +356,10 @@ describe("BalanceService", () => { blockChangedBalances .get("0x0000000000000000000000000000000000008007") .get("0x000000000000000000000000000000000000800a") - ).toBeUndefined(); + ).toEqual({ + balance: undefined, + tokenType: TokenType.ETH, + }); expect( blockChangedBalances .get("0x0000000000000000000000000000000000008007") @@ -320,7 +369,10 @@ describe("BalanceService", () => { blockChangedBalances .get("0x0000000000000000000000000000000000008007") .get("0x0000000000000000000000000000000000008123") - ).toBeUndefined(); + ).toEqual({ + balance: undefined, + tokenType: TokenType.ERC20, + }); expect(blockChangedBalances.has("0x0000000000000000000000000000000000008001")).toBe(true); expect(blockChangedBalances.has("0x36615cf349d7f6344891b1e7ca7c72883f5dc049")).toBe(true); expect(blockChangedBalances.has("0xd206eaf6819007535e893410cfa01885ce40e99a")).toBe(true); @@ -333,7 +385,10 @@ describe("BalanceService", () => { blockChangedBalances .get("0x0000000000000000000000000000000000008001") .get("0x000000000000000000000000000000000000800a") - ).toBeUndefined(); + ).toEqual({ + balance: undefined, + tokenType: TokenType.ETH, + }); expect( blockChangedBalances .get("0x36615cf349d7f6344891b1e7ca7c72883f5dc049") @@ -343,7 +398,10 @@ describe("BalanceService", () => { blockChangedBalances .get("0x36615cf349d7f6344891b1e7ca7c72883f5dc049") .get("0x000000000000000000000000000000000000800a") - ).toBeUndefined(); + ).toEqual({ + balance: undefined, + tokenType: TokenType.ETH, + }); expect( blockChangedBalances .get("0x36615cf349d7f6344891b1e7ca7c72883f5dc049") @@ -353,7 +411,10 @@ describe("BalanceService", () => { blockChangedBalances .get("0x36615cf349d7f6344891b1e7ca7c72883f5dc049") .get("0x2392e98fb47cf05773144db3ce8002fac4f39c84") - ).toBeUndefined(); + ).toEqual({ + balance: undefined, + tokenType: TokenType.ERC20, + }); expect( blockChangedBalances .get("0xd206eaf6819007535e893410cfa01885ce40e99a") @@ -363,7 +424,10 @@ describe("BalanceService", () => { blockChangedBalances .get("0xd206eaf6819007535e893410cfa01885ce40e99a") .get("0x000000000000000000000000000000000000800a") - ).toBeUndefined(); + ).toEqual({ + balance: undefined, + tokenType: TokenType.ETH, + }); }); }); @@ -377,20 +441,25 @@ describe("BalanceService", () => { ]; beforeEach(() => { - const blockBalances = new Map>(); - blockBalances.set(utils.ETH_ADDRESS, new Map([[utils.ETH_ADDRESS, undefined]])); + const blockBalances = new Map>(); + blockBalances.set( + utils.ETH_ADDRESS, + new Map([ + [utils.ETH_ADDRESS, { balance: undefined, tokenType: TokenType.ETH }], + ]) + ); blockBalances.set( addresses[0], - new Map([ - [tokenAddresses[0][0], undefined], - [tokenAddresses[0][1], undefined], + new Map([ + [tokenAddresses[0][0], { balance: undefined, tokenType: TokenType.ERC20 }], + [tokenAddresses[0][1], { balance: undefined, tokenType: TokenType.ETH }], ]) ); blockBalances.set( addresses[1], - new Map([ - [tokenAddresses[1][0], undefined], - [tokenAddresses[1][1], undefined], + new Map([ + [tokenAddresses[1][0], { balance: undefined, tokenType: TokenType.ERC20 }], + [tokenAddresses[1][1], { balance: undefined, tokenType: TokenType.ETH }], ]) ); balanceService.changedBalances.set(blockNumber, blockBalances); @@ -461,9 +530,19 @@ describe("BalanceService", () => { app.useLogger(mock()); balanceService = app.get(BalanceService); - const blockBalances = new Map>(); - blockBalances.set(utils.ETH_ADDRESS, new Map([[utils.ETH_ADDRESS, undefined]])); - blockBalances.set(addresses[1], new Map([[tokenAddresses[1][0], undefined]])); + const blockBalances = new Map>(); + blockBalances.set( + utils.ETH_ADDRESS, + new Map([ + [utils.ETH_ADDRESS, { balance: undefined, tokenType: TokenType.ETH }], + ]) + ); + blockBalances.set( + addresses[1], + new Map([ + [tokenAddresses[1][0], { balance: undefined, tokenType: TokenType.ERC20 }], + ]) + ); balanceService.changedBalances.set(blockNumber, blockBalances); }); @@ -535,6 +614,45 @@ describe("BalanceService", () => { }); }); + describe("getERC20TokensForChangedBalances", () => { + it("returns empty array if there are no changed balances for the specified block number", () => { + const blockNumber = 1; + const blockBalances = new Map>(); + blockBalances.set( + "0x0000000000000000000000000000000000000001", + new Map([ + [utils.ETH_ADDRESS, { balance: undefined, tokenType: TokenType.ETH }], + ]) + ); + balanceService.changedBalances.set(blockNumber, blockBalances); + expect(balanceService.getERC20TokensForChangedBalances(2)).toEqual([]); + }); + it("returns unique ERC20 tokens addresses array for changed balances for specified block number", async () => { + const blockNumber = 1; + const blockBalances = new Map>(); + blockBalances.set( + "0x0000000000000000000000000000000000000001", + new Map([ + [utils.ETH_ADDRESS, { balance: undefined, tokenType: TokenType.ETH }], + ["0x0000000000000000000000000000000000000002", { balance: undefined, tokenType: TokenType.ERC20 }], + ["0x0000000000000000000000000000000000000003", { balance: undefined, tokenType: TokenType.ERC20 }], + ]) + ); + blockBalances.set( + "0x0000000000000000000000000000000000000005", + new Map([ + [utils.ETH_ADDRESS, { balance: undefined, tokenType: TokenType.ETH }], + ["0x0000000000000000000000000000000000000002", { balance: undefined, tokenType: TokenType.ERC20 }], + ]) + ); + balanceService.changedBalances.set(blockNumber, blockBalances); + expect(balanceService.getERC20TokensForChangedBalances(blockNumber)).toEqual([ + "0x0000000000000000000000000000000000000002", + "0x0000000000000000000000000000000000000003", + ]); + }); + }); + describe("deleteOldBalances", () => { const fromBlockNumber = 10; const toBlockNumber = 10; diff --git a/packages/worker/src/balance/balance.service.ts b/packages/worker/src/balance/balance.service.ts index 9ec8baea9e..193413093c 100644 --- a/packages/worker/src/balance/balance.service.ts +++ b/packages/worker/src/balance/balance.service.ts @@ -6,7 +6,7 @@ import { InjectMetric } from "@willsoto/nestjs-prometheus"; import { ConfigService } from "@nestjs/config"; import { BlockchainService } from "../blockchain/blockchain.service"; import { BalanceRepository } from "../repositories"; -import { Balance } from "../entities"; +import { Balance, TokenType } from "../entities"; import { Transfer } from "../transfer/interfaces/transfer.interface"; import { DELETE_OLD_BALANCES_DURATION_METRIC_NAME, DELETE_ZERO_BALANCES_DURATION_METRIC_NAME } from "../metrics"; @@ -15,7 +15,7 @@ import { DELETE_OLD_BALANCES_DURATION_METRIC_NAME, DELETE_ZERO_BALANCES_DURATION export class BalanceService { private readonly disableBalancesProcessing: boolean; private readonly logger: Logger; - public changedBalances: Map>>; + public changedBalances: Map>>; constructor( private readonly blockchainService: BlockchainService, @@ -27,7 +27,7 @@ export class BalanceService { configService: ConfigService ) { this.logger = new Logger(BalanceService.name); - this.changedBalances = new Map>>(); + this.changedBalances = new Map>>(); this.disableBalancesProcessing = configService.get("balances.disableBalancesProcessing"); } @@ -41,7 +41,8 @@ export class BalanceService { } const blockChangedBalances = - this.changedBalances.get(transfers[0].blockNumber) || new Map>(); + this.changedBalances.get(transfers[0].blockNumber) || + new Map>(); for (const transfer of transfers) { const changedBalancesAddresses = new Set([transfer.from, transfer.to]); @@ -51,10 +52,15 @@ export class BalanceService { } if (!blockChangedBalances.has(changedBalanceAddress)) { - blockChangedBalances.set(changedBalanceAddress, new Map()); + blockChangedBalances.set( + changedBalanceAddress, + new Map() + ); } - blockChangedBalances.get(changedBalanceAddress).set(transfer.tokenAddress, undefined); + blockChangedBalances + .get(changedBalanceAddress) + .set(transfer.tokenAddress, { balance: undefined, tokenType: transfer.tokenType }); } } @@ -84,7 +90,11 @@ export class BalanceService { for (let i = 0; i < balances.length; i++) { const [address, tokenAddress] = balanceAddresses[i]; if (balances[i].status === "fulfilled") { - blockChangedBalances.get(address).set(tokenAddress, (balances[i] as PromiseFulfilledResult).value); + const blockChangedBalancesForAddress = blockChangedBalances.get(address); + blockChangedBalancesForAddress.set(tokenAddress, { + balance: (balances[i] as PromiseFulfilledResult).value, + tokenType: blockChangedBalancesForAddress.get(tokenAddress).tokenType, + }); } else { this.logger.warn({ message: "Get balance for token failed", @@ -103,13 +113,13 @@ export class BalanceService { const balanceRecords: Partial[] = []; - for (const [address, balances] of blockChangedBalances) { - for (const [tokenAddress, balance] of balances) { + for (const [address, addressTokenBalances] of blockChangedBalances) { + for (const [tokenAddress, addressTokenBalance] of addressTokenBalances) { balanceRecords.push({ address, tokenAddress, blockNumber, - balance: this.disableBalancesProcessing ? BigNumber.from("-1") : balance, + balance: this.disableBalancesProcessing ? BigNumber.from("-1") : addressTokenBalance.balance, }); } } @@ -117,6 +127,23 @@ export class BalanceService { await this.balanceRepository.addMany(balanceRecords); } + public getERC20TokensForChangedBalances(blockNumber: number): string[] { + if (!this.changedBalances.has(blockNumber)) { + return []; + } + const blockChangedBalances = this.changedBalances.get(blockNumber); + const tokens = new Set(); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + for (const [_, tokenAddresses] of blockChangedBalances) { + for (const [tokenAddress, tokenAddressBalance] of tokenAddresses) { + if (tokenAddressBalance.tokenType === TokenType.ERC20) { + tokens.add(tokenAddress); + } + } + } + return Array.from(tokens); + } + public async deleteOldBalances(fromBlockNumber: number, toBlockNumber: number): Promise { this.logger.log({ message: "Deleting old balances", fromBlockNumber, toBlockNumber }); const stopDeleteBalancesDurationMeasuring = this.deleteOldBalancesDurationMetric.startTimer(); diff --git a/packages/worker/src/block/block.processor.spec.ts b/packages/worker/src/block/block.processor.spec.ts index 35add09882..eda82a36ea 100644 --- a/packages/worker/src/block/block.processor.spec.ts +++ b/packages/worker/src/block/block.processor.spec.ts @@ -11,6 +11,7 @@ import { TransactionProcessor } from "../transaction"; import { LogProcessor } from "../log"; import { BlockchainService } from "../blockchain"; import { BalanceService } from "../balance"; +import { TokenService } from "../token/token.service"; import { Block } from "../entities"; import { BlockRepository } from "../repositories"; import { BLOCKS_REVERT_DETECTED_EVENT } from "../constants"; @@ -24,6 +25,7 @@ describe("BlockProcessor", () => { let transactionProcessorMock: TransactionProcessor; let logProcessorMock: LogProcessor; let balanceServiceMock: BalanceService; + let tokenServiceMock: TokenService; let blockRepositoryMock: BlockRepository; let eventEmitterMock: EventEmitter2; let configServiceMock: ConfigService; @@ -65,6 +67,10 @@ describe("BlockProcessor", () => { provide: BalanceService, useValue: balanceServiceMock, }, + { + provide: TokenService, + useValue: tokenServiceMock, + }, { provide: BlockRepository, useValue: blockRepositoryMock, @@ -125,6 +131,10 @@ describe("BlockProcessor", () => { balanceServiceMock = mock({ saveChangedBalances: jest.fn().mockResolvedValue(null), clearTrackedState: jest.fn(), + getERC20TokensForChangedBalances: jest.fn().mockReturnValue(["0x0000000000000000000000000000000000000001"]), + }); + tokenServiceMock = mock({ + saveERC20Tokens: jest.fn().mockResolvedValue(null), }); blockRepositoryMock = mock({ getLastBlock: jest.fn().mockResolvedValue(null), @@ -601,6 +611,18 @@ describe("BlockProcessor", () => { expect(balanceServiceMock.saveChangedBalances).toHaveBeenCalledWith(blocksToProcess[0].block.number); }); + it("identifies and saves ERC20 tokens based on balances", async () => { + await blockProcessor.processNextBlocksRange(); + expect(balanceServiceMock.getERC20TokensForChangedBalances).toHaveBeenCalledTimes(1); + expect(balanceServiceMock.getERC20TokensForChangedBalances).toHaveBeenCalledWith( + blocksToProcess[0].block.number + ); + expect(tokenServiceMock.saveERC20Tokens).toHaveBeenCalledTimes(1); + expect(tokenServiceMock.saveERC20Tokens).toHaveBeenCalledWith([ + "0x0000000000000000000000000000000000000001", + ]); + }); + it("stops the balances duration metric", async () => { await blockProcessor.processNextBlocksRange(); expect(stopBalancesDurationMetricMock).toHaveBeenCalledTimes(1); diff --git a/packages/worker/src/block/block.processor.ts b/packages/worker/src/block/block.processor.ts index 2b528d0cce..880e4db655 100644 --- a/packages/worker/src/block/block.processor.ts +++ b/packages/worker/src/block/block.processor.ts @@ -8,6 +8,7 @@ import { UnitOfWork } from "../unitOfWork"; import { BlockchainService } from "../blockchain/blockchain.service"; import { BlockInfo, BlockWatcher } from "./block.watcher"; import { BalanceService } from "../balance/balance.service"; +import { TokenService } from "../token/token.service"; import { BlockRepository } from "../repositories"; import { Block } from "../entities"; import { TransactionProcessor } from "../transaction"; @@ -35,6 +36,7 @@ export class BlockProcessor { private readonly transactionProcessor: TransactionProcessor, private readonly logProcessor: LogProcessor, private readonly balanceService: BalanceService, + private readonly tokenService: TokenService, private readonly blockWatcher: BlockWatcher, private readonly blockRepository: BlockRepository, private readonly eventEmitter: EventEmitter2, @@ -152,8 +154,13 @@ export class BlockProcessor { const stopBalancesDurationMeasuring = this.balancesProcessingDurationMetric.startTimer(); - this.logger.debug({ message: "Updating balances", blockNumber }); - await this.balanceService.saveChangedBalances(blockNumber); + this.logger.debug({ message: "Updating balances and tokens", blockNumber }); + + const erc20TokensForChangedBalances = this.balanceService.getERC20TokensForChangedBalances(blockNumber); + await Promise.all([ + this.balanceService.saveChangedBalances(blockNumber), + this.tokenService.saveERC20Tokens(erc20TokensForChangedBalances), + ]); stopBalancesDurationMeasuring(); } catch (error) { diff --git a/packages/worker/src/repositories/base.repository.spec.ts b/packages/worker/src/repositories/base.repository.spec.ts index 1297b6a7b3..1aaccc7c80 100644 --- a/packages/worker/src/repositories/base.repository.spec.ts +++ b/packages/worker/src/repositories/base.repository.spec.ts @@ -87,6 +87,17 @@ describe("BaseRepository", () => { }); }); + describe("find", () => { + it("calls transactionManager find with provided options and returns result of the call", async () => { + const entity = { createdAt: new Date(), updatedAt: new Date() }; + (entityManagerMock.find as jest.Mock).mockResolvedValue([entity]); + + const result = await repository.find({ select: ["createdAt", "updatedAt"] }); + expect(entityManagerMock.find).toBeCalledWith(BaseEntity, { select: ["createdAt", "updatedAt"] }); + expect(result).toEqual([entity]); + }); + }); + describe("delete", () => { it("calls transactionManager delete with provided params", async () => { const entity = { createdAt: new Date(), updatedAt: new Date() }; diff --git a/packages/worker/src/repositories/base.repository.ts b/packages/worker/src/repositories/base.repository.ts index f9816e2a03..2a842a3389 100644 --- a/packages/worker/src/repositories/base.repository.ts +++ b/packages/worker/src/repositories/base.repository.ts @@ -1,5 +1,5 @@ import { Injectable } from "@nestjs/common"; -import { EntityTarget, FindOptionsWhere } from "typeorm"; +import { EntityTarget, FindOptionsWhere, FindManyOptions } from "typeorm"; import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity"; import { UnitOfWork } from "../unitOfWork"; @@ -61,4 +61,9 @@ export abstract class BaseRepository { const transactionManager = this.unitOfWork.getTransactionManager(); return await transactionManager.findOneBy(this.entityTarget, where); } + + public async find(options: FindManyOptions): Promise { + const transactionManager = this.unitOfWork.getTransactionManager(); + return await transactionManager.find(this.entityTarget, options); + } } diff --git a/packages/worker/src/token/token.service.spec.ts b/packages/worker/src/token/token.service.spec.ts index b52b51de38..a067da0d93 100644 --- a/packages/worker/src/token/token.service.spec.ts +++ b/packages/worker/src/token/token.service.spec.ts @@ -4,13 +4,16 @@ import { Test, TestingModule } from "@nestjs/testing"; import { Logger } from "@nestjs/common"; import { BlockchainService } from "../blockchain/blockchain.service"; import { TokenRepository } from "../repositories/token.repository"; +import { AddressRepository } from "../repositories/address.repository"; import { TokenService } from "./token.service"; import { ContractAddress } from "../address/interface/contractAddress.interface"; +import { Token } from "../entities"; describe("TokenService", () => { let tokenService: TokenService; let blockchainServiceMock: BlockchainService; let tokenRepositoryMock: TokenRepository; + let addressRepositoryMock: AddressRepository; let startGetTokenInfoDurationMetricMock: jest.Mock; let stopGetTokenInfoDurationMetricMock: jest.Mock; @@ -21,6 +24,7 @@ describe("TokenService", () => { }, }); tokenRepositoryMock = mock(); + addressRepositoryMock = mock(); stopGetTokenInfoDurationMetricMock = jest.fn(); startGetTokenInfoDurationMetricMock = jest.fn().mockReturnValue(stopGetTokenInfoDurationMetricMock); @@ -36,6 +40,10 @@ describe("TokenService", () => { provide: TokenRepository, useValue: tokenRepositoryMock, }, + { + provide: AddressRepository, + useValue: addressRepositoryMock, + }, { provide: "PROM_METRIC_GET_TOKEN_INFO_DURATION_SECONDS", useValue: { @@ -432,5 +440,74 @@ describe("TokenService", () => { expect(tokenRepositoryMock.upsert).toHaveBeenCalledTimes(0); }); }); + + describe("when transactionReceipt param is not provided", () => { + it("upserts the token without l1Address when token is valid", async () => { + await tokenService.saveERC20Token(deployedContractAddress); + expect(tokenRepositoryMock.upsert).toHaveBeenCalledTimes(1); + expect(tokenRepositoryMock.upsert).toHaveBeenCalledWith({ + ...tokenData, + blockNumber: deployedContractAddress.blockNumber, + transactionHash: deployedContractAddress.transactionHash, + l2Address: deployedContractAddress.address, + l1Address: undefined, + logIndex: deployedContractAddress.logIndex, + }); + }); + }); + }); + + describe("saveERC20Tokens", () => { + beforeEach(() => { + jest.spyOn(tokenService, "saveERC20Token").mockResolvedValue(null); + jest.spyOn(tokenRepositoryMock, "find").mockResolvedValue([ + { + l2Address: "0x0000000000000000000000000000000000000001", + } as Token, + ]); + jest.spyOn(addressRepositoryMock, "find").mockResolvedValue([]); + }); + + describe("when all the specified tokens are already saved", () => { + it("does not save tokens", async () => { + await tokenService.saveERC20Tokens(["0x0000000000000000000000000000000000000001"]); + expect(tokenService.saveERC20Token).not.toBeCalled(); + }); + }); + + describe("when there is no saved contract for all the tokens to save", () => { + it("does not save tokens", async () => { + await tokenService.saveERC20Tokens([ + "0x0000000000000000000000000000000000000001", + "0x0000000000000000000000000000000000000002", + ]); + expect(tokenService.saveERC20Token).not.toBeCalled(); + }); + }); + + it("saves only tokens not saved yet and for which there is a contract already saved in DB", async () => { + (addressRepositoryMock.find as jest.Mock).mockResolvedValueOnce([ + { + address: "0x0000000000000000000000000000000000000003", + createdInBlockNumber: 10, + creatorTxHash: "0x8a008b8dbbc18035e56370abb820e736b705d68d6ac12b203603db8d9ea87e15", + creatorAddress: "0x0000000000000000000000000000000000000009", + createdInLogIndex: 1, + }, + ]); + await tokenService.saveERC20Tokens([ + "0x0000000000000000000000000000000000000001", + "0x0000000000000000000000000000000000000002", + "0x0000000000000000000000000000000000000003", + ]); + expect(tokenService.saveERC20Token).toBeCalledTimes(1); + expect(tokenService.saveERC20Token).toBeCalledWith({ + address: "0x0000000000000000000000000000000000000003", + blockNumber: 10, + transactionHash: "0x8a008b8dbbc18035e56370abb820e736b705d68d6ac12b203603db8d9ea87e15", + creatorAddress: "0x0000000000000000000000000000000000000009", + logIndex: 1, + }); + }); }); }); diff --git a/packages/worker/src/token/token.service.ts b/packages/worker/src/token/token.service.ts index cd11f77b16..69b1d1d08e 100644 --- a/packages/worker/src/token/token.service.ts +++ b/packages/worker/src/token/token.service.ts @@ -1,10 +1,11 @@ import { types, utils } from "zksync-web3"; import { Injectable, Logger } from "@nestjs/common"; import { InjectMetric } from "@willsoto/nestjs-prometheus"; +import { In } from "typeorm"; import { Histogram } from "prom-client"; import { LogType, isLogOfType } from "../log/logType"; import { BlockchainService } from "../blockchain/blockchain.service"; -import { TokenRepository } from "../repositories"; +import { AddressRepository, TokenRepository } from "../repositories"; import { GET_TOKEN_INFO_DURATION_METRIC_NAME } from "../metrics"; import { ContractAddress } from "../address/interface/contractAddress.interface"; import parseLog from "../utils/parseLog"; @@ -27,6 +28,7 @@ export class TokenService { constructor( private readonly blockchainService: BlockchainService, + private readonly addressRepository: AddressRepository, private readonly tokenRepository: TokenRepository, @InjectMetric(GET_TOKEN_INFO_DURATION_METRIC_NAME) private readonly getTokenInfoDurationMetric: Histogram @@ -52,7 +54,7 @@ export class TokenService { public async saveERC20Token( contractAddress: ContractAddress, - transactionReceipt: types.TransactionReceipt + transactionReceipt?: types.TransactionReceipt ): Promise { let erc20Token: { symbol: string; @@ -62,6 +64,7 @@ export class TokenService { }; const bridgeLog = + transactionReceipt && transactionReceipt.to.toLowerCase() === this.blockchainService.bridgeAddresses.l2Erc20DefaultBridge && transactionReceipt.logs?.find( (log) => @@ -106,4 +109,63 @@ export class TokenService { }); } } + + public async saveERC20Tokens(tokenAddresses: string[]): Promise { + const existingTokens = await this.findWhereInList( + (tokenAddressesBatch) => + this.tokenRepository.find({ + where: { + l2Address: In(tokenAddressesBatch), + }, + select: ["l2Address"], + }), + tokenAddresses + ); + + const tokensToSave = tokenAddresses.filter( + (token) => !existingTokens.find((t) => t.l2Address.toLowerCase() === token.toLowerCase()) + ); + // fetch tokens contracts, if contract is not saved yet skip saving token + const tokensToSaveContracts = await this.findWhereInList( + (tokenAddressesBatch) => + this.addressRepository.find({ + where: { + address: In(tokenAddressesBatch), + }, + }), + tokensToSave + ); + + await Promise.all( + tokensToSaveContracts.map((tokenContract) => + this.saveERC20Token({ + address: tokenContract.address, + blockNumber: tokenContract.createdInBlockNumber, + transactionHash: tokenContract.creatorTxHash, + creatorAddress: tokenContract.creatorAddress, + logIndex: tokenContract.createdInLogIndex, + } as ContractAddress) + ) + ); + } + + private async findWhereInList( + handler: (list: string[]) => Promise, + list: string[], + batchSize = 500 + ): Promise { + const result: T[] = []; + + let batch = []; + for (let i = 0; i < list.length; i++) { + batch.push(list[i]); + if (batch.length === batchSize || i === list.length - 1) { + const batchResult = await handler(batch); + result.push(...batchResult); + batch = []; + } + } + + return result; + } }