diff --git a/packages/app/src/components/contract/InfoTable.vue b/packages/app/src/components/contract/InfoTable.vue index 27405d72f..a9cab847f 100644 --- a/packages/app/src/components/contract/InfoTable.vue +++ b/packages/app/src/components/contract/InfoTable.vue @@ -9,7 +9,7 @@ - + {{ t("contract.table.creator") }} diff --git a/packages/worker/src/app.module.ts b/packages/worker/src/app.module.ts index bec09a377..da46beea4 100644 --- a/packages/worker/src/app.module.ts +++ b/packages/worker/src/app.module.ts @@ -52,6 +52,7 @@ import { MetricsModule } from "./metrics"; import { DbMetricsService } from "./dbMetrics.service"; import { UnitOfWorkModule } from "./unitOfWork"; import { DataFetcherService } from "./dataFetcher/dataFetcher.service"; +import { SystemContractService } from "./contract/systemContract.service"; @Module({ imports: [ @@ -130,6 +131,7 @@ import { DataFetcherService } from "./dataFetcher/dataFetcher.service"; Logger, RetryDelayProvider, DbMetricsService, + SystemContractService, ], }) export class AppModule {} diff --git a/packages/worker/src/app.service.spec.ts b/packages/worker/src/app.service.spec.ts index 3d4afb836..60d5a8b29 100644 --- a/packages/worker/src/app.service.spec.ts +++ b/packages/worker/src/app.service.spec.ts @@ -13,6 +13,7 @@ import { BlocksRevertService } from "./blocksRevert"; import { TokenOffChainDataSaverService } from "./token/tokenOffChainData/tokenOffChainDataSaver.service"; import runMigrations from "./utils/runMigrations"; import { BLOCKS_REVERT_DETECTED_EVENT } from "./constants"; +import { SystemContractService } from "./contract/systemContract.service"; jest.mock("./utils/runMigrations"); @@ -39,6 +40,7 @@ describe("AppService", () => { let tokenOffChainDataSaverService: TokenOffChainDataSaverService; let dataSourceMock: DataSource; let configServiceMock: ConfigService; + let systemContractService: SystemContractService; beforeEach(async () => { balancesCleanerService = mock({ @@ -68,6 +70,9 @@ describe("AppService", () => { configServiceMock = mock({ get: jest.fn().mockReturnValue(false), }); + systemContractService = mock({ + addSystemContracts: jest.fn().mockResolvedValue(null), + }); const module = await Test.createTestingModule({ imports: [EventEmitterModule.forRoot()], @@ -106,6 +111,10 @@ describe("AppService", () => { provide: ConfigService, useValue: configServiceMock, }, + { + provide: SystemContractService, + useValue: systemContractService, + }, ], }).compile(); @@ -205,6 +214,13 @@ describe("AppService", () => { appService.onModuleDestroy(); expect(tokenOffChainDataSaverService.stop).toBeCalledTimes(1); }); + + it("adds system contracts", async () => { + appService.onModuleInit(); + await migrationsRunFinished; + expect(systemContractService.addSystemContracts).toBeCalledTimes(1); + appService.onModuleDestroy(); + }); }); describe("onModuleDestroy", () => { diff --git a/packages/worker/src/app.service.ts b/packages/worker/src/app.service.ts index d6ec852c7..71d82079e 100644 --- a/packages/worker/src/app.service.ts +++ b/packages/worker/src/app.service.ts @@ -10,6 +10,7 @@ import { CounterService } from "./counter"; import { BalancesCleanerService } from "./balance"; import { TokenOffChainDataSaverService } from "./token/tokenOffChainData/tokenOffChainDataSaver.service"; import runMigrations from "./utils/runMigrations"; +import { SystemContractService } from "./contract/systemContract.service"; @Injectable() export class AppService implements OnModuleInit, OnModuleDestroy { @@ -23,13 +24,15 @@ export class AppService implements OnModuleInit, OnModuleDestroy { private readonly balancesCleanerService: BalancesCleanerService, private readonly tokenOffChainDataSaverService: TokenOffChainDataSaverService, private readonly dataSource: DataSource, - private readonly configService: ConfigService + private readonly configService: ConfigService, + private readonly systemContractService: SystemContractService ) { this.logger = new Logger(AppService.name); } public onModuleInit() { runMigrations(this.dataSource, this.logger).then(() => { + this.systemContractService.addSystemContracts(); this.startWorkers(); }); } diff --git a/packages/worker/src/contract/systemContract.service.spec.ts b/packages/worker/src/contract/systemContract.service.spec.ts new file mode 100644 index 000000000..8e07ebaa9 --- /dev/null +++ b/packages/worker/src/contract/systemContract.service.spec.ts @@ -0,0 +1,107 @@ +import { mock } from "jest-mock-extended"; +import { Test, TestingModule } from "@nestjs/testing"; +import { Logger } from "@nestjs/common"; +import { BlockchainService } from "../blockchain/blockchain.service"; +import { AddressRepository } from "../repositories/address.repository"; +import { SystemContractService } from "./systemContract.service"; +import { Address } from "../entities"; + +describe("SystemContractService", () => { + let systemContractService: SystemContractService; + let blockchainServiceMock: BlockchainService; + let addressRepositoryMock: AddressRepository; + const systemContracts = SystemContractService.getSystemContracts(); + + beforeEach(async () => { + blockchainServiceMock = mock({ + getCode: jest.fn().mockImplementation((address: string) => Promise.resolve(`${address}-code`)), + }); + + addressRepositoryMock = mock({ + find: jest.fn(), + }); + + const app: TestingModule = await Test.createTestingModule({ + providers: [ + SystemContractService, + { + provide: BlockchainService, + useValue: blockchainServiceMock, + }, + { + provide: AddressRepository, + useValue: addressRepositoryMock, + }, + ], + }).compile(); + + app.useLogger(mock()); + systemContractService = app.get(SystemContractService); + }); + + describe("addSystemContracts", () => { + it("doesn't add any system contracts if they already exist in DB", async () => { + (addressRepositoryMock.find as jest.Mock).mockResolvedValue( + SystemContractService.getSystemContracts().map((contract) => mock
({ address: contract.address })) + ); + await systemContractService.addSystemContracts(); + expect(addressRepositoryMock.add).toBeCalledTimes(0); + }); + + it("adds all system contracts if none of them exist in the DB", async () => { + (addressRepositoryMock.find as jest.Mock).mockResolvedValue([]); + await systemContractService.addSystemContracts(); + expect(addressRepositoryMock.add).toBeCalledTimes(systemContracts.length); + for (const systemContract of systemContracts) { + expect(addressRepositoryMock.add).toBeCalledWith({ + address: systemContract.address, + bytecode: `${systemContract.address}-code`, + }); + } + }); + + it("adds only missing system contracts", async () => { + const existingContractAddresses = [ + "0x000000000000000000000000000000000000800d", + "0x0000000000000000000000000000000000008006", + ]; + (addressRepositoryMock.find as jest.Mock).mockResolvedValue( + existingContractAddresses.map((existingContractAddress) => mock
({ address: existingContractAddress })) + ); + await systemContractService.addSystemContracts(); + expect(addressRepositoryMock.add).toBeCalledTimes(systemContracts.length - existingContractAddresses.length); + for (const systemContract of systemContracts) { + if (!existingContractAddresses.includes(systemContract.address)) { + expect(addressRepositoryMock.add).toBeCalledWith({ + address: systemContract.address, + bytecode: `${systemContract.address}-code`, + }); + } + } + }); + + it("adds contracts only if they are deployed to the network", async () => { + const notDeployedSystemContracts = [ + "0x000000000000000000000000000000000000800d", + "0x0000000000000000000000000000000000008006", + ]; + (addressRepositoryMock.find as jest.Mock).mockResolvedValue([]); + (blockchainServiceMock.getCode as jest.Mock).mockImplementation(async (address: string) => { + if (notDeployedSystemContracts.includes(address)) { + return "0x"; + } + return `${address}-code`; + }); + await systemContractService.addSystemContracts(); + expect(addressRepositoryMock.add).toBeCalledTimes(systemContracts.length - notDeployedSystemContracts.length); + for (const systemContract of systemContracts) { + if (!notDeployedSystemContracts.includes(systemContract.address)) { + expect(addressRepositoryMock.add).toBeCalledWith({ + address: systemContract.address, + bytecode: `${systemContract.address}-code`, + }); + } + } + }); + }); +}); diff --git a/packages/worker/src/contract/systemContract.service.ts b/packages/worker/src/contract/systemContract.service.ts new file mode 100644 index 000000000..09a8b9304 --- /dev/null +++ b/packages/worker/src/contract/systemContract.service.ts @@ -0,0 +1,143 @@ +import { Injectable } from "@nestjs/common"; +import { BlockchainService } from "../blockchain/blockchain.service"; +import { AddressRepository } from "../repositories"; +import { In } from "typeorm"; + +@Injectable() +export class SystemContractService { + constructor( + private readonly addressRepository: AddressRepository, + private readonly blockchainService: BlockchainService + ) {} + + public async addSystemContracts(): Promise { + const systemContracts = SystemContractService.getSystemContracts(); + const existingContracts = await this.addressRepository.find({ + where: { + address: In(systemContracts.map((contract) => contract.address)), + }, + select: { + address: true, + }, + }); + + for (const contract of systemContracts) { + if (!existingContracts.find((existingContract) => existingContract.address === contract.address)) { + const bytecode = await this.blockchainService.getCode(contract.address); + // some contract might not exist on the environment yet + if (bytecode !== "0x") { + await this.addressRepository.add({ + address: contract.address, + bytecode, + }); + } + } + } + } + + public static getSystemContracts() { + // name field is never used, it's just for better readability & understanding + return [ + { + address: "0x0000000000000000000000000000000000000000", + name: "EmptyContract", + }, + { + address: "0x0000000000000000000000000000000000000001", + name: "Ecrecover", + }, + { + address: "0x0000000000000000000000000000000000000002", + name: "SHA256", + }, + { + address: "0x0000000000000000000000000000000000000006", + name: "EcAdd", + }, + { + address: "0x0000000000000000000000000000000000000007", + name: "EcMul", + }, + { + address: "0x0000000000000000000000000000000000000008", + name: "EcPairing", + }, + { + address: "0x0000000000000000000000000000000000008001", + name: "EmptyContract", + }, + { + address: "0x0000000000000000000000000000000000008002", + name: "AccountCodeStorage", + }, + { + address: "0x0000000000000000000000000000000000008003", + name: "NonceHolder", + }, + { + address: "0x0000000000000000000000000000000000008004", + name: "KnownCodesStorage", + }, + { + address: "0x0000000000000000000000000000000000008005", + name: "ImmutableSimulator", + }, + { + address: "0x0000000000000000000000000000000000008006", + name: "ContractDeployer", + }, + { + address: "0x0000000000000000000000000000000000008008", + name: "L1Messenger", + }, + { + address: "0x0000000000000000000000000000000000008009", + name: "MsgValueSimulator", + }, + { + address: "0x000000000000000000000000000000000000800a", + name: "L2BaseToken", + }, + { + address: "0x000000000000000000000000000000000000800b", + name: "SystemContext", + }, + { + address: "0x000000000000000000000000000000000000800c", + name: "BootloaderUtilities", + }, + { + address: "0x000000000000000000000000000000000000800d", + name: "EventWriter", + }, + { + address: "0x000000000000000000000000000000000000800e", + name: "Compressor", + }, + { + address: "0x000000000000000000000000000000000000800f", + name: "ComplexUpgrader", + }, + { + address: "0x0000000000000000000000000000000000008010", + name: "Keccak256", + }, + { + address: "0x0000000000000000000000000000000000008012", + name: "CodeOracle", + }, + { + address: "0x0000000000000000000000000000000000000100", + name: "P256Verify", + }, + { + address: "0x0000000000000000000000000000000000008011", + name: "PubdataChunkPublisher", + }, + { + address: "0x0000000000000000000000000000000000010000", + name: "Create2Factory", + }, + ]; + } +}