From 419767c05735c8160ef3e4e89b295ab5f9ab98c1 Mon Sep 17 00:00:00 2001 From: Rares <6453351+raresserban@users.noreply.github.com> Date: Tue, 5 Dec 2023 16:45:20 +0200 Subject: [PATCH 01/33] Remove unneeded nestjs template code, add apps for mvx event processor and axelar event processor and add prisma. --- .env.example | 7 + .gitignore | 6 +- .prettierrc | 3 +- README.md | 16 +- apps/api/config/config.devnet.yaml | 33 -- apps/api/config/config.mainnet.yaml | 33 -- apps/api/config/config.testnet.yaml | 33 -- apps/api/docs/swagger.md | 3 - .../api/src/endpoints/auth/auth.controller.ts | 18 -- .../src/endpoints/caching/cache.controller.ts | 67 ---- .../endpoints/caching/entities/cache.value.ts | 6 - .../endpoints/endpoints.controllers.module.ts | 20 -- .../endpoints/endpoints.services.module.ts | 19 -- .../test-sockets/test.socket.controller.ts | 14 - .../test-sockets/test.socket.module.ts | 36 --- .../test-sockets/test.socket.service.ts | 18 -- .../endpoints/tokens/schemas/token.schema.ts | 21 -- .../src/endpoints/tokens/token.controller.ts | 18 -- apps/api/src/endpoints/tokens/token.module.ts | 12 - .../api/src/endpoints/tokens/token.service.ts | 20 -- .../users/entities/dto/create.user.dto.ts | 9 - .../endpoints/users/entities/user.entity.ts | 16 - .../src/endpoints/users/user.controller.ts | 35 --- apps/api/src/endpoints/users/user.module.ts | 11 - apps/api/src/endpoints/users/user.service.ts | 37 --- apps/api/src/main.ts | 116 ------- apps/api/src/private.app.module.ts | 28 -- apps/api/src/public.app.module.ts | 20 -- apps/api/src/websockets/events.gateway.ts | 21 -- apps/api/src/websockets/pub.sub.controller.ts | 20 -- apps/api/src/websockets/pub.sub.module.ts | 18 -- apps/api/src/websockets/socket.adapter.ts | 15 - apps/api/test/app.e2e-spec.ts | 25 -- apps/api/test/jest-e2e.json | 17 - .../config/config.devnet.yaml | 4 + .../config/config.mainnet.yaml | 4 + .../config/config.testnet.yaml | 4 + .../config/configuration.ts | 0 .../config/relayer.proto | 16 + .../src/main.ts | 20 +- .../src/processor/index.ts | 0 .../processor/transaction.processor.module.ts | 0 .../transaction.processor.service.ts | 2 +- .../tsconfig.app.json | 0 apps/cache-warmer/config/config.devnet.yaml | 11 - apps/cache-warmer/config/config.mainnet.yaml | 10 - apps/cache-warmer/config/config.testnet.yaml | 10 - .../src/cache-warmer/cache.warmer.module.ts | 18 -- .../src/cache-warmer/cache.warmer.service.ts | 35 --- apps/cache-warmer/src/cache-warmer/index.ts | 2 - apps/cache-warmer/src/main.ts | 39 --- apps/cache-warmer/src/private.app.module.ts | 19 -- apps/cache-warmer/tsconfig.app.json | 16 - .../config/config.devnet.yaml | 7 + .../config/config.mainnet.yaml | 7 + .../config/config.testnet.yaml | 8 + .../config/configuration.ts | 0 .../event-processor/event.processor.module.ts | 21 ++ .../event.processor.service.ts | 50 +++ .../src/event-processor/index.ts | 2 + .../src/event-processor/types.ts | 14 + apps/mvx-event-processor/src/main.ts | 10 + .../tsconfig.app.json | 2 +- apps/queue-worker/config/config.devnet.yaml | 11 - apps/queue-worker/config/config.mainnet.yaml | 10 - apps/queue-worker/config/config.testnet.yaml | 10 - apps/queue-worker/config/configuration.ts | 11 - apps/queue-worker/src/main.ts | 39 --- apps/queue-worker/src/private.app.module.ts | 19 -- .../src/worker/bull.queue.module.ts | 20 -- apps/queue-worker/src/worker/index.ts | 3 - .../src/worker/queue.worker.module.ts | 23 -- .../src/worker/queue.worker.service.ts | 31 -- .../worker/queues/example.queue.service.ts | 18 -- .../config/config.devnet.yaml | 10 - .../config/config.mainnet.yaml | 10 - .../config/config.testnet.yaml | 10 - .../config/configuration.ts | 11 - .../src/private.app.module.ts | 19 -- apps/transactions-processor/tsconfig.app.json | 16 - docker-compose.yml | 21 +- libs/common/src/config/api.config.service.ts | 261 +++------------- libs/common/src/config/index.ts | 1 - .../config/sdk.nestjs.config.service.impl.ts | 30 -- libs/common/src/database/database.module.ts | 35 +-- libs/common/src/database/index.ts | 1 - libs/common/src/database/nosql.module.ts | 18 -- libs/common/src/database/prisma.service.ts | 9 + libs/common/src/entities/index.ts | 1 - libs/common/src/entities/query.paginations.ts | 4 - .../src/example/entities/example.filter.ts | 3 - libs/common/src/example/entities/example.ts | 9 - libs/common/src/example/entities/index.ts | 2 - libs/common/src/example/example.controller.ts | 47 --- libs/common/src/example/example.module.ts | 22 -- libs/common/src/example/example.service.ts | 294 ------------------ libs/common/src/example/index.ts | 4 - .../health-check/health.check.controller.ts | 9 - libs/common/src/health-check/index.ts | 1 - libs/common/src/index.ts | 4 - .../src/metrics/api.metrics.controller.ts | 14 - libs/common/src/metrics/api.metrics.module.ts | 17 - .../common/src/metrics/api.metrics.service.ts | 31 -- .../metrics/entities/elastic.metric.type.ts | 5 - libs/common/src/metrics/index.ts | 3 - libs/common/src/utils/cache.info.ts | 5 - libs/common/src/utils/dynamic.module.utils.ts | 60 +--- nest-cli.json | 60 +--- package-lock.json | 143 ++++++++- package.json | 94 +++--- prisma/schema.prisma | 11 + tsconfig.json | 2 +- 112 files changed, 453 insertions(+), 2161 deletions(-) create mode 100644 .env.example delete mode 100644 apps/api/config/config.devnet.yaml delete mode 100644 apps/api/config/config.mainnet.yaml delete mode 100644 apps/api/config/config.testnet.yaml delete mode 100644 apps/api/docs/swagger.md delete mode 100644 apps/api/src/endpoints/auth/auth.controller.ts delete mode 100644 apps/api/src/endpoints/caching/cache.controller.ts delete mode 100644 apps/api/src/endpoints/caching/entities/cache.value.ts delete mode 100644 apps/api/src/endpoints/endpoints.controllers.module.ts delete mode 100644 apps/api/src/endpoints/endpoints.services.module.ts delete mode 100644 apps/api/src/endpoints/test-sockets/test.socket.controller.ts delete mode 100644 apps/api/src/endpoints/test-sockets/test.socket.module.ts delete mode 100644 apps/api/src/endpoints/test-sockets/test.socket.service.ts delete mode 100644 apps/api/src/endpoints/tokens/schemas/token.schema.ts delete mode 100644 apps/api/src/endpoints/tokens/token.controller.ts delete mode 100644 apps/api/src/endpoints/tokens/token.module.ts delete mode 100644 apps/api/src/endpoints/tokens/token.service.ts delete mode 100644 apps/api/src/endpoints/users/entities/dto/create.user.dto.ts delete mode 100644 apps/api/src/endpoints/users/entities/user.entity.ts delete mode 100644 apps/api/src/endpoints/users/user.controller.ts delete mode 100644 apps/api/src/endpoints/users/user.module.ts delete mode 100644 apps/api/src/endpoints/users/user.service.ts delete mode 100644 apps/api/src/main.ts delete mode 100644 apps/api/src/private.app.module.ts delete mode 100644 apps/api/src/public.app.module.ts delete mode 100644 apps/api/src/websockets/events.gateway.ts delete mode 100644 apps/api/src/websockets/pub.sub.controller.ts delete mode 100644 apps/api/src/websockets/pub.sub.module.ts delete mode 100644 apps/api/src/websockets/socket.adapter.ts delete mode 100644 apps/api/test/app.e2e-spec.ts delete mode 100644 apps/api/test/jest-e2e.json create mode 100644 apps/axelar-event-processor/config/config.devnet.yaml create mode 100644 apps/axelar-event-processor/config/config.mainnet.yaml create mode 100644 apps/axelar-event-processor/config/config.testnet.yaml rename apps/{api => axelar-event-processor}/config/configuration.ts (100%) create mode 100644 apps/axelar-event-processor/config/relayer.proto rename apps/{transactions-processor => axelar-event-processor}/src/main.ts (59%) rename apps/{transactions-processor => axelar-event-processor}/src/processor/index.ts (100%) rename apps/{transactions-processor => axelar-event-processor}/src/processor/transaction.processor.module.ts (100%) rename apps/{transactions-processor => axelar-event-processor}/src/processor/transaction.processor.service.ts (95%) rename apps/{api => axelar-event-processor}/tsconfig.app.json (100%) delete mode 100644 apps/cache-warmer/config/config.devnet.yaml delete mode 100644 apps/cache-warmer/config/config.mainnet.yaml delete mode 100644 apps/cache-warmer/config/config.testnet.yaml delete mode 100644 apps/cache-warmer/src/cache-warmer/cache.warmer.module.ts delete mode 100644 apps/cache-warmer/src/cache-warmer/cache.warmer.service.ts delete mode 100644 apps/cache-warmer/src/cache-warmer/index.ts delete mode 100644 apps/cache-warmer/src/main.ts delete mode 100644 apps/cache-warmer/src/private.app.module.ts delete mode 100644 apps/cache-warmer/tsconfig.app.json create mode 100644 apps/mvx-event-processor/config/config.devnet.yaml create mode 100644 apps/mvx-event-processor/config/config.mainnet.yaml create mode 100644 apps/mvx-event-processor/config/config.testnet.yaml rename apps/{cache-warmer => mvx-event-processor}/config/configuration.ts (100%) create mode 100644 apps/mvx-event-processor/src/event-processor/event.processor.module.ts create mode 100644 apps/mvx-event-processor/src/event-processor/event.processor.service.ts create mode 100644 apps/mvx-event-processor/src/event-processor/index.ts create mode 100644 apps/mvx-event-processor/src/event-processor/types.ts create mode 100644 apps/mvx-event-processor/src/main.ts rename apps/{queue-worker => mvx-event-processor}/tsconfig.app.json (99%) delete mode 100644 apps/queue-worker/config/config.devnet.yaml delete mode 100644 apps/queue-worker/config/config.mainnet.yaml delete mode 100644 apps/queue-worker/config/config.testnet.yaml delete mode 100644 apps/queue-worker/config/configuration.ts delete mode 100644 apps/queue-worker/src/main.ts delete mode 100644 apps/queue-worker/src/private.app.module.ts delete mode 100644 apps/queue-worker/src/worker/bull.queue.module.ts delete mode 100644 apps/queue-worker/src/worker/index.ts delete mode 100644 apps/queue-worker/src/worker/queue.worker.module.ts delete mode 100644 apps/queue-worker/src/worker/queue.worker.service.ts delete mode 100644 apps/queue-worker/src/worker/queues/example.queue.service.ts delete mode 100644 apps/transactions-processor/config/config.devnet.yaml delete mode 100644 apps/transactions-processor/config/config.mainnet.yaml delete mode 100644 apps/transactions-processor/config/config.testnet.yaml delete mode 100644 apps/transactions-processor/config/configuration.ts delete mode 100644 apps/transactions-processor/src/private.app.module.ts delete mode 100644 apps/transactions-processor/tsconfig.app.json delete mode 100644 libs/common/src/config/sdk.nestjs.config.service.impl.ts delete mode 100644 libs/common/src/database/nosql.module.ts create mode 100644 libs/common/src/database/prisma.service.ts delete mode 100644 libs/common/src/entities/index.ts delete mode 100644 libs/common/src/entities/query.paginations.ts delete mode 100644 libs/common/src/example/entities/example.filter.ts delete mode 100644 libs/common/src/example/entities/example.ts delete mode 100644 libs/common/src/example/entities/index.ts delete mode 100644 libs/common/src/example/example.controller.ts delete mode 100644 libs/common/src/example/example.module.ts delete mode 100644 libs/common/src/example/example.service.ts delete mode 100644 libs/common/src/example/index.ts delete mode 100644 libs/common/src/health-check/health.check.controller.ts delete mode 100644 libs/common/src/health-check/index.ts delete mode 100644 libs/common/src/metrics/api.metrics.controller.ts delete mode 100644 libs/common/src/metrics/api.metrics.module.ts delete mode 100644 libs/common/src/metrics/api.metrics.service.ts delete mode 100644 libs/common/src/metrics/entities/elastic.metric.type.ts delete mode 100644 libs/common/src/metrics/index.ts create mode 100644 prisma/schema.prisma diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..1d63b2d --- /dev/null +++ b/.env.example @@ -0,0 +1,7 @@ +# Environment variables declared in this file are automatically made available to Prisma. +# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema + +# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB. +# See the documentation for all the connection string options: https://pris.ly/d/connection-strings + +DATABASE_URL=postgresql://root:password@localhost:5435/example diff --git a/.gitignore b/.gitignore index 84805fa..c0f6fa5 100644 --- a/.gitignore +++ b/.gitignore @@ -36,4 +36,8 @@ lerna-debug.log* # Configs **/*/config/config.yaml -**/*/config/config.custom.yaml \ No newline at end of file +**/*/config/config.custom.yaml + +.db/* + +.env diff --git a/.prettierrc b/.prettierrc index dcb7279..368186a 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,4 +1,5 @@ { "singleQuote": true, - "trailingComma": "all" + "trailingComma": "all", + "printWidth": 120 } \ No newline at end of file diff --git a/README.md b/README.md index 16ad402..fc80dc7 100644 --- a/README.md +++ b/README.md @@ -72,32 +72,32 @@ $ npm run start:mainnet ```bash # development watch mode on devnet -$ npm run start:transactions-processor:devnet:watch +$ npm run start:axelar-event-processor:devnet:watch # development debug mode on devnet -$ npm run start:transactions-processor:devnet:debug +$ npm run start:axelar-event-processor:devnet:debug # development mode on devnet -$ npm run start:transactions-processor:devnet +$ npm run start:axelar-event-processor:devnet # production mode -$ npm run start:transactions-processor:mainnet +$ npm run start:axelar-event-processor:mainnet ``` ## Running the queue-worker ```bash # development watch mode on devnet -$ npm run start:queue-worker:devnet:watch +$ npm run start:mvx-event-processor:devnet:watch # development debug mode on devnet -$ npm run start:queue-worker:devnet:debug +$ npm run start:mvx-event-processor:devnet:debug # development mode on devnet -$ npm run start:queue-worker:devnet +$ npm run start:mvx-event-processor:devnet # production mode -$ npm run start:queue-worker:mainnet +$ npm run start:mvx-event-processor:mainnet ``` Requests can be made to http://localhost:3001 for the api. The app will reload when you'll make edits (if opened in watch mode). You will also see any lint errors in the console.​ diff --git a/apps/api/config/config.devnet.yaml b/apps/api/config/config.devnet.yaml deleted file mode 100644 index 5a3e590..0000000 --- a/apps/api/config/config.devnet.yaml +++ /dev/null @@ -1,33 +0,0 @@ -urls: - api: 'https://devnet-api.multiversx.com' - swagger: - - 'https://devnet-microservice.multiversx.com' - - 'https://testnet-microservice.multiversx.com' - - 'https://microservice.multiversx.com' - redis: '127.0.0.1' -database: - host: 'localhost' - port: 3306 - username: 'root' - password: 'root' - name: 'example' -features: - publicApi: - enabled: true - port: 3000 - privateApi: - enabled: true - port: 4000 - keepAliveAgent: - enabled: true -nativeAuth: - maxExpirySeconds: - acceptedOrigins: - - utils.multiversx.com -security: - admins: -rateLimiterSecret: -keepAliveTimeout: - downstream: 61000 - upstream: 60000 -useCachingInterceptor: false diff --git a/apps/api/config/config.mainnet.yaml b/apps/api/config/config.mainnet.yaml deleted file mode 100644 index 4e58677..0000000 --- a/apps/api/config/config.mainnet.yaml +++ /dev/null @@ -1,33 +0,0 @@ -urls: - api: 'https://api.multiversx.com' - swagger: - - 'https://microservice.multiversx.com' - - 'https://devnet-microservice.multiversx.com' - - 'https://testnet-microservice.multiversx.com' - redis: '127.0.0.1' -database: - host: 'localhost' - port: 3306 - username: 'root' - password: 'root' - name: 'example' -features: - publicApi: - enabled: true - port: 3000 - privateApi: - enabled: true - port: 4000 - keepAliveAgent: - enabled: true -nativeAuth: - maxExpirySeconds: - acceptedOrigins: - - utils.multiversx.com -security: - admins: -rateLimiterSecret: -keepAliveTimeout: - downstream: 61000 - upstream: 60000 -useCachingInterceptor: false diff --git a/apps/api/config/config.testnet.yaml b/apps/api/config/config.testnet.yaml deleted file mode 100644 index eabf29c..0000000 --- a/apps/api/config/config.testnet.yaml +++ /dev/null @@ -1,33 +0,0 @@ -urls: - api: 'https://testnet-api.multiversx.com' - swagger: - - 'https://testnet-microservice.multiversx.com' - - 'https://devnet-microservice.multiversx.com' - - 'https://microservice.multiversx.com' - redis: '127.0.0.1' -database: - host: 'localhost' - port: 3306 - username: 'root' - password: 'root' - name: 'example' -features: - publicApi: - enabled: true - port: 3000 - privateApi: - enabled: true - port: 4000 - keepAliveAgent: - enabled: true -nativeAuth: - maxExpirySeconds: - acceptedOrigins: - - utils.multiversx.com -security: - admins: -rateLimiterSecret: -keepAliveTimeout: - downstream: 61000 - upstream: 60000 -useCachingInterceptor: false diff --git a/apps/api/docs/swagger.md b/apps/api/docs/swagger.md deleted file mode 100644 index 0e91a06..0000000 --- a/apps/api/docs/swagger.md +++ /dev/null @@ -1,3 +0,0 @@ -## Welcome the the MultiversX Microservice API! - -Here you can set your custom documentation in markdown format diff --git a/apps/api/src/endpoints/auth/auth.controller.ts b/apps/api/src/endpoints/auth/auth.controller.ts deleted file mode 100644 index d5177fc..0000000 --- a/apps/api/src/endpoints/auth/auth.controller.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { NativeAuthGuard, NativeAuth } from "@multiversx/sdk-nestjs-auth"; -import { Controller, Get, UseGuards } from "@nestjs/common"; -import { ApiResponse, ApiTags } from "@nestjs/swagger"; - -@Controller() -@ApiTags('auth') -export class AuthController { - @Get("/auth") - @UseGuards(NativeAuthGuard) - @ApiResponse({ - status: 200, - description: 'Authorizes the user and returns the encoded address', - }) - authorize(@NativeAuth('address') address: string - ): string { - return address; - } -} diff --git a/apps/api/src/endpoints/caching/cache.controller.ts b/apps/api/src/endpoints/caching/cache.controller.ts deleted file mode 100644 index 199ffe6..0000000 --- a/apps/api/src/endpoints/caching/cache.controller.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { Body, Controller, Delete, Get, HttpException, HttpStatus, Inject, Param, Put, Query, UseGuards } from "@nestjs/common"; -import { ClientProxy } from "@nestjs/microservices"; -import { ApiResponse } from "@nestjs/swagger"; -import { CacheValue } from "./entities/cache.value"; -import { NativeAuthAdminGuard, NativeAuthGuard } from "@multiversx/sdk-nestjs-auth"; -import { CacheService } from "@multiversx/sdk-nestjs-cache"; - -@Controller() -export class CacheController { - constructor( - private readonly cacheService: CacheService, - @Inject('PUBSUB_SERVICE') private clientProxy: ClientProxy, - ) { } - - @UseGuards(NativeAuthGuard, NativeAuthAdminGuard) - @Get("/caching/:key") - @ApiResponse({ - status: 200, - description: 'The cache value for one key', - type: String, - }) - @ApiResponse({ - status: 404, - description: 'Key not found', - }) - async getCache(@Param('key') key: string): Promise { - const value = await this.cacheService.getRemote(key); - if (!value) { - throw new HttpException('Key not found', HttpStatus.NOT_FOUND); - } - return JSON.stringify(value); - } - - @UseGuards(NativeAuthGuard, NativeAuthAdminGuard) - @Put("/caching/:key") - @ApiResponse({ - status: 200, - description: 'Key has been updated', - }) - async setCache(@Param('key') key: string, @Body() cacheValue: CacheValue) { - await this.cacheService.setRemote(key, cacheValue.value, cacheValue.ttl); - this.clientProxy.emit('deleteCacheKeys', [key]); - } - - @UseGuards(NativeAuthGuard, NativeAuthAdminGuard) - @Delete("/caching/:key") - @ApiResponse({ - status: 200, - description: 'Key has been deleted from cache', - }) - @ApiResponse({ - status: 404, - description: 'Key not found', - }) - async delCache(@Param('key') key: string) { - const keys = await this.cacheService.delete(key); - this.clientProxy.emit('deleteCacheKeys', keys); - } - - @UseGuards(NativeAuthGuard, NativeAuthAdminGuard) - @Get("/caching") - async getKeys( - @Query('keys') keys: string, - ): Promise { - return await this.cacheService.getKeys(keys); - } -} diff --git a/apps/api/src/endpoints/caching/entities/cache.value.ts b/apps/api/src/endpoints/caching/entities/cache.value.ts deleted file mode 100644 index d437ea8..0000000 --- a/apps/api/src/endpoints/caching/entities/cache.value.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Constants } from "@multiversx/sdk-nestjs-common"; - -export class CacheValue { - value?: string; - ttl: number = Constants.oneSecond() * 6; -} diff --git a/apps/api/src/endpoints/endpoints.controllers.module.ts b/apps/api/src/endpoints/endpoints.controllers.module.ts deleted file mode 100644 index 9b8c9f8..0000000 --- a/apps/api/src/endpoints/endpoints.controllers.module.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Module } from "@nestjs/common"; -import { DynamicModuleUtils } from "@mvx-monorepo/common"; -import { AuthController } from "./auth/auth.controller"; -import { EndpointsServicesModule } from "./endpoints.services.module"; -import { ExampleController, HealthCheckController } from "@mvx-monorepo/common"; -import { TokensController } from "./tokens/token.controller"; -import { UsersController } from "./users/user.controller"; - -@Module({ - imports: [ - EndpointsServicesModule, - ], - providers: [ - DynamicModuleUtils.getNestJsApiConfigService(), - ], - controllers: [ - AuthController, ExampleController, HealthCheckController, UsersController, TokensController, - ], -}) -export class EndpointsControllersModule { } diff --git a/apps/api/src/endpoints/endpoints.services.module.ts b/apps/api/src/endpoints/endpoints.services.module.ts deleted file mode 100644 index eb1328b..0000000 --- a/apps/api/src/endpoints/endpoints.services.module.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Module } from "@nestjs/common"; -import { ExampleModule } from "@mvx-monorepo/common"; -import { TestSocketModule } from "./test-sockets/test.socket.module"; -import { TokenModule } from "./tokens/token.module"; -import { UsersModule } from "./users/user.module"; -import configuration from "../../config/configuration"; - -@Module({ - imports: [ - ExampleModule.forRoot(configuration), - TestSocketModule, - UsersModule, - TokenModule, - ], - exports: [ - ExampleModule, TestSocketModule, UsersModule, TokenModule, - ], -}) -export class EndpointsServicesModule { } diff --git a/apps/api/src/endpoints/test-sockets/test.socket.controller.ts b/apps/api/src/endpoints/test-sockets/test.socket.controller.ts deleted file mode 100644 index acfef66..0000000 --- a/apps/api/src/endpoints/test-sockets/test.socket.controller.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Controller, Post } from "@nestjs/common"; -import { TestSocketService } from "./test.socket.service"; - -@Controller('test') -export class TestSocketController { - constructor( - private readonly testSocketService: TestSocketService, - ) { } - - @Post('socket') - async testSocket(): Promise { - await this.testSocketService.testSocket(); - } -} diff --git a/apps/api/src/endpoints/test-sockets/test.socket.module.ts b/apps/api/src/endpoints/test-sockets/test.socket.module.ts deleted file mode 100644 index b7c4de3..0000000 --- a/apps/api/src/endpoints/test-sockets/test.socket.module.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Module } from "@nestjs/common"; -import { ClientOptions, ClientProxyFactory, Transport } from "@nestjs/microservices"; -import { ApiConfigModule, ApiConfigService } from "@mvx-monorepo/common"; -import { TestSocketService } from "./test.socket.service"; -import configuration from "../../../config/configuration"; - -@Module({ - imports: [ - ApiConfigModule.forRoot(configuration), - ], - providers: [ - TestSocketService, - { - provide: 'PUBSUB_SERVICE', - useFactory: (apiConfigService: ApiConfigService) => { - const clientOptions: ClientOptions = { - transport: Transport.REDIS, - options: { - host: apiConfigService.getRedisUrl(), - port: 6379, - retryDelay: 1000, - retryAttempts: 10, - retryStrategy: () => 1000, - }, - }; - - return ClientProxyFactory.create(clientOptions); - }, - inject: [ApiConfigService], - }, - ], - exports: [ - TestSocketService, - ], -}) -export class TestSocketModule { } diff --git a/apps/api/src/endpoints/test-sockets/test.socket.service.ts b/apps/api/src/endpoints/test-sockets/test.socket.service.ts deleted file mode 100644 index 748be64..0000000 --- a/apps/api/src/endpoints/test-sockets/test.socket.service.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Inject, Injectable, Logger } from "@nestjs/common"; -import { ClientProxy } from "@nestjs/microservices"; - -@Injectable() -export class TestSocketService { - private readonly logger: Logger; - - constructor( - @Inject('PUBSUB_SERVICE') private clientProxy: ClientProxy, - ) { - this.logger = new Logger(TestSocketService.name); - } - - testSocket(): void { - this.logger.log('emitting onTest event'); - this.clientProxy.emit('onTest', { test: 'test' }); - } -} diff --git a/apps/api/src/endpoints/tokens/schemas/token.schema.ts b/apps/api/src/endpoints/tokens/schemas/token.schema.ts deleted file mode 100644 index 7d58c87..0000000 --- a/apps/api/src/endpoints/tokens/schemas/token.schema.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; -import { Document } from 'mongoose'; - -export type TokenDocument = Token & Document; - -@Schema() -export class Token { - @Prop({ required: true }) - identifier?: string; - - @Prop() - name?: string; - - @Prop() - accounts?: number; - - @Prop() - transactions?: number; -} - -export const TokenSchema = SchemaFactory.createForClass(Token); diff --git a/apps/api/src/endpoints/tokens/token.controller.ts b/apps/api/src/endpoints/tokens/token.controller.ts deleted file mode 100644 index 11e19c3..0000000 --- a/apps/api/src/endpoints/tokens/token.controller.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Body, Controller, Get, Post } from '@nestjs/common'; -import { Token } from './schemas/token.schema'; -import { TokenService } from './token.service'; - -@Controller('tokens') -export class TokensController { - constructor(private readonly tokenService: TokenService) { } - - @Post() - async create(@Body() token: Token) { - await this.tokenService.create(token); - } - - @Get() - async findAll(): Promise { - return await this.tokenService.findAll(); - } -} diff --git a/apps/api/src/endpoints/tokens/token.module.ts b/apps/api/src/endpoints/tokens/token.module.ts deleted file mode 100644 index 5a83059..0000000 --- a/apps/api/src/endpoints/tokens/token.module.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Module } from '@nestjs/common'; -import { MongooseModule } from '@nestjs/mongoose'; -import { NoSQLDatabaseModule } from '@mvx-monorepo/common'; -import { Token, TokenSchema } from './schemas/token.schema'; -import { TokenService } from './token.service'; - -@Module({ - imports: [NoSQLDatabaseModule, MongooseModule.forFeature([{ name: Token.name, schema: TokenSchema }])], - providers: [TokenService], - exports: [TokenService], -}) -export class TokenModule { } diff --git a/apps/api/src/endpoints/tokens/token.service.ts b/apps/api/src/endpoints/tokens/token.service.ts deleted file mode 100644 index 9cd5adc..0000000 --- a/apps/api/src/endpoints/tokens/token.service.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { InjectModel } from '@nestjs/mongoose'; -import { Model } from 'mongoose'; -import { Token, TokenDocument } from './schemas/token.schema'; - -@Injectable() -export class TokenService { - constructor( - @InjectModel(Token.name) private readonly tokenModel: Model, - ) { } - - async create(token: Token): Promise { - const createdToken = await this.tokenModel.create(token); - return createdToken; - } - - async findAll(): Promise { - return await this.tokenModel.find().exec(); - } -} diff --git a/apps/api/src/endpoints/users/entities/dto/create.user.dto.ts b/apps/api/src/endpoints/users/entities/dto/create.user.dto.ts deleted file mode 100644 index 12d0d5f..0000000 --- a/apps/api/src/endpoints/users/entities/dto/create.user.dto.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { ApiPropertyOptional } from "@nestjs/swagger"; - -export class CreateUserDto { - @ApiPropertyOptional() - firstName?: string; - - @ApiPropertyOptional() - lastName?: string; -} diff --git a/apps/api/src/endpoints/users/entities/user.entity.ts b/apps/api/src/endpoints/users/entities/user.entity.ts deleted file mode 100644 index 15eb138..0000000 --- a/apps/api/src/endpoints/users/entities/user.entity.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; - -@Entity('users') -export class User { - @PrimaryGeneratedColumn() - id?: number; - - @Column() - firstName?: string; - - @Column() - lastName?: string; - - @Column({ default: true }) - isActive?: boolean; -} diff --git a/apps/api/src/endpoints/users/user.controller.ts b/apps/api/src/endpoints/users/user.controller.ts deleted file mode 100644 index e6a8c5c..0000000 --- a/apps/api/src/endpoints/users/user.controller.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Body, Controller, Delete, Get, HttpException, HttpStatus, Param, Post } from '@nestjs/common'; -import { CreateUserDto } from './entities/dto/create.user.dto'; -import { User } from './entities/user.entity'; -import { UsersService } from './user.service'; - -@Controller('users') -export class UsersController { - constructor(private readonly usersService: UsersService) { } - - @Post() - create(@Body() createUserDto: CreateUserDto): Promise { - return this.usersService.create(createUserDto); - } - - @Get() - findAll(): Promise { - return this.usersService.findAll(); - } - - @Get(':id') - async findOne(@Param('id') id: number): Promise { - const user = await this.usersService.findOne(id); - - if (!user) { - throw new HttpException('User not found', HttpStatus.NOT_FOUND); - } - - return user; - } - - @Delete(':id') - remove(@Param('id') id: number): Promise { - return this.usersService.remove(id); - } -} diff --git a/apps/api/src/endpoints/users/user.module.ts b/apps/api/src/endpoints/users/user.module.ts deleted file mode 100644 index 8f6a95e..0000000 --- a/apps/api/src/endpoints/users/user.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Module } from '@nestjs/common'; -import { DatabaseModule } from '@mvx-monorepo/common'; -import { UsersService } from './user.service'; -import { User } from './entities/user.entity'; - -@Module({ - imports: [DatabaseModule.forRoot([User])], - providers: [UsersService], - exports: [UsersService], -}) -export class UsersModule { } diff --git a/apps/api/src/endpoints/users/user.service.ts b/apps/api/src/endpoints/users/user.service.ts deleted file mode 100644 index a6df6bd..0000000 --- a/apps/api/src/endpoints/users/user.service.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; -import { CreateUserDto } from './entities/dto/create.user.dto'; -import { User } from './entities/user.entity'; - -@Injectable() -export class UsersService { - constructor( - @InjectRepository(User) - private readonly usersRepository: Repository, - ) { } - - create(createUserDto: CreateUserDto): Promise { - const user = new User(); - user.firstName = createUserDto.firstName; - user.lastName = createUserDto.lastName; - - return this.usersRepository.save(user); - } - - async findAll(): Promise { - return await this.usersRepository.find(); - } - - async findOne(id: number): Promise { - return await this.usersRepository.findOne({ - where: { - id, - }, - }); - } - - async remove(id: number): Promise { - await this.usersRepository.delete(id); - } -} diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts deleted file mode 100644 index 4ab64a6..0000000 --- a/apps/api/src/main.ts +++ /dev/null @@ -1,116 +0,0 @@ -import 'module-alias/register'; -import { HttpAdapterHost, NestFactory } from '@nestjs/core'; -import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; -import { readFileSync } from 'fs'; -import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; -import { join } from 'path'; -import { ApiConfigService, SdkNestjsConfigServiceImpl } from '@mvx-monorepo/common'; -import { PrivateAppModule } from './private.app.module'; -import { PublicAppModule } from './public.app.module'; -import * as bodyParser from 'body-parser'; -import { Logger, NestInterceptor } from '@nestjs/common'; -import { MicroserviceOptions, Transport } from '@nestjs/microservices'; -import { SocketAdapter } from './websockets/socket.adapter'; -import cookieParser from 'cookie-parser'; -import { PubSubListenerModule } from '@mvx-monorepo/common'; -import { LoggingInterceptor, MetricsService } from '@multiversx/sdk-nestjs-monitoring'; -import { NativeAuthGuard } from '@multiversx/sdk-nestjs-auth'; -import { LoggerInitializer } from '@multiversx/sdk-nestjs-common'; -import { CacheService, CachingInterceptor } from '@multiversx/sdk-nestjs-cache'; - -import '@multiversx/sdk-nestjs-common/lib/utils/extensions/array.extensions'; -import '@multiversx/sdk-nestjs-common/lib/utils/extensions/date.extensions'; -import '@multiversx/sdk-nestjs-common/lib/utils/extensions/number.extensions'; -import '@multiversx/sdk-nestjs-common/lib/utils/extensions/string.extensions'; -import configuration from '../config/configuration'; - -async function bootstrap() { - const publicApp = await NestFactory.create(PublicAppModule); - publicApp.use(bodyParser.json({ limit: '1mb' })); - publicApp.enableCors(); - publicApp.useLogger(publicApp.get(WINSTON_MODULE_NEST_PROVIDER)); - publicApp.use(cookieParser()); - - const apiConfigService = publicApp.get(ApiConfigService); - const cachingService = publicApp.get(CacheService); - const metricsService = publicApp.get(MetricsService); - const httpAdapterHostService = publicApp.get(HttpAdapterHost); - - if (apiConfigService.getIsAuthActive()) { - publicApp.useGlobalGuards(new NativeAuthGuard(new SdkNestjsConfigServiceImpl(apiConfigService), cachingService)); - } - - const httpServer = httpAdapterHostService.httpAdapter.getHttpServer(); - httpServer.keepAliveTimeout = apiConfigService.getServerTimeout(); - httpServer.headersTimeout = apiConfigService.getHeadersTimeout(); //`keepAliveTimeout + server's expected response time` - - const globalInterceptors: NestInterceptor[] = []; - globalInterceptors.push(new LoggingInterceptor(metricsService)); - - if (apiConfigService.getUseCachingInterceptor()) { - const cachingInterceptor = new CachingInterceptor( - cachingService, - httpAdapterHostService, - metricsService, - ); - - globalInterceptors.push(cachingInterceptor); - } - - publicApp.useGlobalInterceptors(...globalInterceptors); - - const description = readFileSync(join(__dirname, '..', 'docs', 'swagger.md'), 'utf8'); - - let documentBuilder = new DocumentBuilder() - .setTitle('MultiversX Microservice API') - .setDescription(description) - .setVersion('1.0.0') - .setExternalDoc('MultiversX Docs', 'https://docs.multiversx.com'); - - const apiUrls = apiConfigService.getSwaggerUrls(); - for (const apiUrl of apiUrls) { - documentBuilder = documentBuilder.addServer(apiUrl); - } - - const config = documentBuilder.build(); - - const document = SwaggerModule.createDocument(publicApp, config); - SwaggerModule.setup('', publicApp, document); - - if (apiConfigService.getIsPublicApiFeatureActive()) { - await publicApp.listen(apiConfigService.getPublicApiFeaturePort()); - } - - if (apiConfigService.getIsPrivateApiFeatureActive()) { - const privateApp = await NestFactory.create(PrivateAppModule); - await privateApp.listen(apiConfigService.getPrivateApiFeaturePort()); - } - - const logger = new Logger('Bootstrapper'); - - LoggerInitializer.initialize(logger); - - const pubSubApp = await NestFactory.createMicroservice( - PubSubListenerModule.forRoot(configuration), - { - transport: Transport.REDIS, - options: { - host: apiConfigService.getRedisUrl(), - port: 6379, - retryAttempts: 100, - retryDelay: 1000, - retryStrategy: () => 1000, - }, - }, - ); - pubSubApp.useLogger(pubSubApp.get(WINSTON_MODULE_NEST_PROVIDER)); - pubSubApp.useWebSocketAdapter(new SocketAdapter(pubSubApp)); - // eslint-disable-next-line @typescript-eslint/no-floating-promises - pubSubApp.listen(); - - logger.log(`Public API active: ${apiConfigService.getIsPublicApiFeatureActive()}`); - logger.log(`Private API active: ${apiConfigService.getIsPrivateApiFeatureActive()}`); -} - -// eslint-disable-next-line @typescript-eslint/no-floating-promises -bootstrap(); diff --git a/apps/api/src/private.app.module.ts b/apps/api/src/private.app.module.ts deleted file mode 100644 index 514c59e..0000000 --- a/apps/api/src/private.app.module.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Module } from '@nestjs/common'; -import { TestSocketController } from './endpoints/test-sockets/test.socket.controller'; -import { TestSocketModule } from './endpoints/test-sockets/test.socket.module'; -import { CacheController } from './endpoints/caching/cache.controller'; -import { ApiMetricsController, HealthCheckController } from '@mvx-monorepo/common'; -import { ApiMetricsModule, DynamicModuleUtils } from '@mvx-monorepo/common'; -import { LoggingModule } from '@multiversx/sdk-nestjs-common'; -import configuration from '../config/configuration'; - -@Module({ - imports: [ - LoggingModule, - ApiMetricsModule, - DynamicModuleUtils.getCachingModule(configuration), - TestSocketModule, - ], - providers: [ - DynamicModuleUtils.getNestJsApiConfigService(), - DynamicModuleUtils.getPubSubService(), - ], - controllers: [ - ApiMetricsController, - CacheController, - HealthCheckController, - TestSocketController, - ], -}) -export class PrivateAppModule { } diff --git a/apps/api/src/public.app.module.ts b/apps/api/src/public.app.module.ts deleted file mode 100644 index 34eec3e..0000000 --- a/apps/api/src/public.app.module.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Module } from '@nestjs/common'; -import { EndpointsServicesModule } from './endpoints/endpoints.services.module'; -import { EndpointsControllersModule } from './endpoints/endpoints.controllers.module'; -import { DynamicModuleUtils } from '@mvx-monorepo/common'; -import { LoggingModule } from '@multiversx/sdk-nestjs-common'; - -@Module({ - imports: [ - LoggingModule, - EndpointsServicesModule, - EndpointsControllersModule, - ], - providers: [ - DynamicModuleUtils.getNestJsApiConfigService(), - ], - exports: [ - EndpointsServicesModule, - ], -}) -export class PublicAppModule { } diff --git a/apps/api/src/websockets/events.gateway.ts b/apps/api/src/websockets/events.gateway.ts deleted file mode 100644 index 8328bc1..0000000 --- a/apps/api/src/websockets/events.gateway.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Injectable, Logger } from "@nestjs/common"; -import { WebSocketGateway, WebSocketServer } from "@nestjs/websockets"; -import { Server } from 'socket.io'; - -@Injectable() -@WebSocketGateway(3005) -export class EventsGateway { - private readonly logger: Logger; - - constructor() { - this.logger = new Logger(EventsGateway.name); - } - - @WebSocketServer() - webSocketServer: Server | undefined; - - onTest(payload: unknown) { - this.logger.log(`Received onTest event with payload '${JSON.stringify(payload)}'`); - this.webSocketServer?.emit('test', payload); - } -} diff --git a/apps/api/src/websockets/pub.sub.controller.ts b/apps/api/src/websockets/pub.sub.controller.ts deleted file mode 100644 index 92e91e4..0000000 --- a/apps/api/src/websockets/pub.sub.controller.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Controller, Logger } from "@nestjs/common"; -import { EventPattern } from "@nestjs/microservices"; -import { EventsGateway } from "./events.gateway"; - -@Controller() -export class PubSubController { - private readonly logger: Logger; - - constructor( - private readonly eventsGateway: EventsGateway, - ) { - this.logger = new Logger(PubSubController.name); - } - - @EventPattern('onTest') - onTest(payload: unknown) { - this.logger.log(`Notifying onTest with payload '${JSON.stringify(payload)}'`); - this.eventsGateway.onTest(payload); - } -} diff --git a/apps/api/src/websockets/pub.sub.module.ts b/apps/api/src/websockets/pub.sub.module.ts deleted file mode 100644 index 75e9b69..0000000 --- a/apps/api/src/websockets/pub.sub.module.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Module } from '@nestjs/common'; -import { PubSubListenerController, PubSubListenerModule } from '@mvx-monorepo/common'; -import { EventsGateway } from './events.gateway'; -import { PubSubController } from './pub.sub.controller'; -import configuration from '../../config/configuration'; - -@Module({ - imports: [ - PubSubListenerModule.forRoot(configuration), - ], - controllers: [ - PubSubController, PubSubListenerController, - ], - providers: [ - EventsGateway, - ], -}) -export class PubSubModule { } diff --git a/apps/api/src/websockets/socket.adapter.ts b/apps/api/src/websockets/socket.adapter.ts deleted file mode 100644 index 78b0ea1..0000000 --- a/apps/api/src/websockets/socket.adapter.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { IoAdapter } from '@nestjs/platform-socket.io'; -import { ServerOptions } from 'socket.io'; - -export class SocketAdapter extends IoAdapter { - createIOServer( - port: number, - options?: ServerOptions & { - namespace?: string; - server?: unknown; - }, - ) { - const server = super.createIOServer(port, { ...options, cors: true }); - return server; - } -} diff --git a/apps/api/test/app.e2e-spec.ts b/apps/api/test/app.e2e-spec.ts deleted file mode 100644 index 732a89c..0000000 --- a/apps/api/test/app.e2e-spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import 'module-alias/register'; -import { Test, TestingModule } from '@nestjs/testing'; -import { INestApplication } from '@nestjs/common'; -import * as request from 'supertest'; -import { PublicAppModule } from '../src/public.app.module'; - -describe('AppController (e2e)', () => { - let app: INestApplication; - - beforeEach(async () => { - const moduleFixture: TestingModule = await Test.createTestingModule({ - imports: [PublicAppModule], - }).compile(); - - app = moduleFixture.createNestApplication(); - await app.init(); - }); - - it('/ (GET)', () => { - return request.default(app.getHttpServer()) - .get('/hello') - .expect(200) - .expect('hello'); - }); -}); diff --git a/apps/api/test/jest-e2e.json b/apps/api/test/jest-e2e.json deleted file mode 100644 index eb4e621..0000000 --- a/apps/api/test/jest-e2e.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "moduleFileExtensions": [ - "js", - "json", - "ts" - ], - "rootDir": ".", - "testEnvironment": "node", - "testRegex": ".e2e-spec.ts$", - "transform": { - "^.+\\.(t|j)s$": "ts-jest" - }, - "moduleNameMapper": { - "^@mvx-monorepo/common(|/.*)$": "/../../../libs/common/src/$1", - "^@mvx-monorepo/common": "/../../../libs/common" - } -} \ No newline at end of file diff --git a/apps/axelar-event-processor/config/config.devnet.yaml b/apps/axelar-event-processor/config/config.devnet.yaml new file mode 100644 index 0000000..32e511e --- /dev/null +++ b/apps/axelar-event-processor/config/config.devnet.yaml @@ -0,0 +1,4 @@ +urls: + api: 'https://devnet-api.multiversx.com' + redis: '127.0.0.1' + axelarApi: 'localhost:5000' diff --git a/apps/axelar-event-processor/config/config.mainnet.yaml b/apps/axelar-event-processor/config/config.mainnet.yaml new file mode 100644 index 0000000..6749a74 --- /dev/null +++ b/apps/axelar-event-processor/config/config.mainnet.yaml @@ -0,0 +1,4 @@ +urls: + api: 'https://api.multiversx.com' + redis: '127.0.0.1' + axelarApi: 'localhost:5000' diff --git a/apps/axelar-event-processor/config/config.testnet.yaml b/apps/axelar-event-processor/config/config.testnet.yaml new file mode 100644 index 0000000..d163767 --- /dev/null +++ b/apps/axelar-event-processor/config/config.testnet.yaml @@ -0,0 +1,4 @@ +urls: + api: 'https://testnet-api.multiversx.com' + redis: '127.0.0.1' + axelarApi: 'localhost:5000' diff --git a/apps/api/config/configuration.ts b/apps/axelar-event-processor/config/configuration.ts similarity index 100% rename from apps/api/config/configuration.ts rename to apps/axelar-event-processor/config/configuration.ts diff --git a/apps/axelar-event-processor/config/relayer.proto b/apps/axelar-event-processor/config/relayer.proto new file mode 100644 index 0000000..0c72d9f --- /dev/null +++ b/apps/axelar-event-processor/config/relayer.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +package axelar; + +service RelayerService { + rpc GetPayload (MessageById) returns (Payload) {} +} + +message MessageById { + int32 id = 1; +} + +message Payload { + int32 id = 1; + string name = 2; +} diff --git a/apps/transactions-processor/src/main.ts b/apps/axelar-event-processor/src/main.ts similarity index 59% rename from apps/transactions-processor/src/main.ts rename to apps/axelar-event-processor/src/main.ts index d029ee8..33a9423 100644 --- a/apps/transactions-processor/src/main.ts +++ b/apps/axelar-event-processor/src/main.ts @@ -2,31 +2,23 @@ import 'module-alias/register'; import { NestFactory } from '@nestjs/core'; import { TransactionProcessorModule } from './processor'; import { ApiConfigService, PubSubListenerModule } from '@mvx-monorepo/common'; -import { PrivateAppModule } from './private.app.module'; import configuration from '../config/configuration'; import { MicroserviceOptions, Transport } from '@nestjs/microservices'; import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; +import { join } from 'path'; async function bootstrap() { - const transactionProcessorApp = await NestFactory.create(TransactionProcessorModule); + const transactionProcessorApp = await NestFactory.createApplicationContext(TransactionProcessorModule); const apiConfigService = transactionProcessorApp.get(ApiConfigService); - await transactionProcessorApp.listen(apiConfigService.getTransactionProcessorFeaturePort()); - - if (apiConfigService.getIsPrivateApiFeatureActive()) { - const privateApp = await NestFactory.create(PrivateAppModule); - await privateApp.listen(apiConfigService.getPrivateApiFeaturePort()); - } const pubSubApp = await NestFactory.createMicroservice( PubSubListenerModule.forRoot(configuration), { - transport: Transport.REDIS, + transport: Transport.GRPC, options: { - host: apiConfigService.getRedisUrl(), - port: 6379, - retryAttempts: 100, - retryDelay: 1000, - retryStrategy: () => 1000, + package: 'axelar', + protoPath: join(__dirname, '../config/relayer.proto'), + url: apiConfigService.getAxelarApiUrl(), }, }, ); diff --git a/apps/transactions-processor/src/processor/index.ts b/apps/axelar-event-processor/src/processor/index.ts similarity index 100% rename from apps/transactions-processor/src/processor/index.ts rename to apps/axelar-event-processor/src/processor/index.ts diff --git a/apps/transactions-processor/src/processor/transaction.processor.module.ts b/apps/axelar-event-processor/src/processor/transaction.processor.module.ts similarity index 100% rename from apps/transactions-processor/src/processor/transaction.processor.module.ts rename to apps/axelar-event-processor/src/processor/transaction.processor.module.ts diff --git a/apps/transactions-processor/src/processor/transaction.processor.service.ts b/apps/axelar-event-processor/src/processor/transaction.processor.service.ts similarity index 95% rename from apps/transactions-processor/src/processor/transaction.processor.service.ts rename to apps/axelar-event-processor/src/processor/transaction.processor.service.ts index 0880109..b355bfe 100644 --- a/apps/transactions-processor/src/processor/transaction.processor.service.ts +++ b/apps/axelar-event-processor/src/processor/transaction.processor.service.ts @@ -23,7 +23,7 @@ export class TransactionProcessorService { await Locker.lock('newTransactions', async () => { await this.transactionProcessor.start({ gatewayUrl: this.apiConfigService.getApiUrl(), - maxLookBehind: this.apiConfigService.getTransactionProcessorMaxLookBehind(), + maxLookBehind: 100, // eslint-disable-next-line require-await onTransactionsReceived: async (shardId, nonce, transactions, statistics) => { this.logger.log(`Received ${transactions.length} transactions on shard ${shardId} and nonce ${nonce}. Time left: ${statistics.secondsLeft}`); diff --git a/apps/api/tsconfig.app.json b/apps/axelar-event-processor/tsconfig.app.json similarity index 100% rename from apps/api/tsconfig.app.json rename to apps/axelar-event-processor/tsconfig.app.json diff --git a/apps/cache-warmer/config/config.devnet.yaml b/apps/cache-warmer/config/config.devnet.yaml deleted file mode 100644 index 40c3321..0000000 --- a/apps/cache-warmer/config/config.devnet.yaml +++ /dev/null @@ -1,11 +0,0 @@ -urls: - api: 'https://devnet-api.multiversx.com' - redis: '127.0.0.1' -features: - cacheWarmer: - enabled: true - port: 5201 - privateApi: - enabled: false - port: 4000 - diff --git a/apps/cache-warmer/config/config.mainnet.yaml b/apps/cache-warmer/config/config.mainnet.yaml deleted file mode 100644 index a72cc88..0000000 --- a/apps/cache-warmer/config/config.mainnet.yaml +++ /dev/null @@ -1,10 +0,0 @@ -urls: - api: 'https://api.multiversx.com' - redis: '127.0.0.1' -features: - cacheWarmer: - enabled: true - port: 5201 - privateApi: - enabled: true - port: 4000 \ No newline at end of file diff --git a/apps/cache-warmer/config/config.testnet.yaml b/apps/cache-warmer/config/config.testnet.yaml deleted file mode 100644 index a554a59..0000000 --- a/apps/cache-warmer/config/config.testnet.yaml +++ /dev/null @@ -1,10 +0,0 @@ -urls: - api: 'https://testnet-api.multiversx.com' - redis: '127.0.0.1' -features: - cacheWarmer: - enabled: true - port: 5201 - privateApi: - enabled: true - port: 4000 \ No newline at end of file diff --git a/apps/cache-warmer/src/cache-warmer/cache.warmer.module.ts b/apps/cache-warmer/src/cache-warmer/cache.warmer.module.ts deleted file mode 100644 index 23684c3..0000000 --- a/apps/cache-warmer/src/cache-warmer/cache.warmer.module.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Module } from '@nestjs/common'; -import { ScheduleModule } from '@nestjs/schedule'; -import { ExampleModule } from '@mvx-monorepo/common'; -import { DynamicModuleUtils } from '@mvx-monorepo/common/utils/dynamic.module.utils'; -import { CacheWarmerService } from './cache.warmer.service'; -import configuration from '../../config/configuration'; - -@Module({ - imports: [ - ScheduleModule.forRoot(), - ExampleModule.forRoot(configuration), - ], - providers: [ - DynamicModuleUtils.getPubSubService(), - CacheWarmerService, - ], -}) -export class CacheWarmerModule { } diff --git a/apps/cache-warmer/src/cache-warmer/cache.warmer.service.ts b/apps/cache-warmer/src/cache-warmer/cache.warmer.service.ts deleted file mode 100644 index 3942baf..0000000 --- a/apps/cache-warmer/src/cache-warmer/cache.warmer.service.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Inject, Injectable } from "@nestjs/common"; -import { Cron } from "@nestjs/schedule"; -import { ClientProxy } from "@nestjs/microservices"; -import { ExampleService } from "@mvx-monorepo/common"; -import { CacheService } from "@multiversx/sdk-nestjs-cache"; -import { Locker } from "@multiversx/sdk-nestjs-common"; -import { CacheInfo } from "@mvx-monorepo/common/utils/cache.info"; - -@Injectable() -export class CacheWarmerService { - constructor( - private readonly cachingService: CacheService, - @Inject('PUBSUB_SERVICE') private clientProxy: ClientProxy, - private readonly exampleService: ExampleService, - ) { } - - @Cron('* * * * *') - async handleExampleInvalidations() { - await Locker.lock('Example invalidations', async () => { - const examples = await this.exampleService.getAllExamplesRaw(); - await this.invalidateKey(CacheInfo.Examples.key, examples, CacheInfo.Examples.ttl); - }, true); - } - - private async invalidateKey(key: string, data: T, ttl: number) { - await Promise.all([ - this.cachingService.set(key, data, ttl), - this.deleteCacheKey(key), - ]); - } - - private async deleteCacheKey(key: string) { - await this.clientProxy.emit('deleteCacheKeys', [key]); - } -} diff --git a/apps/cache-warmer/src/cache-warmer/index.ts b/apps/cache-warmer/src/cache-warmer/index.ts deleted file mode 100644 index ba407fc..0000000 --- a/apps/cache-warmer/src/cache-warmer/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './cache.warmer.module'; -export * from './cache.warmer.service'; diff --git a/apps/cache-warmer/src/main.ts b/apps/cache-warmer/src/main.ts deleted file mode 100644 index ce8ac33..0000000 --- a/apps/cache-warmer/src/main.ts +++ /dev/null @@ -1,39 +0,0 @@ -import 'module-alias/register'; -import { NestFactory } from '@nestjs/core'; -import { ApiConfigService, PubSubListenerModule } from '@mvx-monorepo/common'; -import { CacheWarmerModule } from './cache-warmer'; -import { PrivateAppModule } from './private.app.module'; -import { MicroserviceOptions, Transport } from '@nestjs/microservices'; -import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; -import configuration from '../config/configuration'; - -async function bootstrap() { - const cacheWarmerApp = await NestFactory.create(CacheWarmerModule); - const apiConfigService = cacheWarmerApp.get(ApiConfigService); - await cacheWarmerApp.listen(apiConfigService.getCacheWarmerFeaturePort()); - - if (apiConfigService.getIsPrivateApiFeatureActive()) { - const privateApp = await NestFactory.create(PrivateAppModule); - await privateApp.listen(apiConfigService.getPrivateApiFeaturePort()); - } - - const pubSubApp = await NestFactory.createMicroservice( - PubSubListenerModule.forRoot(configuration), - { - transport: Transport.REDIS, - options: { - host: apiConfigService.getRedisUrl(), - port: 6379, - retryAttempts: 100, - retryDelay: 1000, - retryStrategy: () => 1000, - }, - }, - ); - pubSubApp.useLogger(pubSubApp.get(WINSTON_MODULE_NEST_PROVIDER)); - // eslint-disable-next-line @typescript-eslint/no-floating-promises - pubSubApp.listen(); -} - -// eslint-disable-next-line @typescript-eslint/no-floating-promises -bootstrap(); diff --git a/apps/cache-warmer/src/private.app.module.ts b/apps/cache-warmer/src/private.app.module.ts deleted file mode 100644 index 4e16f1b..0000000 --- a/apps/cache-warmer/src/private.app.module.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Module } from '@nestjs/common'; -import { ApiConfigModule, ApiMetricsController, HealthCheckController } from '@mvx-monorepo/common'; -import { ApiMetricsModule } from '@mvx-monorepo/common'; -import { LoggingModule } from '@multiversx/sdk-nestjs-common'; -import configuration from '../config/configuration'; - -@Module({ - imports: [ - LoggingModule, - ApiMetricsModule, - ApiConfigModule.forRoot(configuration), - ], - providers: [], - controllers: [ - ApiMetricsController, - HealthCheckController, - ], -}) -export class PrivateAppModule { } diff --git a/apps/cache-warmer/tsconfig.app.json b/apps/cache-warmer/tsconfig.app.json deleted file mode 100644 index ab81e80..0000000 --- a/apps/cache-warmer/tsconfig.app.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "declaration": false, - "outDir": "../../dist" - }, - "include": [ - "src/**/*" - ], - "exclude": [ - "node_modules", - "dist", - "test", - "**/*spec.ts" - ] -} \ No newline at end of file diff --git a/apps/mvx-event-processor/config/config.devnet.yaml b/apps/mvx-event-processor/config/config.devnet.yaml new file mode 100644 index 0000000..1ff98a3 --- /dev/null +++ b/apps/mvx-event-processor/config/config.devnet.yaml @@ -0,0 +1,7 @@ +urls: + api: 'https://devnet-api.multiversx.com' + redis: '127.0.0.1' + eventsNotifier: '' +eventsNotifier: + queue: '' + gatewayAddress: '' diff --git a/apps/mvx-event-processor/config/config.mainnet.yaml b/apps/mvx-event-processor/config/config.mainnet.yaml new file mode 100644 index 0000000..1ff98a3 --- /dev/null +++ b/apps/mvx-event-processor/config/config.mainnet.yaml @@ -0,0 +1,7 @@ +urls: + api: 'https://devnet-api.multiversx.com' + redis: '127.0.0.1' + eventsNotifier: '' +eventsNotifier: + queue: '' + gatewayAddress: '' diff --git a/apps/mvx-event-processor/config/config.testnet.yaml b/apps/mvx-event-processor/config/config.testnet.yaml new file mode 100644 index 0000000..8a84f2e --- /dev/null +++ b/apps/mvx-event-processor/config/config.testnet.yaml @@ -0,0 +1,8 @@ +urls: + api: 'https://devnet-api.multiversx.com' + redis: '127.0.0.1' + eventsNotifier: '' +eventsNotifierQueue: '' +eventsNotifier: + queue: '' + gatewayAddress: '' diff --git a/apps/cache-warmer/config/configuration.ts b/apps/mvx-event-processor/config/configuration.ts similarity index 100% rename from apps/cache-warmer/config/configuration.ts rename to apps/mvx-event-processor/config/configuration.ts diff --git a/apps/mvx-event-processor/src/event-processor/event.processor.module.ts b/apps/mvx-event-processor/src/event-processor/event.processor.module.ts new file mode 100644 index 0000000..b93e765 --- /dev/null +++ b/apps/mvx-event-processor/src/event-processor/event.processor.module.ts @@ -0,0 +1,21 @@ +import { RabbitModule, RabbitModuleOptions } from '@multiversx/sdk-nestjs-rabbitmq'; +import { Module } from '@nestjs/common'; +import { EventProcessorService } from './event.processor.service'; +import { ApiConfigModule, ApiConfigService } from '@mvx-monorepo/common'; +import configuration from '../../config/configuration'; + +@Module({ + imports: [ + ApiConfigModule.forRoot(configuration), + RabbitModule.forRootAsync({ + useFactory: (apiConfigService: ApiConfigService) => + new RabbitModuleOptions(apiConfigService.getEventsNotifierUrl(), [], { + timeout: 30000, + }), + inject: [ApiConfigService], + }), + ], + providers: [EventProcessorService], + exports: [EventProcessorService], +}) +export class EventProcessorModule {} diff --git a/apps/mvx-event-processor/src/event-processor/event.processor.service.ts b/apps/mvx-event-processor/src/event-processor/event.processor.service.ts new file mode 100644 index 0000000..3958e1a --- /dev/null +++ b/apps/mvx-event-processor/src/event-processor/event.processor.service.ts @@ -0,0 +1,50 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { ApiConfigService } from '@mvx-monorepo/common'; +import { NotifierBlockEvent, NotifierEvent } from './types'; +import { RabbitSubscribe } from '@golevelup/nestjs-rabbitmq'; +import configuration from '../../config/configuration'; + +@Injectable() +export class EventProcessorService { + private readonly logger: Logger; + + constructor(private readonly apiConfigService: ApiConfigService) { + this.logger = new Logger(EventProcessorService.name); + } + + @RabbitSubscribe({ + queue: configuration().eventsNotifier.queue, + createQueueIfNotExists: false, + }) + async consumeEvents(blockEvent: NotifierBlockEvent) { + try { + for (const event of blockEvent.events) { + await this.handleEvent(event); + } + } catch (error) { + this.logger.error( + `An unhandled error occurred when consuming events from block with hash ${blockEvent.hash}: ${JSON.stringify( + blockEvent.events, + )}`, + ); + this.logger.error(error); + + throw error; + } + } + + // TODO: Implement logic + private handleEvent(event: NotifierEvent) { + this.logger.log('Received event from MultiversX Gateway contract:'); + this.logger.log(JSON.stringify(event)); + + if (event.address !== this.apiConfigService.getEventsNotifierGatewayAddress()) { + return; + } + + if (event.identifier === 'callContract') { + this.logger.log('Received callContract event from MultiversX Gateway contract:'); + this.logger.log(JSON.stringify(event)); + } + } +} diff --git a/apps/mvx-event-processor/src/event-processor/index.ts b/apps/mvx-event-processor/src/event-processor/index.ts new file mode 100644 index 0000000..a8e25b1 --- /dev/null +++ b/apps/mvx-event-processor/src/event-processor/index.ts @@ -0,0 +1,2 @@ +export * from './event.processor.module'; +export * from './event.processor.service'; diff --git a/apps/mvx-event-processor/src/event-processor/types.ts b/apps/mvx-event-processor/src/event-processor/types.ts new file mode 100644 index 0000000..b139e96 --- /dev/null +++ b/apps/mvx-event-processor/src/event-processor/types.ts @@ -0,0 +1,14 @@ +export interface NotifierBlockEvent { + hash: string; + shardId: number; + timestamp: Number; + events: NotifierEvent[]; +} + +export interface NotifierEvent { + txHash: string; + address: string; + identifier: string; + data: string; + topics: string[]; +} diff --git a/apps/mvx-event-processor/src/main.ts b/apps/mvx-event-processor/src/main.ts new file mode 100644 index 0000000..2b285ef --- /dev/null +++ b/apps/mvx-event-processor/src/main.ts @@ -0,0 +1,10 @@ +import 'module-alias/register'; +import { NestFactory } from '@nestjs/core'; +import { EventProcessorModule } from './event-processor'; + +async function bootstrap() { + await NestFactory.createApplicationContext(EventProcessorModule); +} + +// eslint-disable-next-line @typescript-eslint/no-floating-promises +bootstrap(); diff --git a/apps/queue-worker/tsconfig.app.json b/apps/mvx-event-processor/tsconfig.app.json similarity index 99% rename from apps/queue-worker/tsconfig.app.json rename to apps/mvx-event-processor/tsconfig.app.json index ab81e80..e83a6e9 100644 --- a/apps/queue-worker/tsconfig.app.json +++ b/apps/mvx-event-processor/tsconfig.app.json @@ -13,4 +13,4 @@ "test", "**/*spec.ts" ] -} \ No newline at end of file +} diff --git a/apps/queue-worker/config/config.devnet.yaml b/apps/queue-worker/config/config.devnet.yaml deleted file mode 100644 index f364fc7..0000000 --- a/apps/queue-worker/config/config.devnet.yaml +++ /dev/null @@ -1,11 +0,0 @@ -urls: - api: 'https://devnet-api.multiversx.com' - redis: '127.0.0.1' -features: - queueWorker: - enabled: true - port: 8000 - privateApi: - enabled: true - port: 4000 - diff --git a/apps/queue-worker/config/config.mainnet.yaml b/apps/queue-worker/config/config.mainnet.yaml deleted file mode 100644 index 43db027..0000000 --- a/apps/queue-worker/config/config.mainnet.yaml +++ /dev/null @@ -1,10 +0,0 @@ -urls: - api: 'https://devnet-api.multiversx.com' - redis: '127.0.0.1' -features: - queueWorker: - enabled: true - port: 8000 - privateApi: - enabled: true - port: 4000 diff --git a/apps/queue-worker/config/config.testnet.yaml b/apps/queue-worker/config/config.testnet.yaml deleted file mode 100644 index 43db027..0000000 --- a/apps/queue-worker/config/config.testnet.yaml +++ /dev/null @@ -1,10 +0,0 @@ -urls: - api: 'https://devnet-api.multiversx.com' - redis: '127.0.0.1' -features: - queueWorker: - enabled: true - port: 8000 - privateApi: - enabled: true - port: 4000 diff --git a/apps/queue-worker/config/configuration.ts b/apps/queue-worker/config/configuration.ts deleted file mode 100644 index 73ba517..0000000 --- a/apps/queue-worker/config/configuration.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { readFileSync } from 'fs'; -import * as yaml from 'js-yaml'; -import { join } from 'path'; - -const YAML_CONFIG_FILENAME = 'config.yaml'; - -export default () => { - return yaml.load( - readFileSync(join(__dirname, YAML_CONFIG_FILENAME), 'utf8'), - ) as Record; -}; diff --git a/apps/queue-worker/src/main.ts b/apps/queue-worker/src/main.ts deleted file mode 100644 index 29ef98a..0000000 --- a/apps/queue-worker/src/main.ts +++ /dev/null @@ -1,39 +0,0 @@ -import 'module-alias/register'; -import { NestFactory } from '@nestjs/core'; -import { ApiConfigService, PubSubListenerModule } from '@mvx-monorepo/common'; -import { QueueWorkerModule } from './worker'; -import { PrivateAppModule } from './private.app.module'; -import configuration from '../config/configuration'; -import { MicroserviceOptions, Transport } from '@nestjs/microservices'; -import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; - -async function bootstrap() { - const queueWorkerApp = await NestFactory.create(QueueWorkerModule); - const apiConfigService = queueWorkerApp.get(ApiConfigService); - await queueWorkerApp.listen(apiConfigService.getQueueWorkerFeaturePort()); - - if (apiConfigService.getIsPrivateApiFeatureActive()) { - const privateApp = await NestFactory.create(PrivateAppModule); - await privateApp.listen(apiConfigService.getPrivateApiFeaturePort()); - } - - const pubSubApp = await NestFactory.createMicroservice( - PubSubListenerModule.forRoot(configuration), - { - transport: Transport.REDIS, - options: { - host: apiConfigService.getRedisUrl(), - port: 6379, - retryAttempts: 100, - retryDelay: 1000, - retryStrategy: () => 1000, - }, - }, - ); - pubSubApp.useLogger(pubSubApp.get(WINSTON_MODULE_NEST_PROVIDER)); - // eslint-disable-next-line @typescript-eslint/no-floating-promises - pubSubApp.listen(); -} - -// eslint-disable-next-line @typescript-eslint/no-floating-promises -bootstrap(); diff --git a/apps/queue-worker/src/private.app.module.ts b/apps/queue-worker/src/private.app.module.ts deleted file mode 100644 index 4e16f1b..0000000 --- a/apps/queue-worker/src/private.app.module.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Module } from '@nestjs/common'; -import { ApiConfigModule, ApiMetricsController, HealthCheckController } from '@mvx-monorepo/common'; -import { ApiMetricsModule } from '@mvx-monorepo/common'; -import { LoggingModule } from '@multiversx/sdk-nestjs-common'; -import configuration from '../config/configuration'; - -@Module({ - imports: [ - LoggingModule, - ApiMetricsModule, - ApiConfigModule.forRoot(configuration), - ], - providers: [], - controllers: [ - ApiMetricsController, - HealthCheckController, - ], -}) -export class PrivateAppModule { } diff --git a/apps/queue-worker/src/worker/bull.queue.module.ts b/apps/queue-worker/src/worker/bull.queue.module.ts deleted file mode 100644 index c0e7d2f..0000000 --- a/apps/queue-worker/src/worker/bull.queue.module.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { BullModule } from '@nestjs/bull'; -import { Module } from '@nestjs/common'; -import { ApiConfigModule, ApiConfigService } from '@mvx-monorepo/common'; -import configuration from '../../config/configuration'; - -@Module({ - imports: [ - BullModule.forRootAsync({ - useFactory: (apiConfigService: ApiConfigService) => ({ - redis: { - host: apiConfigService.getRedisUrl(), - port: 6379, - }, - }), - imports: [ApiConfigModule.forRoot(configuration)], - inject: [ApiConfigService], - }), - ], -}) -export class BullQueueModule { } diff --git a/apps/queue-worker/src/worker/index.ts b/apps/queue-worker/src/worker/index.ts deleted file mode 100644 index f836c35..0000000 --- a/apps/queue-worker/src/worker/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './bull.queue.module'; -export * from './queue.worker.module'; -export * from './queue.worker.service'; diff --git a/apps/queue-worker/src/worker/queue.worker.module.ts b/apps/queue-worker/src/worker/queue.worker.module.ts deleted file mode 100644 index a1f00f5..0000000 --- a/apps/queue-worker/src/worker/queue.worker.module.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { BullModule } from '@nestjs/bull'; -import { Module } from '@nestjs/common'; -import { ScheduleModule } from '@nestjs/schedule'; -import { BullQueueModule } from './bull.queue.module'; -import { QueueWorkerService } from './queue.worker.service'; -import { ExampleQueueService } from './queues/example.queue.service'; - -@Module({ - imports: [ - ScheduleModule.forRoot(), - BullQueueModule, - BullModule.registerQueue({ - name: 'exampleQueue', - }), - ], - providers: [ - QueueWorkerService, ExampleQueueService, - ], - exports: [ - QueueWorkerService, ExampleQueueService, - ], -}) -export class QueueWorkerModule { } diff --git a/apps/queue-worker/src/worker/queue.worker.service.ts b/apps/queue-worker/src/worker/queue.worker.service.ts deleted file mode 100644 index 34b81d1..0000000 --- a/apps/queue-worker/src/worker/queue.worker.service.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { InjectQueue } from "@nestjs/bull"; -import { Injectable, Logger } from "@nestjs/common"; -import { Cron, CronExpression } from "@nestjs/schedule"; -import { Queue } from "bull"; - -@Injectable() -export class QueueWorkerService { - private readonly logger: Logger; - - constructor( - @InjectQueue('exampleQueue') private exampleQueue: Queue - ) { - this.logger = new Logger(QueueWorkerService.name); - } - - @Cron(CronExpression.EVERY_MINUTE) - async startJob() { - const identifiers = ['job1', 'job2', 'job3', 'job4', 'job5']; - for (const identifier of identifiers) { - const job = await this.exampleQueue.add({ identifier }, { - priority: 1000, - attempts: 3, - timeout: 60000, - delay: 30000, - removeOnComplete: true, - }); - - this.logger.log({ type: 'producer', jobId: job.id, identifier: job.data.identifier }); - } - } -} diff --git a/apps/queue-worker/src/worker/queues/example.queue.service.ts b/apps/queue-worker/src/worker/queues/example.queue.service.ts deleted file mode 100644 index 9c4ab1d..0000000 --- a/apps/queue-worker/src/worker/queues/example.queue.service.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Process, Processor } from "@nestjs/bull"; -import { Injectable, Logger } from "@nestjs/common"; -import { Job } from "bull"; - -@Injectable() -@Processor('exampleQueue') -export class ExampleQueueService { - private readonly logger: Logger; - - constructor() { - this.logger = new Logger(ExampleQueueService.name); - } - - @Process({ concurrency: 4 }) - onNftCreated(job: Job<{ identifier: string }>) { - this.logger.log({ type: 'consumer', jobId: job.id, identifier: job.data.identifier, attemptsMade: job.attemptsMade }); - } -} diff --git a/apps/transactions-processor/config/config.devnet.yaml b/apps/transactions-processor/config/config.devnet.yaml deleted file mode 100644 index 0e59d81..0000000 --- a/apps/transactions-processor/config/config.devnet.yaml +++ /dev/null @@ -1,10 +0,0 @@ -urls: - api: 'https://devnet-api.multiversx.com' - redis: '127.0.0.1' -features: - transactionProcessor: - port: 5202 - maxLookBehind: 100 - privateApi: - enabled: true - port: 4000 diff --git a/apps/transactions-processor/config/config.mainnet.yaml b/apps/transactions-processor/config/config.mainnet.yaml deleted file mode 100644 index 64fc075..0000000 --- a/apps/transactions-processor/config/config.mainnet.yaml +++ /dev/null @@ -1,10 +0,0 @@ -urls: - api: 'https://api.multiversx.com' - redis: '127.0.0.1' -features: - transactionProcessor: - port: 5202 - maxLookBehind: 100 - privateApi: - enabled: true - port: 4000 \ No newline at end of file diff --git a/apps/transactions-processor/config/config.testnet.yaml b/apps/transactions-processor/config/config.testnet.yaml deleted file mode 100644 index cf374dc..0000000 --- a/apps/transactions-processor/config/config.testnet.yaml +++ /dev/null @@ -1,10 +0,0 @@ -urls: - api: 'https://testnet-api.multiversx.com' - redis: '127.0.0.1' -features: - transactionProcessor: - port: 5202 - maxLookBehind: 100 - privateApi: - enabled: true - port: 4000 \ No newline at end of file diff --git a/apps/transactions-processor/config/configuration.ts b/apps/transactions-processor/config/configuration.ts deleted file mode 100644 index 73ba517..0000000 --- a/apps/transactions-processor/config/configuration.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { readFileSync } from 'fs'; -import * as yaml from 'js-yaml'; -import { join } from 'path'; - -const YAML_CONFIG_FILENAME = 'config.yaml'; - -export default () => { - return yaml.load( - readFileSync(join(__dirname, YAML_CONFIG_FILENAME), 'utf8'), - ) as Record; -}; diff --git a/apps/transactions-processor/src/private.app.module.ts b/apps/transactions-processor/src/private.app.module.ts deleted file mode 100644 index 4e16f1b..0000000 --- a/apps/transactions-processor/src/private.app.module.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Module } from '@nestjs/common'; -import { ApiConfigModule, ApiMetricsController, HealthCheckController } from '@mvx-monorepo/common'; -import { ApiMetricsModule } from '@mvx-monorepo/common'; -import { LoggingModule } from '@multiversx/sdk-nestjs-common'; -import configuration from '../config/configuration'; - -@Module({ - imports: [ - LoggingModule, - ApiMetricsModule, - ApiConfigModule.forRoot(configuration), - ], - providers: [], - controllers: [ - ApiMetricsController, - HealthCheckController, - ], -}) -export class PrivateAppModule { } diff --git a/apps/transactions-processor/tsconfig.app.json b/apps/transactions-processor/tsconfig.app.json deleted file mode 100644 index ab81e80..0000000 --- a/apps/transactions-processor/tsconfig.app.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "declaration": false, - "outDir": "../../dist" - }, - "include": [ - "src/**/*" - ], - "exclude": [ - "node_modules", - "dist", - "test", - "**/*spec.ts" - ] -} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index c295093..33c92e4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,18 +8,13 @@ services: environment: - REDIS_REPLICATION_MODE=master - database: - image: "mysql:latest" - ports: - - "3306:3306" - environment: - - MYSQL_ROOT_PASSWORD=root - - MYSQL_DATABASE=example - - mongodb: - image: mongo:latest + postgres: + image: postgres:latest environment: - - MONGODB_DATABASE="example" + - POSTGRES_USER=root + - POSTGRES_PASSWORD=password + - POSTGRES_DB=relayer ports: - - 27017:27017 - + - 5432:5432 + volumes: + - ./.db:/var/lib/postgresql/data diff --git a/libs/common/src/config/api.config.service.ts b/libs/common/src/config/api.config.service.ts index f8d5d16..a235b91 100644 --- a/libs/common/src/config/api.config.service.ts +++ b/libs/common/src/config/api.config.service.ts @@ -1,9 +1,9 @@ -import { Injectable } from "@nestjs/common"; -import { ConfigService } from "@nestjs/config"; +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; @Injectable() export class ApiConfigService { - constructor(private readonly configService: ConfigService) { } + constructor(private readonly configService: ConfigService) {} getApiUrl(): string { const apiUrl = this.configService.get('urls.api'); @@ -14,13 +14,40 @@ export class ApiConfigService { return apiUrl; } - getSwaggerUrls(): string[] { - const swaggerUrls = this.configService.get('urls.swagger'); - if (!swaggerUrls) { - throw new Error('No swagger urls present'); + getAxelarApiUrl(): string { + const axelarApiUrl = this.configService.get('urls.axelarApi'); + if (!axelarApiUrl) { + throw new Error('No Axelar API url present'); } - return swaggerUrls; + return axelarApiUrl; + } + + getEventsNotifierUrl(): string { + const eventsNotifierUrl = this.configService.get('urls.eventsNotifier'); + if (!eventsNotifierUrl) { + throw new Error('No Events Notifier url present'); + } + + return eventsNotifierUrl; + } + + getEventsNotifierQueue(): string { + const eventsNotifierQueue = this.configService.get('eventsNotifier.queue'); + if (!eventsNotifierQueue) { + throw new Error('No Events Notifier Queue present'); + } + + return eventsNotifierQueue; + } + + getEventsNotifierGatewayAddress(): string { + const eventsNotifierGatewayAddress = this.configService.get('eventsNotifier.gatewayAddress'); + if (!eventsNotifierGatewayAddress) { + throw new Error('No Events Notifier Gateway Address present'); + } + + return eventsNotifierGatewayAddress; } getRedisUrl(): string { @@ -49,208 +76,6 @@ export class ApiConfigService { return 6379; } - getDatabaseHost(): string { - const databaseHost = this.configService.get('database.host'); - if (!databaseHost) { - throw new Error('No database.host present'); - } - - return databaseHost; - } - - getDatabasePort(): number { - const databasePort = this.configService.get('database.port'); - if (!databasePort) { - throw new Error('No database.port present'); - } - - return databasePort; - } - - - getDatabaseUsername(): string { - const databaseUsername = this.configService.get('database.username'); - if (!databaseUsername) { - throw new Error('No database.username present'); - } - - return databaseUsername; - } - - getDatabasePassword(): string { - const databasePassword = this.configService.get('database.password'); - if (!databasePassword) { - throw new Error('No database.password present'); - } - - return databasePassword; - } - - getDatabaseName(): string { - const databaseName = this.configService.get('database.name'); - if (!databaseName) { - throw new Error('No database.name present'); - } - - return databaseName; - } - - getDatabaseConnection(): { host: string, port: number, username: string, password: string, database: string } { - return { - host: this.getDatabaseHost(), - port: this.getDatabasePort(), - username: this.getDatabaseUsername(), - password: this.getDatabasePassword(), - database: this.getDatabaseName(), - }; - } - - - getNoSQLDatabaseConnection(): string { - return `mongodb://${this.getDatabaseHost()}:27017/${this.getDatabaseName()}`; - } - - getIsPublicApiFeatureActive(): boolean { - const isApiActive = this.configService.get('features.publicApi.enabled'); - if (isApiActive === undefined) { - throw new Error('No public api feature flag present'); - } - - return isApiActive; - } - - getPublicApiFeaturePort(): number { - const featurePort = this.configService.get('features.publicApi.port'); - if (featurePort === undefined) { - throw new Error('No public api port present'); - } - - return featurePort; - } - - getIsPrivateApiFeatureActive(): boolean { - const isApiActive = this.configService.get('features.privateApi.enabled'); - if (isApiActive === undefined) { - throw new Error('No private api feature flag present'); - } - - return isApiActive; - } - - getPrivateApiFeaturePort(): number { - const featurePort = this.configService.get('features.privateApi.port'); - if (featurePort === undefined) { - throw new Error('No private api port present'); - } - - return featurePort; - } - - getIsCacheWarmerFeatureActive(): boolean { - const isCacheWarmerActive = this.configService.get('features.cacheWarmer.enabled'); - if (isCacheWarmerActive === undefined) { - throw new Error('No cache warmer feature flag present'); - } - - return isCacheWarmerActive; - } - - getCacheWarmerFeaturePort(): number { - const featurePort = this.configService.get('features.cacheWarmer.port'); - if (featurePort === undefined) { - throw new Error('No cache warmer port present'); - } - - return featurePort; - } - - getIsTransactionProcessorFeatureActive(): boolean { - const isTransactionProcessorActive = this.configService.get('features.transactionProcessor.enabled'); - if (isTransactionProcessorActive === undefined) { - throw new Error('No transaction processor feature flag present'); - } - - return isTransactionProcessorActive; - } - - getTransactionProcessorFeaturePort(): number { - const featurePort = this.configService.get('features.transactionProcessor.port'); - if (featurePort === undefined) { - throw new Error('No transaction processor port present'); - } - - return featurePort; - } - - getTransactionProcessorMaxLookBehind(): number { - const maxLookBehind = this.configService.get('features.transactionProcessor.maxLookBehind'); - if (maxLookBehind === undefined) { - throw new Error('No transaction processor max look behind present'); - } - - return maxLookBehind; - } - - getIsQueueWorkerFeatureActive(): boolean { - const isQueueWorkerActive = this.configService.get('features.queueWorker.enabled'); - if (isQueueWorkerActive === undefined) { - throw new Error('No queue worker feature flag present'); - } - - return isQueueWorkerActive; - } - - getQueueWorkerFeaturePort(): number { - const featurePort = this.configService.get('features.queueWorker.port'); - if (featurePort === undefined) { - throw new Error('No transaction processor port present'); - } - - return featurePort; - } - - getSecurityAdmins(): string[] { - const admins = this.configService.get('security.admins'); - if (admins === undefined) { - throw new Error('No security admins value present'); - } - - return admins; - } - - getRateLimiterSecret(): string | undefined { - return this.configService.get('rateLimiterSecret'); - } - - getAxiosTimeout(): number { - return this.configService.get('keepAliveTimeout.downstream') ?? 61000; - } - - getIsKeepAliveAgentFeatureActive(): boolean { - return this.configService.get('keepAliveAgent.enabled') ?? true; - } - - getServerTimeout(): number { - return this.configService.get('keepAliveTimeout.upstream') ?? 60000; - } - - getHeadersTimeout(): number { - return this.getServerTimeout() + 1000; - } - - getUseCachingInterceptor(): boolean { - return this.configService.get('useCachingInterceptor') ?? false; - } - - getElasticUrl(): string { - const elasticUrls = this.configService.get('urls.elastic'); - if (!elasticUrls) { - throw new Error('No elastic urls present'); - } - - return elasticUrls[Math.floor(Math.random() * elasticUrls.length)]; - } - getPoolLimit(): number { return this.configService.get('caching.poolLimit') ?? 100; } @@ -258,20 +83,4 @@ export class ApiConfigService { getProcessTtl(): number { return this.configService.get('caching.processTtl') ?? 60; } - - getUseKeepAliveAgentFlag(): boolean { - return this.configService.get('flags.useKeepAliveAgent') ?? true; - } - - getIsAuthActive(): boolean { - return this.configService.get('api.auth') ?? false; - } - - getNativeAuthMaxExpirySeconds(): number { - return this.configService.get('nativeAuth.maxExpirySeconds') ?? 86400; - } - - getNativeAuthAcceptedOrigins(): string[] { - return this.configService.get('nativeAuth.acceptedOrigins') ?? []; - } } diff --git a/libs/common/src/config/index.ts b/libs/common/src/config/index.ts index c5ad157..de9c6c3 100644 --- a/libs/common/src/config/index.ts +++ b/libs/common/src/config/index.ts @@ -1,3 +1,2 @@ export * from './api.config.module'; export * from './api.config.service'; -export * from './sdk.nestjs.config.service.impl'; diff --git a/libs/common/src/config/sdk.nestjs.config.service.impl.ts b/libs/common/src/config/sdk.nestjs.config.service.impl.ts deleted file mode 100644 index b0c763d..0000000 --- a/libs/common/src/config/sdk.nestjs.config.service.impl.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Injectable } from "@nestjs/common"; -import { ApiConfigService } from "./api.config.service"; -import { ErdnestConfigService } from "@multiversx/sdk-nestjs-common"; - -@Injectable() -export class SdkNestjsConfigServiceImpl implements ErdnestConfigService { - constructor( - private readonly apiConfigService: ApiConfigService, - ) { } - - getSecurityAdmins(): string[] { - return this.apiConfigService.getSecurityAdmins(); - } - - getJwtSecret(): string { - return ''; // We use only NativeAuth in this template, so we don't need a JWT secret - } - - getApiUrl(): string { - return this.apiConfigService.getApiUrl(); - } - - getNativeAuthMaxExpirySeconds(): number { - return this.apiConfigService.getNativeAuthMaxExpirySeconds(); - } - - getNativeAuthAcceptedOrigins(): string[] { - return this.apiConfigService.getNativeAuthAcceptedOrigins(); - } -} diff --git a/libs/common/src/database/database.module.ts b/libs/common/src/database/database.module.ts index 9504f0a..161c792 100644 --- a/libs/common/src/database/database.module.ts +++ b/libs/common/src/database/database.module.ts @@ -1,30 +1,7 @@ -import { Module } from "@nestjs/common"; -import { TypeOrmModule } from "@nestjs/typeorm"; -import { ApiConfigService, ApiConfigModule } from "../config"; -import { EntityClassOrSchema } from "@nestjs/typeorm/dist/interfaces/entity-class-or-schema.type"; +import { Module } from '@nestjs/common'; +import { PrismaService } from '@mvx-monorepo/common/database/prisma.service'; -@Module({}) -export class DatabaseModule { - static forRoot(entities: EntityClassOrSchema[]) { - return { - module: DatabaseModule, - imports: [ - TypeOrmModule.forRootAsync({ - imports: [ApiConfigModule], - useFactory: (apiConfigService: ApiConfigService) => ({ - type: 'mysql', - ...apiConfigService.getDatabaseConnection(), - entities: entities, - keepConnectionAlive: true, - synchronize: true, - }), - inject: [ApiConfigService], - }), - TypeOrmModule.forFeature(entities), - ], - exports: [ - TypeOrmModule.forFeature(entities), - ], - }; - } -} +@Module({ + providers: [PrismaService], +}) +export class DatabaseModule {} diff --git a/libs/common/src/database/index.ts b/libs/common/src/database/index.ts index b2ae4b6..90b0aaa 100644 --- a/libs/common/src/database/index.ts +++ b/libs/common/src/database/index.ts @@ -1,2 +1 @@ export * from './database.module'; -export * from './nosql.module'; diff --git a/libs/common/src/database/nosql.module.ts b/libs/common/src/database/nosql.module.ts deleted file mode 100644 index 0c8185d..0000000 --- a/libs/common/src/database/nosql.module.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Module } from "@nestjs/common"; -import { MongooseModule } from "@nestjs/mongoose"; -import { ApiConfigModule, ApiConfigService } from "../config"; - -@Module({ - imports: [ - MongooseModule.forRootAsync({ - imports: [ApiConfigModule], - useFactory: (configService: ApiConfigService) => ({ - uri: configService.getNoSQLDatabaseConnection(), - }), - inject: [ApiConfigService], - }), - ], - exports: [ - ], -}) -export class NoSQLDatabaseModule { } diff --git a/libs/common/src/database/prisma.service.ts b/libs/common/src/database/prisma.service.ts new file mode 100644 index 0000000..359f950 --- /dev/null +++ b/libs/common/src/database/prisma.service.ts @@ -0,0 +1,9 @@ +import { Injectable, OnModuleInit } from '@nestjs/common'; +import { PrismaClient } from '@prisma/client'; + +@Injectable() +export class PrismaService extends PrismaClient implements OnModuleInit { + async onModuleInit() { + await this.$connect(); + } +} diff --git a/libs/common/src/entities/index.ts b/libs/common/src/entities/index.ts deleted file mode 100644 index b73e4ab..0000000 --- a/libs/common/src/entities/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './query.paginations'; diff --git a/libs/common/src/entities/query.paginations.ts b/libs/common/src/entities/query.paginations.ts deleted file mode 100644 index 7c77927..0000000 --- a/libs/common/src/entities/query.paginations.ts +++ /dev/null @@ -1,4 +0,0 @@ -export class QueryPagination { - from: number = 0; - size: number = 25; -} diff --git a/libs/common/src/example/entities/example.filter.ts b/libs/common/src/example/entities/example.filter.ts deleted file mode 100644 index 8cb5e71..0000000 --- a/libs/common/src/example/entities/example.filter.ts +++ /dev/null @@ -1,3 +0,0 @@ -export class ExampleFilter { - search?: string; -} diff --git a/libs/common/src/example/entities/example.ts b/libs/common/src/example/entities/example.ts deleted file mode 100644 index 99423e5..0000000 --- a/libs/common/src/example/entities/example.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { ApiProperty } from "@nestjs/swagger"; - -export class Example { - @ApiProperty({ description: 'The id of the example' }) - id: string = ''; - - @ApiProperty({ description: 'The description of the example' }) - description: string = ''; -} diff --git a/libs/common/src/example/entities/index.ts b/libs/common/src/example/entities/index.ts deleted file mode 100644 index 9239e02..0000000 --- a/libs/common/src/example/entities/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './example'; -export * from './example.filter'; diff --git a/libs/common/src/example/example.controller.ts b/libs/common/src/example/example.controller.ts deleted file mode 100644 index 001f614..0000000 --- a/libs/common/src/example/example.controller.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Controller, DefaultValuePipe, Get, NotFoundException, Param, ParseIntPipe, Query } from "@nestjs/common"; -import { ApiQuery, ApiResponse, ApiTags } from "@nestjs/swagger"; -import { Example } from "./entities/example"; -import { ExampleService } from "./example.service"; - -@Controller() -@ApiTags('example') -export class ExampleController { - constructor( - private readonly exampleService: ExampleService - ) { } - - @Get("/examples") - @ApiResponse({ - status: 200, - description: 'Returns a list of examples', - type: Example, - isArray: true, - }) - @ApiQuery({ name: 'from', description: 'Numer of items to skip for the result set', required: false }) - @ApiQuery({ name: 'size', description: 'Number of items to retrieve', required: false }) - @ApiQuery({ name: 'search', description: 'Search by example description', required: false }) - async getExamples( - @Query('from', new DefaultValuePipe(0), ParseIntPipe) from: number, - @Query('size', new DefaultValuePipe(25), ParseIntPipe) size: number, - @Query('search') search?: string, - ): Promise { - return await this.exampleService.getExamples({ from, size }, { search }); - } - - @Get("/examples/:id") - @ApiResponse({ - status: 200, - description: 'Returns one example', - type: Example, - }) - async getExample( - @Param('id') id: string, - ): Promise { - const result = await this.exampleService.getExample(id); - if (!result) { - throw new NotFoundException('Example not found'); - } - - return result; - } -} diff --git a/libs/common/src/example/example.module.ts b/libs/common/src/example/example.module.ts deleted file mode 100644 index 3a29ddb..0000000 --- a/libs/common/src/example/example.module.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Module } from "@nestjs/common"; -import { ApiConfigModule, DynamicModuleUtils } from "@mvx-monorepo/common"; -import { ExampleService } from "./example.service"; - -@Module({}) -export class ExampleModule { - static forRoot(configuration: () => Record) { - return { - module: ExampleModule, - imports: [ - ApiConfigModule.forRoot(configuration), - DynamicModuleUtils.getCachingModule(configuration), - ], - providers: [ - ExampleService, - ], - exports: [ - ExampleService, - ], - }; - } -} diff --git a/libs/common/src/example/example.service.ts b/libs/common/src/example/example.service.ts deleted file mode 100644 index 792eb6a..0000000 --- a/libs/common/src/example/example.service.ts +++ /dev/null @@ -1,294 +0,0 @@ -import { Injectable } from "@nestjs/common"; -import { QueryPagination } from "@mvx-monorepo/common"; -import { Example } from "./entities/example"; -import { ExampleFilter } from "./entities/example.filter"; -import { CacheService } from "@multiversx/sdk-nestjs-cache"; -import { CacheInfo } from "@mvx-monorepo/common"; - -@Injectable() -export class ExampleService { - constructor( - private readonly cacheService: CacheService - ) { } - - async getExamples(pagination: QueryPagination, filter: ExampleFilter): Promise { - let examples = await this.getAllExamples(); - - if (filter.search) { - const search = filter.search.toLowerCase(); - - examples = examples.filter(x => x.description.toLowerCase().includes(search)); - } - - return examples.slice(pagination.from, pagination.from + pagination.size); - } - - async getExample(id: string): Promise { - const examples = await this.getAllExamples(); - - return examples.find(example => example.id === id); - } - - async getAllExamples(): Promise { - return await this.cacheService.getOrSet( - CacheInfo.Examples.key, - async () => await this.getAllExamplesRaw(), - CacheInfo.Examples.ttl, - ); - } - - async getAllExamplesRaw(): Promise { - return await new Promise(resolve => { - resolve([ - { - "id": "magna", - "description": "Excepteur sint reprehenderit sint nostrud esse et do eiusmod excepteur sint voluptate laborum exercitation.", - }, - { - "id": "adipisicing", - "description": "Voluptate in aute sit ad esse est amet in aliquip.", - }, - { - "id": "aliquip", - "description": "Incididunt tempor adipisicing deserunt commodo et duis.", - }, - { - "id": "quis", - "description": "Exercitation exercitation aute reprehenderit duis quis.", - }, - { - "id": "adipisicing", - "description": "Dolore tempor irure labore magna voluptate ipsum Lorem deserunt aliquip veniam ex cupidatat ex.", - }, - { - "id": "nostrud", - "description": "Minim veniam aute magna laboris fugiat duis laboris et.", - }, - { - "id": "dolore", - "description": "Minim irure ut laboris ut pariatur.", - }, - { - "id": "sit", - "description": "Incididunt dolor consectetur dolor do exercitation nostrud ut aliquip reprehenderit nisi fugiat dolor.", - }, - { - "id": "fugiat", - "description": "Commodo adipisicing esse sit ipsum sint.", - }, - { - "id": "laborum", - "description": "Commodo excepteur laborum pariatur consectetur laborum proident excepteur fugiat nisi commodo.", - }, - { - "id": "incididunt", - "description": "Nulla occaecat duis proident commodo adipisicing pariatur eiusmod quis nulla ea anim sit.", - }, - { - "id": "est", - "description": "Deserunt amet esse pariatur consequat laborum elit fugiat Lorem amet ea.", - }, - { - "id": "voluptate", - "description": "Nisi non id id duis ex enim sint.", - }, - { - "id": "pariatur", - "description": "Do dolor nulla elit laboris pariatur magna deserunt minim aliqua officia in velit.", - }, - { - "id": "nostrud", - "description": "Irure elit incididunt nulla consequat magna anim.", - }, - { - "id": "proident", - "description": "Exercitation et eu elit laborum culpa in ut et irure irure.", - }, - { - "id": "mollit", - "description": "Sunt cillum labore pariatur duis do excepteur nisi in.", - }, - { - "id": "do", - "description": "Ullamco eu mollit sit quis irure deserunt irure aliquip enim in.", - }, - { - "id": "elit", - "description": "In non ex veniam sit in adipisicing minim ut irure.", - }, - { - "id": "sunt", - "description": "Sunt nulla qui commodo tempor commodo.", - }, - { - "id": "qui", - "description": "Culpa esse dolore veniam occaecat officia proident incididunt Lorem minim aliqua qui eiusmod elit.", - }, - { - "id": "qui", - "description": "Do anim proident reprehenderit commodo.", - }, - { - "id": "duis", - "description": "Cillum cillum irure voluptate ad duis dolor et irure cillum.", - }, - { - "id": "elit", - "description": "Incididunt aliquip aliqua ea labore exercitation voluptate aute ea consequat nisi.", - }, - { - "id": "commodo", - "description": "Aliquip officia eu ad dolor excepteur esse minim labore non velit anim dolore.", - }, - { - "id": "commodo", - "description": "Qui exercitation sunt nisi incididunt dolor cupidatat.", - }, - { - "id": "sit", - "description": "Id duis labore nostrud anim nostrud deserunt ullamco culpa cupidatat.", - }, - { - "id": "laborum", - "description": "Adipisicing ea Lorem fugiat in ad.", - }, - { - "id": "veniam", - "description": "Enim nulla mollit velit nulla.", - }, - { - "id": "amet", - "description": "Ea sunt sit sunt et dolor deserunt qui proident eiusmod sit consectetur.", - }, - { - "id": "in", - "description": "Occaecat et eiusmod pariatur pariatur sunt nostrud do est Lorem irure commodo duis.", - }, - { - "id": "veniam", - "description": "Deserunt dolore cupidatat enim est quis exercitation duis ea ipsum culpa dolor est ullamco.", - }, - { - "id": "dolor", - "description": "Ullamco labore amet anim minim culpa aute veniam eiusmod pariatur.", - }, - { - "id": "laboris", - "description": "Voluptate irure esse eu incididunt tempor irure culpa sint.", - }, - { - "id": "id", - "description": "Consectetur proident do duis ea consequat id magna ad qui laboris magna fugiat consequat.", - }, - { - "id": "minim", - "description": "Qui commodo sunt nisi duis sunt ipsum voluptate in labore anim.", - }, - { - "id": "veniam", - "description": "Ipsum eu dolor laboris pariatur laborum proident cupidatat.", - }, - { - "id": "reprehenderit", - "description": "Consequat laborum magna et consectetur et adipisicing.", - }, - { - "id": "dolore", - "description": "Ex laboris ut commodo incididunt nulla commodo dolore ut cupidatat.", - }, - { - "id": "cupidatat", - "description": "Est nulla sunt irure consequat mollit amet ullamco sint exercitation.", - }, - { - "id": "proident", - "description": "Eu voluptate fugiat tempor ullamco id ullamco qui quis in aliqua aliquip labore Lorem.", - }, - { - "id": "duis", - "description": "Deserunt eiusmod et laboris non do.", - }, - { - "id": "duis", - "description": "Ullamco sint pariatur magna ea excepteur pariatur.", - }, - { - "id": "anim", - "description": "Voluptate qui sunt officia tempor excepteur culpa cupidatat enim id nisi ut adipisicing.", - }, - { - "id": "proident", - "description": "Excepteur aliquip qui et nisi dolor culpa id id dolore dolore eu eu.", - }, - { - "id": "amet", - "description": "Ex cupidatat pariatur eiusmod ea laboris voluptate ex.", - }, - { - "id": "consectetur", - "description": "Ullamco consequat voluptate proident mollit laborum consequat officia elit labore velit tempor velit.", - }, - { - "id": "sunt", - "description": "Anim ea in esse et et irure fugiat.", - }, - { - "id": "amet", - "description": "Laboris id voluptate voluptate proident minim Lorem ad exercitation excepteur eiusmod amet dolor eu.", - }, - { - "id": "id", - "description": "Quis amet reprehenderit esse officia magna cillum proident non.", - }, - { - "id": "occaecat", - "description": "Aliqua sit Lorem laboris reprehenderit anim.", - }, - { - "id": "nulla", - "description": "Voluptate magna ex eiusmod nisi dolor Lorem enim nulla ut ipsum mollit adipisicing minim.", - }, - { - "id": "ad", - "description": "Duis adipisicing labore ipsum qui cupidatat voluptate qui irure culpa.", - }, - { - "id": "cupidatat", - "description": "Consectetur ex voluptate duis dolore nisi sunt ut nisi enim ut aliquip labore.", - }, - { - "id": "ipsum", - "description": "Dolore nostrud ea fugiat sunt.", - }, - { - "id": "do", - "description": "Et laborum nisi quis reprehenderit ullamco adipisicing labore.", - }, - { - "id": "est", - "description": "Laborum laborum exercitation tempor non adipisicing culpa veniam duis adipisicing dolore qui eiusmod.", - }, - { - "id": "ullamco", - "description": "Et mollit dolor do amet elit ullamco veniam voluptate quis deserunt et.", - }, - { - "id": "exercitation", - "description": "Duis sint magna aliqua amet amet minim enim ea.", - }, - { - "id": "cillum", - "description": "Nisi sint sint eiusmod laborum voluptate non duis quis.", - }, - { - "id": "deserunt", - "description": "Esse fugiat velit ea non dolore culpa magna dolor.", - }, - { - "id": "eu", - "description": "Adipisicing commodo est sunt amet ex velit.", - }, - ]); - }); - } -} diff --git a/libs/common/src/example/index.ts b/libs/common/src/example/index.ts deleted file mode 100644 index eba57e7..0000000 --- a/libs/common/src/example/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './example.service'; -export * from './example.controller'; -export * from './example.module'; -export * from './entities'; diff --git a/libs/common/src/health-check/health.check.controller.ts b/libs/common/src/health-check/health.check.controller.ts deleted file mode 100644 index d3b1bce..0000000 --- a/libs/common/src/health-check/health.check.controller.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Controller, Get } from "@nestjs/common"; - -@Controller() -export class HealthCheckController { - @Get("/hello") - getHello(): string { - return 'hello'; - } -} diff --git a/libs/common/src/health-check/index.ts b/libs/common/src/health-check/index.ts deleted file mode 100644 index 4266945..0000000 --- a/libs/common/src/health-check/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './health.check.controller'; diff --git a/libs/common/src/index.ts b/libs/common/src/index.ts index c966b27..868a7fd 100644 --- a/libs/common/src/index.ts +++ b/libs/common/src/index.ts @@ -1,9 +1,5 @@ export * from './abi'; export * from './database'; -export * from './entities'; -export * from './metrics'; export * from './pubsub'; export * from './config'; export * from './utils'; -export * from './example'; -export * from './health-check'; diff --git a/libs/common/src/metrics/api.metrics.controller.ts b/libs/common/src/metrics/api.metrics.controller.ts deleted file mode 100644 index cd9fec6..0000000 --- a/libs/common/src/metrics/api.metrics.controller.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Controller, Get } from "@nestjs/common"; -import { ApiMetricsService } from "./api.metrics.service"; - -@Controller() -export class ApiMetricsController { - constructor( - private readonly metricsService: ApiMetricsService - ) { } - - @Get("/metrics") - async getMetrics(): Promise { - return await this.metricsService.getMetrics(); - } -} diff --git a/libs/common/src/metrics/api.metrics.module.ts b/libs/common/src/metrics/api.metrics.module.ts deleted file mode 100644 index 0b019e1..0000000 --- a/libs/common/src/metrics/api.metrics.module.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Global, Module } from "@nestjs/common"; -import { ApiMetricsService } from "./api.metrics.service"; -import { MetricsModule } from "@multiversx/sdk-nestjs-monitoring"; - -@Global() -@Module({ - imports: [ - MetricsModule, - ], - providers: [ - ApiMetricsService, - ], - exports: [ - ApiMetricsService, - ], -}) -export class ApiMetricsModule { } diff --git a/libs/common/src/metrics/api.metrics.service.ts b/libs/common/src/metrics/api.metrics.service.ts deleted file mode 100644 index 30f9f42..0000000 --- a/libs/common/src/metrics/api.metrics.service.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { MetricsService } from "@multiversx/sdk-nestjs-monitoring"; -import { Injectable } from "@nestjs/common"; -import { register, Gauge } from 'prom-client'; - -@Injectable() -export class ApiMetricsService { - private static lastProcessedNonceGauge: Gauge; - - constructor( - private readonly metricsService: MetricsService, - ) { - if (!ApiMetricsService.lastProcessedNonceGauge) { - ApiMetricsService.lastProcessedNonceGauge = new Gauge({ - name: 'last_processed_nonce', - help: 'Last processed nonce of the given shard', - labelNames: ['shardId'], - }); - } - } - - setLastProcessedNonce(shardId: number, nonce: number) { - ApiMetricsService.lastProcessedNonceGauge.set({ shardId }, nonce); - } - - async getMetrics(): Promise { - const baseMetrics = await this.metricsService.getMetrics(); - const currentMetrics = await register.metrics(); - - return baseMetrics + '\n' + currentMetrics; - } -} diff --git a/libs/common/src/metrics/entities/elastic.metric.type.ts b/libs/common/src/metrics/entities/elastic.metric.type.ts deleted file mode 100644 index 5142962..0000000 --- a/libs/common/src/metrics/entities/elastic.metric.type.ts +++ /dev/null @@ -1,5 +0,0 @@ -export enum ElasticMetricType { - list = 'list', - item = 'item', - count = 'count', -} diff --git a/libs/common/src/metrics/index.ts b/libs/common/src/metrics/index.ts deleted file mode 100644 index 1e31b0c..0000000 --- a/libs/common/src/metrics/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './api.metrics.controller'; -export * from './api.metrics.module'; -export * from './api.metrics.service'; diff --git a/libs/common/src/utils/cache.info.ts b/libs/common/src/utils/cache.info.ts index 8b8f80e..6baf2eb 100644 --- a/libs/common/src/utils/cache.info.ts +++ b/libs/common/src/utils/cache.info.ts @@ -10,9 +10,4 @@ export class CacheInfo { ttl: Constants.oneMonth(), }; } - - static Examples: CacheInfo = { - key: "examples", - ttl: Constants.oneHour(), - }; } diff --git a/libs/common/src/utils/dynamic.module.utils.ts b/libs/common/src/utils/dynamic.module.utils.ts index d83fcb6..3865a43 100644 --- a/libs/common/src/utils/dynamic.module.utils.ts +++ b/libs/common/src/utils/dynamic.module.utils.ts @@ -1,57 +1,27 @@ -import { ERDNEST_CONFIG_SERVICE } from "@multiversx/sdk-nestjs-common"; -import { ElasticModule, ElasticModuleOptions } from "@multiversx/sdk-nestjs-elastic"; -import { CacheModule, RedisCacheModuleOptions } from "@multiversx/sdk-nestjs-cache"; -import { DynamicModule, Provider } from "@nestjs/common"; -import { ClientOptions, ClientProxyFactory, Transport } from "@nestjs/microservices"; -import { ApiConfigModule, ApiConfigService, SdkNestjsConfigServiceImpl } from "../config"; -import { ApiModule, ApiModuleOptions } from "@multiversx/sdk-nestjs-http"; +import { CacheModule, RedisCacheModuleOptions } from '@multiversx/sdk-nestjs-cache'; +import { DynamicModule, Provider } from '@nestjs/common'; +import { ClientOptions, ClientProxyFactory, Transport } from '@nestjs/microservices'; +import { ApiConfigModule, ApiConfigService } from '../config'; export class DynamicModuleUtils { - static getElasticModule(configuration: () => Record): DynamicModule { - return ElasticModule.forRootAsync({ - imports: [ApiConfigModule.forRoot(configuration)], - useFactory: (apiConfigService: ApiConfigService) => new ElasticModuleOptions({ - url: apiConfigService.getElasticUrl(), - customValuePrefix: 'api', - }), - inject: [ApiConfigService], - }); - } - static getCachingModule(configuration: () => Record): DynamicModule { return CacheModule.forRootAsync({ imports: [ApiConfigModule.forRoot(configuration)], - useFactory: (apiConfigService: ApiConfigService) => new RedisCacheModuleOptions({ - host: apiConfigService.getRedisUrl(), - port: apiConfigService.getRedisPort(), - }, { - poolLimit: apiConfigService.getPoolLimit(), - processTtl: apiConfigService.getProcessTtl(), - }), - inject: [ApiConfigService], - }); - } - - static getApiModule(configuration: () => Record): DynamicModule { - return ApiModule.forRootAsync({ - imports: [ApiConfigModule.forRoot(configuration)], - useFactory: (apiConfigService: ApiConfigService) => new ApiModuleOptions({ - axiosTimeout: apiConfigService.getAxiosTimeout(), - rateLimiterSecret: apiConfigService.getRateLimiterSecret(), - serverTimeout: apiConfigService.getServerTimeout(), - useKeepAliveAgent: apiConfigService.getUseKeepAliveAgentFlag(), - }), + useFactory: (apiConfigService: ApiConfigService) => + new RedisCacheModuleOptions( + { + host: apiConfigService.getRedisUrl(), + port: apiConfigService.getRedisPort(), + }, + { + poolLimit: apiConfigService.getPoolLimit(), + processTtl: apiConfigService.getProcessTtl(), + }, + ), inject: [ApiConfigService], }); } - static getNestJsApiConfigService(): Provider { - return { - provide: ERDNEST_CONFIG_SERVICE, - useClass: SdkNestjsConfigServiceImpl, - }; - } - static getPubSubService(): Provider { return { provide: 'PUBSUB_SERVICE', diff --git a/nest-cli.json b/nest-cli.json index 719e86c..eed4a8d 100644 --- a/nest-cli.json +++ b/nest-cli.json @@ -1,77 +1,47 @@ { "$schema": "https://json.schemastore.org/nest-cli", "collection": "@nestjs/schematics", - "sourceRoot": "apps/api/src", + "sourceRoot": "apps/mvx-event-processor/src", "compilerOptions": { "webpack": false, "plugins": [ "@nestjs/swagger" ], - "tsConfigPath": "apps/api/tsconfig.app.json" + "tsConfigPath": "apps/mvx-event-processor/tsconfig.app.json" }, "monorepo": true, - "root": "apps/api", + "root": "apps/mvx-event-processor", "projects": { - "api": { + "axelar-event-processor": { "type": "application", - "root": "apps/api", + "root": "apps/axelar-event-processor", "entryFile": "main", - "sourceRoot": "apps/api/src", + "sourceRoot": "apps/axelar-event-processor/src", "compilerOptions": { - "tsConfigPath": "apps/api/tsconfig.app.json", + "tsConfigPath": "apps/axelar-event-processor/tsconfig.app.json", "assets": [ { "include": "../config/config.yaml", - "outDir": "./dist/apps/api/config" + "outDir": "./dist/apps/axelar-event-processor/config" }, { - "include": "../docs/swagger.md", - "outDir": "./dist/apps/api/docs" + "include": "../config/relayer.proto", + "outDir": "./dist/apps/axelar-event-processor/config" } ] } }, - "transactions-processor": { + "mvx-event-processor": { "type": "application", - "root": "apps/transactions-processor", + "root": "apps/mvx-event-processor", "entryFile": "main", - "sourceRoot": "apps/transactions-processor/src", + "sourceRoot": "apps/mvx-event-processor/src", "compilerOptions": { - "tsConfigPath": "apps/transactions-processor/tsconfig.app.json", + "tsConfigPath": "apps/mvx-event-processor/tsconfig.app.json", "assets": [ { "include": "../config/config.yaml", - "outDir": "./dist/apps/transactions-processor/config" - } - ] - } - }, - "queue-worker": { - "type": "application", - "root": "apps/queue-worker", - "entryFile": "main", - "sourceRoot": "apps/queue-worker/src", - "compilerOptions": { - "tsConfigPath": "apps/queue-worker/tsconfig.app.json", - "assets": [ - { - "include": "../config/config.yaml", - "outDir": "./dist/apps/queue-worker/config" - } - ] - } - }, - "cache-warmer": { - "type": "application", - "root": "apps/cache-warmer", - "entryFile": "main", - "sourceRoot": "apps/cache-warmer/src", - "compilerOptions": { - "tsConfigPath": "apps/cache-warmer/tsconfig.app.json", - "assets": [ - { - "include": "../config/config.yaml", - "outDir": "./dist/apps/cache-warmer/config" + "outDir": "./dist/apps/mvx-event-processor/config" } ] } diff --git a/package-lock.json b/package-lock.json index 1f94096..1a21a39 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,16 @@ { - "name": "starter-monorepo-microservices", + "name": "axelar-mvx-amplifier", "version": "0.0.1", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "starter-monorepo-microservices", + "name": "axelar-mvx-amplifier", "version": "0.0.1", "license": "UNLICENSED", "dependencies": { + "@grpc/grpc-js": "^1.9.12", + "@grpc/proto-loader": "^0.7.10", "@multiversx/sdk-core": "^12.15.0", "@multiversx/sdk-nestjs-auth": "2.4.0", "@multiversx/sdk-nestjs-cache": "2.4.0", @@ -32,6 +34,7 @@ "@nestjs/swagger": "7.1.16", "@nestjs/typeorm": "10.0.0", "@nestjs/websockets": "10.2.8", + "@prisma/client": "^5.6.0", "agentkeepalive": "^4.3.0", "bull": "^4.10.4", "cache-manager": "^5.2.1", @@ -74,6 +77,7 @@ "jest": "^29.5.0", "js-yaml": "^4.1.0", "prettier": "^2.8.8", + "prisma": "^5.6.0", "supertest": "^6.3.3", "ts-jest": "29.0.5", "ts-loader": "9.4.2", @@ -1056,6 +1060,35 @@ "rxjs": "^7.x" } }, + "node_modules/@grpc/grpc-js": { + "version": "1.9.12", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.12.tgz", + "integrity": "sha512-Um5MBuge32TS3lAKX02PGCnFM4xPT996yLgZNb5H03pn6NyJ4Iwn5YcPq6Jj9yxGRk7WOgaZFtVRH5iTdYBeUg==", + "dependencies": { + "@grpc/proto-loader": "^0.7.8", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.10.tgz", + "integrity": "sha512-CAqDfoaQ8ykFd9zqBDn4k6iWT9loLAlc2ETmDFS9JCD70gDcnA4L3AFEo2iV7KyAtAAHFW9ftq1Fz+Vsgq80RQ==", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.4", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.13", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", @@ -2775,6 +2808,38 @@ "node": ">=14" } }, + "node_modules/@prisma/client": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.6.0.tgz", + "integrity": "sha512-mUDefQFa1wWqk4+JhKPYq8BdVoFk9NFMBXUI8jAkBfQTtgx8WPx02U2HB/XbAz3GSUJpeJOKJQtNvaAIDs6sug==", + "hasInstallScript": true, + "dependencies": { + "@prisma/engines-version": "5.6.0-32.e95e739751f42d8ca026f6b910f5a2dc5adeaeee" + }, + "engines": { + "node": ">=16.13" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/engines": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.6.0.tgz", + "integrity": "sha512-Mt2q+GNJpU2vFn6kif24oRSBQv1KOkYaterQsi0k2/lA+dLvhRX6Lm26gon6PYHwUM8/h8KRgXIUMU0PCLB6bw==", + "devOptional": true, + "hasInstallScript": true + }, + "node_modules/@prisma/engines-version": { + "version": "5.6.0-32.e95e739751f42d8ca026f6b910f5a2dc5adeaeee", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.6.0-32.e95e739751f42d8ca026f6b910f5a2dc5adeaeee.tgz", + "integrity": "sha512-UoFgbV1awGL/3wXuUK3GDaX2SolqczeeJ5b4FVec9tzeGbSWJboPSbT0psSrmgYAKiKnkOPFSLlH6+b+IyOwAw==" + }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -7758,6 +7823,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" + }, "node_modules/lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", @@ -9005,6 +9075,22 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/prisma": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.6.0.tgz", + "integrity": "sha512-EEaccku4ZGshdr2cthYHhf7iyvCcXqwJDvnoQRAJg5ge2Tzpv0e2BaMCp+CbbDUwoVTzwgOap9Zp+d4jFa2O9A==", + "devOptional": true, + "hasInstallScript": true, + "dependencies": { + "@prisma/engines": "5.6.0" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=16.13" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -12136,6 +12222,26 @@ "lodash": "^4.17.21" } }, + "@grpc/grpc-js": { + "version": "1.9.12", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.12.tgz", + "integrity": "sha512-Um5MBuge32TS3lAKX02PGCnFM4xPT996yLgZNb5H03pn6NyJ4Iwn5YcPq6Jj9yxGRk7WOgaZFtVRH5iTdYBeUg==", + "requires": { + "@grpc/proto-loader": "^0.7.8", + "@types/node": ">=12.12.47" + } + }, + "@grpc/proto-loader": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.10.tgz", + "integrity": "sha512-CAqDfoaQ8ykFd9zqBDn4k6iWT9loLAlc2ETmDFS9JCD70gDcnA4L3AFEo2iV7KyAtAAHFW9ftq1Fz+Vsgq80RQ==", + "requires": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.4", + "yargs": "^17.7.2" + } + }, "@humanwhocodes/config-array": { "version": "0.11.13", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", @@ -13353,6 +13459,25 @@ "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", "optional": true }, + "@prisma/client": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.6.0.tgz", + "integrity": "sha512-mUDefQFa1wWqk4+JhKPYq8BdVoFk9NFMBXUI8jAkBfQTtgx8WPx02U2HB/XbAz3GSUJpeJOKJQtNvaAIDs6sug==", + "requires": { + "@prisma/engines-version": "5.6.0-32.e95e739751f42d8ca026f6b910f5a2dc5adeaeee" + } + }, + "@prisma/engines": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.6.0.tgz", + "integrity": "sha512-Mt2q+GNJpU2vFn6kif24oRSBQv1KOkYaterQsi0k2/lA+dLvhRX6Lm26gon6PYHwUM8/h8KRgXIUMU0PCLB6bw==", + "devOptional": true + }, + "@prisma/engines-version": { + "version": "5.6.0-32.e95e739751f42d8ca026f6b910f5a2dc5adeaeee", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.6.0-32.e95e739751f42d8ca026f6b910f5a2dc5adeaeee.tgz", + "integrity": "sha512-UoFgbV1awGL/3wXuUK3GDaX2SolqczeeJ5b4FVec9tzeGbSWJboPSbT0psSrmgYAKiKnkOPFSLlH6+b+IyOwAw==" + }, "@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -17233,6 +17358,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" + }, "lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", @@ -18138,6 +18268,15 @@ } } }, + "prisma": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.6.0.tgz", + "integrity": "sha512-EEaccku4ZGshdr2cthYHhf7iyvCcXqwJDvnoQRAJg5ge2Tzpv0e2BaMCp+CbbDUwoVTzwgOap9Zp+d4jFa2O9A==", + "devOptional": true, + "requires": { + "@prisma/engines": "5.6.0" + } + }, "process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", diff --git a/package.json b/package.json index 330d989..9ac603f 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "starter-monorepo-microservices", + "name": "axelar-mvx-amplifier", "version": "0.0.1", "description": "", "author": "", @@ -8,73 +8,47 @@ "scripts": { "prebuild": "rimraf dist", "build": "nest build", - "build:all": "nest build api && nest build transactions-processor && nest build cache-warmer && nest build queue-worker && nest build common", + "build:all": "nest build mvx-event-processor && nest build axelar-event-processor && nest build common", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", - "start:devnet": "npm run copy-devnet-config & nest build api & nest start", - "start:devnet:watch": "npm run copy-devnet-config & nest build api & nest start --watch", - "start:devnet:debug": "npm run copy-devnet-config & nest build api & nest start --watch --debug", - "start:testnet": "npm run copy-testnet-config & nest build api & nest start", - "start:testnet:watch": "npm run copy-testnet-config & nest build api & nest start --watch", - "start:testnet:debug": "npm run copy-testnet-config & nest build api & nest start --watch --debug", - "start:mainnet": "npm run copy-mainnet-config & nest build api & nest start", - "start:custom": "npm run copy-custom-config & nest build api & nest start", - "start:custom:watch": "npm run copy-custom-config & nest build api & nest start --watch", - "start:custom:debug": "npm run copy-custom-config & nest build api & nest start --watch --debug", - "start:transactions-processor:devnet": "npm run copy-devnet-config-transactions & nest build transactions-processor & nest start transactions-processor", - "start:transactions-processor:devnet:watch": "npm run copy-devnet-config-transactions & nest build transactions-processor & nest start transactions-processor --watch", - "start:transactions-processor:devnet:debug": "npm run copy-devnet-config-transactions & nest build transactions-processor & nest start transactions-processor --watch --debug", - "start:transactions-processor:testnet": "npm run copy-testnet-config-transactions & nest build transactions-processor & nest start transactions-processor", - "start:transactions-processor:testnet:watch": "npm run copy-testnet-config-transactions & nest build transactions-processor & nest start transactions-processor --watch", - "start:transactions-processor:testnet:debug": "npm run copy-testnet-config-transactions & nest build transactions-processor & nest start transactions-processor --watch --debug", - "start:transactions-processor:mainnet": "npm run copy-mainnet-config-transactions & nest build transactions-processor & nest start transactions-processor", - "start:transactions-processor:custom": "npm run copy-custom-config-transactions & nest build transactions-processor &nest start transactions-processor", - "start:transactions-processor:custom:watch": "npm run copy-custom-config-transactions & nest build transactions-processor & nest start transactions-processor --watch", - "start:transactions-processor:custom:debug": "npm run copy-custom-config-transactions & nest build transactions-processor & nest start transactions-processor --watch --debug", - "start:queue-worker:devnet": "npm run copy-devnet-config-queue-worker & nest build queue-worker & nest start queue-worker", - "start:queue-worker:devnet:watch": "npm run copy-devnet-config-queue-worker & nest build queue-worker & nest start queue-worker --watch", - "start:queue-worker:devnet:debug": "npm run copy-devnet-config-queue-worker & nest build queue-worker & nest start queue-worker --watch --debug", - "start:queue-worker:testnet": "npm run copy-testnet-config-queue-worker & nest build queue-worker & nest start queue-worker", - "start:queue-worker:testnet:watch": "npm run copy-testnet-config-queue-worker & nest build queue-worker & nest start queue-worker --watch", - "start:queue-worker:testnet:debug": "npm run copy-testnet-config-queue-worker & nest build queue-worker & nest start queue-worker --watch --debug", - "start:queue-worker:mainnet": "npm run copy-mainnet-config-queue-worker & nest build queue-worker & nest start queue-worker", - "start:queue-worker:custom": "npm run copy-custom-config-queue-worker & nest build queue-worker & nest start queue-worker", - "start:queue-worker:custom:watch": "npm run copy-custom-config-queue-worker & nest build queue-worker & nest start queue-worker --watch", - "start:queue-worker:custom:debug": "npm run copy-custom-config-queue-worker & nest build queue-worker & nest start queue-worker --watch --debug", - "start:cache-warmer:devnet": "npm run copy-devnet-config-cache-warmer & nest build cache-warmer & nest start cache-warmer", - "start:cache-warmer:devnet:watch": "npm run copy-devnet-config-cache-warmer & nest build cache-warmer & nest start cache-warmer --watch", - "start:cache-warmer:devnet:debug": "npm run copy-devnet-config-cache-warmer & nest build cache-warmer & nest start cache-warmer --watch --debug", - "start:cache-warmer:testnet": "npm run copy-testnet-config-cache-warmer & nest build cache-warmer & nest start cache-warmer", - "start:cache-warmer:testnet:watch": "npm run copy-testnet-config-cache-warmer & nest build cache-warmer & nest start cache-warmer --watch", - "start:cache-warmer:testnet:debug": "npm run copy-testnet-config-cache-warmer & nest build cache-warmer & nest start cache-warmer --watch --debug", - "start:cache-warmer:mainnet": "npm run copy-mainnet-config-cache-warmer & nest build cache-warmer & nest start cache-warmer", - "start:cache-warmer:custom": "npm run copy-custom-config-cache-warmer & nest build cache-warmer & nest start cache-warmer", - "start:cache-warmer:custom:watch": "npm run copy-custom-config-cache-warmer & nest build cache-warmer & nest start cache-warmer --watch", - "start:cache-warmer:custom:debug": "npm run copy-custom-config-cache-warmer & nest build cache-warmer & nest start cache-warmer --watch --debug", + "start:devnet": "npm run copy-devnet-config & nest build mvx-event-processor & nest start", + "start:devnet:watch": "npm run copy-devnet-config & nest build mvx-event-processor & nest start --watch", + "start:devnet:debug": "npm run copy-devnet-config & nest build mvx-event-processor & nest start --watch --debug", + "start:testnet": "npm run copy-testnet-config & nest build mvx-event-processor & nest start", + "start:testnet:watch": "npm run copy-testnet-config & nest build mvx-event-processor & nest start --watch", + "start:testnet:debug": "npm run copy-testnet-config & nest build mvx-event-processor & nest start --watch --debug", + "start:mainnet": "npm run copy-mainnet-config & nest build mvx-event-processor & nest start", + "start:custom": "npm run copy-custom-config & nest build mvx-event-processor & nest start", + "start:custom:watch": "npm run copy-custom-config & nest build mvx-event-processor & nest start --watch", + "start:custom:debug": "npm run copy-custom-config & nest build mvx-event-processor & nest start --watch --debug", + "start:axelar-event-processor:devnet": "npm run copy-devnet-config-axelar & nest build axelar-event-processor & nest start axelar-event-processor", + "start:axelar-event-processor:devnet:watch": "npm run copy-devnet-config-axelar & nest build axelar-event-processor & nest start axelar-event-processor --watch", + "start:axelar-event-processor:devnet:debug": "npm run copy-devnet-config-axelar & nest build axelar-event-processor & nest start axelar-event-processor --watch --debug", + "start:axelar-event-processor:testnet": "npm run copy-testnet-config-axelar & nest build axelar-event-processor & nest start axelar-event-processor", + "start:axelar-event-processor:testnet:watch": "npm run copy-testnet-config-axelar & nest build axelar-event-processor & nest start axelar-event-processor --watch", + "start:axelar-event-processor:testnet:debug": "npm run copy-testnet-config-axelar & nest build axelar-event-processor & nest start axelar-event-processor --watch --debug", + "start:axelar-event-processor:mainnet": "npm run copy-mainnet-config-axelar & nest build axelar-event-processor & nest start axelar-event-processor", + "start:axelar-event-processor:custom": "npm run copy-custom-config-axelar & nest build axelar-event-processor &nest start axelar-event-processor", + "start:axelar-event-processor:custom:watch": "npm run copy-custom-config-axelar & nest build axelar-event-processor & nest start axelar-event-processor --watch", + "start:axelar-event-processor:custom:debug": "npm run copy-custom-config-axelar & nest build axelar-event-processor & nest start axelar-event-processor --watch --debug", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\"", "lint:fix": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "test": "jest", "test:watch": "jest --watch", "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "jest --config ./apps/api/test/jest-e2e.json", - "copy-devnet-config": "cp ./apps/api/config/config.devnet.yaml ./apps/api/config/config.yaml", - "copy-testnet-config": "cp ./apps/api/config/config.testnet.yaml ./apps/api/config/config.yaml", - "copy-mainnet-config": "cp ./apps/api/config/config.mainnet.yaml ./apps/api/config/config.yaml", - "copy-custom-config": "cp ./apps/api/config/config.custom.yaml ./apps/api/config/config.yaml", - "copy-devnet-config-transactions": "cp ./apps/transactions-processor/config/config.devnet.yaml ./apps/transactions-processor/config/config.yaml", - "copy-testnet-config-transactions": "cp ./apps/transactions-processor/config/config.testnet.yaml ./apps/transactions-processor/config/config.yaml", - "copy-mainnet-config-transactions": "cp ./apps/transactions-processor/config/config.mainnet.yaml ./apps/transactions-processor/config/config.yaml", - "copy-custom-config-transactions": "cp ./apps/transactions-processor/config/config.custom.yaml ./apps/transactions-processor/config/config.yaml", - "copy-devnet-config-queue-worker": "cp ./apps/queue-worker/config/config.devnet.yaml ./apps/queue-worker/config/config.yaml", - "copy-testnet-config-queue-worker": "cp ./apps/queue-worker/config/config.testnet.yaml ./apps/queue-worker/config/config.yaml", - "copy-mainnet-config-queue-worker": "cp ./apps/queue-worker/config/config.mainnet.yaml ./apps/queue-worker/config/config.yaml", - "copy-custom-config-queue-worker": "cp ./apps/queue-worker/config/config.custom.yaml ./apps/queue-worker/config/config.yaml", - "copy-devnet-config-cache-warmer": "cp ./apps/cache-warmer/config/config.devnet.yaml ./apps/cache-warmer/config/config.yaml", - "copy-testnet-config-cache-warmer": "cp ./apps/cache-warmer/config/config.testnet.yaml ./apps/cache-warmer/config/config.yaml", - "copy-mainnet-config-cache-warmer": "cp ./apps/cache-warmer/config/config.mainnet.yaml ./apps/cache-warmer/config/config.yaml", - "copy-custom-config-cache-warmer": "cp ./apps/cache-warmer/config/config.custom.yaml ./apps/cache-warmer/config/config.yaml" + "test:e2e": "jest --config ./apps/mvx-event-processor/test/jest-e2e.json", + "copy-devnet-config": "cp ./apps/mvx-event-processor/config/config.devnet.yaml ./apps/mvx-event-processor/config/config.yaml", + "copy-testnet-config": "cp ./apps/mvx-event-processor/config/config.testnet.yaml ./apps/mvx-event-processor/config/config.yaml", + "copy-mainnet-config": "cp ./apps/mvx-event-processor/config/config.mainnet.yaml ./apps/mvx-event-processor/config/config.yaml", + "copy-custom-config": "cp ./apps/mvx-event-processor/config/config.custom.yaml ./apps/mvx-event-processor/config/config.yaml", + "copy-devnet-config-axelar": "cp apps/axelar-event-processor/config/config.devnet.yaml apps/axelar-event-processor/config/config.yaml", + "copy-testnet-config-axelar": "cp apps/axelar-event-processor/config/config.testnet.yaml apps/axelar-event-processor/config/config.yaml", + "copy-mainnet-config-axelar": "cp apps/axelar-event-processor/config/config.mainnet.yaml apps/axelar-event-processor/config/config.yaml", + "copy-custom-config-axelar": "cp apps/axelar-event-processor/config/config.custom.yaml apps/axelar-event-processor/config/config.yaml" }, "dependencies": { + "@grpc/grpc-js": "^1.9.12", + "@grpc/proto-loader": "^0.7.10", "@multiversx/sdk-core": "^12.15.0", "@multiversx/sdk-nestjs-auth": "2.4.0", "@multiversx/sdk-nestjs-cache": "2.4.0", @@ -98,6 +72,7 @@ "@nestjs/swagger": "7.1.16", "@nestjs/typeorm": "10.0.0", "@nestjs/websockets": "10.2.8", + "@prisma/client": "^5.6.0", "agentkeepalive": "^4.3.0", "bull": "^4.10.4", "cache-manager": "^5.2.1", @@ -140,6 +115,7 @@ "jest": "^29.5.0", "js-yaml": "^4.1.0", "prettier": "^2.8.8", + "prisma": "^5.6.0", "supertest": "^6.3.3", "ts-jest": "29.0.5", "ts-loader": "9.4.2", diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..d205f42 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,11 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} diff --git a/tsconfig.json b/tsconfig.json index 16218b1..55bb7d3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -32,7 +32,7 @@ "forceConsistentCasingInFileNames": true, "importHelpers": true, "typeRoots": [ - "apps/api/src/@types", + "apps/mvx-event-processor/src/@types", "node_modules/@types" ], "skipLibCheck": true, From 802418d48720968d50db4efa4df4506d1b44bf81 Mon Sep 17 00:00:00 2001 From: Rares <6453351+raresserban@users.noreply.github.com> Date: Wed, 6 Dec 2023 16:25:58 +0200 Subject: [PATCH 02/33] Process gateway call contract event and save data into database. --- .env.example | 17 +- abis/gateway.abi.json | 262 ++ .../axelar/relayer.proto | 46 + .../config/config.devnet.yaml | 4 - .../config/config.mainnet.yaml | 4 - .../config/config.testnet.yaml | 4 - .../config/configuration.ts | 11 - .../config/relayer.proto | 16 - apps/axelar-event-processor/src/main.ts | 7 +- .../processor/transaction.processor.module.ts | 13 +- .../config/config.devnet.yaml | 7 - .../config/config.mainnet.yaml | 7 - .../config/config.testnet.yaml | 8 - .../config/configuration.ts | 11 - .../event-processor/event.processor.module.ts | 5 +- .../event.processor.service.spec.ts | 127 + .../event.processor.service.ts | 26 +- .../src/event-processor/types.ts | 1 + .../contract-call.processor.spec.ts | 89 + .../src/processors/contract-call.processor.ts | 37 + .../src/processors/index.ts | 1 + .../src/processors/processors.module.ts | 11 + config/configuration.ts | 10 + docker-compose.yml | 2 - libs/common/src/abi/index.ts | 1 - .../src/abi/interactions/contract.loader.ts | 42 - .../abi/interactions/contract.query.runner.ts | 29 - .../contract.transaction.generator.ts | 56 - libs/common/src/abi/interactions/index.ts | 3 - libs/common/src/config/api.config.module.ts | 37 +- libs/common/src/config/api.config.service.ts | 86 +- libs/common/src/contracts/contract.loader.ts | 47 + libs/common/src/contracts/contracts.module.ts | 59 + .../contracts/entities/contract-call-event.ts | 11 + libs/common/src/contracts/gateway.contract.ts | 36 + libs/common/src/database/database.module.ts | 4 +- .../contract-call-event.repository.ts | 14 + libs/common/src/index.ts | 1 - .../src/pubsub/pub.sub.listener.module.ts | 17 +- libs/common/src/utils/dynamic.module.utils.ts | 4 +- libs/common/src/utils/event.enum.ts | 7 + nest-cli.json | 16 +- package-lock.json | 2296 ++++------------- package.json | 61 +- .../migration.sql | 21 + prisma/migrations/migration_lock.toml | 3 + prisma/schema.prisma | 23 + 47 files changed, 1453 insertions(+), 2147 deletions(-) create mode 100644 abis/gateway.abi.json create mode 100644 apps/axelar-event-processor/axelar/relayer.proto delete mode 100644 apps/axelar-event-processor/config/config.devnet.yaml delete mode 100644 apps/axelar-event-processor/config/config.mainnet.yaml delete mode 100644 apps/axelar-event-processor/config/config.testnet.yaml delete mode 100644 apps/axelar-event-processor/config/configuration.ts delete mode 100644 apps/axelar-event-processor/config/relayer.proto delete mode 100644 apps/mvx-event-processor/config/config.devnet.yaml delete mode 100644 apps/mvx-event-processor/config/config.mainnet.yaml delete mode 100644 apps/mvx-event-processor/config/config.testnet.yaml delete mode 100644 apps/mvx-event-processor/config/configuration.ts create mode 100644 apps/mvx-event-processor/src/event-processor/event.processor.service.spec.ts create mode 100644 apps/mvx-event-processor/src/processors/contract-call.processor.spec.ts create mode 100644 apps/mvx-event-processor/src/processors/contract-call.processor.ts create mode 100644 apps/mvx-event-processor/src/processors/index.ts create mode 100644 apps/mvx-event-processor/src/processors/processors.module.ts create mode 100644 config/configuration.ts delete mode 100644 libs/common/src/abi/index.ts delete mode 100644 libs/common/src/abi/interactions/contract.loader.ts delete mode 100644 libs/common/src/abi/interactions/contract.query.runner.ts delete mode 100644 libs/common/src/abi/interactions/contract.transaction.generator.ts delete mode 100644 libs/common/src/abi/interactions/index.ts create mode 100644 libs/common/src/contracts/contract.loader.ts create mode 100644 libs/common/src/contracts/contracts.module.ts create mode 100644 libs/common/src/contracts/entities/contract-call-event.ts create mode 100644 libs/common/src/contracts/gateway.contract.ts create mode 100644 libs/common/src/database/repository/contract-call-event.repository.ts create mode 100644 libs/common/src/utils/event.enum.ts create mode 100644 prisma/migrations/20231206110005_contract_call_event/migration.sql create mode 100644 prisma/migrations/migration_lock.toml diff --git a/.env.example b/.env.example index 1d63b2d..1645736 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,14 @@ -# Environment variables declared in this file are automatically made available to Prisma. -# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema +DATABASE_URL=postgresql://root:password@localhost:5432/relayer -# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB. -# See the documentation for all the connection string options: https://pris.ly/d/connection-strings +API_URL=https://devnet-api.multiversx.com +GATEWAY_URL=https://devnet-gateway.multiversx.com +REDIS_URL=127.0.0.1 -DATABASE_URL=postgresql://root:password@localhost:5435/example +EVENTS_NOTIFIER_URL=amqp://user:password@rabbitmq:5672 +EVENTS_NOTIFIER_QUEUE=queue + +CONTRACT_GATEWAY= + +AXELAR_API_URL= + +SOURCE_CHAIN_NAME=multiversx-D diff --git a/abis/gateway.abi.json b/abis/gateway.abi.json new file mode 100644 index 0000000..800afbc --- /dev/null +++ b/abis/gateway.abi.json @@ -0,0 +1,262 @@ +{ + "buildInfo": { + "rustc": { + "version": "1.71.0-nightly", + "commitHash": "a2b1646c597329d0a25efa3889b66650f65de1de", + "commitDate": "2023-05-25", + "channel": "Nightly", + "short": "rustc 1.71.0-nightly (a2b1646c5 2023-05-25)" + }, + "contractCrate": { + "name": "gateway", + "version": "0.0.0" + }, + "framework": { + "name": "multiversx-sc", + "version": "0.43.5" + } + }, + "name": "Gateway", + "constructor": { + "inputs": [ + { + "name": "auth_module", + "type": "Address" + }, + { + "name": "chain_id", + "type": "bytes" + } + ], + "outputs": [] + }, + "endpoints": [ + { + "name": "callContract", + "mutability": "mutable", + "inputs": [ + { + "name": "destination_chain", + "type": "bytes" + }, + { + "name": "destination_contract_address", + "type": "bytes" + }, + { + "name": "payload", + "type": "bytes" + } + ], + "outputs": [] + }, + { + "name": "validateContractCall", + "mutability": "mutable", + "inputs": [ + { + "name": "command_id", + "type": "array32" + }, + { + "name": "source_chain", + "type": "bytes" + }, + { + "name": "source_address", + "type": "bytes" + }, + { + "name": "payload_hash", + "type": "array32" + } + ], + "outputs": [ + { + "type": "bool" + } + ] + }, + { + "name": "execute", + "mutability": "mutable", + "inputs": [ + { + "name": "input", + "type": "ExecuteInput" + } + ], + "outputs": [] + }, + { + "name": "isCommandExecuted", + "mutability": "readonly", + "inputs": [ + { + "name": "command_id", + "type": "array32" + } + ], + "outputs": [ + { + "type": "bool" + } + ] + }, + { + "name": "isContractCallApproved", + "mutability": "readonly", + "inputs": [ + { + "name": "command_id", + "type": "array32" + }, + { + "name": "source_chain", + "type": "bytes" + }, + { + "name": "source_address", + "type": "bytes" + }, + { + "name": "contract_address", + "type": "Address" + }, + { + "name": "payload_hash", + "type": "array32" + } + ], + "outputs": [ + { + "type": "bool" + } + ] + }, + { + "name": "chain_id", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "bytes" + } + ] + }, + { + "name": "authModule", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "Address" + } + ] + } + ], + "events": [ + { + "identifier": "contract_call_event", + "inputs": [ + { + "name": "sender", + "type": "Address", + "indexed": true + }, + { + "name": "destination_chain", + "type": "bytes", + "indexed": true + }, + { + "name": "destination_contract_address", + "type": "bytes", + "indexed": true + }, + { + "name": "data", + "type": "ContractCallData" + } + ] + }, + { + "identifier": "executed_event", + "inputs": [ + { + "name": "command_id", + "type": "array32", + "indexed": true + } + ] + }, + { + "identifier": "contract_call_approved_event", + "inputs": [ + { + "name": "command_id", + "type": "array32", + "indexed": true + }, + { + "name": "source_chain", + "type": "bytes", + "indexed": true + }, + { + "name": "source_address", + "type": "bytes", + "indexed": true + }, + { + "name": "contract_address", + "type": "Address", + "indexed": true + }, + { + "name": "payload_hash", + "type": "array32", + "indexed": true + } + ] + }, + { + "identifier": "operatorship_transferred_event", + "inputs": [ + { + "name": "params", + "type": "bytes" + } + ] + } + ], + "hasCallback": false, + "types": { + "ContractCallData": { + "type": "struct", + "fields": [ + { + "name": "hash", + "type": "array32" + }, + { + "name": "payload", + "type": "bytes" + } + ] + }, + "ExecuteInput": { + "type": "struct", + "fields": [ + { + "name": "data", + "type": "bytes" + }, + { + "name": "proof", + "type": "bytes" + } + ] + } + } +} diff --git a/apps/axelar-event-processor/axelar/relayer.proto b/apps/axelar-event-processor/axelar/relayer.proto new file mode 100644 index 0000000..3b9c4f4 --- /dev/null +++ b/apps/axelar-event-processor/axelar/relayer.proto @@ -0,0 +1,46 @@ +syntax = "proto3"; +package axelar.relayer.v1beta1; + +service Relayer{ + rpc Verify(stream VerifyRequest) returns (stream VerifyResponse); + rpc GetPayload(GetPayloadRequest) returns (GetPayloadResponse); + rpc SubscribeToApprovals(SubscribeToApprovalsRequest) returns (stream SubscribeToApprovalsResponse); +} + +message VerifyRequest{ + Message message = 1; +} + +message VerifyResponse{ + Message message = 1; + bool success = 3; +} + +message Message{ + string id = 1; // the unique identifier with which the message can be looked up on the source chain + string source_chain = 2; + string source_address= 3; + string destination_chain = 4; + string destination_address = 5; + bytes payload = 6; + // when we have a better idea of the requirement, we can add an additional optional field here to facilitate verification proofs +} + +message GetPayloadRequest{ + bytes hash = 1; +} + +message GetPayloadResponse{ + bytes payload = 1; +} + +message SubscribeToApprovalsRequest{ + string chain = 1; + optional uint64 start_height = 2; // can be used to replay events +} + +message SubscribeToApprovalsResponse{ + string chain = 1; + bytes execute_data = 2; + uint64 block_height = 3; +} diff --git a/apps/axelar-event-processor/config/config.devnet.yaml b/apps/axelar-event-processor/config/config.devnet.yaml deleted file mode 100644 index 32e511e..0000000 --- a/apps/axelar-event-processor/config/config.devnet.yaml +++ /dev/null @@ -1,4 +0,0 @@ -urls: - api: 'https://devnet-api.multiversx.com' - redis: '127.0.0.1' - axelarApi: 'localhost:5000' diff --git a/apps/axelar-event-processor/config/config.mainnet.yaml b/apps/axelar-event-processor/config/config.mainnet.yaml deleted file mode 100644 index 6749a74..0000000 --- a/apps/axelar-event-processor/config/config.mainnet.yaml +++ /dev/null @@ -1,4 +0,0 @@ -urls: - api: 'https://api.multiversx.com' - redis: '127.0.0.1' - axelarApi: 'localhost:5000' diff --git a/apps/axelar-event-processor/config/config.testnet.yaml b/apps/axelar-event-processor/config/config.testnet.yaml deleted file mode 100644 index d163767..0000000 --- a/apps/axelar-event-processor/config/config.testnet.yaml +++ /dev/null @@ -1,4 +0,0 @@ -urls: - api: 'https://testnet-api.multiversx.com' - redis: '127.0.0.1' - axelarApi: 'localhost:5000' diff --git a/apps/axelar-event-processor/config/configuration.ts b/apps/axelar-event-processor/config/configuration.ts deleted file mode 100644 index 73ba517..0000000 --- a/apps/axelar-event-processor/config/configuration.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { readFileSync } from 'fs'; -import * as yaml from 'js-yaml'; -import { join } from 'path'; - -const YAML_CONFIG_FILENAME = 'config.yaml'; - -export default () => { - return yaml.load( - readFileSync(join(__dirname, YAML_CONFIG_FILENAME), 'utf8'), - ) as Record; -}; diff --git a/apps/axelar-event-processor/config/relayer.proto b/apps/axelar-event-processor/config/relayer.proto deleted file mode 100644 index 0c72d9f..0000000 --- a/apps/axelar-event-processor/config/relayer.proto +++ /dev/null @@ -1,16 +0,0 @@ -syntax = "proto3"; - -package axelar; - -service RelayerService { - rpc GetPayload (MessageById) returns (Payload) {} -} - -message MessageById { - int32 id = 1; -} - -message Payload { - int32 id = 1; - string name = 2; -} diff --git a/apps/axelar-event-processor/src/main.ts b/apps/axelar-event-processor/src/main.ts index 33a9423..813ddca 100644 --- a/apps/axelar-event-processor/src/main.ts +++ b/apps/axelar-event-processor/src/main.ts @@ -2,7 +2,6 @@ import 'module-alias/register'; import { NestFactory } from '@nestjs/core'; import { TransactionProcessorModule } from './processor'; import { ApiConfigService, PubSubListenerModule } from '@mvx-monorepo/common'; -import configuration from '../config/configuration'; import { MicroserviceOptions, Transport } from '@nestjs/microservices'; import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; import { join } from 'path'; @@ -12,12 +11,12 @@ async function bootstrap() { const apiConfigService = transactionProcessorApp.get(ApiConfigService); const pubSubApp = await NestFactory.createMicroservice( - PubSubListenerModule.forRoot(configuration), + PubSubListenerModule.forRoot(), { transport: Transport.GRPC, options: { - package: 'axelar', - protoPath: join(__dirname, '../config/relayer.proto'), + package: 'axelar.relayer.v1beta1', + protoPath: join(__dirname, '../axelar/relayer.proto'), url: apiConfigService.getAxelarApiUrl(), }, }, diff --git a/apps/axelar-event-processor/src/processor/transaction.processor.module.ts b/apps/axelar-event-processor/src/processor/transaction.processor.module.ts index cf3dfa5..a3f9b0f 100644 --- a/apps/axelar-event-processor/src/processor/transaction.processor.module.ts +++ b/apps/axelar-event-processor/src/processor/transaction.processor.module.ts @@ -2,16 +2,9 @@ import { Module } from '@nestjs/common'; import { ScheduleModule } from '@nestjs/schedule'; import { ApiConfigModule, DynamicModuleUtils } from '@mvx-monorepo/common'; import { TransactionProcessorService } from './transaction.processor.service'; -import configuration from '../../config/configuration'; @Module({ - imports: [ - ScheduleModule.forRoot(), - ApiConfigModule.forRoot(configuration), - DynamicModuleUtils.getCachingModule(configuration), - ], - providers: [ - TransactionProcessorService, - ], + imports: [ApiConfigModule, ScheduleModule.forRoot(), DynamicModuleUtils.getCachingModule()], + providers: [TransactionProcessorService], }) -export class TransactionProcessorModule { } +export class TransactionProcessorModule {} diff --git a/apps/mvx-event-processor/config/config.devnet.yaml b/apps/mvx-event-processor/config/config.devnet.yaml deleted file mode 100644 index 1ff98a3..0000000 --- a/apps/mvx-event-processor/config/config.devnet.yaml +++ /dev/null @@ -1,7 +0,0 @@ -urls: - api: 'https://devnet-api.multiversx.com' - redis: '127.0.0.1' - eventsNotifier: '' -eventsNotifier: - queue: '' - gatewayAddress: '' diff --git a/apps/mvx-event-processor/config/config.mainnet.yaml b/apps/mvx-event-processor/config/config.mainnet.yaml deleted file mode 100644 index 1ff98a3..0000000 --- a/apps/mvx-event-processor/config/config.mainnet.yaml +++ /dev/null @@ -1,7 +0,0 @@ -urls: - api: 'https://devnet-api.multiversx.com' - redis: '127.0.0.1' - eventsNotifier: '' -eventsNotifier: - queue: '' - gatewayAddress: '' diff --git a/apps/mvx-event-processor/config/config.testnet.yaml b/apps/mvx-event-processor/config/config.testnet.yaml deleted file mode 100644 index 8a84f2e..0000000 --- a/apps/mvx-event-processor/config/config.testnet.yaml +++ /dev/null @@ -1,8 +0,0 @@ -urls: - api: 'https://devnet-api.multiversx.com' - redis: '127.0.0.1' - eventsNotifier: '' -eventsNotifierQueue: '' -eventsNotifier: - queue: '' - gatewayAddress: '' diff --git a/apps/mvx-event-processor/config/configuration.ts b/apps/mvx-event-processor/config/configuration.ts deleted file mode 100644 index 73ba517..0000000 --- a/apps/mvx-event-processor/config/configuration.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { readFileSync } from 'fs'; -import * as yaml from 'js-yaml'; -import { join } from 'path'; - -const YAML_CONFIG_FILENAME = 'config.yaml'; - -export default () => { - return yaml.load( - readFileSync(join(__dirname, YAML_CONFIG_FILENAME), 'utf8'), - ) as Record; -}; diff --git a/apps/mvx-event-processor/src/event-processor/event.processor.module.ts b/apps/mvx-event-processor/src/event-processor/event.processor.module.ts index b93e765..79852cd 100644 --- a/apps/mvx-event-processor/src/event-processor/event.processor.module.ts +++ b/apps/mvx-event-processor/src/event-processor/event.processor.module.ts @@ -2,11 +2,11 @@ import { RabbitModule, RabbitModuleOptions } from '@multiversx/sdk-nestjs-rabbit import { Module } from '@nestjs/common'; import { EventProcessorService } from './event.processor.service'; import { ApiConfigModule, ApiConfigService } from '@mvx-monorepo/common'; -import configuration from '../../config/configuration'; +import { ProcessorsModule } from '../processors'; @Module({ imports: [ - ApiConfigModule.forRoot(configuration), + ApiConfigModule, RabbitModule.forRootAsync({ useFactory: (apiConfigService: ApiConfigService) => new RabbitModuleOptions(apiConfigService.getEventsNotifierUrl(), [], { @@ -14,6 +14,7 @@ import configuration from '../../config/configuration'; }), inject: [ApiConfigService], }), + ProcessorsModule, ], providers: [EventProcessorService], exports: [EventProcessorService], diff --git a/apps/mvx-event-processor/src/event-processor/event.processor.service.spec.ts b/apps/mvx-event-processor/src/event-processor/event.processor.service.spec.ts new file mode 100644 index 0000000..72e1bcf --- /dev/null +++ b/apps/mvx-event-processor/src/event-processor/event.processor.service.spec.ts @@ -0,0 +1,127 @@ +import { EventProcessorService } from './event.processor.service'; +import { ApiConfigService } from '@mvx-monorepo/common'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ContractCallProcessor } from '../processors/contract-call.processor'; +import { Test } from '@nestjs/testing'; +import { NotifierBlockEvent } from './types'; +import { BinaryUtils } from '@multiversx/sdk-nestjs-common'; +import { Events } from '@mvx-monorepo/common/utils/event.enum'; + +describe('EventProcessorService', () => { + let apiConfigService: DeepMocked; + let contractCallProcessor: DeepMocked; + + let service: EventProcessorService; + + beforeEach(async () => { + apiConfigService = createMock(); + contractCallProcessor = createMock(); + + apiConfigService.getContractGateway.mockReturnValue('mockGatewayAddress'); + + const moduleRef = await Test.createTestingModule({ + providers: [EventProcessorService], + }) + .useMocker((token) => { + if (token === ApiConfigService) { + return apiConfigService; + } + + if (token === ContractCallProcessor) { + return contractCallProcessor; + } + + return undefined; + }) + .compile(); + + service = moduleRef.get(EventProcessorService); + }); + + describe('consumeEvents', () => { + it('Should not consume event', async () => { + const blockEvent: NotifierBlockEvent = { + hash: 'test', + shardId: 1, + timestamp: 123456, + events: [ + { + txHash: 'test', + address: 'someAddress', + identifier: 'someIdentifier', + data: '', + topics: [], + order: 0, + }, + { + txHash: 'test', + address: 'mockGatewayAddress', + identifier: 'someIdentifier', + data: '', + topics: [BinaryUtils.base64Encode(Events.CONTRACT_CALL_EVENT)], + order: 0, + }, + { + txHash: 'test', + address: 'mockGatewayAddress', + identifier: 'callContract', + data: '', + topics: [''], + order: 0, + }, + ], + }; + + await service.consumeEvents(blockEvent); + + expect(apiConfigService.getContractGateway).toHaveBeenCalledTimes(3); + expect(contractCallProcessor.handleEvent).not.toHaveBeenCalled(); + }); + + it('Should consume event', async () => { + const blockEvent: NotifierBlockEvent = { + hash: 'test', + shardId: 1, + timestamp: 123456, + events: [ + { + txHash: 'test', + address: 'mockGatewayAddress', + identifier: 'callContract', + data: '', + topics: [BinaryUtils.base64Encode(Events.CONTRACT_CALL_EVENT)], + order: 0, + }, + ], + }; + + await service.consumeEvents(blockEvent); + + expect(apiConfigService.getContractGateway).toHaveBeenCalledTimes(1); + expect(contractCallProcessor.handleEvent).toHaveBeenCalledTimes(1); + }); + + it('Should throw error', async () => { + const blockEvent: NotifierBlockEvent = { + hash: 'test', + shardId: 1, + timestamp: 123456, + events: [ + { + txHash: 'test', + address: 'mockGatewayAddress', + identifier: 'callContract', + data: '', + topics: [], + order: 0, + }, + ], + }; + + await expect(service.consumeEvents(blockEvent)).rejects.toThrow(); + + expect(apiConfigService.getContractGateway).toHaveBeenCalledTimes(1); + expect(contractCallProcessor.handleEvent).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/apps/mvx-event-processor/src/event-processor/event.processor.service.ts b/apps/mvx-event-processor/src/event-processor/event.processor.service.ts index 3958e1a..586cba0 100644 --- a/apps/mvx-event-processor/src/event-processor/event.processor.service.ts +++ b/apps/mvx-event-processor/src/event-processor/event.processor.service.ts @@ -2,18 +2,24 @@ import { Injectable, Logger } from '@nestjs/common'; import { ApiConfigService } from '@mvx-monorepo/common'; import { NotifierBlockEvent, NotifierEvent } from './types'; import { RabbitSubscribe } from '@golevelup/nestjs-rabbitmq'; -import configuration from '../../config/configuration'; +import { EVENTS_NOTIFIER_QUEUE } from '../../../../config/configuration'; +import { EventIdentifiers, Events } from '@mvx-monorepo/common/utils/event.enum'; +import { BinaryUtils } from '@multiversx/sdk-nestjs-common'; +import { ContractCallProcessor } from '../processors/contract-call.processor'; @Injectable() export class EventProcessorService { private readonly logger: Logger; - constructor(private readonly apiConfigService: ApiConfigService) { + constructor( + private readonly apiConfigService: ApiConfigService, + private readonly contractCallProcessor: ContractCallProcessor, + ) { this.logger = new Logger(EventProcessorService.name); } @RabbitSubscribe({ - queue: configuration().eventsNotifier.queue, + queue: EVENTS_NOTIFIER_QUEUE, createQueueIfNotExists: false, }) async consumeEvents(blockEvent: NotifierBlockEvent) { @@ -33,18 +39,22 @@ export class EventProcessorService { } } - // TODO: Implement logic - private handleEvent(event: NotifierEvent) { - this.logger.log('Received event from MultiversX Gateway contract:'); + private async handleEvent(event: NotifierEvent) { + this.logger.log('Received event from MultiversX:'); this.logger.log(JSON.stringify(event)); - if (event.address !== this.apiConfigService.getEventsNotifierGatewayAddress()) { + if (event.address !== this.apiConfigService.getContractGateway()) { return; } - if (event.identifier === 'callContract') { + if ( + event.identifier === EventIdentifiers.CALL_CONTRACT && + BinaryUtils.base64Decode(event.topics[0]) === Events.CONTRACT_CALL_EVENT + ) { this.logger.log('Received callContract event from MultiversX Gateway contract:'); this.logger.log(JSON.stringify(event)); + + await this.contractCallProcessor.handleEvent(event); } } } diff --git a/apps/mvx-event-processor/src/event-processor/types.ts b/apps/mvx-event-processor/src/event-processor/types.ts index b139e96..30c56fe 100644 --- a/apps/mvx-event-processor/src/event-processor/types.ts +++ b/apps/mvx-event-processor/src/event-processor/types.ts @@ -11,4 +11,5 @@ export interface NotifierEvent { identifier: string; data: string; topics: string[]; + order: number; } diff --git a/apps/mvx-event-processor/src/processors/contract-call.processor.spec.ts b/apps/mvx-event-processor/src/processors/contract-call.processor.spec.ts new file mode 100644 index 0000000..9b3f77b --- /dev/null +++ b/apps/mvx-event-processor/src/processors/contract-call.processor.spec.ts @@ -0,0 +1,89 @@ +import { ApiConfigService } from '@mvx-monorepo/common'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { Test } from '@nestjs/testing'; +import { BinaryUtils } from '@multiversx/sdk-nestjs-common'; +import { Events } from '@mvx-monorepo/common/utils/event.enum'; +import { ContractCallEventRepository } from '@mvx-monorepo/common/database/repository/contract-call-event.repository'; +import { ContractCallProcessor } from './contract-call.processor'; +import { NotifierEvent } from '../event-processor/types'; +import { ContractsModule } from '@mvx-monorepo/common/contracts/contracts.module'; +import { Address } from '@multiversx/sdk-core/out'; +import { ContractCallEventStatus } from '@prisma/client'; + +describe('ContractCallProcessor', () => { + let contractCallEventRepository: DeepMocked; + let apiConfigService: DeepMocked; + + let service: ContractCallProcessor; + + beforeEach(async () => { + contractCallEventRepository = createMock(); + apiConfigService = createMock(); + + apiConfigService.getSourceChainName.mockReturnValue('multiversx-test'); + apiConfigService.getContractGateway.mockReturnValue( + 'erd1qqqqqqqqqqqqqpgqsvzyz88e8v8j6x3wquatxuztnxjwnw92kkls6rdtzx', + ); + + const moduleRef = await Test.createTestingModule({ + imports: [ContractsModule], // it uses real GatewayContract object loaded from abi + providers: [ContractCallProcessor], + }) + .useMocker((token) => { + if (token === ContractCallEventRepository) { + return contractCallEventRepository; + } + + if (token === ApiConfigService) { + return apiConfigService; + } + + return undefined; + }) + .compile(); + + service = moduleRef.get(ContractCallProcessor); + }); + + describe('handleEvent', () => { + it('Should handle event', async () => { + const data = Buffer.concat([ + Buffer.from('ebc84cbd75ba5516bf45e7024a9e12bc3c5c880f73e3a5beca7ebba52b2867a7', 'hex'), + Buffer.from('00000007', 'hex'), // length of payload as u32 + Buffer.from('payload'), + ]); + const rawEvent: NotifierEvent = { + txHash: 'txHash', + address: 'mockGatewayAddress', + identifier: 'callContract', + data: data.toString('base64'), + topics: [ + BinaryUtils.base64Encode(Events.CONTRACT_CALL_EVENT), + Buffer.from( + Address.fromBech32('erd1qqqqqqqqqqqqqpgqzqvm5ywqqf524efwrhr039tjs29w0qltkklsa05pk7').hex(), + 'hex', + ).toString('base64'), + BinaryUtils.base64Encode('ethereum'), + BinaryUtils.base64Encode('destinationAddress'), + ], + order: 1, + }; + + await service.handleEvent(rawEvent); + + expect(contractCallEventRepository.create).toHaveBeenCalledTimes(1); + expect(contractCallEventRepository.create).toHaveBeenCalledWith({ + id: 'multiversx-test:txHash:1', + txHash: 'txHash', + eventIndex: 1, + status: ContractCallEventStatus.PENDING, + sourceAddress: 'erd1qqqqqqqqqqqqqpgqzqvm5ywqqf524efwrhr039tjs29w0qltkklsa05pk7', + sourceChain: 'multiversx-test', + destinationAddress: 'destinationAddress', + destinationChain: 'ethereum', + payloadHash: 'ebc84cbd75ba5516bf45e7024a9e12bc3c5c880f73e3a5beca7ebba52b2867a7', + payload: Buffer.from('payload'), + }); + }); + }); +}); diff --git a/apps/mvx-event-processor/src/processors/contract-call.processor.ts b/apps/mvx-event-processor/src/processors/contract-call.processor.ts new file mode 100644 index 0000000..6c25ab0 --- /dev/null +++ b/apps/mvx-event-processor/src/processors/contract-call.processor.ts @@ -0,0 +1,37 @@ +import { Injectable } from '@nestjs/common'; +import { NotifierEvent } from '../event-processor/types'; +import { GatewayContract } from '@mvx-monorepo/common/contracts/gateway.contract'; +import { TransactionEvent } from '@multiversx/sdk-network-providers/out'; +import { ContractCallEventRepository } from '@mvx-monorepo/common/database/repository/contract-call-event.repository'; +import { ApiConfigService } from '@mvx-monorepo/common'; +import { ContractCallEventStatus } from '@prisma/client'; + +@Injectable() +export class ContractCallProcessor { + private sourceChain: string; + + constructor( + private readonly gatewayContract: GatewayContract, + private readonly contractCallEventRepository: ContractCallEventRepository, + apiConfigService: ApiConfigService, + ) { + this.sourceChain = apiConfigService.getSourceChainName(); + } + + async handleEvent(rawEvent: NotifierEvent) { + const event = this.gatewayContract.decodeContractCallEvent(TransactionEvent.fromHttpResponse(rawEvent)); + + await this.contractCallEventRepository.create({ + id: `${this.sourceChain}:${rawEvent.txHash}:${rawEvent.order}`, + txHash: rawEvent.txHash, + eventIndex: rawEvent.order, + status: ContractCallEventStatus.PENDING, + sourceAddress: event.sender.bech32(), + sourceChain: this.sourceChain, + destinationAddress: event.destination_contract_address, + destinationChain: event.destination_chain, + payloadHash: event.data.hash, + payload: event.data.payload, + }); + } +} diff --git a/apps/mvx-event-processor/src/processors/index.ts b/apps/mvx-event-processor/src/processors/index.ts new file mode 100644 index 0000000..88d706e --- /dev/null +++ b/apps/mvx-event-processor/src/processors/index.ts @@ -0,0 +1 @@ +export * from './processors.module'; diff --git a/apps/mvx-event-processor/src/processors/processors.module.ts b/apps/mvx-event-processor/src/processors/processors.module.ts new file mode 100644 index 0000000..bec724c --- /dev/null +++ b/apps/mvx-event-processor/src/processors/processors.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { ContractCallProcessor } from './contract-call.processor'; +import { ContractsModule } from '@mvx-monorepo/common/contracts/contracts.module'; +import { DatabaseModule } from '@mvx-monorepo/common'; + +@Module({ + imports: [ContractsModule, DatabaseModule], + providers: [ContractCallProcessor], + exports: [ContractCallProcessor], +}) +export class ProcessorsModule {} diff --git a/config/configuration.ts b/config/configuration.ts new file mode 100644 index 0000000..654d33a --- /dev/null +++ b/config/configuration.ts @@ -0,0 +1,10 @@ +import * as process from 'process'; + +require("dotenv").config({ + path: process.env.NODE_ENV == 'test' ? '.env.test' : '.env', +}); + +// Needed here since it is used in a decorator where the ApiConfigService can not be used +export const EVENTS_NOTIFIER_QUEUE: string = process.env['EVENTS_NOTIFIER_QUEUE'] as string; + +export default () => process.env; diff --git a/docker-compose.yml b/docker-compose.yml index 33c92e4..b7bfb5e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,5 +16,3 @@ services: - POSTGRES_DB=relayer ports: - 5432:5432 - volumes: - - ./.db:/var/lib/postgresql/data diff --git a/libs/common/src/abi/index.ts b/libs/common/src/abi/index.ts deleted file mode 100644 index 6d1756d..0000000 --- a/libs/common/src/abi/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './interactions'; diff --git a/libs/common/src/abi/interactions/contract.loader.ts b/libs/common/src/abi/interactions/contract.loader.ts deleted file mode 100644 index c480da6..0000000 --- a/libs/common/src/abi/interactions/contract.loader.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { SmartContract, AbiRegistry, Address } from "@multiversx/sdk-core"; -import { Logger } from "@nestjs/common"; -import * as fs from "fs"; - -export class ContractLoader { - private readonly logger: Logger; - private readonly abiPath: string; - private contract: SmartContract | undefined = undefined; - - constructor(abiPath: string) { - this.abiPath = abiPath; - - this.logger = new Logger(ContractLoader.name); - } - - private async load(contractAddress: string): Promise { - try { - const jsonContent: string = await fs.promises.readFile(this.abiPath, { encoding: "utf8" }); - const json = JSON.parse(jsonContent); - - const abiRegistry = AbiRegistry.create(json); - - return new SmartContract({ - address: new Address(contractAddress), - abi: abiRegistry, - }); - } catch (error) { - this.logger.log(`Unexpected error when trying to create smart contract from abi`); - this.logger.error(error); - - throw new Error('Error when creating contract from abi'); - } - } - - async getContract(contractAddress: string): Promise { - if (!this.contract) { - this.contract = await this.load(contractAddress); - } - - return this.contract; - } -} diff --git a/libs/common/src/abi/interactions/contract.query.runner.ts b/libs/common/src/abi/interactions/contract.query.runner.ts deleted file mode 100644 index d23f026..0000000 --- a/libs/common/src/abi/interactions/contract.query.runner.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { ContractQueryResponse } from "@multiversx/sdk-network-providers"; -import { INetworkProvider } from "@multiversx/sdk-network-providers/out/interface"; -import { ResultsParser, SmartContract, Interaction, TypedOutcomeBundle } from "@multiversx/sdk-core"; -import { Logger } from "@nestjs/common"; - -export class ContractQueryRunner { - private readonly logger: Logger; - private readonly proxy: INetworkProvider; - private readonly parser: ResultsParser = new ResultsParser(); - - constructor(proxy: INetworkProvider) { - this.logger = new Logger(ContractQueryRunner.name); - - this.proxy = proxy; - } - - async runQuery(contract: SmartContract, interaction: Interaction): Promise { - try { - const queryResponse: ContractQueryResponse = await this.proxy.queryContract(interaction.buildQuery()); - - return this.parser.parseQueryResponse(queryResponse, interaction.getEndpoint()); - } catch (error) { - this.logger.log(`Unexpected error when running query '${interaction.buildQuery().func}' to sc '${contract.getAddress().bech32()}' `); - this.logger.error(error); - - throw error; - } - } -} diff --git a/libs/common/src/abi/interactions/contract.transaction.generator.ts b/libs/common/src/abi/interactions/contract.transaction.generator.ts deleted file mode 100644 index 62eaeaf..0000000 --- a/libs/common/src/abi/interactions/contract.transaction.generator.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { NetworkConfig } from "@multiversx/sdk-network-providers"; -import { INetworkProvider } from "@multiversx/sdk-network-providers/out/interface"; -import { Interaction, IAddress, Transaction } from "@multiversx/sdk-core"; -import { Logger } from "@nestjs/common"; - -export class ContractTransactionGenerator { - private readonly logger: Logger; - private readonly proxy: INetworkProvider; - private networkConfig: NetworkConfig | undefined = undefined; - - constructor(proxy: INetworkProvider) { - this.logger = new Logger(ContractTransactionGenerator.name); - - this.proxy = proxy; - } - - private async loadNetworkConfig(): Promise { - try { - const networkConfig: NetworkConfig = await this.proxy.getNetworkConfig(); - - return networkConfig; - } catch (error) { - this.logger.log(`Unexpected error when trying to load network config`); - this.logger.error(error); - - throw new Error('Error when loading network config'); - } - } - - private async getNetworkConfig(): Promise { - if (!this.networkConfig) { - this.networkConfig = await this.loadNetworkConfig(); - } - - return this.networkConfig; - } - - async createTransaction(interaction: Interaction, signerAddress: IAddress): Promise { - try { - const transaction: Transaction = interaction.buildTransaction(); - - const signerAccount = await this.proxy.getAccount(signerAddress); - transaction.setNonce(signerAccount.nonce.valueOf()); - - const networkConfig: NetworkConfig = await this.getNetworkConfig(); - transaction.setChainID(networkConfig.ChainID); - - return transaction; - } catch (error) { - this.logger.log(`Unexpected error when trying to create transaction '${interaction.getFunction().valueOf()}' to contract '${interaction.getContractAddress().bech32()}'`); - this.logger.error(error); - - throw error; - } - } -} diff --git a/libs/common/src/abi/interactions/index.ts b/libs/common/src/abi/interactions/index.ts deleted file mode 100644 index 2aa5047..0000000 --- a/libs/common/src/abi/interactions/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './contract.loader'; -export * from './contract.query.runner'; -export * from './contract.transaction.generator'; diff --git a/libs/common/src/config/api.config.module.ts b/libs/common/src/config/api.config.module.ts index 4d1e639..f020f8f 100644 --- a/libs/common/src/config/api.config.module.ts +++ b/libs/common/src/config/api.config.module.ts @@ -1,24 +1,17 @@ -import { Global, Module } from "@nestjs/common"; -import { ConfigModule } from "@nestjs/config"; -import { ApiConfigService } from "./api.config.service"; +import { Global, Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { ApiConfigService } from './api.config.service'; +import configuration from '../../../../config/configuration'; @Global() -@Module({}) -export class ApiConfigModule { - static forRoot(configFactory: () => Record) { - return { - module: ApiConfigModule, - imports: [ - ConfigModule.forRoot({ - load: [configFactory], - }), - ], - providers: [ - ApiConfigService, - ], - exports: [ - ApiConfigService, - ], - }; - } -} +@Module({ + imports: [ConfigModule.forRoot({ + load: [configuration], + ignoreEnvFile: true, + ignoreEnvVars: true, + cache: true, + })], + providers: [ApiConfigService], + exports: [ApiConfigService], +}) +export class ApiConfigModule {} diff --git a/libs/common/src/config/api.config.service.ts b/libs/common/src/config/api.config.service.ts index a235b91..4a0d3dd 100644 --- a/libs/common/src/config/api.config.service.ts +++ b/libs/common/src/config/api.config.service.ts @@ -1,12 +1,13 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; +import { EVENTS_NOTIFIER_QUEUE } from '../../../../config/configuration'; @Injectable() export class ApiConfigService { constructor(private readonly configService: ConfigService) {} getApiUrl(): string { - const apiUrl = this.configService.get('urls.api'); + const apiUrl = this.configService.get('API_URL'); if (!apiUrl) { throw new Error('No API url present'); } @@ -14,17 +15,37 @@ export class ApiConfigService { return apiUrl; } - getAxelarApiUrl(): string { - const axelarApiUrl = this.configService.get('urls.axelarApi'); - if (!axelarApiUrl) { - throw new Error('No Axelar API url present'); + getGatewayUrl(): string { + const gatewayUrl = this.configService.get('GATEWAY_URL'); + if (!gatewayUrl) { + throw new Error('No Gateway url present'); } - return axelarApiUrl; + return gatewayUrl; + } + + getRedisUrl(): string { + const redisUrl = this.configService.get('REDIS_URL'); + if (!redisUrl) { + throw new Error('No redisUrl present'); + } + + return redisUrl; + } + + getRedisPort(): number { + const url = this.getRedisUrl(); + const components = url.split(':'); + + if (components.length > 1) { + return Number(components[1]); + } + + return 6379; } getEventsNotifierUrl(): string { - const eventsNotifierUrl = this.configService.get('urls.eventsNotifier'); + const eventsNotifierUrl = this.configService.get('EVENTS_NOTIFIER_URL'); if (!eventsNotifierUrl) { throw new Error('No Events Notifier url present'); } @@ -33,16 +54,11 @@ export class ApiConfigService { } getEventsNotifierQueue(): string { - const eventsNotifierQueue = this.configService.get('eventsNotifier.queue'); - if (!eventsNotifierQueue) { - throw new Error('No Events Notifier Queue present'); - } - - return eventsNotifierQueue; + return EVENTS_NOTIFIER_QUEUE; } - getEventsNotifierGatewayAddress(): string { - const eventsNotifierGatewayAddress = this.configService.get('eventsNotifier.gatewayAddress'); + getContractGateway(): string { + const eventsNotifierGatewayAddress = this.configService.get('CONTRACT_GATEWAY'); if (!eventsNotifierGatewayAddress) { throw new Error('No Events Notifier Gateway Address present'); } @@ -50,37 +66,37 @@ export class ApiConfigService { return eventsNotifierGatewayAddress; } - getRedisUrl(): string { - const redisUrl = this.configService.get('urls.redis'); - if (!redisUrl) { - throw new Error('No redisUrl present'); + getAxelarApiUrl(): string { + const axelarApiUrl = this.configService.get('AXELAR_API_URL'); + if (!axelarApiUrl) { + throw new Error('No Axelar API url present'); } - return redisUrl; - } - - getRedisHost(): string { - const url = this.getRedisUrl(); - - return url.split(':')[0]; + return axelarApiUrl; } - getRedisPort(): number { - const url = this.getRedisUrl(); - const components = url.split(':'); - - if (components.length > 1) { - return Number(components[1]); + getSourceChainName(): string { + const sourceChainName = this.configService.get('SOURCE_CHAIN_NAME'); + if (!sourceChainName) { + throw new Error('No Axelar API url present'); } - return 6379; + return sourceChainName; } getPoolLimit(): number { - return this.configService.get('caching.poolLimit') ?? 100; + return this.configService.get('CACHING_POOL_LIMIT') ?? 100; } getProcessTtl(): number { - return this.configService.get('caching.processTtl') ?? 60; + return this.configService.get('CACHING_PROCESS_TTL') ?? 60; + } + + getApiTimeout(): number { + return this.configService.get('API_TIMEOUT') ?? 30_000; // 30 seconds default + } + + getGatewayTimeout(): number { + return this.configService.get('GATEWAY_TIMEOUT') ?? 30_000; // 30 seconds default } } diff --git a/libs/common/src/contracts/contract.loader.ts b/libs/common/src/contracts/contract.loader.ts new file mode 100644 index 0000000..7058c3c --- /dev/null +++ b/libs/common/src/contracts/contract.loader.ts @@ -0,0 +1,47 @@ +import { AbiRegistry, Address, SmartContract } from '@multiversx/sdk-core'; +import { Logger } from '@nestjs/common'; + +export class ContractLoader { + private readonly logger: Logger; + private readonly json: any; + private abiRegistry: AbiRegistry | undefined = undefined; + private contract: SmartContract | undefined = undefined; + + constructor(json: any) { + this.json = json; + + this.logger = new Logger(ContractLoader.name); + } + + private load(contractAddress: string): SmartContract { + try { + this.abiRegistry = AbiRegistry.create(this.json); + + return new SmartContract({ + address: new Address(contractAddress), + abi: this.abiRegistry, + }); + } catch (error) { + this.logger.log(`Unexpected error when trying to create smart contract from abi`); + this.logger.error(error); + + throw new Error('Error when creating contract from abi'); + } + } + + getContract(contractAddress: string): SmartContract { + if (!this.contract) { + this.contract = this.load(contractAddress); + } + + return this.contract; + } + + getAbiRegistry(contractAddress: string): AbiRegistry { + if (!this.abiRegistry) { + this.load(contractAddress); + } + + return this.abiRegistry as AbiRegistry; + } +} diff --git a/libs/common/src/contracts/contracts.module.ts b/libs/common/src/contracts/contracts.module.ts new file mode 100644 index 0000000..1cb2ada --- /dev/null +++ b/libs/common/src/contracts/contracts.module.ts @@ -0,0 +1,59 @@ +import { Module } from '@nestjs/common'; +import { GatewayContract } from './gateway.contract'; +import { ApiConfigService } from '@mvx-monorepo/common'; +import { ApiNetworkProvider, ProxyNetworkProvider } from '@multiversx/sdk-network-providers/out'; +import { ResultsParser } from '@multiversx/sdk-core/out'; +import { ContractLoader } from '@mvx-monorepo/common/contracts/contract.loader'; +import gatewayJson from '../../../../abis/gateway.abi.json'; + +@Module({ + imports: [], + providers: [ + { + provide: ProxyNetworkProvider, + useFactory: (apiConfigService: ApiConfigService) => { + return new ProxyNetworkProvider(apiConfigService.getGatewayUrl(), { + timeout: apiConfigService.getGatewayTimeout(), + }); + }, + inject: [ApiConfigService], + }, + { + provide: ApiNetworkProvider, + useFactory: (apiConfigService: ApiConfigService) => { + return new ApiNetworkProvider(apiConfigService.getApiUrl(), { + timeout: apiConfigService.getApiTimeout(), + }); + }, + inject: [ApiConfigService], + }, + { + provide: ResultsParser, + useValue: new ResultsParser(), + }, + // { + // provide: ContractQueryRunner, + // useFactory: (api: ApiNetworkProvider) => new ContractQueryRunner(api), + // inject: [ApiNetworkProvider], + // }, + // { + // provide: ContractTransactionGenerator, + // useFactory: (api: ApiNetworkProvider) => new ContractTransactionGenerator(api), + // inject: [ApiNetworkProvider], + // }, + { + provide: GatewayContract, + useFactory: (apiConfigService: ApiConfigService, resultsParser: ResultsParser) => { + const contractLoader = new ContractLoader(gatewayJson); + + const smartContract = contractLoader.getContract(apiConfigService.getContractGateway()); + const abi = contractLoader.getAbiRegistry(apiConfigService.getContractGateway()); + + return new GatewayContract(smartContract, abi, resultsParser); + }, + inject: [ApiConfigService, ResultsParser], + }, + ], + exports: [GatewayContract], +}) +export class ContractsModule {} diff --git a/libs/common/src/contracts/entities/contract-call-event.ts b/libs/common/src/contracts/entities/contract-call-event.ts new file mode 100644 index 0000000..e9d63cf --- /dev/null +++ b/libs/common/src/contracts/entities/contract-call-event.ts @@ -0,0 +1,11 @@ +import { IAddress } from '@multiversx/sdk-core/out'; + +export interface ContractCallEvent { + sender: IAddress, + destination_chain: string, + destination_contract_address: string, + data: { + hash: string, + payload: Buffer, + } +} diff --git a/libs/common/src/contracts/gateway.contract.ts b/libs/common/src/contracts/gateway.contract.ts new file mode 100644 index 0000000..2e88c09 --- /dev/null +++ b/libs/common/src/contracts/gateway.contract.ts @@ -0,0 +1,36 @@ +import { AbiRegistry, ResultsParser, SmartContract } from '@multiversx/sdk-core/out'; +import { Injectable, Logger } from '@nestjs/common'; +import { Events } from '../utils/event.enum'; +import { TransactionEvent } from '@multiversx/sdk-network-providers/out'; +import { ContractCallEvent } from '@mvx-monorepo/common/contracts/entities/contract-call-event'; +import BigNumber from 'bignumber.js'; + +@Injectable() +export class GatewayContract { + // @ts-ignore + private readonly logger: Logger; + + constructor( + // @ts-ignore + private readonly smartContract: SmartContract, + private readonly abi: AbiRegistry, + private readonly resultsParser: ResultsParser, + ) { + this.logger = new Logger(GatewayContract.name); + } + + decodeContractCallEvent(event: TransactionEvent): ContractCallEvent { + const eventDefinition = this.abi.getEvent(Events.CONTRACT_CALL_EVENT); + const outcome = this.resultsParser.parseEvent(event, eventDefinition); + + return { + sender: outcome.sender, + destination_chain: outcome.destination_chain.toString(), + destination_contract_address: outcome.destination_contract_address.toString(), + data: { + hash: Buffer.from(outcome.data.hash.map((number: BigNumber) => number.toNumber())).toString('hex'), + payload: outcome.data.payload, + }, + }; + } +} diff --git a/libs/common/src/database/database.module.ts b/libs/common/src/database/database.module.ts index 161c792..0d60c1e 100644 --- a/libs/common/src/database/database.module.ts +++ b/libs/common/src/database/database.module.ts @@ -1,7 +1,9 @@ import { Module } from '@nestjs/common'; import { PrismaService } from '@mvx-monorepo/common/database/prisma.service'; +import { ContractCallEventRepository } from '@mvx-monorepo/common/database/repository/contract-call-event.repository'; @Module({ - providers: [PrismaService], + providers: [PrismaService, ContractCallEventRepository], + exports: [ContractCallEventRepository], }) export class DatabaseModule {} diff --git a/libs/common/src/database/repository/contract-call-event.repository.ts b/libs/common/src/database/repository/contract-call-event.repository.ts new file mode 100644 index 0000000..7643266 --- /dev/null +++ b/libs/common/src/database/repository/contract-call-event.repository.ts @@ -0,0 +1,14 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '@mvx-monorepo/common/database/prisma.service'; +import { ContractCallEvent, Prisma } from '@prisma/client'; + +@Injectable() +export class ContractCallEventRepository { + constructor(private readonly prisma: PrismaService) {} + + create(data: Prisma.ContractCallEventCreateInput): Promise { + return this.prisma.contractCallEvent.create({ + data, + }); + } +} diff --git a/libs/common/src/index.ts b/libs/common/src/index.ts index 868a7fd..ea10999 100644 --- a/libs/common/src/index.ts +++ b/libs/common/src/index.ts @@ -1,4 +1,3 @@ -export * from './abi'; export * from './database'; export * from './pubsub'; export * from './config'; diff --git a/libs/common/src/pubsub/pub.sub.listener.module.ts b/libs/common/src/pubsub/pub.sub.listener.module.ts index d9b731d..48ee2cd 100644 --- a/libs/common/src/pubsub/pub.sub.listener.module.ts +++ b/libs/common/src/pubsub/pub.sub.listener.module.ts @@ -1,25 +1,16 @@ import { DynamicModule, Module } from '@nestjs/common'; import { DynamicModuleUtils } from '@mvx-monorepo/common/utils/dynamic.module.utils'; -import { ApiConfigModule } from '../config/api.config.module'; import { PubSubListenerController } from './pub.sub.listener.controller'; import { LoggingModule } from '@multiversx/sdk-nestjs-common'; @Module({}) export class PubSubListenerModule { - static forRoot(configuration: () => Record): DynamicModule { + static forRoot(): DynamicModule { return { module: PubSubListenerModule, - imports: [ - LoggingModule, - ApiConfigModule, - DynamicModuleUtils.getCachingModule(configuration), - ], - controllers: [ - PubSubListenerController, - ], - providers: [ - DynamicModuleUtils.getPubSubService(), - ], + imports: [LoggingModule, DynamicModuleUtils.getCachingModule()], + controllers: [PubSubListenerController], + providers: [DynamicModuleUtils.getPubSubService()], exports: ['PUBSUB_SERVICE'], }; } diff --git a/libs/common/src/utils/dynamic.module.utils.ts b/libs/common/src/utils/dynamic.module.utils.ts index 3865a43..c79801e 100644 --- a/libs/common/src/utils/dynamic.module.utils.ts +++ b/libs/common/src/utils/dynamic.module.utils.ts @@ -4,9 +4,9 @@ import { ClientOptions, ClientProxyFactory, Transport } from '@nestjs/microservi import { ApiConfigModule, ApiConfigService } from '../config'; export class DynamicModuleUtils { - static getCachingModule(configuration: () => Record): DynamicModule { + static getCachingModule(): DynamicModule { return CacheModule.forRootAsync({ - imports: [ApiConfigModule.forRoot(configuration)], + imports: [ApiConfigModule], useFactory: (apiConfigService: ApiConfigService) => new RedisCacheModuleOptions( { diff --git a/libs/common/src/utils/event.enum.ts b/libs/common/src/utils/event.enum.ts new file mode 100644 index 0000000..d9da865 --- /dev/null +++ b/libs/common/src/utils/event.enum.ts @@ -0,0 +1,7 @@ +export enum EventIdentifiers { + CALL_CONTRACT = 'callContract', +} + +export enum Events { + CONTRACT_CALL_EVENT = 'contract_call_event' +} diff --git a/nest-cli.json b/nest-cli.json index eed4a8d..ff8e56c 100644 --- a/nest-cli.json +++ b/nest-cli.json @@ -21,12 +21,8 @@ "tsConfigPath": "apps/axelar-event-processor/tsconfig.app.json", "assets": [ { - "include": "../config/config.yaml", - "outDir": "./dist/apps/axelar-event-processor/config" - }, - { - "include": "../config/relayer.proto", - "outDir": "./dist/apps/axelar-event-processor/config" + "include": "../axelar/relayer.proto", + "outDir": "./dist/apps/axelar-event-processor/axelar" } ] } @@ -37,13 +33,7 @@ "entryFile": "main", "sourceRoot": "apps/mvx-event-processor/src", "compilerOptions": { - "tsConfigPath": "apps/mvx-event-processor/tsconfig.app.json", - "assets": [ - { - "include": "../config/config.yaml", - "outDir": "./dist/apps/mvx-event-processor/config" - } - ] + "tsConfigPath": "apps/mvx-event-processor/tsconfig.app.json" } }, "common": { diff --git a/package-lock.json b/package-lock.json index 1a21a39..6f1ccf3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,6 @@ "@grpc/grpc-js": "^1.9.12", "@grpc/proto-loader": "^0.7.10", "@multiversx/sdk-core": "^12.15.0", - "@multiversx/sdk-nestjs-auth": "2.4.0", "@multiversx/sdk-nestjs-cache": "2.4.0", "@multiversx/sdk-nestjs-common": "2.4.0", "@multiversx/sdk-nestjs-elastic": "2.4.0", @@ -27,46 +26,30 @@ "@nestjs/config": "3.0.1", "@nestjs/core": "^10.2.4", "@nestjs/microservices": "10.2.4", - "@nestjs/mongoose": "^10.0.2", - "@nestjs/platform-express": "10.2.4", - "@nestjs/platform-socket.io": "10.2.4", "@nestjs/schedule": "^4.0.0", - "@nestjs/swagger": "7.1.16", - "@nestjs/typeorm": "10.0.0", - "@nestjs/websockets": "10.2.8", "@prisma/client": "^5.6.0", "agentkeepalive": "^4.3.0", "bull": "^4.10.4", "cache-manager": "^5.2.1", - "cookie-parser": "^1.4.6", "cron": "^3.1.6", "ioredis": "^5.3.2", - "jsonwebtoken": "^9.0.0", "module-alias": "^2.2.3", - "mongodb": "^5.5.0", - "mongoose": "^7.1.1", - "mysql2": "^3.3.1", "nest-winston": "^1.9.2", "prom-client": "^14.2.0", "reflect-metadata": "^0.1.13", "rimraf": "^5.0.0", "rxjs": "^7.8.1", - "socket.io": "^4.6.1", - "swagger-ui-express": "^4.6.3", - "typeorm": "^0.3.16", "winston": "^3.8.2", "winston-daily-rotate-file": "^4.7.1" }, "devDependencies": { + "@golevelup/ts-jest": "^0.4.0", "@nestjs/cli": "10.1.17", "@nestjs/schematics": "10.0.2", - "@nestjs/testing": "10.2.4", + "@nestjs/testing": "^10.2.4", "@types/cache-manager": "^4.0.2", - "@types/cookie-parser": "^1.4.3", - "@types/express": "^4.17.17", "@types/jest": "^29.5.1", "@types/js-yaml": "^4.0.5", - "@types/jsonwebtoken": "^9.0.2", "@types/node": "^20.1.4", "@types/supertest": "^2.0.12", "@typescript-eslint/eslint-plugin": "^5.59.5", @@ -811,17 +794,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/runtime": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.4.tgz", - "integrity": "sha512-2Yv65nlWnWlSpe3fXEyX5i7fx5kIKo4Qbcj+hMO0odwaneFjfXw5fdum+4yL20O0QiaHpia0cYQ9xpNMqrBwHg==", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/template": { "version": "7.22.15", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", @@ -900,7 +872,7 @@ "version": "0.8.0", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz", "integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==", - "devOptional": true, + "dev": true, "engines": { "node": ">= 12" } @@ -909,7 +881,7 @@ "version": "0.7.0", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz", "integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==", - "devOptional": true, + "dev": true, "dependencies": { "@cspotcode/source-map-consumer": "0.8.0" }, @@ -1060,6 +1032,12 @@ "rxjs": "^7.x" } }, + "node_modules/@golevelup/ts-jest": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@golevelup/ts-jest/-/ts-jest-0.4.0.tgz", + "integrity": "sha512-ehgllV/xU8PC+yVyEUtTzhiSQKsr7k5Jz74B6dtCaVJz7/Vo7JiaACsCLvD7/iATlJUAEqvBson0OHewD3JDzQ==", + "dev": true + }, "node_modules/@grpc/grpc-js": { "version": "1.9.12", "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.12.tgz", @@ -1668,15 +1646,6 @@ "node": ">=8" } }, - "node_modules/@mongodb-js/saslprep": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.1.tgz", - "integrity": "sha512-t7c5K033joZZMspnHg/gWPE4kandgc2OxE74aYOtGKfgB9VPuVJPix0H6fhmm2erj5PBJ21mqcx34lpIGtUCsQ==", - "optional": true, - "dependencies": { - "sparse-bitfield": "^3.0.3" - } - }, "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.2.tgz", @@ -1841,39 +1810,6 @@ "node": ">=10.0.0" } }, - "node_modules/@multiversx/sdk-nestjs-auth": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@multiversx/sdk-nestjs-auth/-/sdk-nestjs-auth-2.4.0.tgz", - "integrity": "sha512-JoZdt/PLY5Y5XnAk30K3/lYWbI/YCZSeT2mZ70YuW6jQZqz9Drj3kEH4ImFdJoisEsUacWijNYSun2zJgBZP/Q==", - "dependencies": { - "@multiversx/sdk-core": "12.6.1", - "@multiversx/sdk-native-auth-server": "^1.0.6", - "@multiversx/sdk-wallet": "3.0.0", - "jsonwebtoken": "^9.0.0" - }, - "peerDependencies": { - "@multiversx/sdk-nestjs-cache": "^2.0.0", - "@multiversx/sdk-nestjs-common": "^2.0.0", - "@multiversx/sdk-nestjs-monitoring": "^2.0.0", - "@multiversx/sdk-wallet": "<=3.0.0", - "@nestjs/common": "^10.x" - } - }, - "node_modules/@multiversx/sdk-nestjs-auth/node_modules/@multiversx/sdk-core": { - "version": "12.6.1", - "resolved": "https://registry.npmjs.org/@multiversx/sdk-core/-/sdk-core-12.6.1.tgz", - "integrity": "sha512-T21TMC3euAi3C0WtB6WOzNYQW4XbKTrH0Q1K7gxcNpLDqnQbMwh0SuVAossQRL/s1Ca829Zjt3zmuGQUtWAU1A==", - "dependencies": { - "@multiversx/sdk-transaction-decoder": "1.0.2", - "bech32": "1.1.4", - "bignumber.js": "9.0.1", - "blake2b": "2.1.3", - "buffer": "6.0.3", - "json-duplicate-key-handle": "1.0.0", - "keccak": "3.0.2", - "protobufjs": "7.2.4" - } - }, "node_modules/@multiversx/sdk-nestjs-cache": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/@multiversx/sdk-nestjs-cache/-/sdk-nestjs-cache-2.4.0.tgz", @@ -2389,6 +2325,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.3.tgz", "integrity": "sha512-40Zdqg98lqoF0+7ThWIZFStxgzisK6GG22+1ABO4kZiGF/Tu2FE+DYLw+Q9D94vcFWizJ+MSjNN4ns9r6hIGxw==", + "peer": true, "peerDependencies": { "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", "class-transformer": "^0.4.0 || ^0.5.0", @@ -2466,22 +2403,12 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, - "node_modules/@nestjs/mongoose": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/@nestjs/mongoose/-/mongoose-10.0.2.tgz", - "integrity": "sha512-ITHh075DynjPIaKeJh6WkarS21WXYslu4nrLkNPbWaCP6JfxVAOftaA2X5tPSiiE/gNJWgs+QFWsfCFZUUenow==", - "peerDependencies": { - "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", - "@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0", - "mongoose": "^6.0.2 || ^7.0.0 || ^8.0.0", - "reflect-metadata": "^0.1.12", - "rxjs": "^7.0.0" - } - }, "node_modules/@nestjs/platform-express": { "version": "10.2.4", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.2.4.tgz", "integrity": "sha512-E9F6WYo6bNwvTT0saJpkr8t4BJLbZRwrX5EKbtBRQqyRcw6NAvlKdacKzoo+Sompdre0IbF8AvNRFk4uLZTWqA==", + "optional": true, + "peer": true, "dependencies": { "body-parser": "1.20.2", "cors": "2.8.5", @@ -2501,12 +2428,16 @@ "node_modules/@nestjs/platform-express/node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "optional": true, + "peer": true }, "node_modules/@nestjs/platform-socket.io": { "version": "10.2.4", "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.2.4.tgz", "integrity": "sha512-cHgMDKi4a73uPZ0G+hoYF7horywcebfvDlJJGsvOHaMDxJrARINZzyQECf9Jkmar2c39bIH8zbVs/Sb8Jk7i2Q==", + "optional": true, + "peer": true, "dependencies": { "socket.io": "4.7.2", "tslib": "2.6.2" @@ -2524,7 +2455,9 @@ "node_modules/@nestjs/platform-socket.io/node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "optional": true, + "peer": true }, "node_modules/@nestjs/schedule": { "version": "4.0.0", @@ -2637,6 +2570,7 @@ "version": "7.1.16", "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-7.1.16.tgz", "integrity": "sha512-f9KBk/BX9MUKPTj7tQNYJ124wV/jP5W2lwWHLGwe/4qQXixuDOo39zP55HIJ44LE7S04B7BOeUOo9GBJD/vRcw==", + "peer": true, "dependencies": { "@nestjs/mapped-types": "2.0.3", "js-yaml": "4.1.0", @@ -2697,33 +2631,12 @@ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", "dev": true }, - "node_modules/@nestjs/typeorm": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-10.0.0.tgz", - "integrity": "sha512-WQU4HCDTz4UavsFzvGUKDHqi0MO5K47yFoPXdmh+Z/hCNO7SHCMmV9jLiLukM8n5nKUqJ3jDqiljkWBcZPdCtA==", - "dependencies": { - "uuid": "9.0.0" - }, - "peerDependencies": { - "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", - "@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0", - "reflect-metadata": "^0.1.13", - "rxjs": "^7.2.0", - "typeorm": "^0.3.0" - } - }, - "node_modules/@nestjs/typeorm/node_modules/uuid": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", - "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/@nestjs/websockets": { "version": "10.2.8", "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.2.8.tgz", "integrity": "sha512-oZN1VJFApN7d2eftr65a36QrV0IJNGba4znqyjFnyGvtDWTDcQwzDcnEfvJBTTYhOSBNS7KDfVhne0ythkl6tg==", + "optional": true, + "peer": true, "dependencies": { "iterare": "1.2.1", "object-hash": "3.0.0", @@ -2745,7 +2658,9 @@ "node_modules/@nestjs/websockets/node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "optional": true, + "peer": true }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", @@ -2921,36 +2836,33 @@ "node_modules/@socket.io/component-emitter": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", - "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" - }, - "node_modules/@sqltools/formatter": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.5.tgz", - "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==" + "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==", + "optional": true, + "peer": true }, "node_modules/@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", - "devOptional": true + "dev": true }, "node_modules/@tsconfig/node12": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "devOptional": true + "dev": true }, "node_modules/@tsconfig/node14": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "devOptional": true + "dev": true }, "node_modules/@tsconfig/node16": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "devOptional": true + "dev": true }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -2993,44 +2905,18 @@ "@babel/types": "^7.20.7" } }, - "node_modules/@types/body-parser": { - "version": "1.19.5", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", - "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", - "dev": true, - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, "node_modules/@types/cache-manager": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/@types/cache-manager/-/cache-manager-4.0.6.tgz", "integrity": "sha512-8qL93MF05/xrzFm/LSPtzNEOE1eQF3VwGHAcQEylgp5hDSTe41jtFwbSYAPfyYcVa28y1vYSjIt0c1fLLUiC/Q==", "dev": true }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/cookie": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", - "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" - }, - "node_modules/@types/cookie-parser": { - "version": "1.4.6", - "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.6.tgz", - "integrity": "sha512-KoooCrD56qlLskXPLGUiJxOMnv5l/8m7cQD2OxJ73NPMhuSz9PmvwRD6EpjDyKBVrdJDdQ4bQK7JFNHnNmax0w==", - "dev": true, - "dependencies": { - "@types/express": "*" - } + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", + "optional": true, + "peer": true }, "node_modules/@types/cookiejar": { "version": "2.1.5", @@ -3042,6 +2928,8 @@ "version": "2.8.17", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "optional": true, + "peer": true, "dependencies": { "@types/node": "*" } @@ -3072,30 +2960,6 @@ "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "dev": true }, - "node_modules/@types/express": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", - "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", - "dev": true, - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "4.17.41", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.41.tgz", - "integrity": "sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==", - "dev": true, - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -3105,12 +2969,6 @@ "@types/node": "*" } }, - "node_modules/@types/http-errors": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", - "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", - "dev": true - }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -3157,26 +3015,11 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, - "node_modules/@types/jsonwebtoken": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.5.tgz", - "integrity": "sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/luxon": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.5.tgz", "integrity": "sha512-1cyf6Ge/94zlaWIZA2ei1pE6SZ8xpad2hXaYa5JEFiaUH0YS494CZwyi4MXNpXD9oEuv6ZH0Bmh0e7F9sPhmZA==" }, - "node_modules/@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true - }, "node_modules/@types/node": { "version": "20.10.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.0.tgz", @@ -3191,45 +3034,12 @@ "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", "dev": true }, - "node_modules/@types/qs": { - "version": "6.9.10", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.10.tgz", - "integrity": "sha512-3Gnx08Ns1sEoCrWssEgTSJs/rsT2vhGP+Ja9cnnk9k4ALxinORlQneLXFeFKOTJMOeZUFD1s7w+w2AphTpvzZw==", - "dev": true - }, - "node_modules/@types/range-parser": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true - }, "node_modules/@types/semver": { "version": "7.5.6", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==", "dev": true }, - "node_modules/@types/send": { - "version": "0.17.4", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", - "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", - "dev": true, - "dependencies": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "node_modules/@types/serve-static": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.5.tgz", - "integrity": "sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==", - "dev": true, - "dependencies": { - "@types/http-errors": "*", - "@types/mime": "*", - "@types/node": "*" - } - }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -3260,20 +3070,6 @@ "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==" }, - "node_modules/@types/webidl-conversions": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", - "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==" - }, - "node_modules/@types/whatwg-url": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.2.tgz", - "integrity": "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==", - "dependencies": { - "@types/node": "*", - "@types/webidl-conversions": "*" - } - }, "node_modules/@types/yargs": { "version": "17.0.32", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", @@ -3645,6 +3441,8 @@ "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "optional": true, + "peer": true, "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" @@ -3657,7 +3455,7 @@ "version": "8.11.2", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", - "devOptional": true, + "dev": true, "bin": { "acorn": "bin/acorn" }, @@ -3687,7 +3485,7 @@ "version": "8.3.0", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.0.tgz", "integrity": "sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA==", - "devOptional": true, + "dev": true, "engines": { "node": ">=0.4.0" } @@ -3825,11 +3623,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==" - }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -3843,24 +3636,18 @@ "node": ">= 8" } }, - "node_modules/app-root-path": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.1.0.tgz", - "integrity": "sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==", - "engines": { - "node": ">= 6.0.0" - } - }, "node_modules/append-field": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", - "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "optional": true, + "peer": true }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "devOptional": true + "dev": true }, "node_modules/argparse": { "version": "2.0.1", @@ -3870,7 +3657,9 @@ "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "optional": true, + "peer": true }, "node_modules/array-timsort": { "version": "1.0.3", @@ -4061,6 +3850,8 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "optional": true, + "peer": true, "engines": { "node": "^4.5.0 || >= 5.9" } @@ -4223,6 +4014,8 @@ "version": "1.20.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "optional": true, + "peer": true, "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", @@ -4246,6 +4039,8 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "optional": true, + "peer": true, "dependencies": { "ms": "2.0.0" } @@ -4253,7 +4048,9 @@ "node_modules/body-parser/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "optional": true, + "peer": true }, "node_modules/brace-expansion": { "version": "1.1.11", @@ -4330,14 +4127,6 @@ "node-int64": "^0.4.0" } }, - "node_modules/bson": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/bson/-/bson-5.5.1.tgz", - "integrity": "sha512-ix0EwukN2EpC0SRWIj/7B5+A6uQMQy6KMREI9qQqvgpkV2frH63T0UDVd1SYedL6dNCmDBYB3QtXi4ISk9YT+g==", - "engines": { - "node": ">=14.20.1" - } - }, "node_modules/buffer": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", @@ -4361,15 +4150,11 @@ "ieee754": "^1.2.1" } }, - "node_modules/buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" - }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "devOptional": true }, "node_modules/buffer-more-ints": { "version": "1.0.0", @@ -4397,6 +4182,8 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "optional": true, + "peer": true, "dependencies": { "streamsearch": "^1.1.0" }, @@ -4408,6 +4195,8 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "optional": true, + "peer": true, "engines": { "node": ">= 0.8" } @@ -4434,6 +4223,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "devOptional": true, "dependencies": { "function-bind": "^1.1.2", "get-intrinsic": "^1.2.1", @@ -4589,77 +4379,6 @@ "node": ">=8" } }, - "node_modules/cli-highlight": { - "version": "2.1.11", - "resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.11.tgz", - "integrity": "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==", - "dependencies": { - "chalk": "^4.0.0", - "highlight.js": "^10.7.1", - "mz": "^2.4.0", - "parse5": "^5.1.1", - "parse5-htmlparser2-tree-adapter": "^6.0.0", - "yargs": "^16.0.0" - }, - "bin": { - "highlight": "bin/highlight" - }, - "engines": { - "node": ">=8.0.0", - "npm": ">=5.0.0" - } - }, - "node_modules/cli-highlight/node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/cli-highlight/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/cli-highlight/node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/cli-highlight/node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "engines": { - "node": ">=10" - } - }, "node_modules/cli-spinners": { "version": "2.9.2", "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", @@ -4872,6 +4591,8 @@ "engines": [ "node >= 0.8" ], + "optional": true, + "peer": true, "dependencies": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", @@ -4882,12 +4603,16 @@ "node_modules/concat-stream/node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "optional": true, + "peer": true }, "node_modules/concat-stream/node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "optional": true, + "peer": true, "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -4901,12 +4626,16 @@ "node_modules/concat-stream/node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "optional": true, + "peer": true }, "node_modules/concat-stream/node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "optional": true, + "peer": true, "dependencies": { "safe-buffer": "~5.1.0" } @@ -4920,6 +4649,8 @@ "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "optional": true, + "peer": true, "dependencies": { "safe-buffer": "5.2.1" }, @@ -4931,6 +4662,8 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "optional": true, + "peer": true, "engines": { "node": ">= 0.6" } @@ -4945,26 +4678,18 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", + "optional": true, + "peer": true, "engines": { "node": ">= 0.6" } }, - "node_modules/cookie-parser": { - "version": "1.4.6", - "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz", - "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==", - "dependencies": { - "cookie": "0.4.1", - "cookie-signature": "1.0.6" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "optional": true, + "peer": true }, "node_modules/cookiejar": { "version": "2.1.4", @@ -4981,6 +4706,8 @@ "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "optional": true, + "peer": true, "dependencies": { "object-assign": "^4", "vary": "^1" @@ -5055,7 +4782,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "devOptional": true + "dev": true }, "node_modules/cron": { "version": "3.1.6", @@ -5090,21 +4817,6 @@ "node": ">= 8" } }, - "node_modules/date-fns": { - "version": "2.30.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", - "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", - "dependencies": { - "@babel/runtime": "^7.21.0" - }, - "engines": { - "node": ">=0.11" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/date-fns" - } - }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -5166,6 +4878,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "devOptional": true, "dependencies": { "get-intrinsic": "^1.2.1", "gopd": "^1.0.1", @@ -5195,6 +4908,8 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "optional": true, + "peer": true, "engines": { "node": ">= 0.8" } @@ -5203,6 +4918,8 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "optional": true, + "peer": true, "engines": { "node": ">= 0.8", "npm": "1.2.8000 || >= 1.4.16" @@ -5231,7 +4948,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "devOptional": true, + "dev": true, "engines": { "node": ">=0.3.1" } @@ -5293,14 +5010,6 @@ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, - "node_modules/ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "dependencies": { - "safe-buffer": "^5.0.1" - } - }, "node_modules/ed25519-hd-key": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/ed25519-hd-key/-/ed25519-hd-key-1.1.2.tgz", @@ -5322,7 +5031,9 @@ "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "optional": true, + "peer": true }, "node_modules/electron-to-chromium": { "version": "1.4.594", @@ -5356,6 +5067,8 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "optional": true, + "peer": true, "engines": { "node": ">= 0.8" } @@ -5373,6 +5086,8 @@ "version": "6.5.4", "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.4.tgz", "integrity": "sha512-KdVSDKhVKyOi+r5uEabrDLZw2qXStVvCsEB/LN3mw4WFi6Gx50jTyuxYVCwAAC0U46FdnzP/ScKRBTXb/NiEOg==", + "optional": true, + "peer": true, "dependencies": { "@types/cookie": "^0.4.1", "@types/cors": "^2.8.12", @@ -5393,6 +5108,8 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz", "integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==", + "optional": true, + "peer": true, "engines": { "node": ">=10.0.0" } @@ -5436,7 +5153,9 @@ "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "optional": true, + "peer": true }, "node_modules/escape-string-regexp": { "version": "4.0.0", @@ -5716,6 +5435,8 @@ "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "optional": true, + "peer": true, "engines": { "node": ">= 0.6" } @@ -5781,6 +5502,8 @@ "version": "4.18.2", "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "optional": true, + "peer": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -5822,6 +5545,8 @@ "version": "1.20.1", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "optional": true, + "peer": true, "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.4", @@ -5845,6 +5570,8 @@ "version": "0.5.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "optional": true, + "peer": true, "engines": { "node": ">= 0.6" } @@ -5853,6 +5580,8 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "optional": true, + "peer": true, "dependencies": { "ms": "2.0.0" } @@ -5860,17 +5589,23 @@ "node_modules/express/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "optional": true, + "peer": true }, "node_modules/express/node_modules/path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", + "optional": true, + "peer": true }, "node_modules/express/node_modules/raw-body": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "optional": true, + "peer": true, "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", @@ -6023,6 +5758,8 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "optional": true, + "peer": true, "dependencies": { "debug": "2.6.9", "encodeurl": "~1.0.2", @@ -6040,6 +5777,8 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "optional": true, + "peer": true, "dependencies": { "ms": "2.0.0" } @@ -6047,7 +5786,9 @@ "node_modules/finalhandler/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "optional": true, + "peer": true }, "node_modules/find-up": { "version": "5.0.0", @@ -6210,6 +5951,8 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "optional": true, + "peer": true, "engines": { "node": ">= 0.6" } @@ -6218,6 +5961,8 @@ "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "optional": true, + "peer": true, "engines": { "node": ">= 0.6" } @@ -6245,7 +5990,8 @@ "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true }, "node_modules/fsevents": { "version": "2.3.3", @@ -6265,18 +6011,11 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "devOptional": true, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/generate-function": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", - "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", - "dependencies": { - "is-property": "^1.0.2" - } - }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -6298,6 +6037,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "devOptional": true, "dependencies": { "function-bind": "^1.1.2", "has-proto": "^1.0.1", @@ -6417,6 +6157,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "devOptional": true, "dependencies": { "get-intrinsic": "^1.1.3" }, @@ -6457,6 +6198,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "devOptional": true, "dependencies": { "get-intrinsic": "^1.2.2" }, @@ -6468,6 +6210,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "devOptional": true, "engines": { "node": ">= 0.4" }, @@ -6479,6 +6222,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "devOptional": true, "engines": { "node": ">= 0.4" }, @@ -6524,6 +6268,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "devOptional": true, "dependencies": { "function-bind": "^1.1.2" }, @@ -6540,14 +6285,6 @@ "node": ">=8" } }, - "node_modules/highlight.js": { - "version": "10.7.3", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", - "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", - "engines": { - "node": "*" - } - }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -6558,6 +6295,8 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "optional": true, + "peer": true, "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", @@ -6590,6 +6329,7 @@ "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "devOptional": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3" }, @@ -6673,6 +6413,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -6741,15 +6482,12 @@ "url": "https://opencollective.com/ioredis" } }, - "node_modules/ip": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", - "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==" - }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "optional": true, + "peer": true, "engines": { "node": ">= 0.10" } @@ -6849,11 +6587,6 @@ "node": ">=8" } }, - "node_modules/is-property": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", - "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==" - }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -7660,54 +7393,6 @@ "graceful-fs": "^4.1.6" } }, - "node_modules/jsonwebtoken": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", - "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", - "dependencies": { - "jws": "^3.2.2", - "lodash.includes": "^4.3.0", - "lodash.isboolean": "^3.0.3", - "lodash.isinteger": "^4.0.4", - "lodash.isnumber": "^3.0.3", - "lodash.isplainobject": "^4.0.6", - "lodash.isstring": "^4.0.1", - "lodash.once": "^4.0.0", - "ms": "^2.1.1", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=12", - "npm": ">=6" - } - }, - "node_modules/jwa": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", - "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", - "dependencies": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", - "dependencies": { - "jwa": "^1.4.1", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/kareem": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.5.1.tgz", - "integrity": "sha512-7jFxRVm+jD+rkq3kY0iZDJfsO2/t4BBPeEb2qKn2lR/9KhuksYk5hxzfRYWMPV8P/x2d0kHD306YyWLzjjH+uA==", - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/keccak": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.2.tgz", @@ -7838,41 +7523,11 @@ "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" }, - "node_modules/lodash.includes": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", - "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" - }, "node_modules/lodash.isarguments": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==" }, - "node_modules/lodash.isboolean": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", - "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" - }, - "node_modules/lodash.isinteger": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", - "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" - }, - "node_modules/lodash.isnumber": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", - "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" - }, - "node_modules/lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" - }, - "node_modules/lodash.isstring": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" - }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -7885,11 +7540,6 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, - "node_modules/lodash.once": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" - }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -7994,7 +7644,7 @@ "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "devOptional": true + "dev": true }, "node_modules/makeerror": { "version": "1.0.12", @@ -8019,6 +7669,8 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "optional": true, + "peer": true, "engines": { "node": ">= 0.6" } @@ -8035,16 +7687,12 @@ "node": ">= 4.0.0" } }, - "node_modules/memory-pager": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", - "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", - "optional": true - }, "node_modules/merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", + "optional": true, + "peer": true }, "node_modules/merge-stream": { "version": "2.0.0", @@ -8065,6 +7713,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "devOptional": true, "engines": { "node": ">= 0.6" } @@ -8086,6 +7735,8 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "optional": true, + "peer": true, "bin": { "mime": "cli.js" }, @@ -8137,6 +7788,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "devOptional": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -8154,6 +7806,8 @@ "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "optional": true, + "peer": true, "dependencies": { "minimist": "^1.2.6" }, @@ -8174,140 +7828,6 @@ "node": "*" } }, - "node_modules/mongodb": { - "version": "5.9.1", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-5.9.1.tgz", - "integrity": "sha512-NBGA8AfJxGPeB12F73xXwozt8ZpeIPmCUeWRwl9xejozTXFes/3zaep9zhzs1B/nKKsw4P3I4iPfXl3K7s6g+Q==", - "dependencies": { - "bson": "^5.5.0", - "mongodb-connection-string-url": "^2.6.0", - "socks": "^2.7.1" - }, - "engines": { - "node": ">=14.20.1" - }, - "optionalDependencies": { - "@mongodb-js/saslprep": "^1.1.0" - }, - "peerDependencies": { - "@aws-sdk/credential-providers": "^3.188.0", - "@mongodb-js/zstd": "^1.0.0", - "kerberos": "^1.0.0 || ^2.0.0", - "mongodb-client-encryption": ">=2.3.0 <3", - "snappy": "^7.2.2" - }, - "peerDependenciesMeta": { - "@aws-sdk/credential-providers": { - "optional": true - }, - "@mongodb-js/zstd": { - "optional": true - }, - "kerberos": { - "optional": true - }, - "mongodb-client-encryption": { - "optional": true - }, - "snappy": { - "optional": true - } - } - }, - "node_modules/mongodb-connection-string-url": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz", - "integrity": "sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==", - "dependencies": { - "@types/whatwg-url": "^8.2.1", - "whatwg-url": "^11.0.0" - } - }, - "node_modules/mongoose": { - "version": "7.6.5", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-7.6.5.tgz", - "integrity": "sha512-ElHgGWVKQUawKBn0DXuHmSd3W5w5Kb8JUbDNQH30odhYCDKq9GCh+E1/SuN8jZGxrHgFyLrvYxLSpC36BpqS+w==", - "dependencies": { - "bson": "^5.5.0", - "kareem": "2.5.1", - "mongodb": "5.9.0", - "mpath": "0.9.0", - "mquery": "5.0.0", - "ms": "2.1.3", - "sift": "16.0.1" - }, - "engines": { - "node": ">=14.20.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mongoose" - } - }, - "node_modules/mongoose/node_modules/mongodb": { - "version": "5.9.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-5.9.0.tgz", - "integrity": "sha512-g+GCMHN1CoRUA+wb1Agv0TI4YTSiWr42B5ulkiAfLLHitGK1R+PkSAf3Lr5rPZwi/3F04LiaZEW0Kxro9Fi2TA==", - "dependencies": { - "bson": "^5.5.0", - "mongodb-connection-string-url": "^2.6.0", - "socks": "^2.7.1" - }, - "engines": { - "node": ">=14.20.1" - }, - "optionalDependencies": { - "@mongodb-js/saslprep": "^1.1.0" - }, - "peerDependencies": { - "@aws-sdk/credential-providers": "^3.188.0", - "@mongodb-js/zstd": "^1.0.0", - "kerberos": "^1.0.0 || ^2.0.0", - "mongodb-client-encryption": ">=2.3.0 <3", - "snappy": "^7.2.2" - }, - "peerDependenciesMeta": { - "@aws-sdk/credential-providers": { - "optional": true - }, - "@mongodb-js/zstd": { - "optional": true - }, - "kerberos": { - "optional": true - }, - "mongodb-client-encryption": { - "optional": true - }, - "snappy": { - "optional": true - } - } - }, - "node_modules/mongoose/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/mpath": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", - "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/mquery": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", - "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", - "dependencies": { - "debug": "4.x" - }, - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -8346,6 +7866,8 @@ "version": "1.4.4-lts.1", "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.4-lts.1.tgz", "integrity": "sha512-WeSGziVj6+Z2/MwQo3GvqzgR+9Uc+qt8SwHKh3gvNPiISKfsMfG4SvCOFYlxxgkXt7yIV2i1yczehm0EOKIxIg==", + "optional": true, + "peer": true, "dependencies": { "append-field": "^1.0.0", "busboy": "^1.0.0", @@ -8365,64 +7887,6 @@ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "dev": true }, - "node_modules/mysql2": { - "version": "3.6.5", - "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.6.5.tgz", - "integrity": "sha512-pS/KqIb0xlXmtmqEuTvBXTmLoQ5LmAz5NW/r8UyQ1ldvnprNEj3P9GbmuQQ2J0A4LO+ynotGi6TbscPa8OUb+w==", - "dependencies": { - "denque": "^2.1.0", - "generate-function": "^2.3.1", - "iconv-lite": "^0.6.3", - "long": "^5.2.1", - "lru-cache": "^8.0.0", - "named-placeholders": "^1.1.3", - "seq-queue": "^0.0.5", - "sqlstring": "^2.3.2" - }, - "engines": { - "node": ">= 8.0" - } - }, - "node_modules/mysql2/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "node_modules/named-placeholders": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz", - "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==", - "dependencies": { - "lru-cache": "^7.14.1" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/named-placeholders/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "engines": { - "node": ">=12" - } - }, "node_modules/nanoassert": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/nanoassert/-/nanoassert-1.1.0.tgz", @@ -8461,6 +7925,8 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "optional": true, + "peer": true, "engines": { "node": ">= 0.6" } @@ -8599,6 +8065,8 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "optional": true, + "peer": true, "engines": { "node": ">=0.10.0" } @@ -8607,6 +8075,8 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "optional": true, + "peer": true, "engines": { "node": ">= 6" } @@ -8615,6 +8085,7 @@ "version": "1.13.1", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "devOptional": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -8623,6 +8094,8 @@ "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "optional": true, + "peer": true, "dependencies": { "ee-first": "1.1.1" }, @@ -8634,6 +8107,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, "dependencies": { "wrappy": "1" } @@ -8795,28 +8269,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/parse5": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", - "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==" - }, - "node_modules/parse5-htmlparser2-tree-adapter": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", - "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", - "dependencies": { - "parse5": "^6.0.1" - } - }, - "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", - "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==" - }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "optional": true, + "peer": true, "engines": { "node": ">= 0.8" } @@ -9094,7 +8552,9 @@ "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "optional": true, + "peer": true }, "node_modules/prom-client": { "version": "14.2.0", @@ -9160,6 +8620,8 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "optional": true, + "peer": true, "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" @@ -9187,6 +8649,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, "engines": { "node": ">=6" } @@ -9211,6 +8674,7 @@ "version": "6.11.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "devOptional": true, "dependencies": { "side-channel": "^1.0.4" }, @@ -9258,6 +8722,8 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "optional": true, + "peer": true, "engines": { "node": ">= 0.6" } @@ -9266,6 +8732,8 @@ "version": "2.5.2", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "optional": true, + "peer": true, "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", @@ -9372,11 +8840,6 @@ "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==" }, - "node_modules/regenerator-runtime": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", - "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" - }, "node_modules/repeat-string": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", @@ -9634,7 +9097,8 @@ "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "devOptional": true }, "node_modules/schema-utils": { "version": "3.3.0", @@ -9724,6 +9188,8 @@ "version": "0.18.0", "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "optional": true, + "peer": true, "dependencies": { "debug": "2.6.9", "depd": "2.0.0", @@ -9747,6 +9213,8 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "optional": true, + "peer": true, "dependencies": { "ms": "2.0.0" } @@ -9754,17 +9222,16 @@ "node_modules/send/node_modules/debug/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "optional": true, + "peer": true }, "node_modules/send/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/seq-queue": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", - "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "optional": true, + "peer": true }, "node_modules/serialize-javascript": { "version": "6.0.1", @@ -9779,6 +9246,8 @@ "version": "1.15.0", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "optional": true, + "peer": true, "dependencies": { "encodeurl": "~1.0.2", "escape-html": "~1.0.3", @@ -9793,6 +9262,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "devOptional": true, "dependencies": { "define-data-property": "^1.1.1", "get-intrinsic": "^1.2.1", @@ -9806,7 +9276,9 @@ "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "optional": true, + "peer": true }, "node_modules/sha.js": { "version": "2.4.11", @@ -9860,6 +9332,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "devOptional": true, "dependencies": { "call-bind": "^1.0.0", "get-intrinsic": "^1.0.2", @@ -9869,11 +9342,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/sift": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/sift/-/sift-16.0.1.tgz", - "integrity": "sha512-Wv6BjQ5zbhW7VFefWusVP33T/EM0vYikCaQ2qR8yULbsilAT8/wQaXvuQ3ptGLpoKx+lihJE3y2UTgKDyyNHZQ==" - }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -9908,19 +9376,12 @@ "node": ">=8" } }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, "node_modules/socket.io": { "version": "4.7.2", "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.2.tgz", "integrity": "sha512-bvKVS29/I5fl2FGLNHuXlQaUH/BlzX1IN6S+NKLNZpBsPZIDH+90eQmCs2Railn4YUiww4SzUedJ6+uzwFnKLw==", + "optional": true, + "peer": true, "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", @@ -9938,6 +9399,8 @@ "version": "2.5.2", "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz", "integrity": "sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==", + "optional": true, + "peer": true, "dependencies": { "ws": "~8.11.0" } @@ -9946,6 +9409,8 @@ "version": "4.2.4", "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "optional": true, + "peer": true, "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1" @@ -9954,19 +9419,6 @@ "node": ">=10.0.0" } }, - "node_modules/socks": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz", - "integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==", - "dependencies": { - "ip": "^2.0.0", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.13.0", - "npm": ">= 3.0.0" - } - }, "node_modules/source-map": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", @@ -9995,29 +9447,12 @@ "node": ">=0.10.0" } }, - "node_modules/sparse-bitfield": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", - "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", - "optional": true, - "dependencies": { - "memory-pager": "^1.0.2" - } - }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true }, - "node_modules/sqlstring": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", - "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/stack-trace": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", @@ -10056,6 +9491,8 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "optional": true, + "peer": true, "engines": { "node": ">= 0.8" } @@ -10064,6 +9501,8 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "optional": true, + "peer": true, "engines": { "node": ">=10.0.0" } @@ -10238,21 +9677,8 @@ "node_modules/swagger-ui-dist": { "version": "5.9.1", "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.9.1.tgz", - "integrity": "sha512-5zAx+hUwJb9T3EAntc7TqYkV716CMqG6sZpNlAAMOMWkNXRYxGkN8ADIvD55dQZ10LxN90ZM/TQmN7y1gpICnw==" - }, - "node_modules/swagger-ui-express": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.6.3.tgz", - "integrity": "sha512-CDje4PndhTD2HkgyKH3pab+LKspDeB/NhPN2OF1j+piYIamQqBYwAXWESOT1Yju2xFg51bRW9sUng2WxDjzArw==", - "dependencies": { - "swagger-ui-dist": ">=4.11.0" - }, - "engines": { - "node": ">= v0.10.32" - }, - "peerDependencies": { - "express": ">=4.0.0 || >=5.0.0-beta" - } + "integrity": "sha512-5zAx+hUwJb9T3EAntc7TqYkV716CMqG6sZpNlAAMOMWkNXRYxGkN8ADIvD55dQZ10LxN90ZM/TQmN7y1gpICnw==", + "peer": true }, "node_modules/symbol-observable": { "version": "4.0.0", @@ -10392,25 +9818,6 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -10476,21 +9883,12 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "optional": true, + "peer": true, "engines": { "node": ">=0.6" } }, - "node_modules/tr46": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", - "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", - "dependencies": { - "punycode": "^2.1.1" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -10574,7 +9972,7 @@ "version": "10.7.0", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.7.0.tgz", "integrity": "sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A==", - "devOptional": true, + "dev": true, "dependencies": { "@cspotcode/source-map-support": "0.7.0", "@tsconfig/node10": "^1.0.7", @@ -10718,6 +10116,8 @@ "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "optional": true, + "peer": true, "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" @@ -10729,181 +10129,15 @@ "node_modules/typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" - }, - "node_modules/typeorm": { - "version": "0.3.17", - "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.17.tgz", - "integrity": "sha512-UDjUEwIQalO9tWw9O2A4GU+sT3oyoUXheHJy4ft+RFdnRdQctdQ34L9SqE2p7LdwzafHx1maxT+bqXON+Qnmig==", - "dependencies": { - "@sqltools/formatter": "^1.2.5", - "app-root-path": "^3.1.0", - "buffer": "^6.0.3", - "chalk": "^4.1.2", - "cli-highlight": "^2.1.11", - "date-fns": "^2.29.3", - "debug": "^4.3.4", - "dotenv": "^16.0.3", - "glob": "^8.1.0", - "mkdirp": "^2.1.3", - "reflect-metadata": "^0.1.13", - "sha.js": "^2.4.11", - "tslib": "^2.5.0", - "uuid": "^9.0.0", - "yargs": "^17.6.2" - }, - "bin": { - "typeorm": "cli.js", - "typeorm-ts-node-commonjs": "cli-ts-node-commonjs.js", - "typeorm-ts-node-esm": "cli-ts-node-esm.js" - }, - "engines": { - "node": ">= 12.9.0" - }, - "funding": { - "url": "https://opencollective.com/typeorm" - }, - "peerDependencies": { - "@google-cloud/spanner": "^5.18.0", - "@sap/hana-client": "^2.12.25", - "better-sqlite3": "^7.1.2 || ^8.0.0", - "hdb-pool": "^0.1.6", - "ioredis": "^5.0.4", - "mongodb": "^5.2.0", - "mssql": "^9.1.1", - "mysql2": "^2.2.5 || ^3.0.1", - "oracledb": "^5.1.0", - "pg": "^8.5.1", - "pg-native": "^3.0.0", - "pg-query-stream": "^4.0.0", - "redis": "^3.1.1 || ^4.0.0", - "sql.js": "^1.4.0", - "sqlite3": "^5.0.3", - "ts-node": "^10.7.0", - "typeorm-aurora-data-api-driver": "^2.0.0" - }, - "peerDependenciesMeta": { - "@google-cloud/spanner": { - "optional": true - }, - "@sap/hana-client": { - "optional": true - }, - "better-sqlite3": { - "optional": true - }, - "hdb-pool": { - "optional": true - }, - "ioredis": { - "optional": true - }, - "mongodb": { - "optional": true - }, - "mssql": { - "optional": true - }, - "mysql2": { - "optional": true - }, - "oracledb": { - "optional": true - }, - "pg": { - "optional": true - }, - "pg-native": { - "optional": true - }, - "pg-query-stream": { - "optional": true - }, - "redis": { - "optional": true - }, - "sql.js": { - "optional": true - }, - "sqlite3": { - "optional": true - }, - "ts-node": { - "optional": true - }, - "typeorm-aurora-data-api-driver": { - "optional": true - } - } - }, - "node_modules/typeorm/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/typeorm/node_modules/glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/typeorm/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/typeorm/node_modules/mkdirp": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-2.1.6.tgz", - "integrity": "sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==", - "bin": { - "mkdirp": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/typeorm/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "bin": { - "uuid": "dist/bin/uuid" - } + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "optional": true, + "peer": true }, "node_modules/typescript": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz", "integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==", - "devOptional": true, + "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -10941,6 +10175,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "optional": true, + "peer": true, "engines": { "node": ">= 0.8" } @@ -11002,6 +10238,8 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "optional": true, + "peer": true, "engines": { "node": ">= 0.4.0" } @@ -11018,7 +10256,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "devOptional": true + "dev": true }, "node_modules/v8-to-istanbul": { "version": "9.2.0", @@ -11038,6 +10276,8 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "optional": true, + "peer": true, "engines": { "node": ">= 0.8" } @@ -11073,14 +10313,6 @@ "defaults": "^1.0.3" } }, - "node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "engines": { - "node": ">=12" - } - }, "node_modules/webpack": { "version": "5.89.0", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.89.0.tgz", @@ -11147,18 +10379,6 @@ "node": ">=10.13.0" } }, - "node_modules/whatwg-url": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", - "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", - "dependencies": { - "tr46": "^3.0.0", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -11378,7 +10598,8 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true }, "node_modules/write-file-atomic": { "version": "4.0.2", @@ -11397,6 +10618,8 @@ "version": "8.11.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "optional": true, + "peer": true, "engines": { "node": ">=10.0.0" }, @@ -11417,6 +10640,8 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "optional": true, + "peer": true, "engines": { "node": ">=0.4" } @@ -11473,7 +10698,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "devOptional": true, + "dev": true, "engines": { "node": ">=6" } @@ -12032,14 +11257,6 @@ "@babel/helper-plugin-utils": "^7.22.5" } }, - "@babel/runtime": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.4.tgz", - "integrity": "sha512-2Yv65nlWnWlSpe3fXEyX5i7fx5kIKo4Qbcj+hMO0odwaneFjfXw5fdum+4yL20O0QiaHpia0cYQ9xpNMqrBwHg==", - "requires": { - "regenerator-runtime": "^0.14.0" - } - }, "@babel/template": { "version": "7.22.15", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", @@ -12105,13 +11322,13 @@ "version": "0.8.0", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz", "integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==", - "devOptional": true + "dev": true }, "@cspotcode/source-map-support": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz", "integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==", - "devOptional": true, + "dev": true, "requires": { "@cspotcode/source-map-consumer": "0.8.0" } @@ -12222,6 +11439,12 @@ "lodash": "^4.17.21" } }, + "@golevelup/ts-jest": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@golevelup/ts-jest/-/ts-jest-0.4.0.tgz", + "integrity": "sha512-ehgllV/xU8PC+yVyEUtTzhiSQKsr7k5Jz74B6dtCaVJz7/Vo7JiaACsCLvD7/iATlJUAEqvBson0OHewD3JDzQ==", + "dev": true + }, "@grpc/grpc-js": { "version": "1.9.12", "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.12.tgz", @@ -12685,15 +11908,6 @@ "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==" }, - "@mongodb-js/saslprep": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.1.tgz", - "integrity": "sha512-t7c5K033joZZMspnHg/gWPE4kandgc2OxE74aYOtGKfgB9VPuVJPix0H6fhmm2erj5PBJ21mqcx34lpIGtUCsQ==", - "optional": true, - "requires": { - "sparse-bitfield": "^3.0.3" - } - }, "@msgpackr-extract/msgpackr-extract-darwin-arm64": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.2.tgz", @@ -12818,34 +12032,6 @@ } } }, - "@multiversx/sdk-nestjs-auth": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@multiversx/sdk-nestjs-auth/-/sdk-nestjs-auth-2.4.0.tgz", - "integrity": "sha512-JoZdt/PLY5Y5XnAk30K3/lYWbI/YCZSeT2mZ70YuW6jQZqz9Drj3kEH4ImFdJoisEsUacWijNYSun2zJgBZP/Q==", - "requires": { - "@multiversx/sdk-core": "12.6.1", - "@multiversx/sdk-native-auth-server": "^1.0.6", - "@multiversx/sdk-wallet": "3.0.0", - "jsonwebtoken": "^9.0.0" - }, - "dependencies": { - "@multiversx/sdk-core": { - "version": "12.6.1", - "resolved": "https://registry.npmjs.org/@multiversx/sdk-core/-/sdk-core-12.6.1.tgz", - "integrity": "sha512-T21TMC3euAi3C0WtB6WOzNYQW4XbKTrH0Q1K7gxcNpLDqnQbMwh0SuVAossQRL/s1Ca829Zjt3zmuGQUtWAU1A==", - "requires": { - "@multiversx/sdk-transaction-decoder": "1.0.2", - "bech32": "1.1.4", - "bignumber.js": "9.0.1", - "blake2b": "2.1.3", - "buffer": "6.0.3", - "json-duplicate-key-handle": "1.0.0", - "keccak": "3.0.2", - "protobufjs": "7.2.4" - } - } - } - }, "@multiversx/sdk-nestjs-cache": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/@multiversx/sdk-nestjs-cache/-/sdk-nestjs-cache-2.4.0.tgz", @@ -13222,6 +12408,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.3.tgz", "integrity": "sha512-40Zdqg98lqoF0+7ThWIZFStxgzisK6GG22+1ABO4kZiGF/Tu2FE+DYLw+Q9D94vcFWizJ+MSjNN4ns9r6hIGxw==", + "peer": true, "requires": {} }, "@nestjs/microservices": { @@ -13240,17 +12427,13 @@ } } }, - "@nestjs/mongoose": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/@nestjs/mongoose/-/mongoose-10.0.2.tgz", - "integrity": "sha512-ITHh075DynjPIaKeJh6WkarS21WXYslu4nrLkNPbWaCP6JfxVAOftaA2X5tPSiiE/gNJWgs+QFWsfCFZUUenow==", - "requires": {} - }, "@nestjs/platform-express": { "version": "10.2.4", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.2.4.tgz", "integrity": "sha512-E9F6WYo6bNwvTT0saJpkr8t4BJLbZRwrX5EKbtBRQqyRcw6NAvlKdacKzoo+Sompdre0IbF8AvNRFk4uLZTWqA==", - "requires": { + "optional": true, + "peer": true, + "requires": { "body-parser": "1.20.2", "cors": "2.8.5", "express": "4.18.2", @@ -13261,7 +12444,9 @@ "tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "optional": true, + "peer": true } } }, @@ -13269,6 +12454,8 @@ "version": "10.2.4", "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.2.4.tgz", "integrity": "sha512-cHgMDKi4a73uPZ0G+hoYF7horywcebfvDlJJGsvOHaMDxJrARINZzyQECf9Jkmar2c39bIH8zbVs/Sb8Jk7i2Q==", + "optional": true, + "peer": true, "requires": { "socket.io": "4.7.2", "tslib": "2.6.2" @@ -13277,7 +12464,9 @@ "tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "optional": true, + "peer": true } } }, @@ -13360,6 +12549,7 @@ "version": "7.1.16", "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-7.1.16.tgz", "integrity": "sha512-f9KBk/BX9MUKPTj7tQNYJ124wV/jP5W2lwWHLGwe/4qQXixuDOo39zP55HIJ44LE7S04B7BOeUOo9GBJD/vRcw==", + "peer": true, "requires": { "@nestjs/mapped-types": "2.0.3", "js-yaml": "4.1.0", @@ -13385,25 +12575,12 @@ } } }, - "@nestjs/typeorm": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-10.0.0.tgz", - "integrity": "sha512-WQU4HCDTz4UavsFzvGUKDHqi0MO5K47yFoPXdmh+Z/hCNO7SHCMmV9jLiLukM8n5nKUqJ3jDqiljkWBcZPdCtA==", - "requires": { - "uuid": "9.0.0" - }, - "dependencies": { - "uuid": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", - "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==" - } - } - }, "@nestjs/websockets": { "version": "10.2.8", "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.2.8.tgz", "integrity": "sha512-oZN1VJFApN7d2eftr65a36QrV0IJNGba4znqyjFnyGvtDWTDcQwzDcnEfvJBTTYhOSBNS7KDfVhne0ythkl6tg==", + "optional": true, + "peer": true, "requires": { "iterare": "1.2.1", "object-hash": "3.0.0", @@ -13413,7 +12590,9 @@ "tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "optional": true, + "peer": true } } }, @@ -13559,36 +12738,33 @@ "@socket.io/component-emitter": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", - "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" - }, - "@sqltools/formatter": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.5.tgz", - "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==" + "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==", + "optional": true, + "peer": true }, "@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", - "devOptional": true + "dev": true }, "@tsconfig/node12": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "devOptional": true + "dev": true }, "@tsconfig/node14": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "devOptional": true + "dev": true }, "@tsconfig/node16": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "devOptional": true + "dev": true }, "@types/babel__core": { "version": "7.20.5", @@ -13631,44 +12807,18 @@ "@babel/types": "^7.20.7" } }, - "@types/body-parser": { - "version": "1.19.5", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", - "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", - "dev": true, - "requires": { - "@types/connect": "*", - "@types/node": "*" - } - }, "@types/cache-manager": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/@types/cache-manager/-/cache-manager-4.0.6.tgz", "integrity": "sha512-8qL93MF05/xrzFm/LSPtzNEOE1eQF3VwGHAcQEylgp5hDSTe41jtFwbSYAPfyYcVa28y1vYSjIt0c1fLLUiC/Q==", "dev": true }, - "@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, "@types/cookie": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", - "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" - }, - "@types/cookie-parser": { - "version": "1.4.6", - "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.6.tgz", - "integrity": "sha512-KoooCrD56qlLskXPLGUiJxOMnv5l/8m7cQD2OxJ73NPMhuSz9PmvwRD6EpjDyKBVrdJDdQ4bQK7JFNHnNmax0w==", - "dev": true, - "requires": { - "@types/express": "*" - } + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", + "optional": true, + "peer": true }, "@types/cookiejar": { "version": "2.1.5", @@ -13680,6 +12830,8 @@ "version": "2.8.17", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "optional": true, + "peer": true, "requires": { "@types/node": "*" } @@ -13710,30 +12862,6 @@ "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "dev": true }, - "@types/express": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", - "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", - "dev": true, - "requires": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, - "@types/express-serve-static-core": { - "version": "4.17.41", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.41.tgz", - "integrity": "sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==", - "dev": true, - "requires": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, "@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -13743,12 +12871,6 @@ "@types/node": "*" } }, - "@types/http-errors": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", - "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", - "dev": true - }, "@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -13795,26 +12917,11 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, - "@types/jsonwebtoken": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.5.tgz", - "integrity": "sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, "@types/luxon": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.5.tgz", "integrity": "sha512-1cyf6Ge/94zlaWIZA2ei1pE6SZ8xpad2hXaYa5JEFiaUH0YS494CZwyi4MXNpXD9oEuv6ZH0Bmh0e7F9sPhmZA==" }, - "@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true - }, "@types/node": { "version": "20.10.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.0.tgz", @@ -13829,45 +12936,12 @@ "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", "dev": true }, - "@types/qs": { - "version": "6.9.10", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.10.tgz", - "integrity": "sha512-3Gnx08Ns1sEoCrWssEgTSJs/rsT2vhGP+Ja9cnnk9k4ALxinORlQneLXFeFKOTJMOeZUFD1s7w+w2AphTpvzZw==", - "dev": true - }, - "@types/range-parser": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true - }, "@types/semver": { "version": "7.5.6", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==", "dev": true }, - "@types/send": { - "version": "0.17.4", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", - "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", - "dev": true, - "requires": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "@types/serve-static": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.5.tgz", - "integrity": "sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==", - "dev": true, - "requires": { - "@types/http-errors": "*", - "@types/mime": "*", - "@types/node": "*" - } - }, "@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -13898,20 +12972,6 @@ "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==" }, - "@types/webidl-conversions": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", - "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==" - }, - "@types/whatwg-url": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.2.tgz", - "integrity": "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==", - "requires": { - "@types/node": "*", - "@types/webidl-conversions": "*" - } - }, "@types/yargs": { "version": "17.0.32", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", @@ -14194,6 +13254,8 @@ "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "optional": true, + "peer": true, "requires": { "mime-types": "~2.1.34", "negotiator": "0.6.3" @@ -14203,7 +13265,7 @@ "version": "8.11.2", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", - "devOptional": true + "dev": true }, "acorn-import-assertions": { "version": "1.9.0", @@ -14223,7 +13285,7 @@ "version": "8.3.0", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.0.tgz", "integrity": "sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA==", - "devOptional": true + "dev": true }, "agentkeepalive": { "version": "4.5.0", @@ -14311,11 +13373,6 @@ "color-convert": "^2.0.1" } }, - "any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==" - }, "anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -14326,21 +13383,18 @@ "picomatch": "^2.0.4" } }, - "app-root-path": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.1.0.tgz", - "integrity": "sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==" - }, "append-field": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", - "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "optional": true, + "peer": true }, "arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "devOptional": true + "dev": true }, "argparse": { "version": "2.0.1", @@ -14350,7 +13404,9 @@ "array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "optional": true, + "peer": true }, "array-timsort": { "version": "1.0.3", @@ -14498,7 +13554,9 @@ "base64id": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", - "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "optional": true, + "peer": true }, "bech32": { "version": "1.1.4", @@ -14638,6 +13696,8 @@ "version": "1.20.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "optional": true, + "peer": true, "requires": { "bytes": "3.1.2", "content-type": "~1.0.5", @@ -14657,6 +13717,8 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "optional": true, + "peer": true, "requires": { "ms": "2.0.0" } @@ -14664,7 +13726,9 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "optional": true, + "peer": true } } }, @@ -14717,11 +13781,6 @@ "node-int64": "^0.4.0" } }, - "bson": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/bson/-/bson-5.5.1.tgz", - "integrity": "sha512-ix0EwukN2EpC0SRWIj/7B5+A6uQMQy6KMREI9qQqvgpkV2frH63T0UDVd1SYedL6dNCmDBYB3QtXi4ISk9YT+g==" - }, "buffer": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", @@ -14731,15 +13790,11 @@ "ieee754": "^1.2.1" } }, - "buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" - }, "buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "devOptional": true }, "buffer-more-ints": { "version": "1.0.0", @@ -14764,6 +13819,8 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "optional": true, + "peer": true, "requires": { "streamsearch": "^1.1.0" } @@ -14771,7 +13828,9 @@ "bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "optional": true, + "peer": true }, "cache-manager": { "version": "5.3.1", @@ -14794,6 +13853,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "devOptional": true, "requires": { "function-bind": "^1.1.2", "get-intrinsic": "^1.2.1", @@ -14891,60 +13951,6 @@ "restore-cursor": "^3.1.0" } }, - "cli-highlight": { - "version": "2.1.11", - "resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.11.tgz", - "integrity": "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==", - "requires": { - "chalk": "^4.0.0", - "highlight.js": "^10.7.1", - "mz": "^2.4.0", - "parse5": "^5.1.1", - "parse5-htmlparser2-tree-adapter": "^6.0.0", - "yargs": "^16.0.0" - }, - "dependencies": { - "cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - }, - "yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "requires": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - } - }, - "yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==" - } - } - }, "cli-spinners": { "version": "2.9.2", "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", @@ -15110,6 +14116,8 @@ "version": "1.6.2", "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "optional": true, + "peer": true, "requires": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", @@ -15120,12 +14128,16 @@ "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "optional": true, + "peer": true }, "readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "optional": true, + "peer": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -15139,12 +14151,16 @@ "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "optional": true, + "peer": true }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "optional": true, + "peer": true, "requires": { "safe-buffer": "~5.1.0" } @@ -15160,6 +14176,8 @@ "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "optional": true, + "peer": true, "requires": { "safe-buffer": "5.2.1" } @@ -15167,7 +14185,9 @@ "content-type": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "optional": true, + "peer": true }, "convert-source-map": { "version": "2.0.0", @@ -15178,21 +14198,16 @@ "cookie": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", - "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==" - }, - "cookie-parser": { - "version": "1.4.6", - "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz", - "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==", - "requires": { - "cookie": "0.4.1", - "cookie-signature": "1.0.6" - } + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", + "optional": true, + "peer": true }, "cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "optional": true, + "peer": true }, "cookiejar": { "version": "2.1.4", @@ -15209,6 +14224,8 @@ "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "optional": true, + "peer": true, "requires": { "object-assign": "^4", "vary": "^1" @@ -15271,7 +14288,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "devOptional": true + "dev": true }, "cron": { "version": "3.1.6", @@ -15300,14 +14317,6 @@ "which": "^2.0.1" } }, - "date-fns": { - "version": "2.30.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", - "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", - "requires": { - "@babel/runtime": "^7.21.0" - } - }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -15348,6 +14357,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "devOptional": true, "requires": { "get-intrinsic": "^1.2.1", "gopd": "^1.0.1", @@ -15367,12 +14377,16 @@ "depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "optional": true, + "peer": true }, "destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "optional": true, + "peer": true }, "detect-newline": { "version": "3.1.0", @@ -15394,7 +14408,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "devOptional": true + "dev": true }, "diff-sequences": { "version": "29.6.3", @@ -15435,14 +14449,6 @@ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, - "ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "requires": { - "safe-buffer": "^5.0.1" - } - }, "ed25519-hd-key": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/ed25519-hd-key/-/ed25519-hd-key-1.1.2.tgz", @@ -15464,7 +14470,9 @@ "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "optional": true, + "peer": true }, "electron-to-chromium": { "version": "1.4.594", @@ -15491,7 +14499,9 @@ "encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "optional": true, + "peer": true }, "end-of-stream": { "version": "1.4.4", @@ -15506,6 +14516,8 @@ "version": "6.5.4", "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.4.tgz", "integrity": "sha512-KdVSDKhVKyOi+r5uEabrDLZw2qXStVvCsEB/LN3mw4WFi6Gx50jTyuxYVCwAAC0U46FdnzP/ScKRBTXb/NiEOg==", + "optional": true, + "peer": true, "requires": { "@types/cookie": "^0.4.1", "@types/cors": "^2.8.12", @@ -15522,7 +14534,9 @@ "engine.io-parser": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz", - "integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==" + "integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==", + "optional": true, + "peer": true }, "enhanced-resolve": { "version": "5.15.0", @@ -15557,7 +14571,9 @@ "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "optional": true, + "peer": true }, "escape-string-regexp": { "version": "4.0.0", @@ -15754,7 +14770,9 @@ "etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "optional": true, + "peer": true }, "events": { "version": "3.3.0", @@ -15802,6 +14820,8 @@ "version": "4.18.2", "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "optional": true, + "peer": true, "requires": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -15840,6 +14860,8 @@ "version": "1.20.1", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "optional": true, + "peer": true, "requires": { "bytes": "3.1.2", "content-type": "~1.0.4", @@ -15858,12 +14880,16 @@ "cookie": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "optional": true, + "peer": true }, "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "optional": true, + "peer": true, "requires": { "ms": "2.0.0" } @@ -15871,17 +14897,23 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "optional": true, + "peer": true }, "path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", + "optional": true, + "peer": true }, "raw-body": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "optional": true, + "peer": true, "requires": { "bytes": "3.1.2", "http-errors": "2.0.0", @@ -16014,6 +15046,8 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "optional": true, + "peer": true, "requires": { "debug": "2.6.9", "encodeurl": "~1.0.2", @@ -16028,6 +15062,8 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "optional": true, + "peer": true, "requires": { "ms": "2.0.0" } @@ -16035,7 +15071,9 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "optional": true, + "peer": true } } }, @@ -16148,12 +15186,16 @@ "forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "optional": true, + "peer": true }, "fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "optional": true, + "peer": true }, "fs-extra": { "version": "10.1.0", @@ -16175,7 +15217,8 @@ "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true }, "fsevents": { "version": "2.3.3", @@ -16187,15 +15230,8 @@ "function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" - }, - "generate-function": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", - "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", - "requires": { - "is-property": "^1.0.2" - } + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "devOptional": true }, "gensync": { "version": "1.0.0-beta.2", @@ -16212,6 +15248,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "devOptional": true, "requires": { "function-bind": "^1.1.2", "has-proto": "^1.0.1", @@ -16292,6 +15329,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "devOptional": true, "requires": { "get-intrinsic": "^1.1.3" } @@ -16323,6 +15361,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "devOptional": true, "requires": { "get-intrinsic": "^1.2.2" } @@ -16330,12 +15369,14 @@ "has-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==" + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "devOptional": true }, "has-symbols": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "devOptional": true }, "hash-base": { "version": "3.1.0", @@ -16371,6 +15412,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "devOptional": true, "requires": { "function-bind": "^1.1.2" } @@ -16381,11 +15423,6 @@ "integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==", "dev": true }, - "highlight.js": { - "version": "10.7.3", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", - "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==" - }, "html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -16396,6 +15433,8 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "optional": true, + "peer": true, "requires": { "depd": "2.0.0", "inherits": "2.0.4", @@ -16422,6 +15461,7 @@ "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "devOptional": true, "requires": { "safer-buffer": ">= 2.1.2 < 3" } @@ -16467,6 +15507,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, "requires": { "once": "^1.3.0", "wrappy": "1" @@ -16522,15 +15563,12 @@ "standard-as-callback": "^2.1.0" } }, - "ip": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", - "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==" - }, "ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "optional": true, + "peer": true }, "is-arrayish": { "version": "0.2.1", @@ -16600,11 +15638,6 @@ "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "dev": true }, - "is-property": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", - "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==" - }, "is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -17225,47 +16258,6 @@ "universalify": "^2.0.0" } }, - "jsonwebtoken": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", - "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", - "requires": { - "jws": "^3.2.2", - "lodash.includes": "^4.3.0", - "lodash.isboolean": "^3.0.3", - "lodash.isinteger": "^4.0.4", - "lodash.isnumber": "^3.0.3", - "lodash.isplainobject": "^4.0.6", - "lodash.isstring": "^4.0.1", - "lodash.once": "^4.0.0", - "ms": "^2.1.1", - "semver": "^7.5.4" - } - }, - "jwa": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", - "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", - "requires": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", - "requires": { - "jwa": "^1.4.1", - "safe-buffer": "^5.0.1" - } - }, - "kareem": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.5.1.tgz", - "integrity": "sha512-7jFxRVm+jD+rkq3kY0iZDJfsO2/t4BBPeEb2qKn2lR/9KhuksYk5hxzfRYWMPV8P/x2d0kHD306YyWLzjjH+uA==" - }, "keccak": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.2.tgz", @@ -17373,41 +16365,11 @@ "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" }, - "lodash.includes": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", - "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" - }, "lodash.isarguments": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==" }, - "lodash.isboolean": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", - "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" - }, - "lodash.isinteger": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", - "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" - }, - "lodash.isnumber": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", - "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" - }, - "lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" - }, - "lodash.isstring": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" - }, "lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -17420,11 +16382,6 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, - "lodash.once": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" - }, "log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -17498,7 +16455,7 @@ "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "devOptional": true + "dev": true }, "makeerror": { "version": "1.0.12", @@ -17522,7 +16479,9 @@ "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "optional": true, + "peer": true }, "memfs": { "version": "3.5.3", @@ -17533,16 +16492,12 @@ "fs-monkey": "^1.0.4" } }, - "memory-pager": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", - "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", - "optional": true - }, "merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", + "optional": true, + "peer": true }, "merge-stream": { "version": "2.0.0", @@ -17559,7 +16514,8 @@ "methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "devOptional": true }, "micromatch": { "version": "4.0.5", @@ -17574,7 +16530,9 @@ "mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "optional": true, + "peer": true }, "mime-db": { "version": "1.52.0", @@ -17607,7 +16565,8 @@ "minimist": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "devOptional": true }, "minipass": { "version": "4.2.8", @@ -17619,6 +16578,8 @@ "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "optional": true, + "peer": true, "requires": { "minimist": "^1.2.6" } @@ -17633,71 +16594,6 @@ "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==" }, - "mongodb": { - "version": "5.9.1", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-5.9.1.tgz", - "integrity": "sha512-NBGA8AfJxGPeB12F73xXwozt8ZpeIPmCUeWRwl9xejozTXFes/3zaep9zhzs1B/nKKsw4P3I4iPfXl3K7s6g+Q==", - "requires": { - "@mongodb-js/saslprep": "^1.1.0", - "bson": "^5.5.0", - "mongodb-connection-string-url": "^2.6.0", - "socks": "^2.7.1" - } - }, - "mongodb-connection-string-url": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz", - "integrity": "sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==", - "requires": { - "@types/whatwg-url": "^8.2.1", - "whatwg-url": "^11.0.0" - } - }, - "mongoose": { - "version": "7.6.5", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-7.6.5.tgz", - "integrity": "sha512-ElHgGWVKQUawKBn0DXuHmSd3W5w5Kb8JUbDNQH30odhYCDKq9GCh+E1/SuN8jZGxrHgFyLrvYxLSpC36BpqS+w==", - "requires": { - "bson": "^5.5.0", - "kareem": "2.5.1", - "mongodb": "5.9.0", - "mpath": "0.9.0", - "mquery": "5.0.0", - "ms": "2.1.3", - "sift": "16.0.1" - }, - "dependencies": { - "mongodb": { - "version": "5.9.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-5.9.0.tgz", - "integrity": "sha512-g+GCMHN1CoRUA+wb1Agv0TI4YTSiWr42B5ulkiAfLLHitGK1R+PkSAf3Lr5rPZwi/3F04LiaZEW0Kxro9Fi2TA==", - "requires": { - "@mongodb-js/saslprep": "^1.1.0", - "bson": "^5.5.0", - "mongodb-connection-string-url": "^2.6.0", - "socks": "^2.7.1" - } - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - } - } - }, - "mpath": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", - "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==" - }, - "mquery": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", - "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", - "requires": { - "debug": "4.x" - } - }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -17730,6 +16626,8 @@ "version": "1.4.4-lts.1", "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.4-lts.1.tgz", "integrity": "sha512-WeSGziVj6+Z2/MwQo3GvqzgR+9Uc+qt8SwHKh3gvNPiISKfsMfG4SvCOFYlxxgkXt7yIV2i1yczehm0EOKIxIg==", + "optional": true, + "peer": true, "requires": { "append-field": "^1.0.0", "busboy": "^1.0.0", @@ -17746,56 +16644,6 @@ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "dev": true }, - "mysql2": { - "version": "3.6.5", - "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.6.5.tgz", - "integrity": "sha512-pS/KqIb0xlXmtmqEuTvBXTmLoQ5LmAz5NW/r8UyQ1ldvnprNEj3P9GbmuQQ2J0A4LO+ynotGi6TbscPa8OUb+w==", - "requires": { - "denque": "^2.1.0", - "generate-function": "^2.3.1", - "iconv-lite": "^0.6.3", - "long": "^5.2.1", - "lru-cache": "^8.0.0", - "named-placeholders": "^1.1.3", - "seq-queue": "^0.0.5", - "sqlstring": "^2.3.2" - }, - "dependencies": { - "iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "requires": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - } - } - } - }, - "mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "requires": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "named-placeholders": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz", - "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==", - "requires": { - "lru-cache": "^7.14.1" - }, - "dependencies": { - "lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==" - } - } - }, "nanoassert": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/nanoassert/-/nanoassert-1.1.0.tgz", @@ -17821,7 +16669,9 @@ "negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "optional": true, + "peer": true }, "neo-async": { "version": "2.6.2", @@ -17927,22 +16777,29 @@ "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "optional": true, + "peer": true }, "object-hash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==" + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "optional": true, + "peer": true }, "object-inspect": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==" + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "devOptional": true }, "on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "optional": true, + "peer": true, "requires": { "ee-first": "1.1.1" } @@ -17951,6 +16808,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, "requires": { "wrappy": "1" } @@ -18064,30 +16922,12 @@ "lines-and-columns": "^1.1.6" } }, - "parse5": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", - "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==" - }, - "parse5-htmlparser2-tree-adapter": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", - "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", - "requires": { - "parse5": "^6.0.1" - }, - "dependencies": { - "parse5": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", - "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==" - } - } - }, "parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "optional": true, + "peer": true }, "path-exists": { "version": "4.0.0", @@ -18280,7 +17120,9 @@ "process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "optional": true, + "peer": true }, "prom-client": { "version": "14.2.0", @@ -18333,6 +17175,8 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "optional": true, + "peer": true, "requires": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" @@ -18356,7 +17200,8 @@ "punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==" + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true }, "pure-rand": { "version": "6.0.4", @@ -18368,6 +17213,7 @@ "version": "6.11.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "devOptional": true, "requires": { "side-channel": "^1.0.4" } @@ -18394,12 +17240,16 @@ "range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "optional": true, + "peer": true }, "raw-body": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "optional": true, + "peer": true, "requires": { "bytes": "3.1.2", "http-errors": "2.0.0", @@ -18483,11 +17333,6 @@ "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==" }, - "regenerator-runtime": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", - "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" - }, "repeat-string": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", @@ -18654,7 +17499,8 @@ "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "devOptional": true }, "schema-utils": { "version": "3.3.0", @@ -18726,6 +17572,8 @@ "version": "0.18.0", "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "optional": true, + "peer": true, "requires": { "debug": "2.6.9", "depd": "2.0.0", @@ -18746,6 +17594,8 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "optional": true, + "peer": true, "requires": { "ms": "2.0.0" }, @@ -18753,22 +17603,21 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "optional": true, + "peer": true } } }, "ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "optional": true, + "peer": true } } }, - "seq-queue": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", - "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" - }, "serialize-javascript": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", @@ -18782,6 +17631,8 @@ "version": "1.15.0", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "optional": true, + "peer": true, "requires": { "encodeurl": "~1.0.2", "escape-html": "~1.0.3", @@ -18793,6 +17644,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "devOptional": true, "requires": { "define-data-property": "^1.1.1", "get-intrinsic": "^1.2.1", @@ -18803,7 +17655,9 @@ "setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "optional": true, + "peer": true }, "sha.js": { "version": "2.4.11", @@ -18842,17 +17696,13 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "devOptional": true, "requires": { "call-bind": "^1.0.0", "get-intrinsic": "^1.0.2", "object-inspect": "^1.9.0" } }, - "sift": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/sift/-/sift-16.0.1.tgz", - "integrity": "sha512-Wv6BjQ5zbhW7VFefWusVP33T/EM0vYikCaQ2qR8yULbsilAT8/wQaXvuQ3ptGLpoKx+lihJE3y2UTgKDyyNHZQ==" - }, "signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -18886,15 +17736,12 @@ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true }, - "smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==" - }, "socket.io": { "version": "4.7.2", "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.2.tgz", "integrity": "sha512-bvKVS29/I5fl2FGLNHuXlQaUH/BlzX1IN6S+NKLNZpBsPZIDH+90eQmCs2Railn4YUiww4SzUedJ6+uzwFnKLw==", + "optional": true, + "peer": true, "requires": { "accepts": "~1.3.4", "base64id": "~2.0.0", @@ -18909,6 +17756,8 @@ "version": "2.5.2", "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz", "integrity": "sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==", + "optional": true, + "peer": true, "requires": { "ws": "~8.11.0" } @@ -18917,20 +17766,13 @@ "version": "4.2.4", "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "optional": true, + "peer": true, "requires": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1" } }, - "socks": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz", - "integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==", - "requires": { - "ip": "^2.0.0", - "smart-buffer": "^4.2.0" - } - }, "source-map": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", @@ -18955,26 +17797,12 @@ } } }, - "sparse-bitfield": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", - "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", - "optional": true, - "requires": { - "memory-pager": "^1.0.2" - } - }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true }, - "sqlstring": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", - "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==" - }, "stack-trace": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", @@ -19005,12 +17833,16 @@ "statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "optional": true, + "peer": true }, "streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", - "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==" + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "optional": true, + "peer": true }, "string_decoder": { "version": "0.10.31", @@ -19134,15 +17966,8 @@ "swagger-ui-dist": { "version": "5.9.1", "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.9.1.tgz", - "integrity": "sha512-5zAx+hUwJb9T3EAntc7TqYkV716CMqG6sZpNlAAMOMWkNXRYxGkN8ADIvD55dQZ10LxN90ZM/TQmN7y1gpICnw==" - }, - "swagger-ui-express": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.6.3.tgz", - "integrity": "sha512-CDje4PndhTD2HkgyKH3pab+LKspDeB/NhPN2OF1j+piYIamQqBYwAXWESOT1Yju2xFg51bRW9sUng2WxDjzArw==", - "requires": { - "swagger-ui-dist": ">=4.11.0" - } + "integrity": "sha512-5zAx+hUwJb9T3EAntc7TqYkV716CMqG6sZpNlAAMOMWkNXRYxGkN8ADIvD55dQZ10LxN90ZM/TQmN7y1gpICnw==", + "peer": true }, "symbol-observable": { "version": "4.0.0", @@ -19241,22 +18066,6 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, - "thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "requires": { - "any-promise": "^1.0.0" - } - }, - "thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "requires": { - "thenify": ">= 3.1.0 < 4" - } - }, "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -19311,15 +18120,9 @@ "toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" - }, - "tr46": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", - "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", - "requires": { - "punycode": "^2.1.1" - } + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "optional": true, + "peer": true }, "tree-kill": { "version": "1.2.2", @@ -19364,7 +18167,7 @@ "version": "10.7.0", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.7.0.tgz", "integrity": "sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A==", - "devOptional": true, + "dev": true, "requires": { "@cspotcode/source-map-support": "0.7.0", "@tsconfig/node10": "^1.0.7", @@ -19463,6 +18266,8 @@ "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "optional": true, + "peer": true, "requires": { "media-typer": "0.3.0", "mime-types": "~2.1.24" @@ -19471,75 +18276,15 @@ "typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" - }, - "typeorm": { - "version": "0.3.17", - "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.17.tgz", - "integrity": "sha512-UDjUEwIQalO9tWw9O2A4GU+sT3oyoUXheHJy4ft+RFdnRdQctdQ34L9SqE2p7LdwzafHx1maxT+bqXON+Qnmig==", - "requires": { - "@sqltools/formatter": "^1.2.5", - "app-root-path": "^3.1.0", - "buffer": "^6.0.3", - "chalk": "^4.1.2", - "cli-highlight": "^2.1.11", - "date-fns": "^2.29.3", - "debug": "^4.3.4", - "dotenv": "^16.0.3", - "glob": "^8.1.0", - "mkdirp": "^2.1.3", - "reflect-metadata": "^0.1.13", - "sha.js": "^2.4.11", - "tslib": "^2.5.0", - "uuid": "^9.0.0", - "yargs": "^17.6.2" - }, - "dependencies": { - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "requires": { - "balanced-match": "^1.0.0" - } - }, - "glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - } - }, - "minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "requires": { - "brace-expansion": "^2.0.1" - } - }, - "mkdirp": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-2.1.6.tgz", - "integrity": "sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==" - }, - "uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==" - } - } + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "optional": true, + "peer": true }, "typescript": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz", "integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==", - "devOptional": true + "dev": true }, "uid": { "version": "2.0.2", @@ -19563,7 +18308,9 @@ "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "optional": true, + "peer": true }, "update-browserslist-db": { "version": "1.0.13", @@ -19601,7 +18348,9 @@ "utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "optional": true, + "peer": true }, "uuid": { "version": "8.3.2", @@ -19612,7 +18361,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "devOptional": true + "dev": true }, "v8-to-istanbul": { "version": "9.2.0", @@ -19628,7 +18377,9 @@ "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "optional": true, + "peer": true }, "walker": { "version": "1.0.8", @@ -19658,11 +18409,6 @@ "defaults": "^1.0.3" } }, - "webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==" - }, "webpack": { "version": "5.89.0", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.89.0.tgz", @@ -19708,15 +18454,6 @@ "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", "dev": true }, - "whatwg-url": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", - "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", - "requires": { - "tr46": "^3.0.0", - "webidl-conversions": "^7.0.0" - } - }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -19883,7 +18620,8 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true }, "write-file-atomic": { "version": "4.0.2", @@ -19899,12 +18637,16 @@ "version": "8.11.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "optional": true, + "peer": true, "requires": {} }, "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "optional": true, + "peer": true }, "y18n": { "version": "5.0.8", @@ -19946,7 +18688,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "devOptional": true + "dev": true }, "yocto-queue": { "version": "0.1.0", diff --git a/package.json b/package.json index 9ac603f..4bdf817 100644 --- a/package.json +++ b/package.json @@ -10,26 +10,12 @@ "build": "nest build", "build:all": "nest build mvx-event-processor && nest build axelar-event-processor && nest build common", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", - "start:devnet": "npm run copy-devnet-config & nest build mvx-event-processor & nest start", - "start:devnet:watch": "npm run copy-devnet-config & nest build mvx-event-processor & nest start --watch", - "start:devnet:debug": "npm run copy-devnet-config & nest build mvx-event-processor & nest start --watch --debug", - "start:testnet": "npm run copy-testnet-config & nest build mvx-event-processor & nest start", - "start:testnet:watch": "npm run copy-testnet-config & nest build mvx-event-processor & nest start --watch", - "start:testnet:debug": "npm run copy-testnet-config & nest build mvx-event-processor & nest start --watch --debug", - "start:mainnet": "npm run copy-mainnet-config & nest build mvx-event-processor & nest start", - "start:custom": "npm run copy-custom-config & nest build mvx-event-processor & nest start", - "start:custom:watch": "npm run copy-custom-config & nest build mvx-event-processor & nest start --watch", - "start:custom:debug": "npm run copy-custom-config & nest build mvx-event-processor & nest start --watch --debug", - "start:axelar-event-processor:devnet": "npm run copy-devnet-config-axelar & nest build axelar-event-processor & nest start axelar-event-processor", - "start:axelar-event-processor:devnet:watch": "npm run copy-devnet-config-axelar & nest build axelar-event-processor & nest start axelar-event-processor --watch", - "start:axelar-event-processor:devnet:debug": "npm run copy-devnet-config-axelar & nest build axelar-event-processor & nest start axelar-event-processor --watch --debug", - "start:axelar-event-processor:testnet": "npm run copy-testnet-config-axelar & nest build axelar-event-processor & nest start axelar-event-processor", - "start:axelar-event-processor:testnet:watch": "npm run copy-testnet-config-axelar & nest build axelar-event-processor & nest start axelar-event-processor --watch", - "start:axelar-event-processor:testnet:debug": "npm run copy-testnet-config-axelar & nest build axelar-event-processor & nest start axelar-event-processor --watch --debug", - "start:axelar-event-processor:mainnet": "npm run copy-mainnet-config-axelar & nest build axelar-event-processor & nest start axelar-event-processor", - "start:axelar-event-processor:custom": "npm run copy-custom-config-axelar & nest build axelar-event-processor &nest start axelar-event-processor", - "start:axelar-event-processor:custom:watch": "npm run copy-custom-config-axelar & nest build axelar-event-processor & nest start axelar-event-processor --watch", - "start:axelar-event-processor:custom:debug": "npm run copy-custom-config-axelar & nest build axelar-event-processor & nest start axelar-event-processor --watch --debug", + "start": "nest build mvx-event-processor & nest start", + "start:watch": "nest build mvx-event-processor & nest start --watch", + "start:debug": "nest build mvx-event-processor & nest start --watch --debug", + "start:axelar-event-processor": "nest build axelar-event-processor & nest start axelar-event-processor", + "start:axelar-event-processor:watch": "nest build axelar-event-processor & nest start axelar-event-processor --watch", + "start:axelar-event-processor:debug": "nest build axelar-event-processor & nest start axelar-event-processor --watch --debug", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\"", "lint:fix": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "test": "jest", @@ -37,20 +23,13 @@ "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", "test:e2e": "jest --config ./apps/mvx-event-processor/test/jest-e2e.json", - "copy-devnet-config": "cp ./apps/mvx-event-processor/config/config.devnet.yaml ./apps/mvx-event-processor/config/config.yaml", - "copy-testnet-config": "cp ./apps/mvx-event-processor/config/config.testnet.yaml ./apps/mvx-event-processor/config/config.yaml", - "copy-mainnet-config": "cp ./apps/mvx-event-processor/config/config.mainnet.yaml ./apps/mvx-event-processor/config/config.yaml", - "copy-custom-config": "cp ./apps/mvx-event-processor/config/config.custom.yaml ./apps/mvx-event-processor/config/config.yaml", - "copy-devnet-config-axelar": "cp apps/axelar-event-processor/config/config.devnet.yaml apps/axelar-event-processor/config/config.yaml", - "copy-testnet-config-axelar": "cp apps/axelar-event-processor/config/config.testnet.yaml apps/axelar-event-processor/config/config.yaml", - "copy-mainnet-config-axelar": "cp apps/axelar-event-processor/config/config.mainnet.yaml apps/axelar-event-processor/config/config.yaml", - "copy-custom-config-axelar": "cp apps/axelar-event-processor/config/config.custom.yaml apps/axelar-event-processor/config/config.yaml" + "migrate": "prisma migrate dev", + "generate": "prisma generate" }, "dependencies": { "@grpc/grpc-js": "^1.9.12", "@grpc/proto-loader": "^0.7.10", "@multiversx/sdk-core": "^12.15.0", - "@multiversx/sdk-nestjs-auth": "2.4.0", "@multiversx/sdk-nestjs-cache": "2.4.0", "@multiversx/sdk-nestjs-common": "2.4.0", "@multiversx/sdk-nestjs-elastic": "2.4.0", @@ -65,46 +44,30 @@ "@nestjs/config": "3.0.1", "@nestjs/core": "^10.2.4", "@nestjs/microservices": "10.2.4", - "@nestjs/mongoose": "^10.0.2", - "@nestjs/platform-express": "10.2.4", - "@nestjs/platform-socket.io": "10.2.4", "@nestjs/schedule": "^4.0.0", - "@nestjs/swagger": "7.1.16", - "@nestjs/typeorm": "10.0.0", - "@nestjs/websockets": "10.2.8", "@prisma/client": "^5.6.0", "agentkeepalive": "^4.3.0", "bull": "^4.10.4", "cache-manager": "^5.2.1", - "cookie-parser": "^1.4.6", "cron": "^3.1.6", "ioredis": "^5.3.2", - "jsonwebtoken": "^9.0.0", "module-alias": "^2.2.3", - "mongodb": "^5.5.0", - "mongoose": "^7.1.1", - "mysql2": "^3.3.1", "nest-winston": "^1.9.2", "prom-client": "^14.2.0", "reflect-metadata": "^0.1.13", "rimraf": "^5.0.0", "rxjs": "^7.8.1", - "socket.io": "^4.6.1", - "swagger-ui-express": "^4.6.3", - "typeorm": "^0.3.16", "winston": "^3.8.2", "winston-daily-rotate-file": "^4.7.1" }, "devDependencies": { + "@golevelup/ts-jest": "^0.4.0", "@nestjs/cli": "10.1.17", "@nestjs/schematics": "10.0.2", - "@nestjs/testing": "10.2.4", + "@nestjs/testing": "^10.2.4", "@types/cache-manager": "^4.0.2", - "@types/cookie-parser": "^1.4.3", - "@types/express": "^4.17.17", "@types/jest": "^29.5.1", "@types/js-yaml": "^4.0.5", - "@types/jsonwebtoken": "^9.0.2", "@types/node": "^20.1.4", "@types/supertest": "^2.0.12", "@typescript-eslint/eslint-plugin": "^5.59.5", @@ -143,8 +106,8 @@ "coverageDirectory": "../coverage", "testEnvironment": "node", "moduleNameMapper": { - "^@mvx-monorepo/common(|/.*)$": "/libs/common/src/$1", - "^@mvx-monorepo/common": "/libs/common" + "^@mvx-monorepo/common(|/.*)$": "../libs/common/src/$1", + "^@mvx-monorepo/common": "../libs/common" } } } diff --git a/prisma/migrations/20231206110005_contract_call_event/migration.sql b/prisma/migrations/20231206110005_contract_call_event/migration.sql new file mode 100644 index 0000000..f341872 --- /dev/null +++ b/prisma/migrations/20231206110005_contract_call_event/migration.sql @@ -0,0 +1,21 @@ +-- CreateEnum +CREATE TYPE "ContractCallEventStatus" AS ENUM ('PENDING', 'APPROVED', 'SUCCESS', 'FAILED'); + +-- CreateTable +CREATE TABLE "ContractCallEvent" ( + "id" VARCHAR(255) NOT NULL, + "txHash" VARCHAR(64) NOT NULL, + "eventIndex" SMALLINT NOT NULL, + "status" "ContractCallEventStatus" NOT NULL, + "sourceAddress" VARCHAR(62) NOT NULL, + "sourceChain" VARCHAR(255) NOT NULL, + "destinationAddress" VARCHAR(255) NOT NULL, + "destinationChain" VARCHAR(255) NOT NULL, + "payloadHash" VARCHAR(64) NOT NULL, + "payload" BYTEA NOT NULL, + "executeTxHash" VARCHAR(64), + "createdAt" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "ContractCallEvent_pkey" PRIMARY KEY ("id") +); diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..fbffa92 --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index d205f42..e770919 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -9,3 +9,26 @@ datasource db { provider = "postgresql" url = env("DATABASE_URL") } + +model ContractCallEvent { + id String @id @db.VarChar(255) // should be formatted as [source_chain]:[unique identifier], i.e. Ethereum:0x74ac0205b1f8f51023942856145182f0e6fdd41ccb2c8058bf2d89fc67564d56:0 + txHash String @db.VarChar(64) + eventIndex Int @db.SmallInt + status ContractCallEventStatus + sourceAddress String @db.VarChar(62) + sourceChain String @db.VarChar(255) + destinationAddress String @db.VarChar(255) + destinationChain String @db.VarChar(255) + payloadHash String @db.VarChar(64) + payload Bytes + executeTxHash String? @db.VarChar(64) + createdAt DateTime @default(now()) @db.Timestamp(6) + updatedAt DateTime @default(now()) @updatedAt @db.Timestamp(6) +} + +enum ContractCallEventStatus { + PENDING + APPROVED + SUCCESS + FAILED +} From 32d41c9960e841757d54a324a5c96aca4b507d59 Mon Sep 17 00:00:00 2001 From: Rares <6453351+raresserban@users.noreply.github.com> Date: Thu, 7 Dec 2023 14:13:11 +0200 Subject: [PATCH 03/33] Generate interfaces from grpc proto file and add grpc service. --- README.md | 124 ++++-------------- apps/axelar-event-processor/src/main.ts | 18 ++- .../src/event-processor/types.ts | 2 +- .../contract-call.processor.spec.ts | 63 +++++---- .../src/processors/contract-call.processor.ts | 11 +- .../src/processors/processors.module.ts | 3 +- .../common/src/assets}/gateway.abi.json | 0 .../common/src/assets}/relayer.proto | 2 +- libs/common/src/contracts/contract.loader.ts | 26 ++-- libs/common/src/contracts/contracts.module.ts | 10 +- libs/common/src/grpc/entities/relayer.ts | 50 +++++++ libs/common/src/grpc/grpc.module.ts | 32 +++++ libs/common/src/grpc/grpc.service.ts | 40 ++++++ libs/common/src/utils/provider.enum.ts | 3 + nest-cli.json | 11 +- package-lock.json | 122 +++++++++++++++++ package.json | 16 ++- 17 files changed, 376 insertions(+), 157 deletions(-) rename {abis => libs/common/src/assets}/gateway.abi.json (100%) rename {apps/axelar-event-processor/axelar => libs/common/src/assets}/relayer.proto (93%) create mode 100644 libs/common/src/grpc/entities/relayer.ts create mode 100644 libs/common/src/grpc/grpc.module.ts create mode 100644 libs/common/src/grpc/grpc.service.ts create mode 100644 libs/common/src/utils/provider.enum.ts diff --git a/README.md b/README.md index fc80dc7..bfa9fc0 100644 --- a/README.md +++ b/README.md @@ -1,108 +1,20 @@ -REST API facade template for microservices that interacts with the MultiversX blockchain. +Axelar Relayer for MultiversX blockchain ## Quick start 1. Run `npm install` in the project directory -2. Optionally make edits to `config.yaml` or create `config.custom.yaml` for each microservice +2. Copy `.env.example` file to `.env` file and update the values +3. Run `docker-compose up -d` +4. Run `npm start` or `npm start:axelar-event-processor` ## Dependencies 1. Redis Server is required to be installed [docs](https://redis.io/). -2. MySQL Server is required to be installed [docs](https://dev.mysql.com/doc/refman/8.0/en/installing.html). -3. MongoDB Server is required to be installed [docs](https://docs.mongodb.com/). +2. PostgreSQL is required to be installed [docs](https://www.postgresql.org/). -You can run `docker-compose up` in a separate terminal to use a local Docker container for all these dependencies. +In this repo there is a `docker-compose.yml` file providing these services so you can run them easily using `docker-compose up -d` -After running the sample, you can stop the Docker container with `docker-compose down` - -## Available Features - -These features can be enabled/disabled in config file - -### `Public API` - -Endpoints that can be used by anyone (public endpoints). - -### `Private API` - -Endpoints that are not exposed on the internet -For example: We do not want to expose our metrics and cache interactions to anyone (/metrics /cache) - -### `Cache Warmer` - -This is used to keep the application cache in sync with new updates. - -### `Transaction Processor` - -This is used for scanning the transactions from MultiversX Blockchain. - -### `Queue Worker` - -This is used for concurrently processing heavy jobs. - -## Available Scripts - -This is a MultiversX project built on Nest.js framework. - -### `npm run start:mainnet` - -​ -Runs the app in the production mode. -Make requests to [http://localhost:3001](http://localhost:3001). - -Redis Server is required to be installed. - -## Running the api - -```bash -# development watch mode on devnet -$ npm run start:devnet:watch - -# development debug mode on devnet -$ npm run start:devnet:debug - -# development mode on devnet -$ npm run start:devnet - -# production mode -$ npm run start:mainnet -``` - -## Running the transactions-processor - -```bash -# development watch mode on devnet -$ npm run start:axelar-event-processor:devnet:watch - -# development debug mode on devnet -$ npm run start:axelar-event-processor:devnet:debug - -# development mode on devnet -$ npm run start:axelar-event-processor:devnet - -# production mode -$ npm run start:axelar-event-processor:mainnet -``` - -## Running the queue-worker - -```bash -# development watch mode on devnet -$ npm run start:mvx-event-processor:devnet:watch - -# development debug mode on devnet -$ npm run start:mvx-event-processor:devnet:debug - -# development mode on devnet -$ npm run start:mvx-event-processor:devnet - -# production mode -$ npm run start:mvx-event-processor:mainnet -``` - -Requests can be made to http://localhost:3001 for the api. The app will reload when you'll make edits (if opened in watch mode). You will also see any lint errors in the console.​ - -### `npm run test` +## Tests ```bash # unit tests @@ -114,3 +26,25 @@ $ npm run test:e2e # test coverage $ npm run test:cov ``` + +## Regenerating gRPC Typescript interfaces from proto file + +Make sure to have `protoc` installed https://grpc.io/docs/protoc-installation/. + +Then you can compile the files using: +``` +TS_ARGS=('lowerCaseServiceMethods=true' + 'outputEncodeMethods=false' + 'outputJsonMethods=false' + 'outputClientImpl=false' + 'snakeToCamel=true') +protoc --plugin=./node_modules/.bin/protoc-gen-ts_proto\ + --ts_proto_out=./libs/common/src/grpc/entities\ + --proto_path=./libs/common/src/assets\ + --ts_proto_opt="$(IFS=, ; echo "${TS_ARGS[*]}")"\ + ./libs/common/src/assets/relayer.proto +``` + +Check out these resources for more information: +- https://github.com/stephenh/ts-proto/blob/main/NESTJS.markdown +- https://blog.stackademic.com/nestjs-grpc-typescript-codegen-9a342bbd32f9 diff --git a/apps/axelar-event-processor/src/main.ts b/apps/axelar-event-processor/src/main.ts index 813ddca..da802b9 100644 --- a/apps/axelar-event-processor/src/main.ts +++ b/apps/axelar-event-processor/src/main.ts @@ -5,22 +5,20 @@ import { ApiConfigService, PubSubListenerModule } from '@mvx-monorepo/common'; import { MicroserviceOptions, Transport } from '@nestjs/microservices'; import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; import { join } from 'path'; +import { protobufPackage } from '@mvx-monorepo/common/grpc/entities/relayer'; async function bootstrap() { const transactionProcessorApp = await NestFactory.createApplicationContext(TransactionProcessorModule); const apiConfigService = transactionProcessorApp.get(ApiConfigService); - const pubSubApp = await NestFactory.createMicroservice( - PubSubListenerModule.forRoot(), - { - transport: Transport.GRPC, - options: { - package: 'axelar.relayer.v1beta1', - protoPath: join(__dirname, '../axelar/relayer.proto'), - url: apiConfigService.getAxelarApiUrl(), - }, + const pubSubApp = await NestFactory.createMicroservice(PubSubListenerModule.forRoot(), { + transport: Transport.GRPC, + options: { + package: protobufPackage, + protoPath: join(__dirname, '../../../libs/common/src/assets/relayer.proto'), + url: apiConfigService.getAxelarApiUrl(), }, - ); + }); pubSubApp.useLogger(pubSubApp.get(WINSTON_MODULE_NEST_PROVIDER)); // eslint-disable-next-line @typescript-eslint/no-floating-promises pubSubApp.listen(); diff --git a/apps/mvx-event-processor/src/event-processor/types.ts b/apps/mvx-event-processor/src/event-processor/types.ts index 30c56fe..937e155 100644 --- a/apps/mvx-event-processor/src/event-processor/types.ts +++ b/apps/mvx-event-processor/src/event-processor/types.ts @@ -11,5 +11,5 @@ export interface NotifierEvent { identifier: string; data: string; topics: string[]; - order: number; + order: number; // TODO: This field doesn't seem to come from the notifier, and is quite needed currently... } diff --git a/apps/mvx-event-processor/src/processors/contract-call.processor.spec.ts b/apps/mvx-event-processor/src/processors/contract-call.processor.spec.ts index 9b3f77b..233bc50 100644 --- a/apps/mvx-event-processor/src/processors/contract-call.processor.spec.ts +++ b/apps/mvx-event-processor/src/processors/contract-call.processor.spec.ts @@ -9,16 +9,19 @@ import { NotifierEvent } from '../event-processor/types'; import { ContractsModule } from '@mvx-monorepo/common/contracts/contracts.module'; import { Address } from '@multiversx/sdk-core/out'; import { ContractCallEventStatus } from '@prisma/client'; +import { GrpcService } from '@mvx-monorepo/common/grpc/grpc.service'; describe('ContractCallProcessor', () => { let contractCallEventRepository: DeepMocked; let apiConfigService: DeepMocked; + let grpcService: DeepMocked; let service: ContractCallProcessor; beforeEach(async () => { contractCallEventRepository = createMock(); apiConfigService = createMock(); + grpcService = createMock(); apiConfigService.getSourceChainName.mockReturnValue('multiversx-test'); apiConfigService.getContractGateway.mockReturnValue( @@ -38,7 +41,11 @@ describe('ContractCallProcessor', () => { return apiConfigService; } - return undefined; + if (token === GrpcService) { + return grpcService; + } + + return null; }) .compile(); @@ -46,29 +53,29 @@ describe('ContractCallProcessor', () => { }); describe('handleEvent', () => { - it('Should handle event', async () => { - const data = Buffer.concat([ - Buffer.from('ebc84cbd75ba5516bf45e7024a9e12bc3c5c880f73e3a5beca7ebba52b2867a7', 'hex'), - Buffer.from('00000007', 'hex'), // length of payload as u32 - Buffer.from('payload'), - ]); - const rawEvent: NotifierEvent = { - txHash: 'txHash', - address: 'mockGatewayAddress', - identifier: 'callContract', - data: data.toString('base64'), - topics: [ - BinaryUtils.base64Encode(Events.CONTRACT_CALL_EVENT), - Buffer.from( - Address.fromBech32('erd1qqqqqqqqqqqqqpgqzqvm5ywqqf524efwrhr039tjs29w0qltkklsa05pk7').hex(), - 'hex', - ).toString('base64'), - BinaryUtils.base64Encode('ethereum'), - BinaryUtils.base64Encode('destinationAddress'), - ], - order: 1, - }; + const data = Buffer.concat([ + Buffer.from('ebc84cbd75ba5516bf45e7024a9e12bc3c5c880f73e3a5beca7ebba52b2867a7', 'hex'), + Buffer.from('00000007', 'hex'), // length of payload as u32 + Buffer.from('payload'), + ]); + const rawEvent: NotifierEvent = { + txHash: 'txHash', + address: 'mockGatewayAddress', + identifier: 'callContract', + data: data.toString('base64'), + topics: [ + BinaryUtils.base64Encode(Events.CONTRACT_CALL_EVENT), + Buffer.from( + Address.fromBech32('erd1qqqqqqqqqqqqqpgqzqvm5ywqqf524efwrhr039tjs29w0qltkklsa05pk7').hex(), + 'hex', + ).toString('base64'), + BinaryUtils.base64Encode('ethereum'), + BinaryUtils.base64Encode('destinationAddress'), + ], + order: 1, + }; + it('Should handle event', async () => { await service.handleEvent(rawEvent); expect(contractCallEventRepository.create).toHaveBeenCalledTimes(1); @@ -84,6 +91,16 @@ describe('ContractCallProcessor', () => { payloadHash: 'ebc84cbd75ba5516bf45e7024a9e12bc3c5c880f73e3a5beca7ebba52b2867a7', payload: Buffer.from('payload'), }); + expect(grpcService.verify).toHaveBeenCalledTimes(1); + }); + + it('Should throw error can not save in database', async () => { + contractCallEventRepository.create.mockReturnValueOnce(Promise.resolve(null)); + + await expect(service.handleEvent(rawEvent)).rejects.toThrow(); + + expect(contractCallEventRepository.create).toHaveBeenCalledTimes(1); + expect(grpcService.verify).not.toHaveBeenCalled(); }); }); }); diff --git a/apps/mvx-event-processor/src/processors/contract-call.processor.ts b/apps/mvx-event-processor/src/processors/contract-call.processor.ts index 6c25ab0..cbf85ba 100644 --- a/apps/mvx-event-processor/src/processors/contract-call.processor.ts +++ b/apps/mvx-event-processor/src/processors/contract-call.processor.ts @@ -5,6 +5,7 @@ import { TransactionEvent } from '@multiversx/sdk-network-providers/out'; import { ContractCallEventRepository } from '@mvx-monorepo/common/database/repository/contract-call-event.repository'; import { ApiConfigService } from '@mvx-monorepo/common'; import { ContractCallEventStatus } from '@prisma/client'; +import { GrpcService } from '@mvx-monorepo/common/grpc/grpc.service'; @Injectable() export class ContractCallProcessor { @@ -13,6 +14,7 @@ export class ContractCallProcessor { constructor( private readonly gatewayContract: GatewayContract, private readonly contractCallEventRepository: ContractCallEventRepository, + private readonly grpcService: GrpcService, apiConfigService: ApiConfigService, ) { this.sourceChain = apiConfigService.getSourceChainName(); @@ -21,7 +23,7 @@ export class ContractCallProcessor { async handleEvent(rawEvent: NotifierEvent) { const event = this.gatewayContract.decodeContractCallEvent(TransactionEvent.fromHttpResponse(rawEvent)); - await this.contractCallEventRepository.create({ + const contractCallEvent = await this.contractCallEventRepository.create({ id: `${this.sourceChain}:${rawEvent.txHash}:${rawEvent.order}`, txHash: rawEvent.txHash, eventIndex: rawEvent.order, @@ -33,5 +35,12 @@ export class ContractCallProcessor { payloadHash: event.data.hash, payload: event.data.payload, }); + + if (!contractCallEvent) { + throw new Error(`Couldn't save contract call event to database for hash ${rawEvent.txHash}`); + } + + // TODO: Should this be batched instead and have this in a separate cronjob? + await this.grpcService.verify(contractCallEvent); } } diff --git a/apps/mvx-event-processor/src/processors/processors.module.ts b/apps/mvx-event-processor/src/processors/processors.module.ts index bec724c..36c1ba7 100644 --- a/apps/mvx-event-processor/src/processors/processors.module.ts +++ b/apps/mvx-event-processor/src/processors/processors.module.ts @@ -2,9 +2,10 @@ import { Module } from '@nestjs/common'; import { ContractCallProcessor } from './contract-call.processor'; import { ContractsModule } from '@mvx-monorepo/common/contracts/contracts.module'; import { DatabaseModule } from '@mvx-monorepo/common'; +import { GrpcModule } from '@mvx-monorepo/common/grpc/grpc.module'; @Module({ - imports: [ContractsModule, DatabaseModule], + imports: [ContractsModule, DatabaseModule, GrpcModule], providers: [ContractCallProcessor], exports: [ContractCallProcessor], }) diff --git a/abis/gateway.abi.json b/libs/common/src/assets/gateway.abi.json similarity index 100% rename from abis/gateway.abi.json rename to libs/common/src/assets/gateway.abi.json diff --git a/apps/axelar-event-processor/axelar/relayer.proto b/libs/common/src/assets/relayer.proto similarity index 93% rename from apps/axelar-event-processor/axelar/relayer.proto rename to libs/common/src/assets/relayer.proto index 3b9c4f4..cb8ab4c 100644 --- a/apps/axelar-event-processor/axelar/relayer.proto +++ b/libs/common/src/assets/relayer.proto @@ -19,7 +19,7 @@ message VerifyResponse{ message Message{ string id = 1; // the unique identifier with which the message can be looked up on the source chain string source_chain = 2; - string source_address= 3; + string source_address = 3; string destination_chain = 4; string destination_address = 5; bytes payload = 6; diff --git a/libs/common/src/contracts/contract.loader.ts b/libs/common/src/contracts/contract.loader.ts index 7058c3c..cbf4299 100644 --- a/libs/common/src/contracts/contract.loader.ts +++ b/libs/common/src/contracts/contract.loader.ts @@ -1,21 +1,25 @@ -import { AbiRegistry, Address, SmartContract } from '@multiversx/sdk-core'; -import { Logger } from '@nestjs/common'; +import { SmartContract, AbiRegistry, Address } from "@multiversx/sdk-core"; +import { Logger } from "@nestjs/common"; +import * as fs from "fs"; export class ContractLoader { private readonly logger: Logger; - private readonly json: any; + private readonly abiPath: string; private abiRegistry: AbiRegistry | undefined = undefined; private contract: SmartContract | undefined = undefined; - constructor(json: any) { - this.json = json; + constructor(abiPath: string) { + this.abiPath = abiPath; this.logger = new Logger(ContractLoader.name); } - private load(contractAddress: string): SmartContract { + private async load(contractAddress: string): Promise { try { - this.abiRegistry = AbiRegistry.create(this.json); + const jsonContent: string = await fs.promises.readFile(this.abiPath, { encoding: "utf8" }); + const json = JSON.parse(jsonContent); + + this.abiRegistry = AbiRegistry.create(json); return new SmartContract({ address: new Address(contractAddress), @@ -29,17 +33,17 @@ export class ContractLoader { } } - getContract(contractAddress: string): SmartContract { + async getContract(contractAddress: string): Promise { if (!this.contract) { - this.contract = this.load(contractAddress); + this.contract = await this.load(contractAddress); } return this.contract; } - getAbiRegistry(contractAddress: string): AbiRegistry { + async getAbiRegistry(contractAddress: string): Promise { if (!this.abiRegistry) { - this.load(contractAddress); + await this.load(contractAddress); } return this.abiRegistry as AbiRegistry; diff --git a/libs/common/src/contracts/contracts.module.ts b/libs/common/src/contracts/contracts.module.ts index 1cb2ada..fc535e4 100644 --- a/libs/common/src/contracts/contracts.module.ts +++ b/libs/common/src/contracts/contracts.module.ts @@ -4,7 +4,7 @@ import { ApiConfigService } from '@mvx-monorepo/common'; import { ApiNetworkProvider, ProxyNetworkProvider } from '@multiversx/sdk-network-providers/out'; import { ResultsParser } from '@multiversx/sdk-core/out'; import { ContractLoader } from '@mvx-monorepo/common/contracts/contract.loader'; -import gatewayJson from '../../../../abis/gateway.abi.json'; +import { join } from 'path'; @Module({ imports: [], @@ -43,11 +43,11 @@ import gatewayJson from '../../../../abis/gateway.abi.json'; // }, { provide: GatewayContract, - useFactory: (apiConfigService: ApiConfigService, resultsParser: ResultsParser) => { - const contractLoader = new ContractLoader(gatewayJson); + useFactory: async (apiConfigService: ApiConfigService, resultsParser: ResultsParser) => { + const contractLoader = new ContractLoader(join(__dirname, '../assets/gateway.abi.json')); - const smartContract = contractLoader.getContract(apiConfigService.getContractGateway()); - const abi = contractLoader.getAbiRegistry(apiConfigService.getContractGateway()); + const smartContract = await contractLoader.getContract(apiConfigService.getContractGateway()); + const abi = await contractLoader.getAbiRegistry(apiConfigService.getContractGateway()); return new GatewayContract(smartContract, abi, resultsParser); }, diff --git a/libs/common/src/grpc/entities/relayer.ts b/libs/common/src/grpc/entities/relayer.ts new file mode 100644 index 0000000..2289a7e --- /dev/null +++ b/libs/common/src/grpc/entities/relayer.ts @@ -0,0 +1,50 @@ +/* eslint-disable */ +import { Observable } from "rxjs"; + +export const protobufPackage = "axelar.relayer.v1beta1"; + +export interface VerifyRequest { + message: Message | undefined; +} + +export interface VerifyResponse { + message: Message | undefined; + success: boolean; +} + +export interface Message { + /** the unique identifier with which the message can be looked up on the source chain */ + id: string; + sourceChain: string; + sourceAddress: string; + destinationChain: string; + destinationAddress: string; + /** when we have a better idea of the requirement, we can add an additional optional field here to facilitate verification proofs */ + payload: Uint8Array; +} + +export interface GetPayloadRequest { + hash: Uint8Array; +} + +export interface GetPayloadResponse { + payload: Uint8Array; +} + +export interface SubscribeToApprovalsRequest { + chain: string; + /** can be used to replay events */ + startHeight?: number | undefined; +} + +export interface SubscribeToApprovalsResponse { + chain: string; + executeData: Uint8Array; + blockHeight: number; +} + +export interface Relayer { + verify(request: Observable): Observable; + getPayload(request: GetPayloadRequest): Promise; + subscribeToApprovals(request: SubscribeToApprovalsRequest): Observable; +} diff --git a/libs/common/src/grpc/grpc.module.ts b/libs/common/src/grpc/grpc.module.ts new file mode 100644 index 0000000..3590090 --- /dev/null +++ b/libs/common/src/grpc/grpc.module.ts @@ -0,0 +1,32 @@ +import { Module } from '@nestjs/common'; +import { ClientsModule, Transport } from '@nestjs/microservices'; +import { ApiConfigModule, ApiConfigService } from '@mvx-monorepo/common'; +import { join } from 'path'; +import { PROVIDER_KEYS } from '@mvx-monorepo/common/utils/provider.enum'; +import { GrpcService } from '@mvx-monorepo/common/grpc/grpc.service'; +import { protobufPackage } from '@mvx-monorepo/common/grpc/entities/relayer'; + +@Module({ + imports: [ + ClientsModule.registerAsync([ + { + name: PROVIDER_KEYS.AXELAR_GRPC_CLIENT, + imports: [ApiConfigModule], + useFactory: (apiConfigService: ApiConfigService) => { + return { + transport: Transport.GRPC, + options: { + package: protobufPackage, + protoPath: join(__dirname, '../assets/relayer.proto'), + url: apiConfigService.getAxelarApiUrl(), + }, + }; + }, + inject: [ApiConfigService], + }, + ]), + ], + providers: [GrpcService], + exports: [GrpcService], +}) +export class GrpcModule {} diff --git a/libs/common/src/grpc/grpc.service.ts b/libs/common/src/grpc/grpc.service.ts new file mode 100644 index 0000000..4f1debd --- /dev/null +++ b/libs/common/src/grpc/grpc.service.ts @@ -0,0 +1,40 @@ +import { Inject, Injectable, OnModuleInit } from '@nestjs/common'; +import { PROVIDER_KEYS } from '@mvx-monorepo/common/utils/provider.enum'; +import { ClientGrpc } from '@nestjs/microservices'; +import { ContractCallEvent } from '@prisma/client'; +import { Relayer, VerifyRequest } from '@mvx-monorepo/common/grpc/entities/relayer'; +import { firstValueFrom, ReplaySubject } from 'rxjs'; + +const RELAYER_SERVICE = 'Relayer'; + +@Injectable() +export class GrpcService implements OnModuleInit { + // @ts-ignore + private relayerService: Relayer; + + constructor(@Inject(PROVIDER_KEYS.AXELAR_GRPC_CLIENT) private readonly client: ClientGrpc) {} + + onModuleInit() { + this.relayerService = this.client.getService(RELAYER_SERVICE); + } + + async verify(contractCallEvent: ContractCallEvent) { + const replaySubject = new ReplaySubject(); + + replaySubject.next({ + message: { + id: contractCallEvent.id, + sourceChain: contractCallEvent.sourceChain, + sourceAddress: contractCallEvent.sourceAddress, + destinationChain: contractCallEvent.destinationChain, + destinationAddress: contractCallEvent.destinationAddress, + payload: contractCallEvent.payload, + }, + }); + replaySubject.complete(); + + // TODO: Check if this works correctly + const result = this.relayerService.verify(replaySubject); + await firstValueFrom(result); + } +} diff --git a/libs/common/src/utils/provider.enum.ts b/libs/common/src/utils/provider.enum.ts new file mode 100644 index 0000000..fd750e9 --- /dev/null +++ b/libs/common/src/utils/provider.enum.ts @@ -0,0 +1,3 @@ +export enum PROVIDER_KEYS { + AXELAR_GRPC_CLIENT = 'AXELAR_GRPC_CLIENT', +} diff --git a/nest-cli.json b/nest-cli.json index ff8e56c..bab760d 100644 --- a/nest-cli.json +++ b/nest-cli.json @@ -7,7 +7,13 @@ "plugins": [ "@nestjs/swagger" ], - "tsConfigPath": "apps/mvx-event-processor/tsconfig.app.json" + "tsConfigPath": "apps/mvx-event-processor/tsconfig.app.json", + "assets": [ + { + "include": "../libs/common/src/assets/*.proto", + "outDir": "dist" + } + ] }, "monorepo": true, "root": "apps/mvx-event-processor", @@ -42,7 +48,8 @@ "entryFile": "index", "sourceRoot": "libs/common/src", "compilerOptions": { - "tsConfigPath": "libs/common/tsconfig.lib.json" + "tsConfigPath": "libs/common/tsconfig.lib.json", + "assets": ["assets/*.proto", "assets/*.abi.json"] } } } diff --git a/package-lock.json b/package-lock.json index 6f1ccf3..f3b49e1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -65,6 +65,7 @@ "ts-jest": "29.0.5", "ts-loader": "9.4.2", "ts-node": "10.7.0", + "ts-proto": "^1.165.1", "tsconfig-paths": "^4.2.0", "typescript": "^5.3.2" } @@ -4271,6 +4272,18 @@ } ] }, + "node_modules/case-anything": { + "version": "2.1.13", + "resolved": "https://registry.npmjs.org/case-anything/-/case-anything-2.1.13.tgz", + "integrity": "sha512-zlOQ80VrQ2Ue+ymH5OuM/DlDq64mEm+B9UTdHULv5osUMD6HalNTblf2b1u/m6QecjsnOkBpqVZ+XPwIVsy7Ng==", + "dev": true, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -5005,6 +5018,27 @@ "node": ">=12" } }, + "node_modules/dprint-node": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/dprint-node/-/dprint-node-1.0.8.tgz", + "integrity": "sha512-iVKnUtYfGrYcW1ZAlfR/F59cUVL8QIhWoBJoSjkkdua/dkWIgjZfiLMeTjiB06X0ZLkQ0M2C1VbUj/CxkIf1zg==", + "dev": true, + "dependencies": { + "detect-libc": "^1.0.3" + } + }, + "node_modules/dprint-node/node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "dev": true, + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -10011,6 +10045,40 @@ } } }, + "node_modules/ts-poet": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/ts-poet/-/ts-poet-6.6.0.tgz", + "integrity": "sha512-4vEH/wkhcjRPFOdBwIh9ItO6jOoumVLRF4aABDX5JSNEubSqwOulihxQPqai+OkuygJm3WYMInxXQX4QwVNMuw==", + "dev": true, + "dependencies": { + "dprint-node": "^1.0.7" + } + }, + "node_modules/ts-proto": { + "version": "1.165.1", + "resolved": "https://registry.npmjs.org/ts-proto/-/ts-proto-1.165.1.tgz", + "integrity": "sha512-tn/sj9i31Q4d3/HtN2PFMU/OQwrBYP2cfhYo75cPpO2ks7unFxf1/oMdIt/2woCcOwRclxruGCrs7Ljdl9BPkw==", + "dev": true, + "dependencies": { + "case-anything": "^2.1.13", + "protobufjs": "^7.2.4", + "ts-poet": "^6.5.0", + "ts-proto-descriptors": "1.15.0" + }, + "bin": { + "protoc-gen-ts_proto": "protoc-gen-ts_proto" + } + }, + "node_modules/ts-proto-descriptors": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/ts-proto-descriptors/-/ts-proto-descriptors-1.15.0.tgz", + "integrity": "sha512-TYyJ7+H+7Jsqawdv+mfsEpZPTIj9siDHS6EMCzG/z3b/PZiphsX+mWtqFfFVe5/N0Th6V3elK9lQqjnrgTOfrg==", + "dev": true, + "dependencies": { + "long": "^5.2.3", + "protobufjs": "^7.2.4" + } + }, "node_modules/tsconfig-paths": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", @@ -13878,6 +13946,12 @@ "integrity": "sha512-xrE//a3O7TP0vaJ8ikzkD2c2NgcVUvsEe2IvFTntV4Yd1Z9FVzh+gW+enX96L0psrbaFMcVcH2l90xNuGDWc8w==", "dev": true }, + "case-anything": { + "version": "2.1.13", + "resolved": "https://registry.npmjs.org/case-anything/-/case-anything-2.1.13.tgz", + "integrity": "sha512-zlOQ80VrQ2Ue+ymH5OuM/DlDq64mEm+B9UTdHULv5osUMD6HalNTblf2b1u/m6QecjsnOkBpqVZ+XPwIVsy7Ng==", + "dev": true + }, "chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -14444,6 +14518,23 @@ "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz", "integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==" }, + "dprint-node": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/dprint-node/-/dprint-node-1.0.8.tgz", + "integrity": "sha512-iVKnUtYfGrYcW1ZAlfR/F59cUVL8QIhWoBJoSjkkdua/dkWIgjZfiLMeTjiB06X0ZLkQ0M2C1VbUj/CxkIf1zg==", + "dev": true, + "requires": { + "detect-libc": "^1.0.3" + }, + "dependencies": { + "detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "dev": true + } + } + }, "eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -18184,6 +18275,37 @@ "yn": "3.1.1" } }, + "ts-poet": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/ts-poet/-/ts-poet-6.6.0.tgz", + "integrity": "sha512-4vEH/wkhcjRPFOdBwIh9ItO6jOoumVLRF4aABDX5JSNEubSqwOulihxQPqai+OkuygJm3WYMInxXQX4QwVNMuw==", + "dev": true, + "requires": { + "dprint-node": "^1.0.7" + } + }, + "ts-proto": { + "version": "1.165.1", + "resolved": "https://registry.npmjs.org/ts-proto/-/ts-proto-1.165.1.tgz", + "integrity": "sha512-tn/sj9i31Q4d3/HtN2PFMU/OQwrBYP2cfhYo75cPpO2ks7unFxf1/oMdIt/2woCcOwRclxruGCrs7Ljdl9BPkw==", + "dev": true, + "requires": { + "case-anything": "^2.1.13", + "protobufjs": "^7.2.4", + "ts-poet": "^6.5.0", + "ts-proto-descriptors": "1.15.0" + } + }, + "ts-proto-descriptors": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/ts-proto-descriptors/-/ts-proto-descriptors-1.15.0.tgz", + "integrity": "sha512-TYyJ7+H+7Jsqawdv+mfsEpZPTIj9siDHS6EMCzG/z3b/PZiphsX+mWtqFfFVe5/N0Th6V3elK9lQqjnrgTOfrg==", + "dev": true, + "requires": { + "long": "^5.2.3", + "protobufjs": "^7.2.4" + } + }, "tsconfig-paths": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", diff --git a/package.json b/package.json index 4bdf817..2034482 100644 --- a/package.json +++ b/package.json @@ -10,12 +10,12 @@ "build": "nest build", "build:all": "nest build mvx-event-processor && nest build axelar-event-processor && nest build common", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", - "start": "nest build mvx-event-processor & nest start", - "start:watch": "nest build mvx-event-processor & nest start --watch", - "start:debug": "nest build mvx-event-processor & nest start --watch --debug", - "start:axelar-event-processor": "nest build axelar-event-processor & nest start axelar-event-processor", - "start:axelar-event-processor:watch": "nest build axelar-event-processor & nest start axelar-event-processor --watch", - "start:axelar-event-processor:debug": "nest build axelar-event-processor & nest start axelar-event-processor --watch --debug", + "start": "nest build common && nest build mvx-event-processor & nest start", + "start:watch": "nest build common && nest build mvx-event-processor & nest start --watch", + "start:debug": "nest build common && nest build mvx-event-processor & nest start --watch --debug", + "start:axelar-event-processor": "nest build common && nest build axelar-event-processor & nest start axelar-event-processor", + "start:axelar-event-processor:watch": "nest build common && nest build axelar-event-processor & nest start axelar-event-processor --watch", + "start:axelar-event-processor:debug": "nest build common && nest build axelar-event-processor & nest start axelar-event-processor --watch --debug", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\"", "lint:fix": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "test": "jest", @@ -24,7 +24,8 @@ "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", "test:e2e": "jest --config ./apps/mvx-event-processor/test/jest-e2e.json", "migrate": "prisma migrate dev", - "generate": "prisma generate" + "generate": "prisma generate", + "buf": "cd libs/common/src/assets && npx buf generate" }, "dependencies": { "@grpc/grpc-js": "^1.9.12", @@ -83,6 +84,7 @@ "ts-jest": "29.0.5", "ts-loader": "9.4.2", "ts-node": "10.7.0", + "ts-proto": "^1.165.1", "tsconfig-paths": "^4.2.0", "typescript": "^5.3.2" }, From 9613ddf2b72703e4d9ae8ecb8eb408f29c395b47 Mon Sep 17 00:00:00 2001 From: Rares <6453351+raresserban@users.noreply.github.com> Date: Thu, 7 Dec 2023 17:38:02 +0200 Subject: [PATCH 04/33] Add processing of gas service events and saving to database. --- .env.example | 1 + .../event.processor.service.ts | 17 +- .../src/processors/contract-call.processor.ts | 5 +- .../entities/processor.interface.ts | 5 + .../processors/gas-service.processor.spec.ts | 18 + .../src/processors/gas-service.processor.ts | 148 ++++ .../src/processors/index.ts | 2 + .../src/processors/processors.module.ts | 5 +- libs/common/src/assets/gas-service.abi.json | 748 ++++++++++++++++++ libs/common/src/config/api.config.service.ts | 17 +- libs/common/src/contracts/contracts.module.ts | 15 +- .../contracts/entities/contract-call-event.ts | 2 +- .../contracts/entities/gas-service-events.ts | 54 ++ .../src/contracts/gas-service.contract.ts | 106 +++ libs/common/src/contracts/gateway.contract.ts | 2 +- libs/common/src/database/database.module.ts | 5 +- .../contract-call-event.repository.ts | 27 +- .../repository/gas-paid.repository.ts | 40 + libs/common/src/utils/event.enum.ts | 8 +- .../migration.sql | 44 ++ prisma/schema.prisma | 30 +- 21 files changed, 1280 insertions(+), 19 deletions(-) create mode 100644 apps/mvx-event-processor/src/processors/entities/processor.interface.ts create mode 100644 apps/mvx-event-processor/src/processors/gas-service.processor.spec.ts create mode 100644 apps/mvx-event-processor/src/processors/gas-service.processor.ts create mode 100644 libs/common/src/assets/gas-service.abi.json create mode 100644 libs/common/src/contracts/entities/gas-service-events.ts create mode 100644 libs/common/src/contracts/gas-service.contract.ts create mode 100644 libs/common/src/database/repository/gas-paid.repository.ts create mode 100644 prisma/migrations/20231207153258_add_gas_paid_and_indexes/migration.sql diff --git a/.env.example b/.env.example index 1645736..3615506 100644 --- a/.env.example +++ b/.env.example @@ -8,6 +8,7 @@ EVENTS_NOTIFIER_URL=amqp://user:password@rabbitmq:5672 EVENTS_NOTIFIER_QUEUE=queue CONTRACT_GATEWAY= +CONTRACT_GAS_SERVICE= AXELAR_API_URL= diff --git a/apps/mvx-event-processor/src/event-processor/event.processor.service.ts b/apps/mvx-event-processor/src/event-processor/event.processor.service.ts index 586cba0..7c3d379 100644 --- a/apps/mvx-event-processor/src/event-processor/event.processor.service.ts +++ b/apps/mvx-event-processor/src/event-processor/event.processor.service.ts @@ -5,16 +5,21 @@ import { RabbitSubscribe } from '@golevelup/nestjs-rabbitmq'; import { EVENTS_NOTIFIER_QUEUE } from '../../../../config/configuration'; import { EventIdentifiers, Events } from '@mvx-monorepo/common/utils/event.enum'; import { BinaryUtils } from '@multiversx/sdk-nestjs-common'; -import { ContractCallProcessor } from '../processors/contract-call.processor'; +import { ContractCallProcessor, GasServiceProcessor } from '../processors'; @Injectable() export class EventProcessorService { + private readonly contractGateway: string; + private readonly contractGasService: string; private readonly logger: Logger; constructor( - private readonly apiConfigService: ApiConfigService, private readonly contractCallProcessor: ContractCallProcessor, + private readonly gasServiceProcessor: GasServiceProcessor, + apiConfigService: ApiConfigService, ) { + this.contractGateway = apiConfigService.getContractGateway(); + this.contractGasService = apiConfigService.getContractGasService(); this.logger = new Logger(EventProcessorService.name); } @@ -43,7 +48,13 @@ export class EventProcessorService { this.logger.log('Received event from MultiversX:'); this.logger.log(JSON.stringify(event)); - if (event.address !== this.apiConfigService.getContractGateway()) { + if (event.address === this.contractGasService) { + await this.gasServiceProcessor.handleEvent(event); + + return; + } + + if (event.address !== this.contractGateway) { return; } diff --git a/apps/mvx-event-processor/src/processors/contract-call.processor.ts b/apps/mvx-event-processor/src/processors/contract-call.processor.ts index cbf85ba..489427b 100644 --- a/apps/mvx-event-processor/src/processors/contract-call.processor.ts +++ b/apps/mvx-event-processor/src/processors/contract-call.processor.ts @@ -6,9 +6,10 @@ import { ContractCallEventRepository } from '@mvx-monorepo/common/database/repos import { ApiConfigService } from '@mvx-monorepo/common'; import { ContractCallEventStatus } from '@prisma/client'; import { GrpcService } from '@mvx-monorepo/common/grpc/grpc.service'; +import { ProcessorInterface } from './entities/processor.interface'; @Injectable() -export class ContractCallProcessor { +export class ContractCallProcessor implements ProcessorInterface { private sourceChain: string; constructor( @@ -32,7 +33,7 @@ export class ContractCallProcessor { sourceChain: this.sourceChain, destinationAddress: event.destination_contract_address, destinationChain: event.destination_chain, - payloadHash: event.data.hash, + payloadHash: event.data.payload_hash, payload: event.data.payload, }); diff --git a/apps/mvx-event-processor/src/processors/entities/processor.interface.ts b/apps/mvx-event-processor/src/processors/entities/processor.interface.ts new file mode 100644 index 0000000..79fc973 --- /dev/null +++ b/apps/mvx-event-processor/src/processors/entities/processor.interface.ts @@ -0,0 +1,5 @@ +import { NotifierEvent } from '../../event-processor/types'; + +export interface ProcessorInterface { + handleEvent(rawEvent: NotifierEvent): Promise; +} diff --git a/apps/mvx-event-processor/src/processors/gas-service.processor.spec.ts b/apps/mvx-event-processor/src/processors/gas-service.processor.spec.ts new file mode 100644 index 0000000..7731d79 --- /dev/null +++ b/apps/mvx-event-processor/src/processors/gas-service.processor.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { GasServiceProcessor } from './gas-service.processor'; + +describe('GasServiceProcessor', () => { + let service: GasServiceProcessor; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [GasServiceProcessor], + }).compile(); + + service = module.get(GasServiceProcessor); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/apps/mvx-event-processor/src/processors/gas-service.processor.ts b/apps/mvx-event-processor/src/processors/gas-service.processor.ts new file mode 100644 index 0000000..5cb050c --- /dev/null +++ b/apps/mvx-event-processor/src/processors/gas-service.processor.ts @@ -0,0 +1,148 @@ +import { Injectable } from '@nestjs/common'; +import { ProcessorInterface } from './entities/processor.interface'; +import { NotifierEvent } from '../event-processor/types'; +import { BinaryUtils } from '@multiversx/sdk-nestjs-common'; +import { Events } from '@mvx-monorepo/common/utils/event.enum'; +import { GasServiceContract } from '@mvx-monorepo/common/contracts/gas-service.contract'; +import { TransactionEvent } from '@multiversx/sdk-network-providers/out'; +import { GasPaidStatus, Prisma } from '@prisma/client'; +import { ContractCallEventRepository } from '@mvx-monorepo/common/database/repository/contract-call-event.repository'; +import { GasPaidRepository } from '@mvx-monorepo/common/database/repository/gas-paid.repository'; + +@Injectable() +export class GasServiceProcessor implements ProcessorInterface { + constructor( + private readonly gasServiceContract: GasServiceContract, + private readonly contractCallEventRepository: ContractCallEventRepository, + private readonly gasPaidRepository: GasPaidRepository, + ) {} + + async handleEvent(rawEvent: NotifierEvent) { + const eventName = BinaryUtils.base64Decode(rawEvent.topics[0]); + + if (eventName === Events.GAS_PAID_FOR_CONTRACT_CALL_EVENT) { + const event = this.gasServiceContract.decodeGasPaidForContractCallEvent( + TransactionEvent.fromHttpResponse(rawEvent), + ); + + const gasPaid = { + txHash: rawEvent.txHash, + sourceAddress: event.sender.bech32(), + destinationAddress: event.destination_contract_address, + destinationChain: event.destination_chain, + payloadHash: event.data.payload_hash, + gasToken: event.data.gas_token, + gasValue: event.data.gas_fee_amount.toString(), + refundAddress: event.data.refund_address.bech32(), + status: GasPaidStatus.PENDING, + }; + + await this.handleGasPaidEvents(gasPaid); + + return; + } + + if (eventName === Events.NATIVE_GAS_PAID_FOR_CONTRACT_CALL_EVENT) { + const event = this.gasServiceContract.decodeNativeGasPaidForContractCallEvent( + TransactionEvent.fromHttpResponse(rawEvent), + ); + + const gasPaid = { + txHash: rawEvent.txHash, + sourceAddress: event.sender.bech32(), + destinationAddress: event.destination_contract_address, + destinationChain: event.destination_chain, + payloadHash: event.data.payload_hash, + gasToken: null, + gasValue: event.data.value.toString(), + refundAddress: event.data.refund_address.bech32(), + status: GasPaidStatus.PENDING, + }; + + await this.handleGasPaidEvents(gasPaid); + + return; + } + + if (eventName === Events.GAS_ADDED_EVENT) { + const event = this.gasServiceContract.decodeGasAddedEvent(TransactionEvent.fromHttpResponse(rawEvent)); + + await this.handleGasAddedEvents( + event.tx_hash, + event.log_index, + event.data.gas_token, + event.data.gas_fee_amount.toString(), + event.data.refund_address.bech32(), + rawEvent.txHash, + ); + + return; + } + + if (eventName === Events.NATIVE_GAS_ADDED_EVENT) { + const event = this.gasServiceContract.decodeNativeGasAddedEvent(TransactionEvent.fromHttpResponse(rawEvent)); + + await this.handleGasAddedEvents( + event.tx_hash, + event.log_index, + null, + event.data.value.toString(), + event.data.refund_address.bech32(), + rawEvent.txHash, + ); + + return; + } + + if (eventName === Events.REFUNDED_EVENT) { + const event = this.gasServiceContract.decodeRefundedEvent(TransactionEvent.fromHttpResponse(rawEvent)); + + await this.gasPaidRepository.updateRefundedValue( + event.tx_hash, + event.log_index, + event.data.token, + event.data.receiver.bech32(), + event.data.amount.toString(), + ); + } + } + + async handleGasPaidEvents(gasPaid: Prisma.GasPaidCreateInput) { + const contractCallEvent = await this.contractCallEventRepository.findWithoutGasPaid(gasPaid); + + if (contractCallEvent) { + gasPaid.ContractCallEvent = { connect: contractCallEvent }; + } + + await this.gasPaidRepository.create(gasPaid); + } + + async handleGasAddedEvents( + txHash: string, + logIndex: number, + gasToken: string | null, + gasValue: string, + refundAddress: string, + rawEventTxHash: string + ) { + const contractCallEvent = await this.contractCallEventRepository.findPending(txHash, logIndex); + + if (!contractCallEvent) { + return; + } + + const gasPaid = { + txHash: rawEventTxHash, + sourceAddress: contractCallEvent.sourceAddress, + destinationAddress: contractCallEvent.destinationAddress, + destinationChain: contractCallEvent.destinationChain, + payloadHash: contractCallEvent.payloadHash, + gasToken, + gasValue, + refundAddress, + status: GasPaidStatus.PENDING, + }; + + await this.gasPaidRepository.create(gasPaid); + } +} diff --git a/apps/mvx-event-processor/src/processors/index.ts b/apps/mvx-event-processor/src/processors/index.ts index 88d706e..9293663 100644 --- a/apps/mvx-event-processor/src/processors/index.ts +++ b/apps/mvx-event-processor/src/processors/index.ts @@ -1 +1,3 @@ export * from './processors.module'; +export * from './contract-call.processor'; +export * from './gas-service.processor'; diff --git a/apps/mvx-event-processor/src/processors/processors.module.ts b/apps/mvx-event-processor/src/processors/processors.module.ts index 36c1ba7..e22c0cf 100644 --- a/apps/mvx-event-processor/src/processors/processors.module.ts +++ b/apps/mvx-event-processor/src/processors/processors.module.ts @@ -3,10 +3,11 @@ import { ContractCallProcessor } from './contract-call.processor'; import { ContractsModule } from '@mvx-monorepo/common/contracts/contracts.module'; import { DatabaseModule } from '@mvx-monorepo/common'; import { GrpcModule } from '@mvx-monorepo/common/grpc/grpc.module'; +import { GasServiceProcessor } from './gas-service.processor'; @Module({ imports: [ContractsModule, DatabaseModule, GrpcModule], - providers: [ContractCallProcessor], - exports: [ContractCallProcessor], + providers: [ContractCallProcessor, GasServiceProcessor], + exports: [ContractCallProcessor, GasServiceProcessor], }) export class ProcessorsModule {} diff --git a/libs/common/src/assets/gas-service.abi.json b/libs/common/src/assets/gas-service.abi.json new file mode 100644 index 0000000..f26dd93 --- /dev/null +++ b/libs/common/src/assets/gas-service.abi.json @@ -0,0 +1,748 @@ +{ + "buildInfo": { + "rustc": { + "version": "1.71.0-nightly", + "commitHash": "a2b1646c597329d0a25efa3889b66650f65de1de", + "commitDate": "2023-05-25", + "channel": "Nightly", + "short": "rustc 1.71.0-nightly (a2b1646c5 2023-05-25)" + }, + "contractCrate": { + "name": "gas-service", + "version": "0.0.0" + }, + "framework": { + "name": "multiversx-sc", + "version": "0.43.5" + } + }, + "name": "GasService", + "constructor": { + "inputs": [ + { + "name": "gas_collector", + "type": "Address" + } + ], + "outputs": [] + }, + "endpoints": [ + { + "name": "payGasForContractCall", + "mutability": "mutable", + "payableInTokens": [ + "*" + ], + "inputs": [ + { + "name": "destination_chain", + "type": "bytes" + }, + { + "name": "destination_address", + "type": "bytes" + }, + { + "name": "payload", + "type": "bytes" + }, + { + "name": "refund_address", + "type": "Address" + } + ], + "outputs": [] + }, + { + "name": "payGasForContractCallWithToken", + "mutability": "mutable", + "payableInTokens": [ + "*" + ], + "inputs": [ + { + "name": "destination_chain", + "type": "bytes" + }, + { + "name": "destination_address", + "type": "bytes" + }, + { + "name": "payload", + "type": "bytes" + }, + { + "name": "symbol", + "type": "bytes" + }, + { + "name": "amount", + "type": "BigUint" + }, + { + "name": "refund_address", + "type": "Address" + } + ], + "outputs": [] + }, + { + "name": "payNativeGasForContractCall", + "mutability": "mutable", + "payableInTokens": [ + "EGLD" + ], + "inputs": [ + { + "name": "destination_chain", + "type": "bytes" + }, + { + "name": "destination_address", + "type": "bytes" + }, + { + "name": "payload", + "type": "bytes" + }, + { + "name": "refund_address", + "type": "Address" + } + ], + "outputs": [] + }, + { + "name": "payNativeGasForContractCallWithToken", + "mutability": "mutable", + "payableInTokens": [ + "EGLD" + ], + "inputs": [ + { + "name": "destination_chain", + "type": "bytes" + }, + { + "name": "destination_address", + "type": "bytes" + }, + { + "name": "payload", + "type": "bytes" + }, + { + "name": "symbol", + "type": "bytes" + }, + { + "name": "amount", + "type": "BigUint" + }, + { + "name": "refund_address", + "type": "Address" + } + ], + "outputs": [] + }, + { + "name": "payGasForExpressCallWithToken", + "mutability": "mutable", + "payableInTokens": [ + "*" + ], + "inputs": [ + { + "name": "destination_chain", + "type": "bytes" + }, + { + "name": "destination_address", + "type": "bytes" + }, + { + "name": "payload", + "type": "bytes" + }, + { + "name": "symbol", + "type": "bytes" + }, + { + "name": "amount", + "type": "BigUint" + }, + { + "name": "refund_address", + "type": "Address" + } + ], + "outputs": [] + }, + { + "name": "payNativeGasForExpressCallWithToken", + "mutability": "mutable", + "payableInTokens": [ + "EGLD" + ], + "inputs": [ + { + "name": "destination_chain", + "type": "bytes" + }, + { + "name": "destination_address", + "type": "bytes" + }, + { + "name": "payload", + "type": "bytes" + }, + { + "name": "symbol", + "type": "bytes" + }, + { + "name": "amount", + "type": "BigUint" + }, + { + "name": "refund_address", + "type": "Address" + } + ], + "outputs": [] + }, + { + "name": "addGas", + "mutability": "mutable", + "payableInTokens": [ + "*" + ], + "inputs": [ + { + "name": "tx_hash", + "type": "bytes" + }, + { + "name": "log_index", + "type": "BigUint" + }, + { + "name": "refund_address", + "type": "Address" + } + ], + "outputs": [] + }, + { + "name": "addNativeGas", + "mutability": "mutable", + "payableInTokens": [ + "EGLD" + ], + "inputs": [ + { + "name": "tx_hash", + "type": "bytes" + }, + { + "name": "log_index", + "type": "BigUint" + }, + { + "name": "refund_address", + "type": "Address" + } + ], + "outputs": [] + }, + { + "name": "addExpressGas", + "mutability": "mutable", + "payableInTokens": [ + "*" + ], + "inputs": [ + { + "name": "tx_hash", + "type": "bytes" + }, + { + "name": "log_index", + "type": "BigUint" + }, + { + "name": "refund_address", + "type": "Address" + } + ], + "outputs": [] + }, + { + "name": "addNativeExpressGas", + "mutability": "mutable", + "payableInTokens": [ + "EGLD" + ], + "inputs": [ + { + "name": "tx_hash", + "type": "bytes" + }, + { + "name": "log_index", + "type": "BigUint" + }, + { + "name": "refund_address", + "type": "Address" + } + ], + "outputs": [] + }, + { + "name": "collectFees", + "mutability": "mutable", + "inputs": [ + { + "name": "receiver", + "type": "Address" + }, + { + "name": "tokens", + "type": "counted-variadic", + "multi_arg": true + }, + { + "name": "amounts", + "type": "counted-variadic", + "multi_arg": true + } + ], + "outputs": [] + }, + { + "name": "refund", + "mutability": "mutable", + "inputs": [ + { + "name": "tx_hash", + "type": "bytes" + }, + { + "name": "log_index", + "type": "BigUint" + }, + { + "name": "receiver", + "type": "Address" + }, + { + "name": "token", + "type": "EgldOrEsdtTokenIdentifier" + }, + { + "name": "amount", + "type": "BigUint" + } + ], + "outputs": [] + }, + { + "name": "gas_collector", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "Address" + } + ] + } + ], + "events": [ + { + "identifier": "gas_paid_for_contract_call_event", + "inputs": [ + { + "name": "sender", + "type": "Address", + "indexed": true + }, + { + "name": "destination_chain", + "type": "bytes", + "indexed": true + }, + { + "name": "destination_contract_address", + "type": "bytes", + "indexed": true + }, + { + "name": "data", + "type": "GasPaidForContractCallData" + } + ] + }, + { + "identifier": "gas_paid_for_contract_call_with_token_event", + "inputs": [ + { + "name": "sender", + "type": "Address", + "indexed": true + }, + { + "name": "destination_chain", + "type": "bytes", + "indexed": true + }, + { + "name": "destination_contract_address", + "type": "bytes", + "indexed": true + }, + { + "name": "data", + "type": "GasPaidForContractCallWithTokenData" + } + ] + }, + { + "identifier": "native_gas_paid_for_contract_call_event", + "inputs": [ + { + "name": "sender", + "type": "Address", + "indexed": true + }, + { + "name": "destination_chain", + "type": "bytes", + "indexed": true + }, + { + "name": "destination_contract_address", + "type": "bytes", + "indexed": true + }, + { + "name": "data", + "type": "NativeGasPaidForContractCallData" + } + ] + }, + { + "identifier": "native_gas_paid_for_contract_call_with_token_event", + "inputs": [ + { + "name": "sender", + "type": "Address", + "indexed": true + }, + { + "name": "destination_chain", + "type": "bytes", + "indexed": true + }, + { + "name": "destination_contract_address", + "type": "bytes", + "indexed": true + }, + { + "name": "data", + "type": "NativeGasPaidForContractCallWithTokenData" + } + ] + }, + { + "identifier": "gas_paid_for_express_call_with_token_event", + "inputs": [ + { + "name": "sender", + "type": "Address", + "indexed": true + }, + { + "name": "destination_chain", + "type": "bytes", + "indexed": true + }, + { + "name": "destination_contract_address", + "type": "bytes", + "indexed": true + }, + { + "name": "data", + "type": "GasPaidForContractCallWithTokenData" + } + ] + }, + { + "identifier": "native_gas_paid_for_express_call_with_token_event", + "inputs": [ + { + "name": "sender", + "type": "Address", + "indexed": true + }, + { + "name": "destination_chain", + "type": "bytes", + "indexed": true + }, + { + "name": "destination_contract_address", + "type": "bytes", + "indexed": true + }, + { + "name": "data", + "type": "NativeGasPaidForContractCallWithTokenData" + } + ] + }, + { + "identifier": "gas_added_event", + "inputs": [ + { + "name": "tx_hash", + "type": "bytes", + "indexed": true + }, + { + "name": "log_index", + "type": "BigUint", + "indexed": true + }, + { + "name": "data", + "type": "AddGasData" + } + ] + }, + { + "identifier": "native_gas_added_event", + "inputs": [ + { + "name": "tx_hash", + "type": "bytes", + "indexed": true + }, + { + "name": "log_index", + "type": "BigUint", + "indexed": true + }, + { + "name": "data", + "type": "AddNativeGasData" + } + ] + }, + { + "identifier": "express_gas_added_event", + "inputs": [ + { + "name": "tx_hash", + "type": "bytes", + "indexed": true + }, + { + "name": "log_index", + "type": "BigUint", + "indexed": true + }, + { + "name": "data", + "type": "AddGasData" + } + ] + }, + { + "identifier": "native_express_gas_added_event", + "inputs": [ + { + "name": "tx_hash", + "type": "bytes", + "indexed": true + }, + { + "name": "log_index", + "type": "BigUint", + "indexed": true + }, + { + "name": "data", + "type": "AddNativeGasData" + } + ] + }, + { + "identifier": "refunded_event", + "inputs": [ + { + "name": "tx_hash", + "type": "bytes", + "indexed": true + }, + { + "name": "log_index", + "type": "BigUint", + "indexed": true + }, + { + "name": "data", + "type": "RefundedData" + } + ] + } + ], + "hasCallback": false, + "types": { + "AddGasData": { + "type": "struct", + "fields": [ + { + "name": "gas_token", + "type": "TokenIdentifier" + }, + { + "name": "gas_fee_amount", + "type": "BigUint" + }, + { + "name": "refund_address", + "type": "Address" + } + ] + }, + "AddNativeGasData": { + "type": "struct", + "fields": [ + { + "name": "value", + "type": "BigUint" + }, + { + "name": "refund_address", + "type": "Address" + } + ] + }, + "GasPaidForContractCallData": { + "type": "struct", + "fields": [ + { + "name": "hash", + "type": "array32" + }, + { + "name": "gas_token", + "type": "TokenIdentifier" + }, + { + "name": "gas_fee_amount", + "type": "BigUint" + }, + { + "name": "refund_address", + "type": "Address" + } + ] + }, + "GasPaidForContractCallWithTokenData": { + "type": "struct", + "fields": [ + { + "name": "hash", + "type": "array32" + }, + { + "name": "symbol", + "type": "bytes" + }, + { + "name": "amount", + "type": "BigUint" + }, + { + "name": "gas_token", + "type": "TokenIdentifier" + }, + { + "name": "gas_fee_amount", + "type": "BigUint" + }, + { + "name": "refund_address", + "type": "Address" + } + ] + }, + "NativeGasPaidForContractCallData": { + "type": "struct", + "fields": [ + { + "name": "hash", + "type": "array32" + }, + { + "name": "value", + "type": "BigUint" + }, + { + "name": "refund_address", + "type": "Address" + } + ] + }, + "NativeGasPaidForContractCallWithTokenData": { + "type": "struct", + "fields": [ + { + "name": "hash", + "type": "array32" + }, + { + "name": "symbol", + "type": "bytes" + }, + { + "name": "amount", + "type": "BigUint" + }, + { + "name": "value", + "type": "BigUint" + }, + { + "name": "refund_address", + "type": "Address" + } + ] + }, + "RefundedData": { + "type": "struct", + "fields": [ + { + "name": "receiver", + "type": "Address" + }, + { + "name": "token", + "type": "EgldOrEsdtTokenIdentifier" + }, + { + "name": "amount", + "type": "BigUint" + } + ] + } + } +} diff --git a/libs/common/src/config/api.config.service.ts b/libs/common/src/config/api.config.service.ts index 4a0d3dd..0cb0242 100644 --- a/libs/common/src/config/api.config.service.ts +++ b/libs/common/src/config/api.config.service.ts @@ -58,12 +58,21 @@ export class ApiConfigService { } getContractGateway(): string { - const eventsNotifierGatewayAddress = this.configService.get('CONTRACT_GATEWAY'); - if (!eventsNotifierGatewayAddress) { - throw new Error('No Events Notifier Gateway Address present'); + const contractGateway = this.configService.get('CONTRACT_GATEWAY'); + if (!contractGateway) { + throw new Error('No Contract Gateway present'); } - return eventsNotifierGatewayAddress; + return contractGateway; + } + + getContractGasService(): string { + const contractGasService = this.configService.get('CONTRACT_GAS_SERVICE'); + if (!contractGasService) { + throw new Error('No Contract Gas Service present'); + } + + return contractGasService; } getAxelarApiUrl(): string { diff --git a/libs/common/src/contracts/contracts.module.ts b/libs/common/src/contracts/contracts.module.ts index fc535e4..ada9dc8 100644 --- a/libs/common/src/contracts/contracts.module.ts +++ b/libs/common/src/contracts/contracts.module.ts @@ -5,6 +5,7 @@ import { ApiNetworkProvider, ProxyNetworkProvider } from '@multiversx/sdk-networ import { ResultsParser } from '@multiversx/sdk-core/out'; import { ContractLoader } from '@mvx-monorepo/common/contracts/contract.loader'; import { join } from 'path'; +import { GasServiceContract } from '@mvx-monorepo/common/contracts/gas-service.contract'; @Module({ imports: [], @@ -53,7 +54,19 @@ import { join } from 'path'; }, inject: [ApiConfigService, ResultsParser], }, + { + provide: GasServiceContract, + useFactory: async (apiConfigService: ApiConfigService, resultsParser: ResultsParser) => { + const contractLoader = new ContractLoader(join(__dirname, '../assets/gas-service.abi.json')); + + const smartContract = await contractLoader.getContract(apiConfigService.getContractGasService()); + const abi = await contractLoader.getAbiRegistry(apiConfigService.getContractGasService()); + + return new GasServiceContract(smartContract, abi, resultsParser); + }, + inject: [ApiConfigService, ResultsParser], + }, ], - exports: [GatewayContract], + exports: [GatewayContract, GasServiceContract], }) export class ContractsModule {} diff --git a/libs/common/src/contracts/entities/contract-call-event.ts b/libs/common/src/contracts/entities/contract-call-event.ts index e9d63cf..dd82f4b 100644 --- a/libs/common/src/contracts/entities/contract-call-event.ts +++ b/libs/common/src/contracts/entities/contract-call-event.ts @@ -5,7 +5,7 @@ export interface ContractCallEvent { destination_chain: string, destination_contract_address: string, data: { - hash: string, + payload_hash: string, payload: Buffer, } } diff --git a/libs/common/src/contracts/entities/gas-service-events.ts b/libs/common/src/contracts/entities/gas-service-events.ts new file mode 100644 index 0000000..ff25755 --- /dev/null +++ b/libs/common/src/contracts/entities/gas-service-events.ts @@ -0,0 +1,54 @@ +import { IAddress } from '@multiversx/sdk-core/out'; +import BigNumber from 'bignumber.js'; + +export interface GasPaidForContractCallEvent { + sender: IAddress, + destination_chain: string, + destination_contract_address: string, + data: { + payload_hash: string, + gas_token: string, + gas_fee_amount: BigNumber, + refund_address: IAddress, + } +} + +export interface NativeGasPaidForContractCallEvent { + sender: IAddress, + destination_chain: string, + destination_contract_address: string, + data: { + payload_hash: string, + value: BigNumber, + refund_address: IAddress, + } +} + +export interface GasAddedEvent { + tx_hash: string, + log_index: number, + data: { + gas_token: string, + gas_fee_amount: BigNumber, + refund_address: IAddress, + } +} + +export interface NativeGasAddedEvent { + tx_hash: string, + log_index: number, + data: { + value: BigNumber, + refund_address: IAddress, + } +} + +export interface RefundedEvent { + tx_hash: string, + log_index: number, + data: { + receiver: IAddress, + token: string | null, + amount: BigNumber, + } +} diff --git a/libs/common/src/contracts/gas-service.contract.ts b/libs/common/src/contracts/gas-service.contract.ts new file mode 100644 index 0000000..22cc48b --- /dev/null +++ b/libs/common/src/contracts/gas-service.contract.ts @@ -0,0 +1,106 @@ +import { AbiRegistry, ResultsParser, SmartContract } from '@multiversx/sdk-core/out'; +import { Injectable, Logger } from '@nestjs/common'; +import { Events } from '../utils/event.enum'; +import { TransactionEvent } from '@multiversx/sdk-network-providers/out'; +import BigNumber from 'bignumber.js'; +import { + GasAddedEvent, + GasPaidForContractCallEvent, + NativeGasAddedEvent, + NativeGasPaidForContractCallEvent, + RefundedEvent, +} from '@mvx-monorepo/common/contracts/entities/gas-service-events'; + +@Injectable() +export class GasServiceContract { + // @ts-ignore + private readonly logger: Logger; + + constructor( + // @ts-ignore + private readonly smartContract: SmartContract, + private readonly abi: AbiRegistry, + private readonly resultsParser: ResultsParser, + ) { + this.logger = new Logger(GasServiceContract.name); + } + + decodeGasPaidForContractCallEvent(event: TransactionEvent): GasPaidForContractCallEvent { + const eventDefinition = this.abi.getEvent(Events.GAS_PAID_FOR_CONTRACT_CALL_EVENT); + const outcome = this.resultsParser.parseEvent(event, eventDefinition); + + return { + sender: outcome.sender, + destination_chain: outcome.destination_chain.toString(), + destination_contract_address: outcome.destination_contract_address.toString(), + data: { + payload_hash: Buffer.from(outcome.data.hash.map((number: BigNumber) => number.toNumber())).toString('hex'), + gas_token: outcome.data.gas_token.toString(), + gas_fee_amount: outcome.data.gas_fee_amount, + refund_address: outcome.data.refund_address, + }, + }; + } + + decodeNativeGasPaidForContractCallEvent(event: TransactionEvent): NativeGasPaidForContractCallEvent { + const eventDefinition = this.abi.getEvent(Events.GAS_PAID_FOR_CONTRACT_CALL_EVENT); + const outcome = this.resultsParser.parseEvent(event, eventDefinition); + + return { + sender: outcome.sender, + destination_chain: outcome.destination_chain.toString(), + destination_contract_address: outcome.destination_contract_address.toString(), + data: { + payload_hash: Buffer.from(outcome.data.hash.map((number: BigNumber) => number.toNumber())).toString('hex'), + value: outcome.data.value, + refund_address: outcome.data.refund_address, + }, + }; + } + + decodeGasAddedEvent(event: TransactionEvent): GasAddedEvent { + const eventDefinition = this.abi.getEvent(Events.GAS_ADDED_EVENT); + const outcome = this.resultsParser.parseEvent(event, eventDefinition); + + return { + tx_hash: outcome.tx_hash.toString('hex'), + log_index: outcome.log_index.toNumber(), + data: { + gas_token: outcome.data.gas_token.toString(), + gas_fee_amount: outcome.data.value, + refund_address: outcome.data.refund_address, + }, + }; + } + + decodeNativeGasAddedEvent(event: TransactionEvent): NativeGasAddedEvent { + const eventDefinition = this.abi.getEvent(Events.NATIVE_GAS_ADDED_EVENT); + const outcome = this.resultsParser.parseEvent(event, eventDefinition); + + return { + tx_hash: outcome.tx_hash.toString('hex'), + log_index: outcome.log_index.toNumber(), + data: { + value: outcome.data.value, + refund_address: outcome.data.refund_address, + }, + }; + } + + decodeRefundedEvent(event: TransactionEvent): RefundedEvent { + const eventDefinition = this.abi.getEvent(Events.REFUNDED_EVENT); + const outcome = this.resultsParser.parseEvent(event, eventDefinition); + + const token = outcome.data.gas_token.toString(); + + return { + tx_hash: outcome.tx_hash.toString('hex'), + log_index: outcome.log_index.toNumber(), + data: { + receiver: outcome.data.receiver, + token: token === 'EGLD' ? null : token, // TODO: Save 'EGLD' to a const + amount: outcome.data.amount, + }, + }; + } +} diff --git a/libs/common/src/contracts/gateway.contract.ts b/libs/common/src/contracts/gateway.contract.ts index 2e88c09..bec8d14 100644 --- a/libs/common/src/contracts/gateway.contract.ts +++ b/libs/common/src/contracts/gateway.contract.ts @@ -28,7 +28,7 @@ export class GatewayContract { destination_chain: outcome.destination_chain.toString(), destination_contract_address: outcome.destination_contract_address.toString(), data: { - hash: Buffer.from(outcome.data.hash.map((number: BigNumber) => number.toNumber())).toString('hex'), + payload_hash: Buffer.from(outcome.data.hash.map((number: BigNumber) => number.toNumber())).toString('hex'), payload: outcome.data.payload, }, }; diff --git a/libs/common/src/database/database.module.ts b/libs/common/src/database/database.module.ts index 0d60c1e..40802f1 100644 --- a/libs/common/src/database/database.module.ts +++ b/libs/common/src/database/database.module.ts @@ -1,9 +1,10 @@ import { Module } from '@nestjs/common'; import { PrismaService } from '@mvx-monorepo/common/database/prisma.service'; import { ContractCallEventRepository } from '@mvx-monorepo/common/database/repository/contract-call-event.repository'; +import { GasPaidRepository } from '@mvx-monorepo/common/database/repository/gas-paid.repository'; @Module({ - providers: [PrismaService, ContractCallEventRepository], - exports: [ContractCallEventRepository], + providers: [PrismaService, ContractCallEventRepository, GasPaidRepository], + exports: [ContractCallEventRepository, GasPaidRepository], }) export class DatabaseModule {} diff --git a/libs/common/src/database/repository/contract-call-event.repository.ts b/libs/common/src/database/repository/contract-call-event.repository.ts index 7643266..09d74d6 100644 --- a/libs/common/src/database/repository/contract-call-event.repository.ts +++ b/libs/common/src/database/repository/contract-call-event.repository.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { PrismaService } from '@mvx-monorepo/common/database/prisma.service'; -import { ContractCallEvent, Prisma } from '@prisma/client'; +import { ContractCallEvent, ContractCallEventStatus, Prisma } from '@prisma/client'; @Injectable() export class ContractCallEventRepository { @@ -11,4 +11,29 @@ export class ContractCallEventRepository { data, }); } + + findWithoutGasPaid(gasPaid: Prisma.GasPaidCreateInput): Promise { + return this.prisma.contractCallEvent.findFirst({ + where: { + status: ContractCallEventStatus.PENDING, + sourceAddress: gasPaid.sourceAddress, + destinationAddress: gasPaid.destinationAddress, + destinationChain: gasPaid.destinationChain, + payloadHash: gasPaid.payloadHash, + gasPaidEntries: { + none: {}, + }, + }, + }); + } + + findPending(txHash: string, eventIndex: number): Promise { + return this.prisma.contractCallEvent.findUnique({ + where: { + status: ContractCallEventStatus.PENDING, + txHash, + eventIndex, + }, + }); + } } diff --git a/libs/common/src/database/repository/gas-paid.repository.ts b/libs/common/src/database/repository/gas-paid.repository.ts new file mode 100644 index 0000000..57347cd --- /dev/null +++ b/libs/common/src/database/repository/gas-paid.repository.ts @@ -0,0 +1,40 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '@mvx-monorepo/common/database/prisma.service'; +import { ContractCallEventStatus, GasPaid, Prisma } from '@prisma/client'; + +@Injectable() +export class GasPaidRepository { + constructor(private readonly prisma: PrismaService) {} + + create(data: Prisma.GasPaidCreateInput): Promise { + return this.prisma.gasPaid.create({ + data, + }); + } + + updateRefundedValue( + txHash: string, + eventIndex: number, + gasToken: string | null, + refundAddress: string, + refundedValue: string, + ) { + this.prisma.gasPaid.updateMany({ + where: { + status: { + in: [ContractCallEventStatus.SUCCESS, ContractCallEventStatus.FAILED], + }, + gasToken, + refundAddress, + ContractCallEvent: { + txHash, + eventIndex, + }, + refundedValue: null, + }, + data: { + refundedValue, + }, + }); + } +} diff --git a/libs/common/src/utils/event.enum.ts b/libs/common/src/utils/event.enum.ts index d9da865..8e9540c 100644 --- a/libs/common/src/utils/event.enum.ts +++ b/libs/common/src/utils/event.enum.ts @@ -3,5 +3,11 @@ export enum EventIdentifiers { } export enum Events { - CONTRACT_CALL_EVENT = 'contract_call_event' + CONTRACT_CALL_EVENT = 'contract_call_event', + + GAS_PAID_FOR_CONTRACT_CALL_EVENT = 'gas_paid_for_contract_call_event', + NATIVE_GAS_PAID_FOR_CONTRACT_CALL_EVENT = 'native_gas_paid_for_contract_call_event', + GAS_ADDED_EVENT = 'gas_added_event', + NATIVE_GAS_ADDED_EVENT = 'native_gas_added_event', + REFUNDED_EVENT = 'refunded_event', } diff --git a/prisma/migrations/20231207153258_add_gas_paid_and_indexes/migration.sql b/prisma/migrations/20231207153258_add_gas_paid_and_indexes/migration.sql new file mode 100644 index 0000000..4dab0fa --- /dev/null +++ b/prisma/migrations/20231207153258_add_gas_paid_and_indexes/migration.sql @@ -0,0 +1,44 @@ +/* + Warnings: + + - A unique constraint covering the columns `[txHash]` on the table `ContractCallEvent` will be added. If there are existing duplicate values, this will fail. + +*/ +-- CreateEnum +CREATE TYPE "GasPaidStatus" AS ENUM ('PENDING', 'SUCCESS', 'FAILED'); + +-- CreateTable +CREATE TABLE "GasPaid" ( + "id" SERIAL NOT NULL, + "txHash" VARCHAR(64) NOT NULL, + "sourceAddress" VARCHAR(62) NOT NULL, + "destinationAddress" VARCHAR(255) NOT NULL, + "destinationChain" VARCHAR(255) NOT NULL, + "payloadHash" VARCHAR(64) NOT NULL, + "gasToken" VARCHAR(17), + "gasValue" VARCHAR(255) NOT NULL, + "refundAddress" VARCHAR(62) NOT NULL, + "refundedValue" VARCHAR(255), + "status" "GasPaidStatus" NOT NULL, + "contractCallEventId" VARCHAR(255), + + CONSTRAINT "GasPaid_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "GasPaid_txHash_key" ON "GasPaid"("txHash"); + +-- CreateIndex +CREATE INDEX "GasPaid_refundAddress_gasToken_idx" ON "GasPaid"("refundAddress", "gasToken"); + +-- CreateIndex +CREATE UNIQUE INDEX "ContractCallEvent_txHash_key" ON "ContractCallEvent"("txHash"); + +-- CreateIndex +CREATE INDEX "ContractCallEvent_txHash_eventIndex_idx" ON "ContractCallEvent"("txHash", "eventIndex"); + +-- CreateIndex +CREATE INDEX "ContractCallEvent_sourceAddress_payloadHash_idx" ON "ContractCallEvent"("sourceAddress", "payloadHash"); + +-- AddForeignKey +ALTER TABLE "GasPaid" ADD CONSTRAINT "GasPaid_contractCallEventId_fkey" FOREIGN KEY ("contractCallEventId") REFERENCES "ContractCallEvent"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e770919..49cb6dd 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -12,7 +12,7 @@ datasource db { model ContractCallEvent { id String @id @db.VarChar(255) // should be formatted as [source_chain]:[unique identifier], i.e. Ethereum:0x74ac0205b1f8f51023942856145182f0e6fdd41ccb2c8058bf2d89fc67564d56:0 - txHash String @db.VarChar(64) + txHash String @unique @db.VarChar(64) eventIndex Int @db.SmallInt status ContractCallEventStatus sourceAddress String @db.VarChar(62) @@ -24,6 +24,10 @@ model ContractCallEvent { executeTxHash String? @db.VarChar(64) createdAt DateTime @default(now()) @db.Timestamp(6) updatedAt DateTime @default(now()) @updatedAt @db.Timestamp(6) + gasPaidEntries GasPaid[] + + @@index([txHash, eventIndex]) + @@index([sourceAddress, payloadHash]) } enum ContractCallEventStatus { @@ -32,3 +36,27 @@ enum ContractCallEventStatus { SUCCESS FAILED } + +model GasPaid { + id Int @id @default(autoincrement()) + txHash String @unique @db.VarChar(64) + sourceAddress String @db.VarChar(62) + destinationAddress String @db.VarChar(255) + destinationChain String @db.VarChar(255) + payloadHash String @db.VarChar(64) + gasToken String? @db.VarChar(17) + gasValue String @db.VarChar(255) + refundAddress String @db.VarChar(62) + refundedValue String? @db.VarChar(255) + status GasPaidStatus + ContractCallEvent ContractCallEvent? @relation(fields: [contractCallEventId], references: [id]) + contractCallEventId String? @db.VarChar(255) + + @@index([refundAddress, gasToken]) +} + +enum GasPaidStatus { + PENDING + SUCCESS + FAILED +} From 40d7192e4f13cc823804a380e6a751e97f67fa66 Mon Sep 17 00:00:00 2001 From: Rares <6453351+raresserban@users.noreply.github.com> Date: Fri, 8 Dec 2023 16:31:54 +0200 Subject: [PATCH 05/33] Improvement for gas service event handling and tests for various services. --- .../event.processor.service.spec.ts | 55 ++- .../contract-call.processor.spec.ts | 55 ++- .../processors/gas-service.processor.spec.ts | 346 +++++++++++++++++- .../src/processors/gas-service.processor.ts | 113 +++--- apps/mvx-event-processor/test/jest-e2e.json | 17 + libs/common/src/contracts/contract.loader.ts | 8 +- .../contracts/entities/gas-service-events.ts | 88 ++--- .../contracts/gas-service.contract.spec.ts | 97 +++++ .../src/contracts/gas-service.contract.ts | 52 +-- .../src/contracts/gateway.contract.spec.ts | 91 +++++ .../database/entities/contract-call-event.ts | 7 + .../contract-call-event.repository.ts | 6 +- .../repository/gas-paid.repository.ts | 13 +- package-lock.json | 36 +- package.json | 19 +- 15 files changed, 787 insertions(+), 216 deletions(-) create mode 100644 apps/mvx-event-processor/test/jest-e2e.json create mode 100644 libs/common/src/contracts/gas-service.contract.spec.ts create mode 100644 libs/common/src/contracts/gateway.contract.spec.ts create mode 100644 libs/common/src/database/entities/contract-call-event.ts diff --git a/apps/mvx-event-processor/src/event-processor/event.processor.service.spec.ts b/apps/mvx-event-processor/src/event-processor/event.processor.service.spec.ts index 72e1bcf..ee14145 100644 --- a/apps/mvx-event-processor/src/event-processor/event.processor.service.spec.ts +++ b/apps/mvx-event-processor/src/event-processor/event.processor.service.spec.ts @@ -1,37 +1,44 @@ import { EventProcessorService } from './event.processor.service'; import { ApiConfigService } from '@mvx-monorepo/common'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { ContractCallProcessor } from '../processors/contract-call.processor'; import { Test } from '@nestjs/testing'; import { NotifierBlockEvent } from './types'; import { BinaryUtils } from '@multiversx/sdk-nestjs-common'; import { Events } from '@mvx-monorepo/common/utils/event.enum'; +import { ContractCallProcessor, GasServiceProcessor } from '../processors'; describe('EventProcessorService', () => { - let apiConfigService: DeepMocked; let contractCallProcessor: DeepMocked; + let gasServiceProcessor: DeepMocked; + let apiConfigService: DeepMocked; let service: EventProcessorService; beforeEach(async () => { - apiConfigService = createMock(); contractCallProcessor = createMock(); + gasServiceProcessor = createMock(); + apiConfigService = createMock(); apiConfigService.getContractGateway.mockReturnValue('mockGatewayAddress'); + apiConfigService.getContractGasService.mockReturnValue('mockGasServiceAddress'); const moduleRef = await Test.createTestingModule({ providers: [EventProcessorService], }) .useMocker((token) => { - if (token === ApiConfigService) { - return apiConfigService; - } - if (token === ContractCallProcessor) { return contractCallProcessor; } - return undefined; + if (token === GasServiceProcessor) { + return gasServiceProcessor; + } + + if (token === ApiConfigService) { + return apiConfigService; + } + + return null; }) .compile(); @@ -39,7 +46,7 @@ describe('EventProcessorService', () => { }); describe('consumeEvents', () => { - it('Should not consume event', async () => { + it('Should not consume events', async () => { const blockEvent: NotifierBlockEvent = { hash: 'test', shardId: 1, @@ -74,11 +81,12 @@ describe('EventProcessorService', () => { await service.consumeEvents(blockEvent); - expect(apiConfigService.getContractGateway).toHaveBeenCalledTimes(3); + expect(apiConfigService.getContractGateway).toHaveBeenCalledTimes(1); expect(contractCallProcessor.handleEvent).not.toHaveBeenCalled(); + expect(gasServiceProcessor.handleEvent).not.toHaveBeenCalled(); }); - it('Should consume event', async () => { + it('Should consume gateway event', async () => { const blockEvent: NotifierBlockEvent = { hash: 'test', shardId: 1, @@ -99,6 +107,31 @@ describe('EventProcessorService', () => { expect(apiConfigService.getContractGateway).toHaveBeenCalledTimes(1); expect(contractCallProcessor.handleEvent).toHaveBeenCalledTimes(1); + expect(gasServiceProcessor.handleEvent).not.toHaveBeenCalled(); + }); + + it('Should consume gas contract event', async () => { + const blockEvent: NotifierBlockEvent = { + hash: 'test', + shardId: 1, + timestamp: 123456, + events: [ + { + txHash: 'test', + address: 'mockGasServiceAddress', + identifier: 'payGasForContractCall', + data: '', + topics: [BinaryUtils.base64Encode(Events.GAS_PAID_FOR_CONTRACT_CALL_EVENT)], + order: 0, + }, + ], + }; + + await service.consumeEvents(blockEvent); + + expect(apiConfigService.getContractGateway).toHaveBeenCalledTimes(1); + expect(gasServiceProcessor.handleEvent).toHaveBeenCalledTimes(1); + expect(contractCallProcessor.handleEvent).not.toHaveBeenCalled(); }); it('Should throw error', async () => { diff --git a/apps/mvx-event-processor/src/processors/contract-call.processor.spec.ts b/apps/mvx-event-processor/src/processors/contract-call.processor.spec.ts index 233bc50..6094037 100644 --- a/apps/mvx-event-processor/src/processors/contract-call.processor.spec.ts +++ b/apps/mvx-event-processor/src/processors/contract-call.processor.spec.ts @@ -6,57 +6,73 @@ import { Events } from '@mvx-monorepo/common/utils/event.enum'; import { ContractCallEventRepository } from '@mvx-monorepo/common/database/repository/contract-call-event.repository'; import { ContractCallProcessor } from './contract-call.processor'; import { NotifierEvent } from '../event-processor/types'; -import { ContractsModule } from '@mvx-monorepo/common/contracts/contracts.module'; import { Address } from '@multiversx/sdk-core/out'; import { ContractCallEventStatus } from '@prisma/client'; import { GrpcService } from '@mvx-monorepo/common/grpc/grpc.service'; +import { GatewayContract } from '@mvx-monorepo/common/contracts/gateway.contract'; +import { ContractCallEvent } from '@mvx-monorepo/common/contracts/entities/contract-call-event'; +import { TransactionEvent } from '@multiversx/sdk-network-providers/out'; describe('ContractCallProcessor', () => { + let gatewayContract: DeepMocked; let contractCallEventRepository: DeepMocked; - let apiConfigService: DeepMocked; let grpcService: DeepMocked; + let apiConfigService: DeepMocked; let service: ContractCallProcessor; + const event: ContractCallEvent = { + sender: Address.fromBech32('erd1qqqqqqqqqqqqqpgqzqvm5ywqqf524efwrhr039tjs29w0qltkklsa05pk7'), + destination_chain: 'ethereum', + destination_contract_address: 'destinationAddress', + data: { + payload_hash: 'ebc84cbd75ba5516bf45e7024a9e12bc3c5c880f73e3a5beca7ebba52b2867a7', + payload: Buffer.from('payload'), + }, + }; + beforeEach(async () => { + gatewayContract = createMock(); contractCallEventRepository = createMock(); - apiConfigService = createMock(); grpcService = createMock(); + apiConfigService = createMock(); apiConfigService.getSourceChainName.mockReturnValue('multiversx-test'); - apiConfigService.getContractGateway.mockReturnValue( - 'erd1qqqqqqqqqqqqqpgqsvzyz88e8v8j6x3wquatxuztnxjwnw92kkls6rdtzx', - ); const moduleRef = await Test.createTestingModule({ - imports: [ContractsModule], // it uses real GatewayContract object loaded from abi providers: [ContractCallProcessor], }) .useMocker((token) => { - if (token === ContractCallEventRepository) { - return contractCallEventRepository; + if (token === GatewayContract) { + return gatewayContract; } - if (token === ApiConfigService) { - return apiConfigService; + if (token === ContractCallEventRepository) { + return contractCallEventRepository; } if (token === GrpcService) { return grpcService; } + if (token === ApiConfigService) { + return apiConfigService; + } + return null; }) .compile(); + gatewayContract.decodeContractCallEvent.mockReturnValue(event); + service = moduleRef.get(ContractCallProcessor); }); describe('handleEvent', () => { const data = Buffer.concat([ - Buffer.from('ebc84cbd75ba5516bf45e7024a9e12bc3c5c880f73e3a5beca7ebba52b2867a7', 'hex'), + Buffer.from(event.data.payload_hash, 'hex'), Buffer.from('00000007', 'hex'), // length of payload as u32 - Buffer.from('payload'), + event.data.payload, ]); const rawEvent: NotifierEvent = { txHash: 'txHash', @@ -65,12 +81,9 @@ describe('ContractCallProcessor', () => { data: data.toString('base64'), topics: [ BinaryUtils.base64Encode(Events.CONTRACT_CALL_EVENT), - Buffer.from( - Address.fromBech32('erd1qqqqqqqqqqqqqpgqzqvm5ywqqf524efwrhr039tjs29w0qltkklsa05pk7').hex(), - 'hex', - ).toString('base64'), - BinaryUtils.base64Encode('ethereum'), - BinaryUtils.base64Encode('destinationAddress'), + Buffer.from((event.sender as Address).hex(), 'hex').toString('base64'), + BinaryUtils.base64Encode(event.destination_chain), + BinaryUtils.base64Encode(event.destination_contract_address), ], order: 1, }; @@ -78,6 +91,8 @@ describe('ContractCallProcessor', () => { it('Should handle event', async () => { await service.handleEvent(rawEvent); + expect(gatewayContract.decodeContractCallEvent).toHaveBeenCalledTimes(1); + expect(gatewayContract.decodeContractCallEvent).toHaveBeenCalledWith(TransactionEvent.fromHttpResponse(rawEvent)); expect(contractCallEventRepository.create).toHaveBeenCalledTimes(1); expect(contractCallEventRepository.create).toHaveBeenCalledWith({ id: 'multiversx-test:txHash:1', @@ -99,6 +114,8 @@ describe('ContractCallProcessor', () => { await expect(service.handleEvent(rawEvent)).rejects.toThrow(); + expect(gatewayContract.decodeContractCallEvent).toHaveBeenCalledTimes(1); + expect(gatewayContract.decodeContractCallEvent).toHaveBeenCalledWith(TransactionEvent.fromHttpResponse(rawEvent)); expect(contractCallEventRepository.create).toHaveBeenCalledTimes(1); expect(grpcService.verify).not.toHaveBeenCalled(); }); diff --git a/apps/mvx-event-processor/src/processors/gas-service.processor.spec.ts b/apps/mvx-event-processor/src/processors/gas-service.processor.spec.ts index 7731d79..662ca3a 100644 --- a/apps/mvx-event-processor/src/processors/gas-service.processor.spec.ts +++ b/apps/mvx-event-processor/src/processors/gas-service.processor.spec.ts @@ -1,18 +1,358 @@ import { Test, TestingModule } from '@nestjs/testing'; import { GasServiceProcessor } from './gas-service.processor'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { GasServiceContract } from '@mvx-monorepo/common/contracts/gas-service.contract'; +import { ContractCallEventRepository } from '@mvx-monorepo/common/database/repository/contract-call-event.repository'; +import { GasPaidRepository } from '@mvx-monorepo/common/database/repository/gas-paid.repository'; +import { NotifierEvent } from '../event-processor/types'; +import { BinaryUtils } from '@multiversx/sdk-nestjs-common'; +import { Events } from '@mvx-monorepo/common/utils/event.enum'; +import { + GasAddedEvent, + GasPaidForContractCallEvent, + RefundedEvent, +} from '@mvx-monorepo/common/contracts/entities/gas-service-events'; +import { Address } from '@multiversx/sdk-core/out'; +import BigNumber from 'bignumber.js'; +import { ContractCallEvent, GasPaidStatus } from '@prisma/client'; +import { ContractCallEventWithGasPaid } from '@mvx-monorepo/common/database/entities/contract-call-event'; describe('GasServiceProcessor', () => { + let gasServiceContract: DeepMocked; + let contractCallEventRepository: DeepMocked; + let gasPaidRepository: DeepMocked; + let service: GasServiceProcessor; beforeEach(async () => { + gasServiceContract = createMock(); + contractCallEventRepository = createMock(); + gasPaidRepository = createMock(); + const module: TestingModule = await Test.createTestingModule({ providers: [GasServiceProcessor], - }).compile(); + }) + .useMocker((token) => { + if (token === GasServiceContract) { + return gasServiceContract; + } + + if (token === ContractCallEventRepository) { + return contractCallEventRepository; + } + + if (token === GasPaidRepository) { + return gasPaidRepository; + } + + return null; + }) + .compile(); service = module.get(GasServiceProcessor); }); - it('should be defined', () => { - expect(service).toBeDefined(); + it('Should not handle event', async () => { + const rawEvent: NotifierEvent = { + txHash: 'txHash', + address: 'mockGatewayAddress', + identifier: 'callContract', + data: '', + topics: [BinaryUtils.base64Encode(Events.CONTRACT_CALL_EVENT)], + order: 1, + }; + + await service.handleEvent(rawEvent); + + expect(gasPaidRepository.create).not.toHaveBeenCalled(); + expect(gasPaidRepository.updateRefundedValue).not.toHaveBeenCalled(); + }); + + const getMockGasPaid = ( + eventName: string = Events.GAS_PAID_FOR_CONTRACT_CALL_EVENT, + gasToken: string | null = 'WEGLD-123456', + ) => { + const rawEvent: NotifierEvent = { + txHash: 'txHash', + address: 'mockGasServiceContract', + identifier: 'any', + data: '', + topics: [BinaryUtils.base64Encode(eventName)], + order: 1, + }; + + const event: GasPaidForContractCallEvent = { + sender: Address.fromBech32('erd1qqqqqqqqqqqqqpgqzqvm5ywqqf524efwrhr039tjs29w0qltkklsa05pk7'), + destinationChain: 'ethereum', + destinationAddress: 'destinationAddress', + data: { + payloadHash: 'ebc84cbd75ba5516bf45e7024a9e12bc3c5c880f73e3a5beca7ebba52b2867a7', + gasToken, + gasFeeAmount: new BigNumber('654321'), + refundAddress: Address.fromBech32('erd1qqqqqqqqqqqqqpgqzqvm5ywqqf524efwrhr039tjs29w0qltkklsa05pk7'), + }, + }; + + const gasPaid: any = { + txHash: rawEvent.txHash, + sourceAddress: event.sender.bech32(), + destinationAddress: event.destinationAddress, + destinationChain: event.destinationChain, + payloadHash: event.data.payloadHash, + gasToken: event.data.gasToken, + gasValue: event.data.gasFeeAmount.toString(), + refundAddress: event.data.refundAddress.bech32(), + status: GasPaidStatus.PENDING, + }; + + return { rawEvent, event, gasPaid }; + }; + + async function assertEventGasPaidForContractCall( + rawEvent: NotifierEvent, + gasPaid: any, + contractCallEvent: any = null, + ) { + contractCallEventRepository.findWithoutGasPaid.mockReturnValueOnce(Promise.resolve(contractCallEvent)); + + await service.handleEvent(rawEvent); + + expect(contractCallEventRepository.findWithoutGasPaid).toHaveBeenCalledTimes(1); + expect(contractCallEventRepository.findWithoutGasPaid).toHaveBeenCalledWith(gasPaid); + + expect(gasPaidRepository.create).toHaveBeenCalledTimes(1); + expect(gasPaidRepository.create).toHaveBeenCalledWith(gasPaid); + } + + describe('Handle event gas paid for contract call', () => { + const { rawEvent, event, gasPaid } = getMockGasPaid(); + + it('Should handle no existing contract call', async () => { + gasServiceContract.decodeGasPaidForContractCallEvent.mockReturnValueOnce(event); + + await assertEventGasPaidForContractCall(rawEvent, gasPaid); + }); + + it('Should handle with existing contract call', async () => { + gasServiceContract.decodeGasPaidForContractCallEvent.mockReturnValueOnce(event); + + const contractCallEvent: DeepMocked = createMock(); + gasPaid.ContractCallEvent = { connect: contractCallEvent }; + + await assertEventGasPaidForContractCall(rawEvent, gasPaid, contractCallEvent); + }); + }); + + describe('Handle event native gas paid for contract call', () => { + const { rawEvent, event, gasPaid } = getMockGasPaid(Events.NATIVE_GAS_PAID_FOR_CONTRACT_CALL_EVENT, null); + + it('Should handle no existing contract call', async () => { + gasServiceContract.decodeNativeGasPaidForContractCallEvent.mockReturnValueOnce(event); + + await assertEventGasPaidForContractCall(rawEvent, gasPaid); + }); + + it('Should handle with existing contract call', async () => { + gasServiceContract.decodeNativeGasPaidForContractCallEvent.mockReturnValueOnce(event); + + const contractCallEvent: DeepMocked = createMock(); + gasPaid.ContractCallEvent = { connect: contractCallEvent }; + + await assertEventGasPaidForContractCall(rawEvent, gasPaid, contractCallEvent); + }); + }); + + const getMockGasAdded = (eventName: string = Events.GAS_ADDED_EVENT, gasToken: string | null = 'WEGLD-123456') => { + const rawEvent: NotifierEvent = { + txHash: 'txHash', + address: 'mockGasServiceContract', + identifier: 'any', + data: '', + topics: [BinaryUtils.base64Encode(eventName)], + order: 1, + }; + + const event: GasAddedEvent = { + txHash: 'txHash', + logIndex: 1, + data: { + gasToken, + gasFeeAmount: new BigNumber('1000'), + refundAddress: Address.fromBech32('erd1qqqqqqqqqqqqqpgqzqvm5ywqqf524efwrhr039tjs29w0qltkklsa05pk7'), + }, + }; + + return { rawEvent, event }; + }; + + async function assertGasAddedEvent( + rawEvent: NotifierEvent, + event: GasAddedEvent, + contractCallEvent: any = null, + extraGasPaid: any = null, + ) { + contractCallEventRepository.findPending.mockReturnValueOnce(Promise.resolve(contractCallEvent)); + + await service.handleEvent(rawEvent); + + expect(contractCallEventRepository.findPending).toHaveBeenCalledTimes(1); + expect(contractCallEventRepository.findPending).toHaveBeenCalledWith(event.txHash, event.logIndex); + + if (!extraGasPaid) { + expect(gasPaidRepository.update).not.toHaveBeenCalled(); + } else { + expect(gasPaidRepository.update).toHaveBeenCalledTimes(1); + expect(gasPaidRepository.update).toHaveBeenCalledWith(extraGasPaid.id, extraGasPaid); + } + } + + describe('Handle event gas added', () => { + const { rawEvent, event } = getMockGasAdded(); + + it('Should handle no existing contract call', async () => { + gasServiceContract.decodeGasAddedEvent.mockReturnValueOnce(event); + + await assertGasAddedEvent(rawEvent, event); + }); + + it('Should handle no gas paid different token', async () => { + gasServiceContract.decodeGasAddedEvent.mockReturnValueOnce(event); + + const contractCallEvent: DeepMocked = createMock(); + contractCallEvent.gasPaidEntries = [ + { + gasToken: 'other', + refundAddress: event.data.refundAddress.bech32(), + }, + ] as any; + + await assertGasAddedEvent(rawEvent, event, contractCallEvent); + }); + + it('Should handle no gas paid different refund address', async () => { + gasServiceContract.decodeGasAddedEvent.mockReturnValueOnce(event); + + const contractCallEvent: DeepMocked = createMock(); + contractCallEvent.gasPaidEntries = [ + { + gasToken: event.data.gasToken, + refundAddress: 'other address', + }, + ] as any; + + await assertGasAddedEvent(rawEvent, event, contractCallEvent); + }); + + it('Should handle no gas paid update', async () => { + gasServiceContract.decodeGasAddedEvent.mockReturnValueOnce(event); + + const contractCallEvent: DeepMocked = createMock(); + const gasPaid: any = { + id: 1234, + gasToken: event.data.gasToken, + refundAddress: event.data.refundAddress.bech32(), + gasValue: '2000', + }; + contractCallEvent.gasPaidEntries = [gasPaid]; + + await assertGasAddedEvent(rawEvent, event, contractCallEvent, { + ...gasPaid, + txHash: rawEvent.txHash, + gasValue: '3000', + }); + }); + }); + + describe('Handle event native gas added', () => { + const { rawEvent, event } = getMockGasAdded(Events.NATIVE_GAS_ADDED_EVENT, null); + + it('Should handle no existing contract call', async () => { + gasServiceContract.decodeNativeGasAddedEvent.mockReturnValueOnce(event); + + await assertGasAddedEvent(rawEvent, event); + }); + + it('Should handle no gas paid different token', async () => { + gasServiceContract.decodeNativeGasAddedEvent.mockReturnValueOnce(event); + + const contractCallEvent: DeepMocked = createMock(); + contractCallEvent.gasPaidEntries = [ + { + gasToken: 'other', + refundAddress: event.data.refundAddress.bech32(), + }, + ] as any; + + await assertGasAddedEvent(rawEvent, event, contractCallEvent); + }); + + it('Should handle no gas paid different refund address', async () => { + gasServiceContract.decodeNativeGasAddedEvent.mockReturnValueOnce(event); + + const contractCallEvent: DeepMocked = createMock(); + contractCallEvent.gasPaidEntries = [ + { + gasToken: event.data.gasToken, + refundAddress: 'other address', + }, + ] as any; + + await assertGasAddedEvent(rawEvent, event, contractCallEvent); + }); + + it('Should handle no gas paid update', async () => { + gasServiceContract.decodeNativeGasAddedEvent.mockReturnValueOnce(event); + + const contractCallEvent: DeepMocked = createMock(); + const gasPaid: any = { + id: 1234, + gasToken: event.data.gasToken, + refundAddress: event.data.refundAddress.bech32(), + gasValue: '2000', + }; + contractCallEvent.gasPaidEntries = [gasPaid]; + + await assertGasAddedEvent(rawEvent, event, contractCallEvent, { + ...gasPaid, + txHash: rawEvent.txHash, + gasValue: '3000', + }); + }); + }); + + describe('Handle event refunded event', () => { + const rawEvent: NotifierEvent = { + txHash: 'txHash', + address: 'mockGasServiceContract', + identifier: 'any', + data: '', + topics: [BinaryUtils.base64Encode(Events.REFUNDED_EVENT)], + order: 1, + }; + + const event: RefundedEvent = { + txHash: 'txHash', + logIndex: 1, + data: { + receiver: Address.fromBech32('erd1qqqqqqqqqqqqqpgqzqvm5ywqqf524efwrhr039tjs29w0qltkklsa05pk7'), + token: 'WEGLD-654321', + amount: new BigNumber('1000'), + }, + }; + + it('Should handle', async () => { + gasServiceContract.decodeRefundedEvent.mockReturnValueOnce(event); + + await service.handleEvent(rawEvent); + + expect(gasPaidRepository.updateRefundedValue).toHaveBeenCalledTimes(1); + expect(gasPaidRepository.updateRefundedValue).toHaveBeenCalledWith( + event.txHash, + event.logIndex, + event.data.token, + event.data.receiver.bech32(), + event.data.amount.toString(), + ); + }); }); }); diff --git a/apps/mvx-event-processor/src/processors/gas-service.processor.ts b/apps/mvx-event-processor/src/processors/gas-service.processor.ts index 5cb050c..aac9ae6 100644 --- a/apps/mvx-event-processor/src/processors/gas-service.processor.ts +++ b/apps/mvx-event-processor/src/processors/gas-service.processor.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; import { ProcessorInterface } from './entities/processor.interface'; import { NotifierEvent } from '../event-processor/types'; import { BinaryUtils } from '@multiversx/sdk-nestjs-common'; @@ -8,14 +8,20 @@ import { TransactionEvent } from '@multiversx/sdk-network-providers/out'; import { GasPaidStatus, Prisma } from '@prisma/client'; import { ContractCallEventRepository } from '@mvx-monorepo/common/database/repository/contract-call-event.repository'; import { GasPaidRepository } from '@mvx-monorepo/common/database/repository/gas-paid.repository'; +import { GasAddedEvent, GasPaidForContractCallEvent } from '@mvx-monorepo/common/contracts/entities/gas-service-events'; +import BigNumber from 'bignumber.js'; @Injectable() export class GasServiceProcessor implements ProcessorInterface { + private logger: Logger; + constructor( private readonly gasServiceContract: GasServiceContract, private readonly contractCallEventRepository: ContractCallEventRepository, private readonly gasPaidRepository: GasPaidRepository, - ) {} + ) { + this.logger = new Logger(GasServiceProcessor.name); + } async handleEvent(rawEvent: NotifierEvent) { const eventName = BinaryUtils.base64Decode(rawEvent.topics[0]); @@ -25,19 +31,7 @@ export class GasServiceProcessor implements ProcessorInterface { TransactionEvent.fromHttpResponse(rawEvent), ); - const gasPaid = { - txHash: rawEvent.txHash, - sourceAddress: event.sender.bech32(), - destinationAddress: event.destination_contract_address, - destinationChain: event.destination_chain, - payloadHash: event.data.payload_hash, - gasToken: event.data.gas_token, - gasValue: event.data.gas_fee_amount.toString(), - refundAddress: event.data.refund_address.bech32(), - status: GasPaidStatus.PENDING, - }; - - await this.handleGasPaidEvents(gasPaid); + await this.handleGasPaidEvents(event, rawEvent.txHash); return; } @@ -47,19 +41,7 @@ export class GasServiceProcessor implements ProcessorInterface { TransactionEvent.fromHttpResponse(rawEvent), ); - const gasPaid = { - txHash: rawEvent.txHash, - sourceAddress: event.sender.bech32(), - destinationAddress: event.destination_contract_address, - destinationChain: event.destination_chain, - payloadHash: event.data.payload_hash, - gasToken: null, - gasValue: event.data.value.toString(), - refundAddress: event.data.refund_address.bech32(), - status: GasPaidStatus.PENDING, - }; - - await this.handleGasPaidEvents(gasPaid); + await this.handleGasPaidEvents(event, rawEvent.txHash); return; } @@ -67,14 +49,7 @@ export class GasServiceProcessor implements ProcessorInterface { if (eventName === Events.GAS_ADDED_EVENT) { const event = this.gasServiceContract.decodeGasAddedEvent(TransactionEvent.fromHttpResponse(rawEvent)); - await this.handleGasAddedEvents( - event.tx_hash, - event.log_index, - event.data.gas_token, - event.data.gas_fee_amount.toString(), - event.data.refund_address.bech32(), - rawEvent.txHash, - ); + await this.handleGasAddedEvents(event, rawEvent.txHash); return; } @@ -82,14 +57,7 @@ export class GasServiceProcessor implements ProcessorInterface { if (eventName === Events.NATIVE_GAS_ADDED_EVENT) { const event = this.gasServiceContract.decodeNativeGasAddedEvent(TransactionEvent.fromHttpResponse(rawEvent)); - await this.handleGasAddedEvents( - event.tx_hash, - event.log_index, - null, - event.data.value.toString(), - event.data.refund_address.bech32(), - rawEvent.txHash, - ); + await this.handleGasAddedEvents(event, rawEvent.txHash); return; } @@ -98,8 +66,8 @@ export class GasServiceProcessor implements ProcessorInterface { const event = this.gasServiceContract.decodeRefundedEvent(TransactionEvent.fromHttpResponse(rawEvent)); await this.gasPaidRepository.updateRefundedValue( - event.tx_hash, - event.log_index, + event.txHash, + event.logIndex, event.data.token, event.data.receiver.bech32(), event.data.amount.toString(), @@ -107,7 +75,19 @@ export class GasServiceProcessor implements ProcessorInterface { } } - async handleGasPaidEvents(gasPaid: Prisma.GasPaidCreateInput) { + async handleGasPaidEvents(event: GasPaidForContractCallEvent, txHash: string) { + const gasPaid: Prisma.GasPaidCreateInput = { + txHash: txHash, + sourceAddress: event.sender.bech32(), + destinationAddress: event.destinationAddress, + destinationChain: event.destinationChain, + payloadHash: event.data.payloadHash, + gasToken: event.data.gasToken, + gasValue: event.data.gasFeeAmount.toString(), + refundAddress: event.data.refundAddress.bech32(), + status: GasPaidStatus.PENDING, + }; + const contractCallEvent = await this.contractCallEventRepository.findWithoutGasPaid(gasPaid); if (contractCallEvent) { @@ -117,32 +97,29 @@ export class GasServiceProcessor implements ProcessorInterface { await this.gasPaidRepository.create(gasPaid); } - async handleGasAddedEvents( - txHash: string, - logIndex: number, - gasToken: string | null, - gasValue: string, - refundAddress: string, - rawEventTxHash: string - ) { - const contractCallEvent = await this.contractCallEventRepository.findPending(txHash, logIndex); + async handleGasAddedEvents(event: GasAddedEvent, rawEventTxHash: string) { + const contractCallEvent = await this.contractCallEventRepository.findPending(event.txHash, event.logIndex); if (!contractCallEvent) { + this.logger.warn('Received a GasAddedEvent but could find existing contract call entry'); + return; } - const gasPaid = { - txHash: rawEventTxHash, - sourceAddress: contractCallEvent.sourceAddress, - destinationAddress: contractCallEvent.destinationAddress, - destinationChain: contractCallEvent.destinationChain, - payloadHash: contractCallEvent.payloadHash, - gasToken, - gasValue, - refundAddress, - status: GasPaidStatus.PENDING, - }; + const gasPaid = contractCallEvent.gasPaidEntries.find( + (gasPaid) => + gasPaid.gasToken === event.data.gasToken && gasPaid.refundAddress === event.data.refundAddress.bech32(), + ); - await this.gasPaidRepository.create(gasPaid); + if (!gasPaid) { + this.logger.warn('Received a GasAddedEvent but could find existing gas paid entry'); + + return; + } + + gasPaid.txHash = rawEventTxHash; + gasPaid.gasValue = new BigNumber(gasPaid.gasValue).plus(event.data.gasFeeAmount).toString(); + + await this.gasPaidRepository.update(gasPaid.id, gasPaid); } } diff --git a/apps/mvx-event-processor/test/jest-e2e.json b/apps/mvx-event-processor/test/jest-e2e.json new file mode 100644 index 0000000..263f891 --- /dev/null +++ b/apps/mvx-event-processor/test/jest-e2e.json @@ -0,0 +1,17 @@ +{ + "moduleFileExtensions": [ + "js", + "json", + "ts" + ], + "rootDir": ".", + "testEnvironment": "node", + "testRegex": ".e2e-spec.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + }, + "moduleNameMapper": { + "^@mvx-monorepo/common(|/.*)$": "/../../../libs/common/src/$1", + "^@mvx-monorepo/common": "/../../../libs/common" + } +} diff --git a/libs/common/src/contracts/contract.loader.ts b/libs/common/src/contracts/contract.loader.ts index cbf4299..1616f4c 100644 --- a/libs/common/src/contracts/contract.loader.ts +++ b/libs/common/src/contracts/contract.loader.ts @@ -1,6 +1,6 @@ -import { SmartContract, AbiRegistry, Address } from "@multiversx/sdk-core"; -import { Logger } from "@nestjs/common"; -import * as fs from "fs"; +import { AbiRegistry, Address, SmartContract } from '@multiversx/sdk-core'; +import { Logger } from '@nestjs/common'; +import * as fs from 'fs'; export class ContractLoader { private readonly logger: Logger; @@ -16,7 +16,7 @@ export class ContractLoader { private async load(contractAddress: string): Promise { try { - const jsonContent: string = await fs.promises.readFile(this.abiPath, { encoding: "utf8" }); + const jsonContent: string = await fs.promises.readFile(this.abiPath, { encoding: 'utf8' }); const json = JSON.parse(jsonContent); this.abiRegistry = AbiRegistry.create(json); diff --git a/libs/common/src/contracts/entities/gas-service-events.ts b/libs/common/src/contracts/entities/gas-service-events.ts index ff25755..581b7e4 100644 --- a/libs/common/src/contracts/entities/gas-service-events.ts +++ b/libs/common/src/contracts/entities/gas-service-events.ts @@ -1,54 +1,34 @@ -import { IAddress } from '@multiversx/sdk-core/out'; -import BigNumber from 'bignumber.js'; - -export interface GasPaidForContractCallEvent { - sender: IAddress, - destination_chain: string, - destination_contract_address: string, - data: { - payload_hash: string, - gas_token: string, - gas_fee_amount: BigNumber, - refund_address: IAddress, - } -} - -export interface NativeGasPaidForContractCallEvent { - sender: IAddress, - destination_chain: string, - destination_contract_address: string, - data: { - payload_hash: string, - value: BigNumber, - refund_address: IAddress, - } -} - -export interface GasAddedEvent { - tx_hash: string, - log_index: number, - data: { - gas_token: string, - gas_fee_amount: BigNumber, - refund_address: IAddress, - } -} - -export interface NativeGasAddedEvent { - tx_hash: string, - log_index: number, - data: { - value: BigNumber, - refund_address: IAddress, - } -} - -export interface RefundedEvent { - tx_hash: string, - log_index: number, - data: { - receiver: IAddress, - token: string | null, - amount: BigNumber, - } -} +import { IAddress } from '@multiversx/sdk-core/out'; +import BigNumber from 'bignumber.js'; + +export interface GasPaidForContractCallEvent { + sender: IAddress; + destinationChain: string; + destinationAddress: string; + data: { + payloadHash: string; + gasToken: string | null; + gasFeeAmount: BigNumber; + refundAddress: IAddress; + }; +} + +export interface GasAddedEvent { + txHash: string, + logIndex: number, + data: { + gasToken: string | null, + gasFeeAmount: BigNumber, + refundAddress: IAddress, + } +} + +export interface RefundedEvent { + txHash: string, + logIndex: number, + data: { + receiver: IAddress, + token: string | null, + amount: BigNumber, + } +} diff --git a/libs/common/src/contracts/gas-service.contract.spec.ts b/libs/common/src/contracts/gas-service.contract.spec.ts new file mode 100644 index 0000000..e4ade74 --- /dev/null +++ b/libs/common/src/contracts/gas-service.contract.spec.ts @@ -0,0 +1,97 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { Test } from '@nestjs/testing'; +import { BinaryUtils } from '@multiversx/sdk-nestjs-common'; +import { Events } from '@mvx-monorepo/common/utils/event.enum'; +import { AbiRegistry, Address, ResultsParser, SmartContract } from '@multiversx/sdk-core/out'; +import { TransactionEvent } from '@multiversx/sdk-network-providers/out'; +import { NotifierEvent } from '../../../../apps/mvx-event-processor/src/event-processor/types'; +import { GasServiceContract } from '@mvx-monorepo/common/contracts/gas-service.contract'; + +import gasServiceAbi from '../assets/gas-service.abi.json'; +import BigNumber from 'bignumber.js'; + +describe('GasServiceContract', () => { + let smartContract: DeepMocked; + let abi: AbiRegistry; + let resultsParser: ResultsParser; + + let contract: GasServiceContract; + + beforeEach(async () => { + smartContract = createMock(); + abi = AbiRegistry.create(gasServiceAbi); + resultsParser = new ResultsParser(); + + const moduleRef = await Test.createTestingModule({ + providers: [GasServiceContract], + }) + .useMocker((token) => { + if (token === SmartContract) { + return smartContract; + } + + if (token === AbiRegistry) { + return abi; + } + + if (token === ResultsParser) { + return resultsParser; + } + + return null; + }) + .compile(); + + contract = moduleRef.get(GasServiceContract); + }); + + describe('decodeGasPaidForContractCallEvent', () => { + const data = Buffer.concat([ + Buffer.from('ebc84cbd75ba5516bf45e7024a9e12bc3c5c880f73e3a5beca7ebba52b2867a7', 'hex'), + Buffer.from('00000005', 'hex'), // length of token as u32 + Buffer.from('token'), + Buffer.from('00000002', 'hex'), // length of amount as u32 + Buffer.from('03e8', 'hex'), // 1000 in hex + Buffer.from('000000000000000005001019ba11c00268aae52e1dc6f89572828ae783ebb5bf', 'hex'), + ]); + const rawEvent: NotifierEvent = { + txHash: 'txHash', + address: 'mockGasServiceAddress', + identifier: 'any', + data: data.toString('base64'), + topics: [ + BinaryUtils.base64Encode(Events.GAS_PAID_FOR_CONTRACT_CALL_EVENT), + Buffer.from( + Address.fromBech32('erd1qqqqqqqqqqqqqpgqzqvm5ywqqf524efwrhr039tjs29w0qltkklsa05pk7').hex(), + 'hex', + ).toString('base64'), + BinaryUtils.base64Encode('ethereum'), + BinaryUtils.base64Encode('destinationAddress'), + ], + order: 1, + }; + const event = TransactionEvent.fromHttpResponse(rawEvent); + + it('Should decode event', () => { + const result = contract.decodeGasPaidForContractCallEvent(event); + + expect(result).toEqual({ + sender: Address.fromBech32('erd1qqqqqqqqqqqqqpgqzqvm5ywqqf524efwrhr039tjs29w0qltkklsa05pk7'), + destinationChain: 'ethereum', + destinationAddress: 'destinationAddress', + data: { + payloadHash: 'ebc84cbd75ba5516bf45e7024a9e12bc3c5c880f73e3a5beca7ebba52b2867a7', + gasToken: 'token', + gasFeeAmount: new BigNumber('1000'), + refundAddress: Address.fromBech32('erd1qqqqqqqqqqqqqpgqzqvm5ywqqf524efwrhr039tjs29w0qltkklsa05pk7'), + }, + }); + }); + + it('Should throw error while decoding', () => { + event.topics = []; + + expect(() => contract.decodeGasPaidForContractCallEvent(event)).toThrow(); + }); + }); +}); diff --git a/libs/common/src/contracts/gas-service.contract.ts b/libs/common/src/contracts/gas-service.contract.ts index 22cc48b..2e57a1b 100644 --- a/libs/common/src/contracts/gas-service.contract.ts +++ b/libs/common/src/contracts/gas-service.contract.ts @@ -6,8 +6,6 @@ import BigNumber from 'bignumber.js'; import { GasAddedEvent, GasPaidForContractCallEvent, - NativeGasAddedEvent, - NativeGasPaidForContractCallEvent, RefundedEvent, } from '@mvx-monorepo/common/contracts/entities/gas-service-events'; @@ -31,29 +29,30 @@ export class GasServiceContract { return { sender: outcome.sender, - destination_chain: outcome.destination_chain.toString(), - destination_contract_address: outcome.destination_contract_address.toString(), + destinationChain: outcome.destination_chain.toString(), + destinationAddress: outcome.destination_contract_address.toString(), data: { - payload_hash: Buffer.from(outcome.data.hash.map((number: BigNumber) => number.toNumber())).toString('hex'), - gas_token: outcome.data.gas_token.toString(), - gas_fee_amount: outcome.data.gas_fee_amount, - refund_address: outcome.data.refund_address, + payloadHash: Buffer.from(outcome.data.hash.map((number: BigNumber) => number.toNumber())).toString('hex'), + gasToken: outcome.data.gas_token.toString(), + gasFeeAmount: outcome.data.gas_fee_amount, + refundAddress: outcome.data.refund_address, }, }; } - decodeNativeGasPaidForContractCallEvent(event: TransactionEvent): NativeGasPaidForContractCallEvent { + decodeNativeGasPaidForContractCallEvent(event: TransactionEvent): GasPaidForContractCallEvent { const eventDefinition = this.abi.getEvent(Events.GAS_PAID_FOR_CONTRACT_CALL_EVENT); const outcome = this.resultsParser.parseEvent(event, eventDefinition); return { sender: outcome.sender, - destination_chain: outcome.destination_chain.toString(), - destination_contract_address: outcome.destination_contract_address.toString(), + destinationChain: outcome.destination_chain.toString(), + destinationAddress: outcome.destination_contract_address.toString(), data: { - payload_hash: Buffer.from(outcome.data.hash.map((number: BigNumber) => number.toNumber())).toString('hex'), - value: outcome.data.value, - refund_address: outcome.data.refund_address, + payloadHash: Buffer.from(outcome.data.hash.map((number: BigNumber) => number.toNumber())).toString('hex'), + gasToken: null, + gasFeeAmount: outcome.data.value, + refundAddress: outcome.data.refund_address, }, }; } @@ -63,26 +62,27 @@ export class GasServiceContract { const outcome = this.resultsParser.parseEvent(event, eventDefinition); return { - tx_hash: outcome.tx_hash.toString('hex'), - log_index: outcome.log_index.toNumber(), + txHash: outcome.tx_hash.toString('hex'), + logIndex: outcome.log_index.toNumber(), data: { - gas_token: outcome.data.gas_token.toString(), - gas_fee_amount: outcome.data.value, - refund_address: outcome.data.refund_address, + gasToken: outcome.data.gas_token.toString(), + gasFeeAmount: outcome.data.value, + refundAddress: outcome.data.refund_address, }, }; } - decodeNativeGasAddedEvent(event: TransactionEvent): NativeGasAddedEvent { + decodeNativeGasAddedEvent(event: TransactionEvent): GasAddedEvent { const eventDefinition = this.abi.getEvent(Events.NATIVE_GAS_ADDED_EVENT); const outcome = this.resultsParser.parseEvent(event, eventDefinition); return { - tx_hash: outcome.tx_hash.toString('hex'), - log_index: outcome.log_index.toNumber(), + txHash: outcome.tx_hash.toString('hex'), + logIndex: outcome.log_index.toNumber(), data: { - value: outcome.data.value, - refund_address: outcome.data.refund_address, + gasToken: null, + gasFeeAmount: outcome.data.value, + refundAddress: outcome.data.refund_address, }, }; } @@ -94,8 +94,8 @@ export class GasServiceContract { const token = outcome.data.gas_token.toString(); return { - tx_hash: outcome.tx_hash.toString('hex'), - log_index: outcome.log_index.toNumber(), + txHash: outcome.tx_hash.toString('hex'), + logIndex: outcome.log_index.toNumber(), data: { receiver: outcome.data.receiver, token: token === 'EGLD' ? null : token, // TODO: Save 'EGLD' to a const diff --git a/libs/common/src/contracts/gateway.contract.spec.ts b/libs/common/src/contracts/gateway.contract.spec.ts new file mode 100644 index 0000000..1eea24c --- /dev/null +++ b/libs/common/src/contracts/gateway.contract.spec.ts @@ -0,0 +1,91 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { Test } from '@nestjs/testing'; +import { BinaryUtils } from '@multiversx/sdk-nestjs-common'; +import { Events } from '@mvx-monorepo/common/utils/event.enum'; +import { AbiRegistry, Address, ResultsParser, SmartContract } from '@multiversx/sdk-core/out'; +import { GatewayContract } from '@mvx-monorepo/common/contracts/gateway.contract'; +import { TransactionEvent } from '@multiversx/sdk-network-providers/out'; +import { NotifierEvent } from '../../../../apps/mvx-event-processor/src/event-processor/types'; + +import gatewayAbi from '../assets/gateway.abi.json'; + +describe('GatewayContract', () => { + let smartContract: DeepMocked; + let abi: AbiRegistry; + let resultsParser: ResultsParser; + + let contract: GatewayContract; + + beforeEach(async () => { + smartContract = createMock(); + abi = AbiRegistry.create(gatewayAbi); + resultsParser = new ResultsParser(); + + const moduleRef = await Test.createTestingModule({ + providers: [GatewayContract], + }) + .useMocker((token) => { + if (token === SmartContract) { + return smartContract; + } + + if (token === AbiRegistry) { + return abi; + } + + if (token === ResultsParser) { + return resultsParser; + } + + return null; + }) + .compile(); + + contract = moduleRef.get(GatewayContract); + }); + + describe('decodeContractCallEvent', () => { + const data = Buffer.concat([ + Buffer.from('ebc84cbd75ba5516bf45e7024a9e12bc3c5c880f73e3a5beca7ebba52b2867a7', 'hex'), + Buffer.from('00000007', 'hex'), // length of payload as u32 + Buffer.from('payload'), + ]); + const rawEvent: NotifierEvent = { + txHash: 'txHash', + address: 'mockGatewayAddress', + identifier: 'callContract', + data: data.toString('base64'), + topics: [ + BinaryUtils.base64Encode(Events.CONTRACT_CALL_EVENT), + Buffer.from( + Address.fromBech32('erd1qqqqqqqqqqqqqpgqzqvm5ywqqf524efwrhr039tjs29w0qltkklsa05pk7').hex(), + 'hex', + ).toString('base64'), + BinaryUtils.base64Encode('ethereum'), + BinaryUtils.base64Encode('destinationAddress'), + ], + order: 1, + }; + const event = TransactionEvent.fromHttpResponse(rawEvent); + + it('Should decode event', () => { + const result = contract.decodeContractCallEvent(event); + + expect(result).toEqual({ + sender: Address.fromBech32('erd1qqqqqqqqqqqqqpgqzqvm5ywqqf524efwrhr039tjs29w0qltkklsa05pk7'), + destinationChain: 'ethereum', + destinationAddress: 'destinationAddress', + data: { + payloadHash: 'ebc84cbd75ba5516bf45e7024a9e12bc3c5c880f73e3a5beca7ebba52b2867a7', + payload: Buffer.from('payload'), + }, + }); + }); + + it('Should throw error while decoding', () => { + event.topics = []; + + expect(() => contract.decodeContractCallEvent(event)).toThrow(); + }); + }); +}); diff --git a/libs/common/src/database/entities/contract-call-event.ts b/libs/common/src/database/entities/contract-call-event.ts new file mode 100644 index 0000000..8b7bfda --- /dev/null +++ b/libs/common/src/database/entities/contract-call-event.ts @@ -0,0 +1,7 @@ +import { Prisma } from '@prisma/client'; + +const callContractEventWithGasPaid = Prisma.validator()({ + include: { gasPaidEntries: true }, +}); + +export type ContractCallEventWithGasPaid = Prisma.ContractCallEventGetPayload; diff --git a/libs/common/src/database/repository/contract-call-event.repository.ts b/libs/common/src/database/repository/contract-call-event.repository.ts index 09d74d6..d41facd 100644 --- a/libs/common/src/database/repository/contract-call-event.repository.ts +++ b/libs/common/src/database/repository/contract-call-event.repository.ts @@ -1,6 +1,7 @@ import { Injectable } from '@nestjs/common'; import { PrismaService } from '@mvx-monorepo/common/database/prisma.service'; import { ContractCallEvent, ContractCallEventStatus, Prisma } from '@prisma/client'; +import { ContractCallEventWithGasPaid } from '@mvx-monorepo/common/database/entities/contract-call-event'; @Injectable() export class ContractCallEventRepository { @@ -27,13 +28,16 @@ export class ContractCallEventRepository { }); } - findPending(txHash: string, eventIndex: number): Promise { + findPending(txHash: string, eventIndex: number): Promise { return this.prisma.contractCallEvent.findUnique({ where: { status: ContractCallEventStatus.PENDING, txHash, eventIndex, }, + include: { + gasPaidEntries: true, + }, }); } } diff --git a/libs/common/src/database/repository/gas-paid.repository.ts b/libs/common/src/database/repository/gas-paid.repository.ts index 57347cd..707584b 100644 --- a/libs/common/src/database/repository/gas-paid.repository.ts +++ b/libs/common/src/database/repository/gas-paid.repository.ts @@ -12,14 +12,23 @@ export class GasPaidRepository { }); } - updateRefundedValue( + update(id: number, data: Prisma.GasPaidUpdateInput): Promise { + return this.prisma.gasPaid.update({ + where: { + id, + }, + data, + }); + } + + async updateRefundedValue( txHash: string, eventIndex: number, gasToken: string | null, refundAddress: string, refundedValue: string, ) { - this.prisma.gasPaid.updateMany({ + await this.prisma.gasPaid.updateMany({ where: { status: { in: [ContractCallEventStatus.SUCCESS, ContractCallEventStatus.FAILED], diff --git a/package-lock.json b/package-lock.json index f3b49e1..23b8b3e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,7 +48,7 @@ "@nestjs/schematics": "10.0.2", "@nestjs/testing": "^10.2.4", "@types/cache-manager": "^4.0.2", - "@types/jest": "^29.5.1", + "@types/jest": "^29.5.0", "@types/js-yaml": "^4.0.5", "@types/node": "^20.1.4", "@types/supertest": "^2.0.12", @@ -57,12 +57,12 @@ "eslint": "^8.40.0", "eslint-config-prettier": "^8.8.0", "eslint-plugin-prettier": "^4.2.1", - "jest": "^29.5.0", + "jest": "^29.7.0", "js-yaml": "^4.1.0", "prettier": "^2.8.8", "prisma": "^5.6.0", "supertest": "^6.3.3", - "ts-jest": "29.0.5", + "ts-jest": "^29.1.0", "ts-loader": "9.4.2", "ts-node": "10.7.0", "ts-proto": "^1.165.1", @@ -2995,9 +2995,9 @@ } }, "node_modules/@types/jest": { - "version": "29.5.10", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.10.tgz", - "integrity": "sha512-tE4yxKEphEyxj9s4inideLHktW/x6DwesIwWZ9NN1FKf9zbJYsnhBoA9vrHA/IuIOKwPa5PcFBNV4lpMIOEzyQ==", + "version": "29.5.11", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.11.tgz", + "integrity": "sha512-S2mHmYIVe13vrm6q4kN6fLYYAka15ALQki/vgDC3mIukEOx8WJlv0kQPM+d4w8Gp6u0uSdKND04IlTXBv0rwnQ==", "dev": true, "dependencies": { "expect": "^29.0.0", @@ -9941,9 +9941,9 @@ } }, "node_modules/ts-jest": { - "version": "29.0.5", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.0.5.tgz", - "integrity": "sha512-PL3UciSgIpQ7f6XjVOmbi96vmDHUqAyqDr8YxzopDqX3kfgYtX1cuNeBjP+L9sFXi6nzsGGA6R3fP3DDDJyrxA==", + "version": "29.1.1", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.1.tgz", + "integrity": "sha512-D6xjnnbP17cC85nliwGiL+tpoKN0StpgE0TeOjXQTU6MVCfsB4v7aW05CgQ/1OywGb0x/oy9hHFnN+sczTiRaA==", "dev": true, "dependencies": { "bs-logger": "0.x", @@ -9952,7 +9952,7 @@ "json5": "^2.2.3", "lodash.memoize": "4.x", "make-error": "1.x", - "semver": "7.x", + "semver": "^7.5.3", "yargs-parser": "^21.0.1" }, "bin": { @@ -9966,7 +9966,7 @@ "@jest/types": "^29.0.0", "babel-jest": "^29.0.0", "jest": "^29.0.0", - "typescript": ">=4.3" + "typescript": ">=4.3 <6" }, "peerDependenciesMeta": { "@babel/core": { @@ -12964,9 +12964,9 @@ } }, "@types/jest": { - "version": "29.5.10", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.10.tgz", - "integrity": "sha512-tE4yxKEphEyxj9s4inideLHktW/x6DwesIwWZ9NN1FKf9zbJYsnhBoA9vrHA/IuIOKwPa5PcFBNV4lpMIOEzyQ==", + "version": "29.5.11", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.11.tgz", + "integrity": "sha512-S2mHmYIVe13vrm6q4kN6fLYYAka15ALQki/vgDC3mIukEOx8WJlv0kQPM+d4w8Gp6u0uSdKND04IlTXBv0rwnQ==", "dev": true, "requires": { "expect": "^29.0.0", @@ -18227,9 +18227,9 @@ "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==" }, "ts-jest": { - "version": "29.0.5", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.0.5.tgz", - "integrity": "sha512-PL3UciSgIpQ7f6XjVOmbi96vmDHUqAyqDr8YxzopDqX3kfgYtX1cuNeBjP+L9sFXi6nzsGGA6R3fP3DDDJyrxA==", + "version": "29.1.1", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.1.tgz", + "integrity": "sha512-D6xjnnbP17cC85nliwGiL+tpoKN0StpgE0TeOjXQTU6MVCfsB4v7aW05CgQ/1OywGb0x/oy9hHFnN+sczTiRaA==", "dev": true, "requires": { "bs-logger": "0.x", @@ -18238,7 +18238,7 @@ "json5": "^2.2.3", "lodash.memoize": "4.x", "make-error": "1.x", - "semver": "7.x", + "semver": "^7.5.3", "yargs-parser": "^21.0.1" } }, diff --git a/package.json b/package.json index 2034482..df60fd5 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,8 @@ "license": "UNLICENSED", "scripts": { "prebuild": "rimraf dist", - "build": "nest build", - "build:all": "nest build mvx-event-processor && nest build axelar-event-processor && nest build common", + "build": "nest build common && nest build", + "build:all": "nest build common && nest build mvx-event-processor && nest build axelar-event-processor", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", "start": "nest build common && nest build mvx-event-processor & nest start", "start:watch": "nest build common && nest build mvx-event-processor & nest start --watch", @@ -24,8 +24,7 @@ "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", "test:e2e": "jest --config ./apps/mvx-event-processor/test/jest-e2e.json", "migrate": "prisma migrate dev", - "generate": "prisma generate", - "buf": "cd libs/common/src/assets && npx buf generate" + "generate": "prisma generate" }, "dependencies": { "@grpc/grpc-js": "^1.9.12", @@ -67,7 +66,7 @@ "@nestjs/schematics": "10.0.2", "@nestjs/testing": "^10.2.4", "@types/cache-manager": "^4.0.2", - "@types/jest": "^29.5.1", + "@types/jest": "^29.5.0", "@types/js-yaml": "^4.0.5", "@types/node": "^20.1.4", "@types/supertest": "^2.0.12", @@ -76,12 +75,12 @@ "eslint": "^8.40.0", "eslint-config-prettier": "^8.8.0", "eslint-plugin-prettier": "^4.2.1", - "jest": "^29.5.0", + "jest": "^29.7.0", "js-yaml": "^4.1.0", "prettier": "^2.8.8", "prisma": "^5.6.0", "supertest": "^6.3.3", - "ts-jest": "29.0.5", + "ts-jest": "^29.1.0", "ts-loader": "9.4.2", "ts-node": "10.7.0", "ts-proto": "^1.165.1", @@ -97,7 +96,7 @@ "json", "ts" ], - "rootDir": "apps", + "rootDir": ".", "testRegex": ".*\\.spec\\.ts$", "transform": { "^.+\\.(t|j)s$": "ts-jest" @@ -108,8 +107,8 @@ "coverageDirectory": "../coverage", "testEnvironment": "node", "moduleNameMapper": { - "^@mvx-monorepo/common(|/.*)$": "../libs/common/src/$1", - "^@mvx-monorepo/common": "../libs/common" + "^@mvx-monorepo/common(|/.*)$": "/libs/common/src/$1", + "^@mvx-monorepo/common": "/libs/common" } } } From db4e26cbcf5131e486a37fdd8090bd3e7c026dd7 Mon Sep 17 00:00:00 2001 From: Rares <6453351+raresserban@users.noreply.github.com> Date: Fri, 8 Dec 2023 17:34:21 +0200 Subject: [PATCH 06/33] Finish tests for gas service contract. --- .../event.processor.service.spec.ts | 49 +---- .../event.processor.service.ts | 23 +-- .../contract-call.processor.spec.ts | 48 ++++- .../src/processors/contract-call.processor.ts | 20 +- .../contracts/entities/contract-call-event.ts | 6 +- .../contracts/gas-service.contract.spec.ts | 189 ++++++++++++++++-- .../src/contracts/gas-service.contract.ts | 9 +- libs/common/src/contracts/gateway.contract.ts | 6 +- libs/common/src/utils/constants.enum.ts | 3 + 9 files changed, 261 insertions(+), 92 deletions(-) create mode 100644 libs/common/src/utils/constants.enum.ts diff --git a/apps/mvx-event-processor/src/event-processor/event.processor.service.spec.ts b/apps/mvx-event-processor/src/event-processor/event.processor.service.spec.ts index ee14145..13e8db8 100644 --- a/apps/mvx-event-processor/src/event-processor/event.processor.service.spec.ts +++ b/apps/mvx-event-processor/src/event-processor/event.processor.service.spec.ts @@ -3,8 +3,6 @@ import { ApiConfigService } from '@mvx-monorepo/common'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test } from '@nestjs/testing'; import { NotifierBlockEvent } from './types'; -import { BinaryUtils } from '@multiversx/sdk-nestjs-common'; -import { Events } from '@mvx-monorepo/common/utils/event.enum'; import { ContractCallProcessor, GasServiceProcessor } from '../processors'; describe('EventProcessorService', () => { @@ -55,25 +53,17 @@ describe('EventProcessorService', () => { { txHash: 'test', address: 'someAddress', - identifier: 'someIdentifier', + identifier: 'callContract', data: '', topics: [], order: 0, }, { txHash: 'test', - address: 'mockGatewayAddress', - identifier: 'someIdentifier', - data: '', - topics: [BinaryUtils.base64Encode(Events.CONTRACT_CALL_EVENT)], - order: 0, - }, - { - txHash: 'test', - address: 'mockGatewayAddress', - identifier: 'callContract', + address: 'someOtherAddress', + identifier: 'any', data: '', - topics: [''], + topics: [], order: 0, }, ], @@ -95,9 +85,9 @@ describe('EventProcessorService', () => { { txHash: 'test', address: 'mockGatewayAddress', - identifier: 'callContract', + identifier: 'any', data: '', - topics: [BinaryUtils.base64Encode(Events.CONTRACT_CALL_EVENT)], + topics: [], order: 0, }, ], @@ -119,9 +109,9 @@ describe('EventProcessorService', () => { { txHash: 'test', address: 'mockGasServiceAddress', - identifier: 'payGasForContractCall', + identifier: 'any', data: '', - topics: [BinaryUtils.base64Encode(Events.GAS_PAID_FOR_CONTRACT_CALL_EVENT)], + topics: [], order: 0, }, ], @@ -133,28 +123,5 @@ describe('EventProcessorService', () => { expect(gasServiceProcessor.handleEvent).toHaveBeenCalledTimes(1); expect(contractCallProcessor.handleEvent).not.toHaveBeenCalled(); }); - - it('Should throw error', async () => { - const blockEvent: NotifierBlockEvent = { - hash: 'test', - shardId: 1, - timestamp: 123456, - events: [ - { - txHash: 'test', - address: 'mockGatewayAddress', - identifier: 'callContract', - data: '', - topics: [], - order: 0, - }, - ], - }; - - await expect(service.consumeEvents(blockEvent)).rejects.toThrow(); - - expect(apiConfigService.getContractGateway).toHaveBeenCalledTimes(1); - expect(contractCallProcessor.handleEvent).not.toHaveBeenCalled(); - }); }); }); diff --git a/apps/mvx-event-processor/src/event-processor/event.processor.service.ts b/apps/mvx-event-processor/src/event-processor/event.processor.service.ts index 7c3d379..0c7b525 100644 --- a/apps/mvx-event-processor/src/event-processor/event.processor.service.ts +++ b/apps/mvx-event-processor/src/event-processor/event.processor.service.ts @@ -3,8 +3,6 @@ import { ApiConfigService } from '@mvx-monorepo/common'; import { NotifierBlockEvent, NotifierEvent } from './types'; import { RabbitSubscribe } from '@golevelup/nestjs-rabbitmq'; import { EVENTS_NOTIFIER_QUEUE } from '../../../../config/configuration'; -import { EventIdentifiers, Events } from '@mvx-monorepo/common/utils/event.enum'; -import { BinaryUtils } from '@multiversx/sdk-nestjs-common'; import { ContractCallProcessor, GasServiceProcessor } from '../processors'; @Injectable() @@ -45,27 +43,22 @@ export class EventProcessorService { } private async handleEvent(event: NotifierEvent) { - this.logger.log('Received event from MultiversX:'); - this.logger.log(JSON.stringify(event)); - if (event.address === this.contractGasService) { - await this.gasServiceProcessor.handleEvent(event); + this.logger.debug('Received Gas Service event from MultiversX:'); + this.logger.debug(JSON.stringify(event)); - return; - } + await this.gasServiceProcessor.handleEvent(event); - if (event.address !== this.contractGateway) { return; } - if ( - event.identifier === EventIdentifiers.CALL_CONTRACT && - BinaryUtils.base64Decode(event.topics[0]) === Events.CONTRACT_CALL_EVENT - ) { - this.logger.log('Received callContract event from MultiversX Gateway contract:'); - this.logger.log(JSON.stringify(event)); + if (event.address === this.contractGateway) { + this.logger.debug('Received Gateway event from MultiversX:'); + this.logger.debug(JSON.stringify(event)); await this.contractCallProcessor.handleEvent(event); + + return; } } } diff --git a/apps/mvx-event-processor/src/processors/contract-call.processor.spec.ts b/apps/mvx-event-processor/src/processors/contract-call.processor.spec.ts index 6094037..2ecfcab 100644 --- a/apps/mvx-event-processor/src/processors/contract-call.processor.spec.ts +++ b/apps/mvx-event-processor/src/processors/contract-call.processor.spec.ts @@ -2,7 +2,7 @@ import { ApiConfigService } from '@mvx-monorepo/common'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test } from '@nestjs/testing'; import { BinaryUtils } from '@multiversx/sdk-nestjs-common'; -import { Events } from '@mvx-monorepo/common/utils/event.enum'; +import { EventIdentifiers, Events } from '@mvx-monorepo/common/utils/event.enum'; import { ContractCallEventRepository } from '@mvx-monorepo/common/database/repository/contract-call-event.repository'; import { ContractCallProcessor } from './contract-call.processor'; import { NotifierEvent } from '../event-processor/types'; @@ -23,10 +23,10 @@ describe('ContractCallProcessor', () => { const event: ContractCallEvent = { sender: Address.fromBech32('erd1qqqqqqqqqqqqqpgqzqvm5ywqqf524efwrhr039tjs29w0qltkklsa05pk7'), - destination_chain: 'ethereum', - destination_contract_address: 'destinationAddress', + destinationChain: 'ethereum', + destinationAddress: 'destinationAddress', data: { - payload_hash: 'ebc84cbd75ba5516bf45e7024a9e12bc3c5c880f73e3a5beca7ebba52b2867a7', + payloadHash: 'ebc84cbd75ba5516bf45e7024a9e12bc3c5c880f73e3a5beca7ebba52b2867a7', payload: Buffer.from('payload'), }, }; @@ -70,7 +70,7 @@ describe('ContractCallProcessor', () => { describe('handleEvent', () => { const data = Buffer.concat([ - Buffer.from(event.data.payload_hash, 'hex'), + Buffer.from(event.data.payloadHash, 'hex'), Buffer.from('00000007', 'hex'), // length of payload as u32 event.data.payload, ]); @@ -82,8 +82,8 @@ describe('ContractCallProcessor', () => { topics: [ BinaryUtils.base64Encode(Events.CONTRACT_CALL_EVENT), Buffer.from((event.sender as Address).hex(), 'hex').toString('base64'), - BinaryUtils.base64Encode(event.destination_chain), - BinaryUtils.base64Encode(event.destination_contract_address), + BinaryUtils.base64Encode(event.destinationChain), + BinaryUtils.base64Encode(event.destinationAddress), ], order: 1, }; @@ -119,5 +119,39 @@ describe('ContractCallProcessor', () => { expect(contractCallEventRepository.create).toHaveBeenCalledTimes(1); expect(grpcService.verify).not.toHaveBeenCalled(); }); + + it('Should not handle event wrong identifier', async () => { + const rawEvent: NotifierEvent = { + txHash: 'txHash', + address: 'mockGatewayAddress', + identifier: 'any', + data: data.toString('base64'), + topics: [BinaryUtils.base64Encode(Events.CONTRACT_CALL_EVENT)], + order: 1, + }; + + await service.handleEvent(rawEvent); + + expect(gatewayContract.decodeContractCallEvent).not.toHaveBeenCalled(); + expect(contractCallEventRepository.create).not.toHaveBeenCalled(); + expect(grpcService.verify).not.toHaveBeenCalled(); + }); + + it('Should not handle event wrong event', async () => { + const rawEvent: NotifierEvent = { + txHash: 'txHash', + address: 'mockGatewayAddress', + identifier: EventIdentifiers.CALL_CONTRACT, + data: data.toString('base64'), + topics: [BinaryUtils.base64Encode('any')], + order: 1, + }; + + await service.handleEvent(rawEvent); + + expect(gatewayContract.decodeContractCallEvent).not.toHaveBeenCalled(); + expect(contractCallEventRepository.create).not.toHaveBeenCalled(); + expect(grpcService.verify).not.toHaveBeenCalled(); + }); }); }); diff --git a/apps/mvx-event-processor/src/processors/contract-call.processor.ts b/apps/mvx-event-processor/src/processors/contract-call.processor.ts index 489427b..e716496 100644 --- a/apps/mvx-event-processor/src/processors/contract-call.processor.ts +++ b/apps/mvx-event-processor/src/processors/contract-call.processor.ts @@ -7,6 +7,8 @@ import { ApiConfigService } from '@mvx-monorepo/common'; import { ContractCallEventStatus } from '@prisma/client'; import { GrpcService } from '@mvx-monorepo/common/grpc/grpc.service'; import { ProcessorInterface } from './entities/processor.interface'; +import { EventIdentifiers, Events } from '@mvx-monorepo/common/utils/event.enum'; +import { BinaryUtils } from '@multiversx/sdk-nestjs-common'; @Injectable() export class ContractCallProcessor implements ProcessorInterface { @@ -22,6 +24,13 @@ export class ContractCallProcessor implements ProcessorInterface { } async handleEvent(rawEvent: NotifierEvent) { + if ( + rawEvent.identifier !== EventIdentifiers.CALL_CONTRACT || + BinaryUtils.base64Decode(rawEvent.topics[0]) !== Events.CONTRACT_CALL_EVENT + ) { + return; + } + const event = this.gatewayContract.decodeContractCallEvent(TransactionEvent.fromHttpResponse(rawEvent)); const contractCallEvent = await this.contractCallEventRepository.create({ @@ -31,9 +40,9 @@ export class ContractCallProcessor implements ProcessorInterface { status: ContractCallEventStatus.PENDING, sourceAddress: event.sender.bech32(), sourceChain: this.sourceChain, - destinationAddress: event.destination_contract_address, - destinationChain: event.destination_chain, - payloadHash: event.data.payload_hash, + destinationAddress: event.destinationAddress, + destinationChain: event.destinationChain, + payloadHash: event.data.payloadHash, payload: event.data.payload, }); @@ -43,5 +52,10 @@ export class ContractCallProcessor implements ProcessorInterface { // TODO: Should this be batched instead and have this in a separate cronjob? await this.grpcService.verify(contractCallEvent); + // TODO: We should mark here the message as successfull after sending to grpc + // Maybe this sending should be async in a cron? + // For now the ContractCallEvent in db will remain as PENDING if it was not successfully sent to the Relayer API + // Verify endpoint. After it was sent, it can be marked as APPROVED + // GasPaid will remain as PENDING status for now } } diff --git a/libs/common/src/contracts/entities/contract-call-event.ts b/libs/common/src/contracts/entities/contract-call-event.ts index dd82f4b..bbd86e0 100644 --- a/libs/common/src/contracts/entities/contract-call-event.ts +++ b/libs/common/src/contracts/entities/contract-call-event.ts @@ -2,10 +2,10 @@ import { IAddress } from '@multiversx/sdk-core/out'; export interface ContractCallEvent { sender: IAddress, - destination_chain: string, - destination_contract_address: string, + destinationChain: string, + destinationAddress: string, data: { - payload_hash: string, + payloadHash: string, payload: Buffer, } } diff --git a/libs/common/src/contracts/gas-service.contract.spec.ts b/libs/common/src/contracts/gas-service.contract.spec.ts index e4ade74..86b5317 100644 --- a/libs/common/src/contracts/gas-service.contract.spec.ts +++ b/libs/common/src/contracts/gas-service.contract.spec.ts @@ -4,7 +4,6 @@ import { BinaryUtils } from '@multiversx/sdk-nestjs-common'; import { Events } from '@mvx-monorepo/common/utils/event.enum'; import { AbiRegistry, Address, ResultsParser, SmartContract } from '@multiversx/sdk-core/out'; import { TransactionEvent } from '@multiversx/sdk-network-providers/out'; -import { NotifierEvent } from '../../../../apps/mvx-event-processor/src/event-processor/types'; import { GasServiceContract } from '@mvx-monorepo/common/contracts/gas-service.contract'; import gasServiceAbi from '../assets/gas-service.abi.json'; @@ -45,22 +44,13 @@ describe('GasServiceContract', () => { contract = moduleRef.get(GasServiceContract); }); - describe('decodeGasPaidForContractCallEvent', () => { - const data = Buffer.concat([ - Buffer.from('ebc84cbd75ba5516bf45e7024a9e12bc3c5c880f73e3a5beca7ebba52b2867a7', 'hex'), - Buffer.from('00000005', 'hex'), // length of token as u32 - Buffer.from('token'), - Buffer.from('00000002', 'hex'), // length of amount as u32 - Buffer.from('03e8', 'hex'), // 1000 in hex - Buffer.from('000000000000000005001019ba11c00268aae52e1dc6f89572828ae783ebb5bf', 'hex'), - ]); - const rawEvent: NotifierEvent = { - txHash: 'txHash', + const getGasPaidEvent = (event: string, data: Buffer): TransactionEvent => + TransactionEvent.fromHttpResponse({ address: 'mockGasServiceAddress', identifier: 'any', data: data.toString('base64'), topics: [ - BinaryUtils.base64Encode(Events.GAS_PAID_FOR_CONTRACT_CALL_EVENT), + BinaryUtils.base64Encode(event), Buffer.from( Address.fromBech32('erd1qqqqqqqqqqqqqpgqzqvm5ywqqf524efwrhr039tjs29w0qltkklsa05pk7').hex(), 'hex', @@ -68,9 +58,18 @@ describe('GasServiceContract', () => { BinaryUtils.base64Encode('ethereum'), BinaryUtils.base64Encode('destinationAddress'), ], - order: 1, - }; - const event = TransactionEvent.fromHttpResponse(rawEvent); + }); + + describe('decodeGasPaidForContractCallEvent', () => { + const data = Buffer.concat([ + Buffer.from('ebc84cbd75ba5516bf45e7024a9e12bc3c5c880f73e3a5beca7ebba52b2867a7', 'hex'), + Buffer.from('00000005', 'hex'), // length of token as u32 + Buffer.from('token'), + Buffer.from('00000002', 'hex'), // length of amount as u32 + Buffer.from('03e8', 'hex'), // 1000 in hex + Buffer.from('000000000000000005001019ba11c00268aae52e1dc6f89572828ae783ebb5bf', 'hex'), + ]); + const event = getGasPaidEvent(Events.GAS_PAID_FOR_CONTRACT_CALL_EVENT, data); it('Should decode event', () => { const result = contract.decodeGasPaidForContractCallEvent(event); @@ -94,4 +93,162 @@ describe('GasServiceContract', () => { expect(() => contract.decodeGasPaidForContractCallEvent(event)).toThrow(); }); }); + + describe('decodeNativeGasPaidForContractCallEvent', () => { + const data = Buffer.concat([ + Buffer.from('ebc84cbd75ba5516bf45e7024a9e12bc3c5c880f73e3a5beca7ebba52b2867a7', 'hex'), + Buffer.from('00000002', 'hex'), // length of amount as u32 + Buffer.from('03e8', 'hex'), // 1000 in hex + Buffer.from('000000000000000005001019ba11c00268aae52e1dc6f89572828ae783ebb5bf', 'hex'), + ]); + const event = getGasPaidEvent(Events.GAS_PAID_FOR_CONTRACT_CALL_EVENT, data); + + it('Should decode event', () => { + const result = contract.decodeNativeGasPaidForContractCallEvent(event); + + expect(result).toEqual({ + sender: Address.fromBech32('erd1qqqqqqqqqqqqqpgqzqvm5ywqqf524efwrhr039tjs29w0qltkklsa05pk7'), + destinationChain: 'ethereum', + destinationAddress: 'destinationAddress', + data: { + payloadHash: 'ebc84cbd75ba5516bf45e7024a9e12bc3c5c880f73e3a5beca7ebba52b2867a7', + gasToken: null, + gasFeeAmount: new BigNumber('1000'), + refundAddress: Address.fromBech32('erd1qqqqqqqqqqqqqpgqzqvm5ywqqf524efwrhr039tjs29w0qltkklsa05pk7'), + }, + }); + }); + + it('Should throw error while decoding', () => { + event.topics = []; + + expect(() => contract.decodeNativeGasPaidForContractCallEvent(event)).toThrow(); + }); + }); + + const getGasAddedEvent = (event: string, data: Buffer): TransactionEvent => + TransactionEvent.fromHttpResponse({ + address: 'mockGasServiceAddress', + identifier: 'any', + data: data.toString('base64'), + topics: [ + BinaryUtils.base64Encode(event), + Buffer.from('ebc84cbd75ba5516bf45e7024a9e12bc3c5c880f73e3a5beca7ebba52b2867a7', 'hex').toString('base64'), + BinaryUtils.hexToBase64('01'), + ], + }); + + describe('decodeGasAddedEvent', () => { + const data = Buffer.concat([ + Buffer.from('00000005', 'hex'), // length of token as u32 + Buffer.from('token'), + Buffer.from('00000002', 'hex'), // length of amount as u32 + Buffer.from('03e8', 'hex'), // 1000 in hex + Buffer.from('000000000000000005001019ba11c00268aae52e1dc6f89572828ae783ebb5bf', 'hex'), + ]); + const event = getGasAddedEvent(Events.GAS_ADDED_EVENT, data); + + it('Should decode event', () => { + const result = contract.decodeGasAddedEvent(event); + + expect(result).toEqual({ + txHash: 'ebc84cbd75ba5516bf45e7024a9e12bc3c5c880f73e3a5beca7ebba52b2867a7', + logIndex: 1, + data: { + gasToken: 'token', + gasFeeAmount: new BigNumber('1000'), + refundAddress: Address.fromBech32('erd1qqqqqqqqqqqqqpgqzqvm5ywqqf524efwrhr039tjs29w0qltkklsa05pk7'), + }, + }); + }); + + it('Should throw error while decoding', () => { + event.topics = []; + + expect(() => contract.decodeGasAddedEvent(event)).toThrow(); + }); + }); + + describe('decodeNativeGasAddedEvent', () => { + const data = Buffer.concat([ + Buffer.from('00000002', 'hex'), // length of amount as u32 + Buffer.from('03e8', 'hex'), // 1000 in hex + Buffer.from('000000000000000005001019ba11c00268aae52e1dc6f89572828ae783ebb5bf', 'hex'), + ]); + const event = getGasAddedEvent(Events.NATIVE_GAS_ADDED_EVENT, data); + + it('Should decode event', () => { + const result = contract.decodeNativeGasAddedEvent(event); + + expect(result).toEqual({ + txHash: 'ebc84cbd75ba5516bf45e7024a9e12bc3c5c880f73e3a5beca7ebba52b2867a7', + logIndex: 1, + data: { + gasToken: null, + gasFeeAmount: new BigNumber('1000'), + refundAddress: Address.fromBech32('erd1qqqqqqqqqqqqqpgqzqvm5ywqqf524efwrhr039tjs29w0qltkklsa05pk7'), + }, + }); + }); + + it('Should throw error while decoding', () => { + event.topics = []; + + expect(() => contract.decodeNativeGasAddedEvent(event)).toThrow(); + }); + }); + + describe('decodeRefundedEvent', () => { + it('Should decode event egld', () => { + const data = Buffer.concat([ + Buffer.from('000000000000000005001019ba11c00268aae52e1dc6f89572828ae783ebb5bf', 'hex'), + Buffer.from('00000004', 'hex'), // length of token as u32 + Buffer.from('EGLD'), + Buffer.from('00000002', 'hex'), // length of amount as u32 + Buffer.from('03e8', 'hex'), // 1000 in hex + ]); + const event = getGasAddedEvent(Events.REFUNDED_EVENT, data); + + const result = contract.decodeRefundedEvent(event); + + expect(result).toEqual({ + txHash: 'ebc84cbd75ba5516bf45e7024a9e12bc3c5c880f73e3a5beca7ebba52b2867a7', + logIndex: 1, + data: { + receiver: Address.fromBech32('erd1qqqqqqqqqqqqqpgqzqvm5ywqqf524efwrhr039tjs29w0qltkklsa05pk7'), + token: null, + amount: new BigNumber('1000'), + }, + }); + }); + + const data = Buffer.concat([ + Buffer.from('000000000000000005001019ba11c00268aae52e1dc6f89572828ae783ebb5bf', 'hex'), + Buffer.from('00000005', 'hex'), // length of token as u32 + Buffer.from('token'), + Buffer.from('00000002', 'hex'), // length of amount as u32 + Buffer.from('03e8', 'hex'), // 1000 in hex + ]); + const event = getGasAddedEvent(Events.REFUNDED_EVENT, data); + + it('Should decode event token', () => { + const result = contract.decodeRefundedEvent(event); + + expect(result).toEqual({ + txHash: 'ebc84cbd75ba5516bf45e7024a9e12bc3c5c880f73e3a5beca7ebba52b2867a7', + logIndex: 1, + data: { + receiver: Address.fromBech32('erd1qqqqqqqqqqqqqpgqzqvm5ywqqf524efwrhr039tjs29w0qltkklsa05pk7'), + token: 'token', + amount: new BigNumber('1000'), + }, + }); + }); + + it('Should throw error while decoding', () => { + event.topics = []; + + expect(() => contract.decodeRefundedEvent(event)).toThrow(); + }); + }); }); diff --git a/libs/common/src/contracts/gas-service.contract.ts b/libs/common/src/contracts/gas-service.contract.ts index 2e57a1b..cc467d9 100644 --- a/libs/common/src/contracts/gas-service.contract.ts +++ b/libs/common/src/contracts/gas-service.contract.ts @@ -8,6 +8,7 @@ import { GasPaidForContractCallEvent, RefundedEvent, } from '@mvx-monorepo/common/contracts/entities/gas-service-events'; +import { CONSTANTS } from '@mvx-monorepo/common/utils/constants.enum'; @Injectable() export class GasServiceContract { @@ -41,7 +42,7 @@ export class GasServiceContract { } decodeNativeGasPaidForContractCallEvent(event: TransactionEvent): GasPaidForContractCallEvent { - const eventDefinition = this.abi.getEvent(Events.GAS_PAID_FOR_CONTRACT_CALL_EVENT); + const eventDefinition = this.abi.getEvent(Events.NATIVE_GAS_PAID_FOR_CONTRACT_CALL_EVENT); const outcome = this.resultsParser.parseEvent(event, eventDefinition); return { @@ -66,7 +67,7 @@ export class GasServiceContract { logIndex: outcome.log_index.toNumber(), data: { gasToken: outcome.data.gas_token.toString(), - gasFeeAmount: outcome.data.value, + gasFeeAmount: outcome.data.gas_fee_amount, refundAddress: outcome.data.refund_address, }, }; @@ -91,14 +92,14 @@ export class GasServiceContract { const eventDefinition = this.abi.getEvent(Events.REFUNDED_EVENT); const outcome = this.resultsParser.parseEvent(event, eventDefinition); - const token = outcome.data.gas_token.toString(); + const token = outcome.data.token.toString(); return { txHash: outcome.tx_hash.toString('hex'), logIndex: outcome.log_index.toNumber(), data: { receiver: outcome.data.receiver, - token: token === 'EGLD' ? null : token, // TODO: Save 'EGLD' to a const + token: token === CONSTANTS.EGLD_IDENTIFIER ? null : token, amount: outcome.data.amount, }, }; diff --git a/libs/common/src/contracts/gateway.contract.ts b/libs/common/src/contracts/gateway.contract.ts index bec8d14..c097e1d 100644 --- a/libs/common/src/contracts/gateway.contract.ts +++ b/libs/common/src/contracts/gateway.contract.ts @@ -25,10 +25,10 @@ export class GatewayContract { return { sender: outcome.sender, - destination_chain: outcome.destination_chain.toString(), - destination_contract_address: outcome.destination_contract_address.toString(), + destinationChain: outcome.destination_chain.toString(), + destinationAddress: outcome.destination_contract_address.toString(), data: { - payload_hash: Buffer.from(outcome.data.hash.map((number: BigNumber) => number.toNumber())).toString('hex'), + payloadHash: Buffer.from(outcome.data.hash.map((number: BigNumber) => number.toNumber())).toString('hex'), payload: outcome.data.payload, }, }; diff --git a/libs/common/src/utils/constants.enum.ts b/libs/common/src/utils/constants.enum.ts new file mode 100644 index 0000000..3691476 --- /dev/null +++ b/libs/common/src/utils/constants.enum.ts @@ -0,0 +1,3 @@ +export enum CONSTANTS { + EGLD_IDENTIFIER = 'EGLD', +} From 018d5d105f87726b31bbf9d73a8d81640761035e Mon Sep 17 00:00:00 2001 From: Rares <6453351+raresserban@users.noreply.github.com> Date: Mon, 11 Dec 2023 12:10:23 +0200 Subject: [PATCH 07/33] Remove order from event processor. --- .../src/event-processor/event.processor.service.spec.ts | 4 ---- apps/mvx-event-processor/src/event-processor/types.ts | 1 - .../src/processors/contract-call.processor.spec.ts | 7 ++----- .../src/processors/contract-call.processor.ts | 8 ++++++-- .../src/processors/gas-service.processor.spec.ts | 4 ---- libs/common/src/contracts/gateway.contract.spec.ts | 1 - 6 files changed, 8 insertions(+), 17 deletions(-) diff --git a/apps/mvx-event-processor/src/event-processor/event.processor.service.spec.ts b/apps/mvx-event-processor/src/event-processor/event.processor.service.spec.ts index 13e8db8..29bc930 100644 --- a/apps/mvx-event-processor/src/event-processor/event.processor.service.spec.ts +++ b/apps/mvx-event-processor/src/event-processor/event.processor.service.spec.ts @@ -56,7 +56,6 @@ describe('EventProcessorService', () => { identifier: 'callContract', data: '', topics: [], - order: 0, }, { txHash: 'test', @@ -64,7 +63,6 @@ describe('EventProcessorService', () => { identifier: 'any', data: '', topics: [], - order: 0, }, ], }; @@ -88,7 +86,6 @@ describe('EventProcessorService', () => { identifier: 'any', data: '', topics: [], - order: 0, }, ], }; @@ -112,7 +109,6 @@ describe('EventProcessorService', () => { identifier: 'any', data: '', topics: [], - order: 0, }, ], }; diff --git a/apps/mvx-event-processor/src/event-processor/types.ts b/apps/mvx-event-processor/src/event-processor/types.ts index 937e155..b139e96 100644 --- a/apps/mvx-event-processor/src/event-processor/types.ts +++ b/apps/mvx-event-processor/src/event-processor/types.ts @@ -11,5 +11,4 @@ export interface NotifierEvent { identifier: string; data: string; topics: string[]; - order: number; // TODO: This field doesn't seem to come from the notifier, and is quite needed currently... } diff --git a/apps/mvx-event-processor/src/processors/contract-call.processor.spec.ts b/apps/mvx-event-processor/src/processors/contract-call.processor.spec.ts index 2ecfcab..ac28351 100644 --- a/apps/mvx-event-processor/src/processors/contract-call.processor.spec.ts +++ b/apps/mvx-event-processor/src/processors/contract-call.processor.spec.ts @@ -85,7 +85,6 @@ describe('ContractCallProcessor', () => { BinaryUtils.base64Encode(event.destinationChain), BinaryUtils.base64Encode(event.destinationAddress), ], - order: 1, }; it('Should handle event', async () => { @@ -95,9 +94,9 @@ describe('ContractCallProcessor', () => { expect(gatewayContract.decodeContractCallEvent).toHaveBeenCalledWith(TransactionEvent.fromHttpResponse(rawEvent)); expect(contractCallEventRepository.create).toHaveBeenCalledTimes(1); expect(contractCallEventRepository.create).toHaveBeenCalledWith({ - id: 'multiversx-test:txHash:1', + id: 'multiversx-test:txHash:999999', txHash: 'txHash', - eventIndex: 1, + eventIndex: 999999, status: ContractCallEventStatus.PENDING, sourceAddress: 'erd1qqqqqqqqqqqqqpgqzqvm5ywqqf524efwrhr039tjs29w0qltkklsa05pk7', sourceChain: 'multiversx-test', @@ -127,7 +126,6 @@ describe('ContractCallProcessor', () => { identifier: 'any', data: data.toString('base64'), topics: [BinaryUtils.base64Encode(Events.CONTRACT_CALL_EVENT)], - order: 1, }; await service.handleEvent(rawEvent); @@ -144,7 +142,6 @@ describe('ContractCallProcessor', () => { identifier: EventIdentifiers.CALL_CONTRACT, data: data.toString('base64'), topics: [BinaryUtils.base64Encode('any')], - order: 1, }; await service.handleEvent(rawEvent); diff --git a/apps/mvx-event-processor/src/processors/contract-call.processor.ts b/apps/mvx-event-processor/src/processors/contract-call.processor.ts index e716496..8822c04 100644 --- a/apps/mvx-event-processor/src/processors/contract-call.processor.ts +++ b/apps/mvx-event-processor/src/processors/contract-call.processor.ts @@ -10,6 +10,10 @@ import { ProcessorInterface } from './entities/processor.interface'; import { EventIdentifiers, Events } from '@mvx-monorepo/common/utils/event.enum'; import { BinaryUtils } from '@multiversx/sdk-nestjs-common'; +// order/logIndex is unsupported since we can't easily get it in the relayer +// so we use a sufficiently large u32 value here instead +const UNSUPPORTED_LOG_INDEX: number = 999_999; + @Injectable() export class ContractCallProcessor implements ProcessorInterface { private sourceChain: string; @@ -34,9 +38,9 @@ export class ContractCallProcessor implements ProcessorInterface { const event = this.gatewayContract.decodeContractCallEvent(TransactionEvent.fromHttpResponse(rawEvent)); const contractCallEvent = await this.contractCallEventRepository.create({ - id: `${this.sourceChain}:${rawEvent.txHash}:${rawEvent.order}`, + id: `${this.sourceChain}:${rawEvent.txHash}:${UNSUPPORTED_LOG_INDEX}`, txHash: rawEvent.txHash, - eventIndex: rawEvent.order, + eventIndex: UNSUPPORTED_LOG_INDEX, status: ContractCallEventStatus.PENDING, sourceAddress: event.sender.bech32(), sourceChain: this.sourceChain, diff --git a/apps/mvx-event-processor/src/processors/gas-service.processor.spec.ts b/apps/mvx-event-processor/src/processors/gas-service.processor.spec.ts index 662ca3a..fd8b9eb 100644 --- a/apps/mvx-event-processor/src/processors/gas-service.processor.spec.ts +++ b/apps/mvx-event-processor/src/processors/gas-service.processor.spec.ts @@ -59,7 +59,6 @@ describe('GasServiceProcessor', () => { identifier: 'callContract', data: '', topics: [BinaryUtils.base64Encode(Events.CONTRACT_CALL_EVENT)], - order: 1, }; await service.handleEvent(rawEvent); @@ -78,7 +77,6 @@ describe('GasServiceProcessor', () => { identifier: 'any', data: '', topics: [BinaryUtils.base64Encode(eventName)], - order: 1, }; const event: GasPaidForContractCallEvent = { @@ -169,7 +167,6 @@ describe('GasServiceProcessor', () => { identifier: 'any', data: '', topics: [BinaryUtils.base64Encode(eventName)], - order: 1, }; const event: GasAddedEvent = { @@ -327,7 +324,6 @@ describe('GasServiceProcessor', () => { identifier: 'any', data: '', topics: [BinaryUtils.base64Encode(Events.REFUNDED_EVENT)], - order: 1, }; const event: RefundedEvent = { diff --git a/libs/common/src/contracts/gateway.contract.spec.ts b/libs/common/src/contracts/gateway.contract.spec.ts index 1eea24c..5e075c4 100644 --- a/libs/common/src/contracts/gateway.contract.spec.ts +++ b/libs/common/src/contracts/gateway.contract.spec.ts @@ -64,7 +64,6 @@ describe('GatewayContract', () => { BinaryUtils.base64Encode('ethereum'), BinaryUtils.base64Encode('destinationAddress'), ], - order: 1, }; const event = TransactionEvent.fromHttpResponse(rawEvent); From 8f85884d347f1c2d20504fb28640564e7b05120d Mon Sep 17 00:00:00 2001 From: Rares <6453351+raresserban@users.noreply.github.com> Date: Mon, 11 Dec 2023 15:17:03 +0200 Subject: [PATCH 08/33] Process contract call approved event. --- .../event.processor.service.spec.ts | 6 +- .../event.processor.service.ts | 6 +- .../contract-call.processor.spec.ts | 154 ------------- .../src/processors/gateway.processor.spec.ts | 217 ++++++++++++++++++ ...call.processor.ts => gateway.processor.ts} | 45 +++- .../src/processors/index.ts | 2 +- .../src/processors/processors.module.ts | 6 +- libs/common/src/assets/gateway.abi.json | 12 +- .../contracts/entities/contract-call-event.ts | 11 - .../src/contracts/entities/gateway-events.ts | 19 ++ .../src/contracts/gas-service.contract.ts | 6 +- .../src/contracts/gateway.contract.spec.ts | 67 +++++- libs/common/src/contracts/gateway.contract.ts | 26 ++- libs/common/src/database/database.module.ts | 5 +- .../contract-call-approved.repository.ts | 14 ++ libs/common/src/grpc/grpc.service.ts | 8 + libs/common/src/utils/decoding.utils.ts | 7 + libs/common/src/utils/event.enum.ts | 4 + .../migration.sql | 23 ++ prisma/schema.prisma | 23 +- 20 files changed, 469 insertions(+), 192 deletions(-) delete mode 100644 apps/mvx-event-processor/src/processors/contract-call.processor.spec.ts create mode 100644 apps/mvx-event-processor/src/processors/gateway.processor.spec.ts rename apps/mvx-event-processor/src/processors/{contract-call.processor.ts => gateway.processor.ts} (61%) delete mode 100644 libs/common/src/contracts/entities/contract-call-event.ts create mode 100644 libs/common/src/contracts/entities/gateway-events.ts create mode 100644 libs/common/src/database/repository/contract-call-approved.repository.ts create mode 100644 libs/common/src/utils/decoding.utils.ts create mode 100644 prisma/migrations/20231211131553_add_contract_call_approved/migration.sql diff --git a/apps/mvx-event-processor/src/event-processor/event.processor.service.spec.ts b/apps/mvx-event-processor/src/event-processor/event.processor.service.spec.ts index 29bc930..dc64644 100644 --- a/apps/mvx-event-processor/src/event-processor/event.processor.service.spec.ts +++ b/apps/mvx-event-processor/src/event-processor/event.processor.service.spec.ts @@ -3,10 +3,10 @@ import { ApiConfigService } from '@mvx-monorepo/common'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test } from '@nestjs/testing'; import { NotifierBlockEvent } from './types'; -import { ContractCallProcessor, GasServiceProcessor } from '../processors'; +import { GatewayProcessor, GasServiceProcessor } from '../processors'; describe('EventProcessorService', () => { - let contractCallProcessor: DeepMocked; + let contractCallProcessor: DeepMocked; let gasServiceProcessor: DeepMocked; let apiConfigService: DeepMocked; @@ -24,7 +24,7 @@ describe('EventProcessorService', () => { providers: [EventProcessorService], }) .useMocker((token) => { - if (token === ContractCallProcessor) { + if (token === GatewayProcessor) { return contractCallProcessor; } diff --git a/apps/mvx-event-processor/src/event-processor/event.processor.service.ts b/apps/mvx-event-processor/src/event-processor/event.processor.service.ts index 0c7b525..0b15b22 100644 --- a/apps/mvx-event-processor/src/event-processor/event.processor.service.ts +++ b/apps/mvx-event-processor/src/event-processor/event.processor.service.ts @@ -3,7 +3,7 @@ import { ApiConfigService } from '@mvx-monorepo/common'; import { NotifierBlockEvent, NotifierEvent } from './types'; import { RabbitSubscribe } from '@golevelup/nestjs-rabbitmq'; import { EVENTS_NOTIFIER_QUEUE } from '../../../../config/configuration'; -import { ContractCallProcessor, GasServiceProcessor } from '../processors'; +import { GatewayProcessor, GasServiceProcessor } from '../processors'; @Injectable() export class EventProcessorService { @@ -12,7 +12,7 @@ export class EventProcessorService { private readonly logger: Logger; constructor( - private readonly contractCallProcessor: ContractCallProcessor, + private readonly gatewayProcessor: GatewayProcessor, private readonly gasServiceProcessor: GasServiceProcessor, apiConfigService: ApiConfigService, ) { @@ -56,7 +56,7 @@ export class EventProcessorService { this.logger.debug('Received Gateway event from MultiversX:'); this.logger.debug(JSON.stringify(event)); - await this.contractCallProcessor.handleEvent(event); + await this.gatewayProcessor.handleEvent(event); return; } diff --git a/apps/mvx-event-processor/src/processors/contract-call.processor.spec.ts b/apps/mvx-event-processor/src/processors/contract-call.processor.spec.ts deleted file mode 100644 index ac28351..0000000 --- a/apps/mvx-event-processor/src/processors/contract-call.processor.spec.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { ApiConfigService } from '@mvx-monorepo/common'; -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { Test } from '@nestjs/testing'; -import { BinaryUtils } from '@multiversx/sdk-nestjs-common'; -import { EventIdentifiers, Events } from '@mvx-monorepo/common/utils/event.enum'; -import { ContractCallEventRepository } from '@mvx-monorepo/common/database/repository/contract-call-event.repository'; -import { ContractCallProcessor } from './contract-call.processor'; -import { NotifierEvent } from '../event-processor/types'; -import { Address } from '@multiversx/sdk-core/out'; -import { ContractCallEventStatus } from '@prisma/client'; -import { GrpcService } from '@mvx-monorepo/common/grpc/grpc.service'; -import { GatewayContract } from '@mvx-monorepo/common/contracts/gateway.contract'; -import { ContractCallEvent } from '@mvx-monorepo/common/contracts/entities/contract-call-event'; -import { TransactionEvent } from '@multiversx/sdk-network-providers/out'; - -describe('ContractCallProcessor', () => { - let gatewayContract: DeepMocked; - let contractCallEventRepository: DeepMocked; - let grpcService: DeepMocked; - let apiConfigService: DeepMocked; - - let service: ContractCallProcessor; - - const event: ContractCallEvent = { - sender: Address.fromBech32('erd1qqqqqqqqqqqqqpgqzqvm5ywqqf524efwrhr039tjs29w0qltkklsa05pk7'), - destinationChain: 'ethereum', - destinationAddress: 'destinationAddress', - data: { - payloadHash: 'ebc84cbd75ba5516bf45e7024a9e12bc3c5c880f73e3a5beca7ebba52b2867a7', - payload: Buffer.from('payload'), - }, - }; - - beforeEach(async () => { - gatewayContract = createMock(); - contractCallEventRepository = createMock(); - grpcService = createMock(); - apiConfigService = createMock(); - - apiConfigService.getSourceChainName.mockReturnValue('multiversx-test'); - - const moduleRef = await Test.createTestingModule({ - providers: [ContractCallProcessor], - }) - .useMocker((token) => { - if (token === GatewayContract) { - return gatewayContract; - } - - if (token === ContractCallEventRepository) { - return contractCallEventRepository; - } - - if (token === GrpcService) { - return grpcService; - } - - if (token === ApiConfigService) { - return apiConfigService; - } - - return null; - }) - .compile(); - - gatewayContract.decodeContractCallEvent.mockReturnValue(event); - - service = moduleRef.get(ContractCallProcessor); - }); - - describe('handleEvent', () => { - const data = Buffer.concat([ - Buffer.from(event.data.payloadHash, 'hex'), - Buffer.from('00000007', 'hex'), // length of payload as u32 - event.data.payload, - ]); - const rawEvent: NotifierEvent = { - txHash: 'txHash', - address: 'mockGatewayAddress', - identifier: 'callContract', - data: data.toString('base64'), - topics: [ - BinaryUtils.base64Encode(Events.CONTRACT_CALL_EVENT), - Buffer.from((event.sender as Address).hex(), 'hex').toString('base64'), - BinaryUtils.base64Encode(event.destinationChain), - BinaryUtils.base64Encode(event.destinationAddress), - ], - }; - - it('Should handle event', async () => { - await service.handleEvent(rawEvent); - - expect(gatewayContract.decodeContractCallEvent).toHaveBeenCalledTimes(1); - expect(gatewayContract.decodeContractCallEvent).toHaveBeenCalledWith(TransactionEvent.fromHttpResponse(rawEvent)); - expect(contractCallEventRepository.create).toHaveBeenCalledTimes(1); - expect(contractCallEventRepository.create).toHaveBeenCalledWith({ - id: 'multiversx-test:txHash:999999', - txHash: 'txHash', - eventIndex: 999999, - status: ContractCallEventStatus.PENDING, - sourceAddress: 'erd1qqqqqqqqqqqqqpgqzqvm5ywqqf524efwrhr039tjs29w0qltkklsa05pk7', - sourceChain: 'multiversx-test', - destinationAddress: 'destinationAddress', - destinationChain: 'ethereum', - payloadHash: 'ebc84cbd75ba5516bf45e7024a9e12bc3c5c880f73e3a5beca7ebba52b2867a7', - payload: Buffer.from('payload'), - }); - expect(grpcService.verify).toHaveBeenCalledTimes(1); - }); - - it('Should throw error can not save in database', async () => { - contractCallEventRepository.create.mockReturnValueOnce(Promise.resolve(null)); - - await expect(service.handleEvent(rawEvent)).rejects.toThrow(); - - expect(gatewayContract.decodeContractCallEvent).toHaveBeenCalledTimes(1); - expect(gatewayContract.decodeContractCallEvent).toHaveBeenCalledWith(TransactionEvent.fromHttpResponse(rawEvent)); - expect(contractCallEventRepository.create).toHaveBeenCalledTimes(1); - expect(grpcService.verify).not.toHaveBeenCalled(); - }); - - it('Should not handle event wrong identifier', async () => { - const rawEvent: NotifierEvent = { - txHash: 'txHash', - address: 'mockGatewayAddress', - identifier: 'any', - data: data.toString('base64'), - topics: [BinaryUtils.base64Encode(Events.CONTRACT_CALL_EVENT)], - }; - - await service.handleEvent(rawEvent); - - expect(gatewayContract.decodeContractCallEvent).not.toHaveBeenCalled(); - expect(contractCallEventRepository.create).not.toHaveBeenCalled(); - expect(grpcService.verify).not.toHaveBeenCalled(); - }); - - it('Should not handle event wrong event', async () => { - const rawEvent: NotifierEvent = { - txHash: 'txHash', - address: 'mockGatewayAddress', - identifier: EventIdentifiers.CALL_CONTRACT, - data: data.toString('base64'), - topics: [BinaryUtils.base64Encode('any')], - }; - - await service.handleEvent(rawEvent); - - expect(gatewayContract.decodeContractCallEvent).not.toHaveBeenCalled(); - expect(contractCallEventRepository.create).not.toHaveBeenCalled(); - expect(grpcService.verify).not.toHaveBeenCalled(); - }); - }); -}); diff --git a/apps/mvx-event-processor/src/processors/gateway.processor.spec.ts b/apps/mvx-event-processor/src/processors/gateway.processor.spec.ts new file mode 100644 index 0000000..73f7a63 --- /dev/null +++ b/apps/mvx-event-processor/src/processors/gateway.processor.spec.ts @@ -0,0 +1,217 @@ +import { ApiConfigService } from '@mvx-monorepo/common'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { Test } from '@nestjs/testing'; +import { BinaryUtils } from '@multiversx/sdk-nestjs-common'; +import { Events } from '@mvx-monorepo/common/utils/event.enum'; +import { ContractCallEventRepository } from '@mvx-monorepo/common/database/repository/contract-call-event.repository'; +import { GatewayProcessor } from './gateway.processor'; +import { NotifierEvent } from '../event-processor/types'; +import { Address } from '@multiversx/sdk-core/out'; +import { ContractCallApprovedStatus, ContractCallEventStatus } from '@prisma/client'; +import { GrpcService } from '@mvx-monorepo/common/grpc/grpc.service'; +import { GatewayContract } from '@mvx-monorepo/common/contracts/gateway.contract'; +import { ContractCallApprovedEvent, ContractCallEvent } from '@mvx-monorepo/common/contracts/entities/gateway-events'; +import { TransactionEvent } from '@multiversx/sdk-network-providers/out'; +import { ContractCallApprovedRepository } from '@mvx-monorepo/common/database/repository/contract-call-approved.repository'; + +describe('ContractCallProcessor', () => { + let gatewayContract: DeepMocked; + let contractCallEventRepository: DeepMocked; + let contractCallApprovedRepository: DeepMocked; + let grpcService: DeepMocked; + let apiConfigService: DeepMocked; + + let service: GatewayProcessor; + + const contractCallEvent: ContractCallEvent = { + sender: Address.fromBech32('erd1qqqqqqqqqqqqqpgqzqvm5ywqqf524efwrhr039tjs29w0qltkklsa05pk7'), + destinationChain: 'ethereum', + destinationAddress: 'destinationAddress', + data: { + payloadHash: 'ebc84cbd75ba5516bf45e7024a9e12bc3c5c880f73e3a5beca7ebba52b2867a7', + payload: Buffer.from('payload'), + }, + }; + const contractCallApprovedEvent: ContractCallApprovedEvent = { + commandId: '0c38359b7a35c755573659d797afec315bb0e51374a056745abd9764715a15da', + sourceChain: 'ethereum', + sourceAddress: 'sourceAddress', + contractAddress: Address.fromBech32('erd1qqqqqqqqqqqqqpgqzqvm5ywqqf524efwrhr039tjs29w0qltkklsa05pk7'), + payloadHash: 'ebc84cbd75ba5516bf45e7024a9e12bc3c5c880f73e3a5beca7ebba52b2867a7', + }; + + beforeEach(async () => { + gatewayContract = createMock(); + contractCallEventRepository = createMock(); + contractCallApprovedRepository = createMock(); + grpcService = createMock(); + apiConfigService = createMock(); + + apiConfigService.getSourceChainName.mockReturnValue('multiversx-test'); + + const moduleRef = await Test.createTestingModule({ + providers: [GatewayProcessor], + }) + .useMocker((token) => { + if (token === GatewayContract) { + return gatewayContract; + } + + if (token === ContractCallEventRepository) { + return contractCallEventRepository; + } + + if (token === ContractCallApprovedRepository) { + return contractCallApprovedRepository; + } + + if (token === GrpcService) { + return grpcService; + } + + if (token === ApiConfigService) { + return apiConfigService; + } + + return null; + }) + .compile(); + + gatewayContract.decodeContractCallEvent.mockReturnValue(contractCallEvent); + gatewayContract.decodeContractCallApprovedEvent.mockReturnValue(contractCallApprovedEvent); + + service = moduleRef.get(GatewayProcessor); + }); + + it('Should not handle event', async () => { + const rawEvent: NotifierEvent = { + txHash: 'txHash', + address: 'mockGatewayAddress', + identifier: 'any', + data: '', + topics: [BinaryUtils.base64Encode('any')], + }; + + await service.handleEvent(rawEvent); + + expect(gatewayContract.decodeContractCallEvent).not.toHaveBeenCalled(); + expect(gatewayContract.decodeContractCallApprovedEvent).not.toHaveBeenCalled(); + expect(contractCallEventRepository.create).not.toHaveBeenCalled(); + expect(contractCallApprovedRepository.create).not.toHaveBeenCalled(); + expect(grpcService.verify).not.toHaveBeenCalled(); + expect(grpcService.getPayload).not.toHaveBeenCalled(); + }); + + describe('handleContractCallEvent', () => { + const data = Buffer.concat([ + Buffer.from(contractCallEvent.data.payloadHash, 'hex'), + Buffer.from('00000007', 'hex'), // length of payload as u32 + contractCallEvent.data.payload, + ]); + const rawEvent: NotifierEvent = { + txHash: 'txHash', + address: 'mockGatewayAddress', + identifier: 'callContract', + data: data.toString('base64'), + topics: [ + BinaryUtils.base64Encode(Events.CONTRACT_CALL_EVENT), + Buffer.from((contractCallEvent.sender as Address).hex(), 'hex').toString('base64'), + BinaryUtils.base64Encode(contractCallEvent.destinationChain), + BinaryUtils.base64Encode(contractCallEvent.destinationAddress), + ], + }; + + it('Should handle event', async () => { + await service.handleEvent(rawEvent); + + expect(gatewayContract.decodeContractCallEvent).toHaveBeenCalledTimes(1); + expect(gatewayContract.decodeContractCallEvent).toHaveBeenCalledWith(TransactionEvent.fromHttpResponse(rawEvent)); + expect(contractCallEventRepository.create).toHaveBeenCalledTimes(1); + expect(contractCallEventRepository.create).toHaveBeenCalledWith({ + id: 'multiversx-test:txHash:999999', + txHash: 'txHash', + eventIndex: 999999, + status: ContractCallEventStatus.PENDING, + sourceAddress: 'erd1qqqqqqqqqqqqqpgqzqvm5ywqqf524efwrhr039tjs29w0qltkklsa05pk7', + sourceChain: 'multiversx-test', + destinationAddress: 'destinationAddress', + destinationChain: 'ethereum', + payloadHash: 'ebc84cbd75ba5516bf45e7024a9e12bc3c5c880f73e3a5beca7ebba52b2867a7', + payload: Buffer.from('payload'), + }); + expect(grpcService.verify).toHaveBeenCalledTimes(1); + }); + + it('Should throw error can not save in database', async () => { + contractCallEventRepository.create.mockReturnValueOnce(Promise.resolve(null)); + + await expect(service.handleEvent(rawEvent)).rejects.toThrow(); + + expect(gatewayContract.decodeContractCallEvent).toHaveBeenCalledTimes(1); + expect(gatewayContract.decodeContractCallEvent).toHaveBeenCalledWith(TransactionEvent.fromHttpResponse(rawEvent)); + expect(contractCallEventRepository.create).toHaveBeenCalledTimes(1); + expect(grpcService.verify).not.toHaveBeenCalled(); + }); + }); + + describe('handleContractCallApprovedEvent', () => { + const rawEvent: NotifierEvent = { + txHash: 'txHash', + address: 'mockGatewayAddress', + identifier: 'execute', + data: '', + topics: [ + BinaryUtils.base64Encode(Events.CONTRACT_CALL_APPROVED_EVENT), + Buffer.from('0c38359b7a35c755573659d797afec315bb0e51374a056745abd9764715a15da', 'hex').toString('base64'), + BinaryUtils.base64Encode('ethereum'), + BinaryUtils.base64Encode('sourceAddress'), + Buffer.from( + Address.fromBech32('erd1qqqqqqqqqqqqqpgqzqvm5ywqqf524efwrhr039tjs29w0qltkklsa05pk7').hex(), + 'hex', + ).toString('base64'), + Buffer.from('ebc84cbd75ba5516bf45e7024a9e12bc3c5c880f73e3a5beca7ebba52b2867a7', 'hex').toString('base64'), + ], + }; + + it('Should handle event', async () => { + grpcService.getPayload.mockReturnValueOnce(Promise.resolve(Buffer.from('payload'))); + + await service.handleEvent(rawEvent); + + expect(gatewayContract.decodeContractCallApprovedEvent).toHaveBeenCalledTimes(1); + expect(gatewayContract.decodeContractCallApprovedEvent).toHaveBeenCalledWith( + TransactionEvent.fromHttpResponse(rawEvent), + ); + expect(grpcService.getPayload).toHaveBeenCalledTimes(1); + expect(grpcService.getPayload).toHaveBeenCalledWith(contractCallApprovedEvent.payloadHash); + expect(contractCallApprovedRepository.create).toHaveBeenCalledTimes(1); + expect(contractCallApprovedRepository.create).toHaveBeenCalledWith({ + commandId: '0c38359b7a35c755573659d797afec315bb0e51374a056745abd9764715a15da', + txHash: 'txHash', + status: ContractCallApprovedStatus.PENDING, + sourceAddress: 'sourceAddress', + sourceChain: 'ethereum', + contractAddress: 'erd1qqqqqqqqqqqqqpgqzqvm5ywqqf524efwrhr039tjs29w0qltkklsa05pk7', + payloadHash: 'ebc84cbd75ba5516bf45e7024a9e12bc3c5c880f73e3a5beca7ebba52b2867a7', + payload: Buffer.from('payload'), + retry: 0, + }); + }); + + it('Should throw error can not save in database', async () => { + grpcService.getPayload.mockReturnValueOnce(Promise.resolve(Buffer.from('payload'))); + + contractCallApprovedRepository.create.mockReturnValueOnce(Promise.resolve(null)); + + await expect(service.handleEvent(rawEvent)).rejects.toThrow(); + + expect(gatewayContract.decodeContractCallApprovedEvent).toHaveBeenCalledTimes(1); + expect(gatewayContract.decodeContractCallApprovedEvent).toHaveBeenCalledWith( + TransactionEvent.fromHttpResponse(rawEvent), + ); + expect(grpcService.getPayload).toHaveBeenCalledTimes(1); + expect(grpcService.getPayload).toHaveBeenCalledWith(contractCallApprovedEvent.payloadHash); + expect(contractCallApprovedRepository.create).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/apps/mvx-event-processor/src/processors/contract-call.processor.ts b/apps/mvx-event-processor/src/processors/gateway.processor.ts similarity index 61% rename from apps/mvx-event-processor/src/processors/contract-call.processor.ts rename to apps/mvx-event-processor/src/processors/gateway.processor.ts index 8822c04..018ed6e 100644 --- a/apps/mvx-event-processor/src/processors/contract-call.processor.ts +++ b/apps/mvx-event-processor/src/processors/gateway.processor.ts @@ -4,23 +4,25 @@ import { GatewayContract } from '@mvx-monorepo/common/contracts/gateway.contract import { TransactionEvent } from '@multiversx/sdk-network-providers/out'; import { ContractCallEventRepository } from '@mvx-monorepo/common/database/repository/contract-call-event.repository'; import { ApiConfigService } from '@mvx-monorepo/common'; -import { ContractCallEventStatus } from '@prisma/client'; +import { ContractCallApprovedStatus, ContractCallEventStatus } from '@prisma/client'; import { GrpcService } from '@mvx-monorepo/common/grpc/grpc.service'; import { ProcessorInterface } from './entities/processor.interface'; import { EventIdentifiers, Events } from '@mvx-monorepo/common/utils/event.enum'; import { BinaryUtils } from '@multiversx/sdk-nestjs-common'; +import { ContractCallApprovedRepository } from '@mvx-monorepo/common/database/repository/contract-call-approved.repository'; // order/logIndex is unsupported since we can't easily get it in the relayer // so we use a sufficiently large u32 value here instead const UNSUPPORTED_LOG_INDEX: number = 999_999; @Injectable() -export class ContractCallProcessor implements ProcessorInterface { +export class GatewayProcessor implements ProcessorInterface { private sourceChain: string; constructor( private readonly gatewayContract: GatewayContract, private readonly contractCallEventRepository: ContractCallEventRepository, + private readonly contractCallApprovedRepository: ContractCallApprovedRepository, private readonly grpcService: GrpcService, apiConfigService: ApiConfigService, ) { @@ -28,13 +30,22 @@ export class ContractCallProcessor implements ProcessorInterface { } async handleEvent(rawEvent: NotifierEvent) { - if ( - rawEvent.identifier !== EventIdentifiers.CALL_CONTRACT || - BinaryUtils.base64Decode(rawEvent.topics[0]) !== Events.CONTRACT_CALL_EVENT - ) { + const eventName = BinaryUtils.base64Decode(rawEvent.topics[0]); + + if (rawEvent.identifier === EventIdentifiers.CALL_CONTRACT && eventName === Events.CONTRACT_CALL_EVENT) { + await this.handleContractCallEvent(rawEvent); + + return; + } + + if (rawEvent.identifier === EventIdentifiers.EXECUTE && eventName === Events.CONTRACT_CALL_APPROVED_EVENT) { + await this.handleContractCallApprovedEvent(rawEvent); + return; } + } + private async handleContractCallEvent(rawEvent: NotifierEvent) { const event = this.gatewayContract.decodeContractCallEvent(TransactionEvent.fromHttpResponse(rawEvent)); const contractCallEvent = await this.contractCallEventRepository.create({ @@ -62,4 +73,26 @@ export class ContractCallProcessor implements ProcessorInterface { // Verify endpoint. After it was sent, it can be marked as APPROVED // GasPaid will remain as PENDING status for now } + + private async handleContractCallApprovedEvent(rawEvent: NotifierEvent) { + const event = this.gatewayContract.decodeContractCallApprovedEvent(TransactionEvent.fromHttpResponse(rawEvent)); + + const payload = await this.grpcService.getPayload(event.payloadHash); + + const contractCallApproved = await this.contractCallApprovedRepository.create({ + commandId: event.commandId, + txHash: rawEvent.txHash, + status: ContractCallApprovedStatus.PENDING, + sourceAddress: event.sourceAddress, + sourceChain: event.sourceChain, + contractAddress: event.contractAddress.bech32(), + payloadHash: event.payloadHash, + payload, + retry: 0, + }); + + if (!contractCallApproved) { + throw new Error(`Couldn't save contract call approved to database for hash ${rawEvent.txHash}`); + } + } } diff --git a/apps/mvx-event-processor/src/processors/index.ts b/apps/mvx-event-processor/src/processors/index.ts index 9293663..036f95a 100644 --- a/apps/mvx-event-processor/src/processors/index.ts +++ b/apps/mvx-event-processor/src/processors/index.ts @@ -1,3 +1,3 @@ export * from './processors.module'; -export * from './contract-call.processor'; +export * from './gateway.processor'; export * from './gas-service.processor'; diff --git a/apps/mvx-event-processor/src/processors/processors.module.ts b/apps/mvx-event-processor/src/processors/processors.module.ts index e22c0cf..0a3730c 100644 --- a/apps/mvx-event-processor/src/processors/processors.module.ts +++ b/apps/mvx-event-processor/src/processors/processors.module.ts @@ -1,5 +1,5 @@ import { Module } from '@nestjs/common'; -import { ContractCallProcessor } from './contract-call.processor'; +import { GatewayProcessor } from './gateway.processor'; import { ContractsModule } from '@mvx-monorepo/common/contracts/contracts.module'; import { DatabaseModule } from '@mvx-monorepo/common'; import { GrpcModule } from '@mvx-monorepo/common/grpc/grpc.module'; @@ -7,7 +7,7 @@ import { GasServiceProcessor } from './gas-service.processor'; @Module({ imports: [ContractsModule, DatabaseModule, GrpcModule], - providers: [ContractCallProcessor, GasServiceProcessor], - exports: [ContractCallProcessor, GasServiceProcessor], + providers: [GatewayProcessor, GasServiceProcessor], + exports: [GatewayProcessor, GasServiceProcessor], }) export class ProcessorsModule {} diff --git a/libs/common/src/assets/gateway.abi.json b/libs/common/src/assets/gateway.abi.json index 800afbc..8da5f98 100644 --- a/libs/common/src/assets/gateway.abi.json +++ b/libs/common/src/assets/gateway.abi.json @@ -13,7 +13,7 @@ }, "framework": { "name": "multiversx-sc", - "version": "0.43.5" + "version": "0.43.4" } }, "name": "Gateway", @@ -220,6 +220,16 @@ } ] }, + { + "identifier": "contract_call_executed_event", + "inputs": [ + { + "name": "command_id", + "type": "array32", + "indexed": true + } + ] + }, { "identifier": "operatorship_transferred_event", "inputs": [ diff --git a/libs/common/src/contracts/entities/contract-call-event.ts b/libs/common/src/contracts/entities/contract-call-event.ts deleted file mode 100644 index bbd86e0..0000000 --- a/libs/common/src/contracts/entities/contract-call-event.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { IAddress } from '@multiversx/sdk-core/out'; - -export interface ContractCallEvent { - sender: IAddress, - destinationChain: string, - destinationAddress: string, - data: { - payloadHash: string, - payload: Buffer, - } -} diff --git a/libs/common/src/contracts/entities/gateway-events.ts b/libs/common/src/contracts/entities/gateway-events.ts new file mode 100644 index 0000000..11639d1 --- /dev/null +++ b/libs/common/src/contracts/entities/gateway-events.ts @@ -0,0 +1,19 @@ +import { IAddress } from '@multiversx/sdk-core/out'; + +export interface ContractCallEvent { + sender: IAddress; + destinationChain: string; + destinationAddress: string; + data: { + payloadHash: string; + payload: Buffer; + }; +} + +export interface ContractCallApprovedEvent { + commandId: string; + sourceChain: string; + sourceAddress: string; + contractAddress: IAddress; + payloadHash: string; +} diff --git a/libs/common/src/contracts/gas-service.contract.ts b/libs/common/src/contracts/gas-service.contract.ts index cc467d9..0694a37 100644 --- a/libs/common/src/contracts/gas-service.contract.ts +++ b/libs/common/src/contracts/gas-service.contract.ts @@ -2,13 +2,13 @@ import { AbiRegistry, ResultsParser, SmartContract } from '@multiversx/sdk-core/ import { Injectable, Logger } from '@nestjs/common'; import { Events } from '../utils/event.enum'; import { TransactionEvent } from '@multiversx/sdk-network-providers/out'; -import BigNumber from 'bignumber.js'; import { GasAddedEvent, GasPaidForContractCallEvent, RefundedEvent, } from '@mvx-monorepo/common/contracts/entities/gas-service-events'; import { CONSTANTS } from '@mvx-monorepo/common/utils/constants.enum'; +import { DecodingUtils } from '@mvx-monorepo/common/utils/decoding.utils'; @Injectable() export class GasServiceContract { @@ -33,7 +33,7 @@ export class GasServiceContract { destinationChain: outcome.destination_chain.toString(), destinationAddress: outcome.destination_contract_address.toString(), data: { - payloadHash: Buffer.from(outcome.data.hash.map((number: BigNumber) => number.toNumber())).toString('hex'), + payloadHash: DecodingUtils.decodeKeccak256Hash(outcome.data.hash), gasToken: outcome.data.gas_token.toString(), gasFeeAmount: outcome.data.gas_fee_amount, refundAddress: outcome.data.refund_address, @@ -50,7 +50,7 @@ export class GasServiceContract { destinationChain: outcome.destination_chain.toString(), destinationAddress: outcome.destination_contract_address.toString(), data: { - payloadHash: Buffer.from(outcome.data.hash.map((number: BigNumber) => number.toNumber())).toString('hex'), + payloadHash: DecodingUtils.decodeKeccak256Hash(outcome.data.hash), gasToken: null, gasFeeAmount: outcome.data.value, refundAddress: outcome.data.refund_address, diff --git a/libs/common/src/contracts/gateway.contract.spec.ts b/libs/common/src/contracts/gateway.contract.spec.ts index 5e075c4..f342c1b 100644 --- a/libs/common/src/contracts/gateway.contract.spec.ts +++ b/libs/common/src/contracts/gateway.contract.spec.ts @@ -18,7 +18,7 @@ describe('GatewayContract', () => { beforeEach(async () => { smartContract = createMock(); - abi = AbiRegistry.create(gatewayAbi); + abi = AbiRegistry.create(gatewayAbi); // use real Gateway contract abi resultsParser = new ResultsParser(); const moduleRef = await Test.createTestingModule({ @@ -87,4 +87,69 @@ describe('GatewayContract', () => { expect(() => contract.decodeContractCallEvent(event)).toThrow(); }); }); + + describe('decodeContractCallApprovedEvent', () => { + const rawEvent: NotifierEvent = { + txHash: 'txHash', + address: 'mockGatewayAddress', + identifier: 'callContract', + data: '', + topics: [ + BinaryUtils.base64Encode(Events.CONTRACT_CALL_APPROVED_EVENT), + Buffer.from('0c38359b7a35c755573659d797afec315bb0e51374a056745abd9764715a15da', 'hex').toString('base64'), + BinaryUtils.base64Encode('ethereum'), + BinaryUtils.base64Encode('sourceAddress'), + Buffer.from( + Address.fromBech32('erd1qqqqqqqqqqqqqpgqzqvm5ywqqf524efwrhr039tjs29w0qltkklsa05pk7').hex(), + 'hex', + ).toString('base64'), + Buffer.from('ebc84cbd75ba5516bf45e7024a9e12bc3c5c880f73e3a5beca7ebba52b2867a7', 'hex').toString('base64'), + ], + }; + const event = TransactionEvent.fromHttpResponse(rawEvent); + + it('Should decode event', () => { + const result = contract.decodeContractCallApprovedEvent(event); + + expect(result).toEqual({ + commandId: '0c38359b7a35c755573659d797afec315bb0e51374a056745abd9764715a15da', + sourceChain: 'ethereum', + sourceAddress: 'sourceAddress', + contractAddress: Address.fromBech32('erd1qqqqqqqqqqqqqpgqzqvm5ywqqf524efwrhr039tjs29w0qltkklsa05pk7'), + payloadHash: 'ebc84cbd75ba5516bf45e7024a9e12bc3c5c880f73e3a5beca7ebba52b2867a7', + }); + }); + + it('Should throw error while decoding', () => { + event.topics = []; + + expect(() => contract.decodeContractCallApprovedEvent(event)).toThrow(); + }); + }); + + describe('decodeContractCallExecutedEvent', () => { + const rawEvent: NotifierEvent = { + txHash: 'txHash', + address: 'mockGatewayAddress', + identifier: 'callContract', + data: '', + topics: [ + BinaryUtils.base64Encode(Events.CONTRACT_CALL_EXECUTED_EVENT), + Buffer.from('0c38359b7a35c755573659d797afec315bb0e51374a056745abd9764715a15da', 'hex').toString('base64'), + ], + }; + const event = TransactionEvent.fromHttpResponse(rawEvent); + + it('Should decode event', () => { + const result = contract.decodeContractCallExecutedEvent(event); + + expect(result).toEqual('0c38359b7a35c755573659d797afec315bb0e51374a056745abd9764715a15da'); + }); + + it('Should throw error while decoding', () => { + event.topics = []; + + expect(() => contract.decodeContractCallExecutedEvent(event)).toThrow(); + }); + }); }); diff --git a/libs/common/src/contracts/gateway.contract.ts b/libs/common/src/contracts/gateway.contract.ts index c097e1d..5c14b5f 100644 --- a/libs/common/src/contracts/gateway.contract.ts +++ b/libs/common/src/contracts/gateway.contract.ts @@ -2,8 +2,8 @@ import { AbiRegistry, ResultsParser, SmartContract } from '@multiversx/sdk-core/ import { Injectable, Logger } from '@nestjs/common'; import { Events } from '../utils/event.enum'; import { TransactionEvent } from '@multiversx/sdk-network-providers/out'; -import { ContractCallEvent } from '@mvx-monorepo/common/contracts/entities/contract-call-event'; -import BigNumber from 'bignumber.js'; +import { ContractCallApprovedEvent, ContractCallEvent } from '@mvx-monorepo/common/contracts/entities/gateway-events'; +import { DecodingUtils } from '@mvx-monorepo/common/utils/decoding.utils'; @Injectable() export class GatewayContract { @@ -28,9 +28,29 @@ export class GatewayContract { destinationChain: outcome.destination_chain.toString(), destinationAddress: outcome.destination_contract_address.toString(), data: { - payloadHash: Buffer.from(outcome.data.hash.map((number: BigNumber) => number.toNumber())).toString('hex'), + payloadHash: DecodingUtils.decodeKeccak256Hash(outcome.data.hash), payload: outcome.data.payload, }, }; } + + decodeContractCallApprovedEvent(event: TransactionEvent): ContractCallApprovedEvent { + const eventDefinition = this.abi.getEvent(Events.CONTRACT_CALL_APPROVED_EVENT); + const outcome = this.resultsParser.parseEvent(event, eventDefinition); + + return { + commandId: DecodingUtils.decodeKeccak256Hash(outcome.command_id), + sourceChain: outcome.source_chain.toString(), + sourceAddress: outcome.source_address.toString(), + contractAddress: outcome.contract_address, + payloadHash: DecodingUtils.decodeKeccak256Hash(outcome.payload_hash), + }; + } + + decodeContractCallExecutedEvent(event: TransactionEvent): string { + const eventDefinition = this.abi.getEvent(Events.CONTRACT_CALL_EXECUTED_EVENT); + const outcome = this.resultsParser.parseEvent(event, eventDefinition); + + return DecodingUtils.decodeKeccak256Hash(outcome.command_id); + } } diff --git a/libs/common/src/database/database.module.ts b/libs/common/src/database/database.module.ts index 40802f1..00d22fe 100644 --- a/libs/common/src/database/database.module.ts +++ b/libs/common/src/database/database.module.ts @@ -2,9 +2,10 @@ import { Module } from '@nestjs/common'; import { PrismaService } from '@mvx-monorepo/common/database/prisma.service'; import { ContractCallEventRepository } from '@mvx-monorepo/common/database/repository/contract-call-event.repository'; import { GasPaidRepository } from '@mvx-monorepo/common/database/repository/gas-paid.repository'; +import { ContractCallApprovedRepository } from '@mvx-monorepo/common/database/repository/contract-call-approved.repository'; @Module({ - providers: [PrismaService, ContractCallEventRepository, GasPaidRepository], - exports: [ContractCallEventRepository, GasPaidRepository], + providers: [PrismaService, ContractCallEventRepository, GasPaidRepository, ContractCallApprovedRepository], + exports: [ContractCallEventRepository, GasPaidRepository, ContractCallApprovedRepository], }) export class DatabaseModule {} diff --git a/libs/common/src/database/repository/contract-call-approved.repository.ts b/libs/common/src/database/repository/contract-call-approved.repository.ts new file mode 100644 index 0000000..4b954b0 --- /dev/null +++ b/libs/common/src/database/repository/contract-call-approved.repository.ts @@ -0,0 +1,14 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '@mvx-monorepo/common/database/prisma.service'; +import { ContractCallApproved, Prisma } from '@prisma/client'; + +@Injectable() +export class ContractCallApprovedRepository { + constructor(private readonly prisma: PrismaService) {} + + create(data: Prisma.ContractCallApprovedCreateInput): Promise { + return this.prisma.contractCallApproved.create({ + data, + }); + } +} diff --git a/libs/common/src/grpc/grpc.service.ts b/libs/common/src/grpc/grpc.service.ts index 4f1debd..d642fb4 100644 --- a/libs/common/src/grpc/grpc.service.ts +++ b/libs/common/src/grpc/grpc.service.ts @@ -37,4 +37,12 @@ export class GrpcService implements OnModuleInit { const result = this.relayerService.verify(replaySubject); await firstValueFrom(result); } + + async getPayload(payloadHash: string): Promise { + const result = await this.relayerService.getPayload({ + hash: Buffer.from(payloadHash, 'hex'), + }); + + return Buffer.from(result.payload); + } } diff --git a/libs/common/src/utils/decoding.utils.ts b/libs/common/src/utils/decoding.utils.ts new file mode 100644 index 0000000..4fe66a9 --- /dev/null +++ b/libs/common/src/utils/decoding.utils.ts @@ -0,0 +1,7 @@ +import BigNumber from 'bignumber.js'; + +export class DecodingUtils { + static decodeKeccak256Hash(hash: BigNumber[]): string { + return Buffer.from(hash.map((number: BigNumber) => number.toNumber())).toString('hex'); + } +} diff --git a/libs/common/src/utils/event.enum.ts b/libs/common/src/utils/event.enum.ts index 8e9540c..10a1766 100644 --- a/libs/common/src/utils/event.enum.ts +++ b/libs/common/src/utils/event.enum.ts @@ -1,9 +1,13 @@ export enum EventIdentifiers { CALL_CONTRACT = 'callContract', + EXECUTE = 'execute', + VALIDATE_CONTRACT_CALL = 'validateContractCall', } export enum Events { CONTRACT_CALL_EVENT = 'contract_call_event', + CONTRACT_CALL_APPROVED_EVENT = 'contract_call_approved_event', + CONTRACT_CALL_EXECUTED_EVENT = 'contract_call_executed_event', GAS_PAID_FOR_CONTRACT_CALL_EVENT = 'gas_paid_for_contract_call_event', NATIVE_GAS_PAID_FOR_CONTRACT_CALL_EVENT = 'native_gas_paid_for_contract_call_event', diff --git a/prisma/migrations/20231211131553_add_contract_call_approved/migration.sql b/prisma/migrations/20231211131553_add_contract_call_approved/migration.sql new file mode 100644 index 0000000..5c08d91 --- /dev/null +++ b/prisma/migrations/20231211131553_add_contract_call_approved/migration.sql @@ -0,0 +1,23 @@ +-- CreateEnum +CREATE TYPE "ContractCallApprovedStatus" AS ENUM ('PENDING', 'SUCCESS', 'FAILED'); + +-- CreateTable +CREATE TABLE "ContractCallApproved" ( + "commandId" VARCHAR(64) NOT NULL, + "txHash" VARCHAR(64) NOT NULL, + "status" "ContractCallApprovedStatus" NOT NULL, + "sourceAddress" VARCHAR(255) NOT NULL, + "sourceChain" VARCHAR(255) NOT NULL, + "contractAddress" VARCHAR(62) NOT NULL, + "payloadHash" VARCHAR(64) NOT NULL, + "payload" BYTEA NOT NULL, + "executeTxHash" VARCHAR(64), + "retry" SMALLINT NOT NULL, + "createdAt" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "ContractCallApproved_pkey" PRIMARY KEY ("commandId") +); + +-- CreateIndex +CREATE UNIQUE INDEX "ContractCallApproved_txHash_key" ON "ContractCallApproved"("txHash"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 49cb6dd..46ed028 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -11,7 +11,7 @@ datasource db { } model ContractCallEvent { - id String @id @db.VarChar(255) // should be formatted as [source_chain]:[unique identifier], i.e. Ethereum:0x74ac0205b1f8f51023942856145182f0e6fdd41ccb2c8058bf2d89fc67564d56:0 + id String @id @db.VarChar(255) // should be formatted as [source_chain]:[unique identifier]:[log index], i.e. Ethereum:0x74ac0205b1f8f51023942856145182f0e6fdd41ccb2c8058bf2d89fc67564d56:0 txHash String @unique @db.VarChar(64) eventIndex Int @db.SmallInt status ContractCallEventStatus @@ -60,3 +60,24 @@ enum GasPaidStatus { SUCCESS FAILED } + +model ContractCallApproved { + commandId String @id @db.VarChar(64) + txHash String @unique @db.VarChar(64) + status ContractCallApprovedStatus + sourceAddress String @db.VarChar(255) + sourceChain String @db.VarChar(255) + contractAddress String @db.VarChar(62) + payloadHash String @db.VarChar(64) + payload Bytes + executeTxHash String? @db.VarChar(64) + retry Int @db.SmallInt + createdAt DateTime @default(now()) @db.Timestamp(6) + updatedAt DateTime @default(now()) @updatedAt @db.Timestamp(6) +} + +enum ContractCallApprovedStatus { + PENDING + SUCCESS + FAILED +} From c98880edf3a5193e10bc4e9e3512d142fc47c566 Mon Sep 17 00:00:00 2001 From: Rares <6453351+raresserban@users.noreply.github.com> Date: Mon, 11 Dec 2023 17:24:15 +0200 Subject: [PATCH 09/33] Start processing and sending of call contract approved transactions. --- .env.example | 2 + ...call-contract-approved.processor.module.ts | 16 +++ ...all-contract-approved.processor.service.ts | 93 +++++++++++++++ apps/mvx-event-processor/src/main.ts | 2 + libs/common/src/config/api.config.service.ts | 9 ++ libs/common/src/contracts/contracts.module.ts | 11 ++ .../contract-call-approved.repository.ts | 16 ++- libs/common/src/grpc/grpc.module.ts | 4 +- libs/common/src/grpc/grpc.service.ts | 4 +- libs/common/src/utils/provider.enum.ts | 4 +- package-lock.json | 107 +++++++++++++++++- package.json | 1 + 12 files changed, 257 insertions(+), 12 deletions(-) create mode 100644 apps/mvx-event-processor/src/call-contract-approved-processor/call-contract-approved.processor.module.ts create mode 100644 apps/mvx-event-processor/src/call-contract-approved-processor/call-contract-approved.processor.service.ts diff --git a/.env.example b/.env.example index 3615506..75b68e4 100644 --- a/.env.example +++ b/.env.example @@ -13,3 +13,5 @@ CONTRACT_GAS_SERVICE= AXELAR_API_URL= SOURCE_CHAIN_NAME=multiversx-D + +WALLET_MNEMONIC= diff --git a/apps/mvx-event-processor/src/call-contract-approved-processor/call-contract-approved.processor.module.ts b/apps/mvx-event-processor/src/call-contract-approved-processor/call-contract-approved.processor.module.ts new file mode 100644 index 0000000..d8decf5 --- /dev/null +++ b/apps/mvx-event-processor/src/call-contract-approved-processor/call-contract-approved.processor.module.ts @@ -0,0 +1,16 @@ +import { Module } from '@nestjs/common'; +import { ScheduleModule } from '@nestjs/schedule'; +import { DatabaseModule, DynamicModuleUtils } from '@mvx-monorepo/common'; +import { ContractsModule } from '@mvx-monorepo/common/contracts/contracts.module'; +import { CallContractApprovedProcessorService } from './call-contract-approved.processor.service'; + +@Module({ + imports: [ + ScheduleModule.forRoot(), + DynamicModuleUtils.getCachingModule(), + DatabaseModule, + ContractsModule, + ], + providers: [CallContractApprovedProcessorService], +}) +export class CallContractApprovedProcessorModule {} diff --git a/apps/mvx-event-processor/src/call-contract-approved-processor/call-contract-approved.processor.service.ts b/apps/mvx-event-processor/src/call-contract-approved-processor/call-contract-approved.processor.service.ts new file mode 100644 index 0000000..947f3cf --- /dev/null +++ b/apps/mvx-event-processor/src/call-contract-approved-processor/call-contract-approved.processor.service.ts @@ -0,0 +1,93 @@ +import { Inject, Injectable, Logger } from '@nestjs/common'; +import { Cron } from '@nestjs/schedule'; +import { Locker } from '@multiversx/sdk-nestjs-common'; +import { ContractCallApprovedRepository } from '@mvx-monorepo/common/database/repository/contract-call-approved.repository'; +import { ProviderKeys } from '@mvx-monorepo/common/utils/provider.enum'; +import { UserSigner } from '@multiversx/sdk-wallet/out'; +import { ProxyNetworkProvider } from '@multiversx/sdk-network-providers/out'; +import { Address, BytesValue, SmartContract, Transaction } from '@multiversx/sdk-core/out'; +import { ContractCallApproved } from '@prisma/client'; + +@Injectable() +export class CallContractApprovedProcessorService { + private readonly logger: Logger; + + constructor( + private readonly callContractApprovedRepository: ContractCallApprovedRepository, + @Inject(ProviderKeys.WALLET_SIGNER) private readonly walletSigner: UserSigner, + private readonly proxy: ProxyNetworkProvider, + ) { + this.logger = new Logger(CallContractApprovedProcessorService.name); + } + + @Cron('*/30 * * * * *') + async processPendingCallContractApproved() { + await Locker.lock('processPendingCallContractApproved', async () => { + let accountNonce = null; + + let page = 0; + let entries; + while ((entries = await this.callContractApprovedRepository.findPendingNoRetries(page))?.length) { + if (accountNonce === null) { + accountNonce = await this.getAccountNonce(); + } + + this.logger.log(`Found ${entries.length} CallContractApproved transactions to execute`); + + let transactionsToSend = []; + for (const callContractApproved of entries) { + this.logger.debug( + `Trying to execute CallContractApproved transaction with commandId ${callContractApproved.commandId}`, + ); + + const transaction = this.buildTransaction(callContractApproved); + transaction.setNonce(accountNonce); + + accountNonce++; + + transactionsToSend.push(transaction); + + // TODO: Verify that 10 is ok max for gateway + if (transactionsToSend.length === 10) { + await this.sendTransactionsAndLog(transactionsToSend); + + transactionsToSend = []; + } + } + } + }); + } + + private async getAccountNonce(): Promise { + const accountOnNetwork = await this.proxy.getAccount(this.walletSigner.getAddress()); + + return accountOnNetwork.nonce; + } + + // TODO: + private buildTransaction(callContractApproved: ContractCallApproved): Transaction { + const contract = new SmartContract({ address: new Address(callContractApproved.contractAddress) }); + + return contract.call({ + caller: this.walletSigner.getAddress(), + func: 'execute', + gasLimit: 100_000_000, // TODO + args: [ + new BytesValue(callContractApproved.payload), + ], + chainID: 'D', // TODO, + }); + } + + private async sendTransactionsAndLog(transactions: Transaction[]) { + try { + await this.proxy.sendTransactions(transactions); + + this.logger.log( + `Send ${transactions.length} transactions to proxy: ${transactions.map((trans) => trans.getHash())}`, + ); + } catch (e) { + this.logger.error(`Can not send CallContractApproved transactions to proxy... ${e}`); + } + } +} diff --git a/apps/mvx-event-processor/src/main.ts b/apps/mvx-event-processor/src/main.ts index 2b285ef..042d72e 100644 --- a/apps/mvx-event-processor/src/main.ts +++ b/apps/mvx-event-processor/src/main.ts @@ -1,9 +1,11 @@ import 'module-alias/register'; import { NestFactory } from '@nestjs/core'; import { EventProcessorModule } from './event-processor'; +import { CallContractApprovedProcessorModule } from './call-contract-approved-processor/call-contract-approved.processor.module'; async function bootstrap() { await NestFactory.createApplicationContext(EventProcessorModule); + await NestFactory.createApplicationContext(CallContractApprovedProcessorModule); } // eslint-disable-next-line @typescript-eslint/no-floating-promises diff --git a/libs/common/src/config/api.config.service.ts b/libs/common/src/config/api.config.service.ts index 0cb0242..7ff5d5b 100644 --- a/libs/common/src/config/api.config.service.ts +++ b/libs/common/src/config/api.config.service.ts @@ -93,6 +93,15 @@ export class ApiConfigService { return sourceChainName; } + getWalletMnemonic(): string { + const walletMnemonic = this.configService.get('WALLET_MNEMONIC'); + if (!walletMnemonic) { + throw new Error('No Wallet Mnemonic present'); + } + + return walletMnemonic; + } + getPoolLimit(): number { return this.configService.get('CACHING_POOL_LIMIT') ?? 100; } diff --git a/libs/common/src/contracts/contracts.module.ts b/libs/common/src/contracts/contracts.module.ts index ada9dc8..c49fb0a 100644 --- a/libs/common/src/contracts/contracts.module.ts +++ b/libs/common/src/contracts/contracts.module.ts @@ -6,6 +6,8 @@ import { ResultsParser } from '@multiversx/sdk-core/out'; import { ContractLoader } from '@mvx-monorepo/common/contracts/contract.loader'; import { join } from 'path'; import { GasServiceContract } from '@mvx-monorepo/common/contracts/gas-service.contract'; +import { ProviderKeys } from '@mvx-monorepo/common/utils/provider.enum'; +import { Mnemonic, UserSigner } from '@multiversx/sdk-wallet/out'; @Module({ imports: [], @@ -66,6 +68,15 @@ import { GasServiceContract } from '@mvx-monorepo/common/contracts/gas-service.c }, inject: [ApiConfigService, ResultsParser], }, + { + provide: ProviderKeys.WALLET_SIGNER, + useFactory: (apiConfigService: ApiConfigService) => { + const mnemonic = Mnemonic.fromString(apiConfigService.getWalletMnemonic()).deriveKey(0); + + return new UserSigner(mnemonic); + }, + inject: [ApiConfigService, ResultsParser], + }, ], exports: [GatewayContract, GasServiceContract], }) diff --git a/libs/common/src/database/repository/contract-call-approved.repository.ts b/libs/common/src/database/repository/contract-call-approved.repository.ts index 4b954b0..165ae88 100644 --- a/libs/common/src/database/repository/contract-call-approved.repository.ts +++ b/libs/common/src/database/repository/contract-call-approved.repository.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { PrismaService } from '@mvx-monorepo/common/database/prisma.service'; -import { ContractCallApproved, Prisma } from '@prisma/client'; +import { ContractCallApproved, ContractCallApprovedStatus, Prisma } from '@prisma/client'; @Injectable() export class ContractCallApprovedRepository { @@ -11,4 +11,18 @@ export class ContractCallApprovedRepository { data, }); } + + findPendingNoRetries(page: number = 0, take: number = 10): Promise { + return this.prisma.contractCallApproved.findMany({ + where: { + status: ContractCallApprovedStatus.PENDING, + retry: 0, + }, + orderBy: { + createdAt: 'desc', + }, + skip: page * take, + take, + }); + } } diff --git a/libs/common/src/grpc/grpc.module.ts b/libs/common/src/grpc/grpc.module.ts index 3590090..58f092d 100644 --- a/libs/common/src/grpc/grpc.module.ts +++ b/libs/common/src/grpc/grpc.module.ts @@ -2,7 +2,7 @@ import { Module } from '@nestjs/common'; import { ClientsModule, Transport } from '@nestjs/microservices'; import { ApiConfigModule, ApiConfigService } from '@mvx-monorepo/common'; import { join } from 'path'; -import { PROVIDER_KEYS } from '@mvx-monorepo/common/utils/provider.enum'; +import { ProviderKeys } from '@mvx-monorepo/common/utils/provider.enum'; import { GrpcService } from '@mvx-monorepo/common/grpc/grpc.service'; import { protobufPackage } from '@mvx-monorepo/common/grpc/entities/relayer'; @@ -10,7 +10,7 @@ import { protobufPackage } from '@mvx-monorepo/common/grpc/entities/relayer'; imports: [ ClientsModule.registerAsync([ { - name: PROVIDER_KEYS.AXELAR_GRPC_CLIENT, + name: ProviderKeys.AXELAR_GRPC_CLIENT, imports: [ApiConfigModule], useFactory: (apiConfigService: ApiConfigService) => { return { diff --git a/libs/common/src/grpc/grpc.service.ts b/libs/common/src/grpc/grpc.service.ts index d642fb4..9144a35 100644 --- a/libs/common/src/grpc/grpc.service.ts +++ b/libs/common/src/grpc/grpc.service.ts @@ -1,5 +1,5 @@ import { Inject, Injectable, OnModuleInit } from '@nestjs/common'; -import { PROVIDER_KEYS } from '@mvx-monorepo/common/utils/provider.enum'; +import { ProviderKeys } from '@mvx-monorepo/common/utils/provider.enum'; import { ClientGrpc } from '@nestjs/microservices'; import { ContractCallEvent } from '@prisma/client'; import { Relayer, VerifyRequest } from '@mvx-monorepo/common/grpc/entities/relayer'; @@ -12,7 +12,7 @@ export class GrpcService implements OnModuleInit { // @ts-ignore private relayerService: Relayer; - constructor(@Inject(PROVIDER_KEYS.AXELAR_GRPC_CLIENT) private readonly client: ClientGrpc) {} + constructor(@Inject(ProviderKeys.AXELAR_GRPC_CLIENT) private readonly client: ClientGrpc) {} onModuleInit() { this.relayerService = this.client.getService(RELAYER_SERVICE); diff --git a/libs/common/src/utils/provider.enum.ts b/libs/common/src/utils/provider.enum.ts index fd750e9..2ef1e9e 100644 --- a/libs/common/src/utils/provider.enum.ts +++ b/libs/common/src/utils/provider.enum.ts @@ -1,3 +1,5 @@ -export enum PROVIDER_KEYS { +export enum ProviderKeys { AXELAR_GRPC_CLIENT = 'AXELAR_GRPC_CLIENT', + + WALLET_SIGNER = 'WALLET_SIGNER', } diff --git a/package-lock.json b/package-lock.json index 23b8b3e..8943683 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "@multiversx/sdk-nestjs-redis": "2.4.0", "@multiversx/sdk-network-providers": "^2.2.0", "@multiversx/sdk-transaction-processor": "^0.1.30", + "@multiversx/sdk-wallet": "^4.2.0", "@nestjs/bull": "^10.0.1", "@nestjs/common": "10.2.0", "@nestjs/config": "3.0.1", @@ -1912,6 +1913,36 @@ "@nestjs/core": "^10.x" } }, + "node_modules/@multiversx/sdk-nestjs-http/node_modules/@multiversx/sdk-wallet": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@multiversx/sdk-wallet/-/sdk-wallet-3.0.0.tgz", + "integrity": "sha512-nDVBtva1mpfutXA8TfUnpdeFqhY9O+deNU3U/Z41yPBcju1trHDC9gcKPyQqcQ3qjG/6LwEXmIm7Dc5fIsvVjg==", + "dependencies": { + "@multiversx/sdk-bls-wasm": "0.3.5", + "bech32": "1.1.4", + "bip39": "3.0.2", + "blake2b": "2.1.3", + "ed25519-hd-key": "1.1.2", + "ed2curve": "0.3.0", + "keccak": "3.0.1", + "scryptsy": "2.1.0", + "tweetnacl": "1.0.3", + "uuid": "8.3.2" + } + }, + "node_modules/@multiversx/sdk-nestjs-http/node_modules/keccak": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.1.tgz", + "integrity": "sha512-epq90L9jlFWCW7+pQa6JOnKn2Xgl2mtI664seYR6MHskvI9agt7AnDqmAlp9TqU4/caMYbA08Hi5DMZAl5zdkA==", + "hasInstallScript": true, + "dependencies": { + "node-addon-api": "^2.0.0", + "node-gyp-build": "^4.2.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/@multiversx/sdk-nestjs-monitoring": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/@multiversx/sdk-nestjs-monitoring/-/sdk-nestjs-monitoring-2.4.0.tgz", @@ -2001,11 +2032,13 @@ } }, "node_modules/@multiversx/sdk-wallet": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@multiversx/sdk-wallet/-/sdk-wallet-3.0.0.tgz", - "integrity": "sha512-nDVBtva1mpfutXA8TfUnpdeFqhY9O+deNU3U/Z41yPBcju1trHDC9gcKPyQqcQ3qjG/6LwEXmIm7Dc5fIsvVjg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@multiversx/sdk-wallet/-/sdk-wallet-4.2.0.tgz", + "integrity": "sha512-EjSb9AnqMcpmDjZ7ebkUpOzpTfxj1plTuVXwZ6AaqJsdpxMfrE2izbPy18+bg5xFlr8V27wYZcW8zOhkBR50BA==", "dependencies": { "@multiversx/sdk-bls-wasm": "0.3.5", + "@noble/ed25519": "1.7.3", + "@noble/hashes": "1.3.0", "bech32": "1.1.4", "bip39": "3.0.2", "blake2b": "2.1.3", @@ -2663,6 +2696,28 @@ "optional": true, "peer": true }, + "node_modules/@noble/ed25519": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@noble/ed25519/-/ed25519-1.7.3.tgz", + "integrity": "sha512-iR8GBkDt0Q3GyaVcIu7mSsVIqnFbkbRzGLWlvhwunacoLwt4J3swfKhfaM6rN6WY+TBGoYT1GtT1mIh2/jGbRQ==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ] + }, + "node_modules/@noble/hashes": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.0.tgz", + "integrity": "sha512-ilHEACi9DwqJB0pw7kv+Apvh50jiiSyR/cQ3y4W7lOR5mhvn/50FLUfsnfJz0BDZtl/RR16kXvptiv6q1msYZg==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ] + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -12177,6 +12232,34 @@ "@multiversx/sdk-wallet": "3.0.0", "agentkeepalive": "^4.3.0", "axios": "^0.27.2" + }, + "dependencies": { + "@multiversx/sdk-wallet": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@multiversx/sdk-wallet/-/sdk-wallet-3.0.0.tgz", + "integrity": "sha512-nDVBtva1mpfutXA8TfUnpdeFqhY9O+deNU3U/Z41yPBcju1trHDC9gcKPyQqcQ3qjG/6LwEXmIm7Dc5fIsvVjg==", + "requires": { + "@multiversx/sdk-bls-wasm": "0.3.5", + "bech32": "1.1.4", + "bip39": "3.0.2", + "blake2b": "2.1.3", + "ed25519-hd-key": "1.1.2", + "ed2curve": "0.3.0", + "keccak": "3.0.1", + "scryptsy": "2.1.0", + "tweetnacl": "1.0.3", + "uuid": "8.3.2" + } + }, + "keccak": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.1.tgz", + "integrity": "sha512-epq90L9jlFWCW7+pQa6JOnKn2Xgl2mtI664seYR6MHskvI9agt7AnDqmAlp9TqU4/caMYbA08Hi5DMZAl5zdkA==", + "requires": { + "node-addon-api": "^2.0.0", + "node-gyp-build": "^4.2.0" + } + } } }, "@multiversx/sdk-nestjs-monitoring": { @@ -12264,11 +12347,13 @@ } }, "@multiversx/sdk-wallet": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@multiversx/sdk-wallet/-/sdk-wallet-3.0.0.tgz", - "integrity": "sha512-nDVBtva1mpfutXA8TfUnpdeFqhY9O+deNU3U/Z41yPBcju1trHDC9gcKPyQqcQ3qjG/6LwEXmIm7Dc5fIsvVjg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@multiversx/sdk-wallet/-/sdk-wallet-4.2.0.tgz", + "integrity": "sha512-EjSb9AnqMcpmDjZ7ebkUpOzpTfxj1plTuVXwZ6AaqJsdpxMfrE2izbPy18+bg5xFlr8V27wYZcW8zOhkBR50BA==", "requires": { "@multiversx/sdk-bls-wasm": "0.3.5", + "@noble/ed25519": "1.7.3", + "@noble/hashes": "1.3.0", "bech32": "1.1.4", "bip39": "3.0.2", "blake2b": "2.1.3", @@ -12664,6 +12749,16 @@ } } }, + "@noble/ed25519": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@noble/ed25519/-/ed25519-1.7.3.tgz", + "integrity": "sha512-iR8GBkDt0Q3GyaVcIu7mSsVIqnFbkbRzGLWlvhwunacoLwt4J3swfKhfaM6rN6WY+TBGoYT1GtT1mIh2/jGbRQ==" + }, + "@noble/hashes": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.0.tgz", + "integrity": "sha512-ilHEACi9DwqJB0pw7kv+Apvh50jiiSyR/cQ3y4W7lOR5mhvn/50FLUfsnfJz0BDZtl/RR16kXvptiv6q1msYZg==" + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", diff --git a/package.json b/package.json index df60fd5..1351288 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "@multiversx/sdk-nestjs-redis": "2.4.0", "@multiversx/sdk-network-providers": "^2.2.0", "@multiversx/sdk-transaction-processor": "^0.1.30", + "@multiversx/sdk-wallet": "^4.2.0", "@nestjs/bull": "^10.0.1", "@nestjs/common": "10.2.0", "@nestjs/config": "3.0.1", From 6ef49f1b3df605c2b4ddc06d9bd7271efbe78de1 Mon Sep 17 00:00:00 2001 From: Rares <6453351+raresserban@users.noreply.github.com> Date: Tue, 12 Dec 2023 13:02:25 +0200 Subject: [PATCH 10/33] Working cronjobs for processing and sending contract call transactions. --- ...all-contract-approved.processor.service.ts | 172 +++++++++++++++--- .../event.processor.service.ts | 3 + libs/common/src/contracts/contracts.module.ts | 2 +- .../contract-call-approved.repository.ts | 46 +++++ .../common/src/decorators/get.or.set.cache.ts | 28 +++ libs/common/src/utils/cache.info.ts | 7 + 6 files changed, 228 insertions(+), 30 deletions(-) create mode 100644 libs/common/src/decorators/get.or.set.cache.ts diff --git a/apps/mvx-event-processor/src/call-contract-approved-processor/call-contract-approved.processor.service.ts b/apps/mvx-event-processor/src/call-contract-approved-processor/call-contract-approved.processor.service.ts index 947f3cf..8f29bcf 100644 --- a/apps/mvx-event-processor/src/call-contract-approved-processor/call-contract-approved.processor.service.ts +++ b/apps/mvx-event-processor/src/call-contract-approved-processor/call-contract-approved.processor.service.ts @@ -1,12 +1,25 @@ import { Inject, Injectable, Logger } from '@nestjs/common'; import { Cron } from '@nestjs/schedule'; import { Locker } from '@multiversx/sdk-nestjs-common'; -import { ContractCallApprovedRepository } from '@mvx-monorepo/common/database/repository/contract-call-approved.repository'; +import { + ContractCallApprovedRepository, + MAX_NUMBER_OF_RETRIES, +} from '@mvx-monorepo/common/database/repository/contract-call-approved.repository'; import { ProviderKeys } from '@mvx-monorepo/common/utils/provider.enum'; import { UserSigner } from '@multiversx/sdk-wallet/out'; import { ProxyNetworkProvider } from '@multiversx/sdk-network-providers/out'; -import { Address, BytesValue, SmartContract, Transaction } from '@multiversx/sdk-core/out'; -import { ContractCallApproved } from '@prisma/client'; +import { + Address, + BytesValue, + ContractFunction, + Interaction, + SmartContract, + StringValue, + Transaction, +} from '@multiversx/sdk-core/out'; +import { ContractCallApproved, ContractCallApprovedStatus } from '@prisma/client'; +import { GetOrSetCache } from '@mvx-monorepo/common/decorators/get.or.set.cache'; +import { CacheInfo } from '@mvx-monorepo/common'; @Injectable() export class CallContractApprovedProcessorService { @@ -20,10 +33,14 @@ export class CallContractApprovedProcessorService { this.logger = new Logger(CallContractApprovedProcessorService.name); } - @Cron('*/30 * * * * *') + // execute every 30 seconds, starting from second 0 + @Cron('0/30 * * * * *') async processPendingCallContractApproved() { - await Locker.lock('processPendingCallContractApproved', async () => { + await Locker.lock('processCallContractApproved', async () => { + this.logger.debug('Running processPendingCallContractApproved cron'); + let accountNonce = null; + const chainId = await this.getChainId(); let page = 0; let entries; @@ -34,25 +51,86 @@ export class CallContractApprovedProcessorService { this.logger.log(`Found ${entries.length} CallContractApproved transactions to execute`); - let transactionsToSend = []; - for (const callContractApproved of entries) { + const transactionsToSend = []; + for (const contractCallApproved of entries) { this.logger.debug( - `Trying to execute CallContractApproved transaction with commandId ${callContractApproved.commandId}`, + `Trying to execute ContractCallApproved transaction with commandId ${contractCallApproved.commandId}`, ); - const transaction = this.buildTransaction(callContractApproved); - transaction.setNonce(accountNonce); + const transaction = await this.buildTransaction(contractCallApproved, accountNonce, chainId); accountNonce++; transactionsToSend.push(transaction); - // TODO: Verify that 10 is ok max for gateway - if (transactionsToSend.length === 10) { - await this.sendTransactionsAndLog(transactionsToSend); + contractCallApproved.executeTxHash = transaction.getHash().toString(); + } + + const result = await this.sendTransactionsAndUpdateEntries(transactionsToSend); + + if (result) { + // Page is not modified if database records are updated + await this.callContractApprovedRepository.updateManyStatusRetryExecuteTxHash(entries); + } else { + page++; + } + } + }); + } + + // execute every 60 seconds, starting from second 15 (so it shouldn't intersect with the cronjob above) + @Cron('15/60 * * * * *') + async processRetryCallContractApproved() { + // Use same lock as above to make sure account nonce is handled correctly + await Locker.lock('processCallContractApproved', async () => { + this.logger.debug('Running processRetryCallContractApproved cron'); + + let accountNonce = null; + const chainId = await this.getChainId(); + + let page = 0; + let entries; + while ((entries = await this.callContractApprovedRepository.findPendingForRetry(page))?.length) { + if (accountNonce === null) { + accountNonce = await this.getAccountNonce(); + } + + this.logger.log(`Found ${entries.length} CallContractApproved transactions to retry execute`); + + const transactionsToSend = []; + for (const contractCallApproved of entries) { + contractCallApproved.retry += 1; + + if (contractCallApproved.retry === MAX_NUMBER_OF_RETRIES) { + this.logger.error( + `Could not execute ContractCallApproved transaction with commandId ${contractCallApproved.commandId}`, + ); + + contractCallApproved.status = ContractCallApprovedStatus.FAILED; - transactionsToSend = []; + continue; } + + this.logger.debug( + `Trying to execute ContractCallApproved transaction with commandId ${contractCallApproved.commandId}`, + ); + + const transaction = await this.buildTransaction(contractCallApproved, accountNonce, chainId); + + accountNonce++; + + transactionsToSend.push(transaction); + + contractCallApproved.executeTxHash = transaction.getHash().toString(); + } + + const result = await this.sendTransactionsAndUpdateEntries(transactionsToSend); + + if (result) { + // Page is not modified if database records are updated + await this.callContractApprovedRepository.updateManyStatusRetryExecuteTxHash(entries); + } else { + page++; } } }); @@ -64,30 +142,66 @@ export class CallContractApprovedProcessorService { return accountOnNetwork.nonce; } - // TODO: - private buildTransaction(callContractApproved: ContractCallApproved): Transaction { - const contract = new SmartContract({ address: new Address(callContractApproved.contractAddress) }); - - return contract.call({ - caller: this.walletSigner.getAddress(), - func: 'execute', - gasLimit: 100_000_000, // TODO - args: [ - new BytesValue(callContractApproved.payload), - ], - chainID: 'D', // TODO, - }); + @GetOrSetCache(CacheInfo.ChainId) + private async getChainId(): Promise { + const result = await this.proxy.getNetworkConfig(); + + return result.ChainID; + } + + private async buildTransaction( + contractCallApproved: ContractCallApproved, + accountNonce: number, + chainId: string, + ): Promise { + const contract = new SmartContract({ address: new Address(contractCallApproved.contractAddress) }); + + // TODO: Check if this encoding is correct + const args = [ + new BytesValue(Buffer.from(contractCallApproved.commandId, 'hex')), + new StringValue(contractCallApproved.sourceChain), + new StringValue(contractCallApproved.sourceAddress), + new BytesValue(contractCallApproved.payload), + ]; + + const interaction = new Interaction(contract, new ContractFunction('execute'), args); + + const transaction = interaction + .withSender(this.walletSigner.getAddress()) + .withNonce(accountNonce) + // .withValue() // TODO: Handle ITS transactions where EGLD value needs to be sent for deploying ESDT token + .withChainID(chainId) + .buildTransaction(); + + const gas = await this.getTransactionGas(transaction, contractCallApproved.retry); + transaction.setGasLimit(gas); + + const signature = await this.walletSigner.sign(transaction.serializeForSigning()); + transaction.applySignature(signature); + + return transaction; } - private async sendTransactionsAndLog(transactions: Transaction[]) { + private async sendTransactionsAndUpdateEntries(transactions: Transaction[]) { try { await this.proxy.sendTransactions(transactions); this.logger.log( - `Send ${transactions.length} transactions to proxy: ${transactions.map((trans) => trans.getHash())}`, + `Sent ${transactions.length} transactions to proxy: ${transactions.map((trans) => trans.getHash())}`, ); + + return true; } catch (e) { this.logger.error(`Can not send CallContractApproved transactions to proxy... ${e}`); + + return false; } } + + // TODO: Check if this works properly + private async getTransactionGas(transaction: Transaction, retry: number): Promise { + const result = await this.proxy.doPostGeneric('transaction/cost', transaction.toSendable()); + + return (result.data.txGasUnits * (11 + retry * 2)) / 10; // add 10% extra gas initially, and more gas with each retry + } } diff --git a/apps/mvx-event-processor/src/event-processor/event.processor.service.ts b/apps/mvx-event-processor/src/event-processor/event.processor.service.ts index 0b15b22..c9c83c7 100644 --- a/apps/mvx-event-processor/src/event-processor/event.processor.service.ts +++ b/apps/mvx-event-processor/src/event-processor/event.processor.service.ts @@ -43,6 +43,9 @@ export class EventProcessorService { } private async handleEvent(event: NotifierEvent) { + this.logger.debug('Received event from MultiversX:'); + this.logger.debug(JSON.stringify(event)); + if (event.address === this.contractGasService) { this.logger.debug('Received Gas Service event from MultiversX:'); this.logger.debug(JSON.stringify(event)); diff --git a/libs/common/src/contracts/contracts.module.ts b/libs/common/src/contracts/contracts.module.ts index c49fb0a..c1c2880 100644 --- a/libs/common/src/contracts/contracts.module.ts +++ b/libs/common/src/contracts/contracts.module.ts @@ -78,6 +78,6 @@ import { Mnemonic, UserSigner } from '@multiversx/sdk-wallet/out'; inject: [ApiConfigService, ResultsParser], }, ], - exports: [GatewayContract, GasServiceContract], + exports: [GatewayContract, GasServiceContract, ProviderKeys.WALLET_SIGNER, ProxyNetworkProvider, ApiNetworkProvider], }) export class ContractsModule {} diff --git a/libs/common/src/database/repository/contract-call-approved.repository.ts b/libs/common/src/database/repository/contract-call-approved.repository.ts index 165ae88..2872dc7 100644 --- a/libs/common/src/database/repository/contract-call-approved.repository.ts +++ b/libs/common/src/database/repository/contract-call-approved.repository.ts @@ -2,6 +2,9 @@ import { Injectable } from '@nestjs/common'; import { PrismaService } from '@mvx-monorepo/common/database/prisma.service'; import { ContractCallApproved, ContractCallApprovedStatus, Prisma } from '@prisma/client'; +// Support a max of 3 retries (mainly because some Interchain Token Service endpoints need to be called 3 times) +export const MAX_NUMBER_OF_RETRIES: number = 3; + @Injectable() export class ContractCallApprovedRepository { constructor(private readonly prisma: PrismaService) {} @@ -17,6 +20,32 @@ export class ContractCallApprovedRepository { where: { status: ContractCallApprovedStatus.PENDING, retry: 0, + executeTxHash: null, + }, + orderBy: { + createdAt: 'desc', + }, + skip: page * take, + take, + }); + } + + findPendingForRetry(page: number = 0, take: number = 10): Promise { + // Last updated more than one minute ago + const lastUpdatedAt = new Date(new Date().getTime() - 60_000); + + return this.prisma.contractCallApproved.findMany({ + where: { + status: ContractCallApprovedStatus.PENDING, + retry: { + lt: MAX_NUMBER_OF_RETRIES, + }, + executeTxHash: { + not: null, + }, + updatedAt: { + lt: lastUpdatedAt, + }, }, orderBy: { createdAt: 'desc', @@ -25,4 +54,21 @@ export class ContractCallApprovedRepository { take, }); } + + async updateManyStatusRetryExecuteTxHash(entries: ContractCallApproved[]) { + await this.prisma.$transaction( + entries.map((data) => { + return this.prisma.contractCallApproved.update({ + where: { + commandId: data.commandId, + }, + data: { + status: data.status, + retry: data.retry, + executeTxHash: data.executeTxHash, + }, + }); + }), + ); + } } diff --git a/libs/common/src/decorators/get.or.set.cache.ts b/libs/common/src/decorators/get.or.set.cache.ts new file mode 100644 index 0000000..ae50b60 --- /dev/null +++ b/libs/common/src/decorators/get.or.set.cache.ts @@ -0,0 +1,28 @@ +import { CacheService } from '@multiversx/sdk-nestjs-cache'; +import { Inject } from '@nestjs/common'; +import { CacheInfo } from '@mvx-monorepo/common'; + +export function GetOrSetCache(cacheInfoFunc: (...args: any[]) => CacheInfo) { + const injectCachingService = Inject(CacheService); + + return ( + target: any, + _key: string | symbol, + descriptor: PropertyDescriptor, + ) => { + injectCachingService(target, 'cachingService'); + + const childMethod = descriptor.value; + + descriptor.value = async function (...args: any[]) { + const { key, ttl } = cacheInfoFunc(...args); + + const cachingService: CacheService = (this as any).cachingService; + + const funcValue = () => childMethod.apply(this, args); + return await cachingService.getOrSet(key, funcValue, ttl); + }; + + return descriptor; + }; +} diff --git a/libs/common/src/utils/cache.info.ts b/libs/common/src/utils/cache.info.ts index 6baf2eb..aa2a35b 100644 --- a/libs/common/src/utils/cache.info.ts +++ b/libs/common/src/utils/cache.info.ts @@ -10,4 +10,11 @@ export class CacheInfo { ttl: Constants.oneMonth(), }; } + + static ChainId(): CacheInfo { + return { + key: `chainId`, + ttl: Constants.oneWeek(), + }; + } } From 5e89dea734892e14f555202a47e9efc012691e81 Mon Sep 17 00:00:00 2001 From: Rares <6453351+raresserban@users.noreply.github.com> Date: Tue, 12 Dec 2023 13:40:21 +0200 Subject: [PATCH 11/33] Process contract call executed event. --- apps/mvx-event-processor/src/main.ts | 1 - .../src/processors/gateway.processor.spec.ts | 75 ++++++++++++++++++- .../src/processors/gateway.processor.ts | 20 +++++ .../contract-call-approved.repository.ts | 8 ++ 4 files changed, 99 insertions(+), 5 deletions(-) diff --git a/apps/mvx-event-processor/src/main.ts b/apps/mvx-event-processor/src/main.ts index 042d72e..debfb44 100644 --- a/apps/mvx-event-processor/src/main.ts +++ b/apps/mvx-event-processor/src/main.ts @@ -1,4 +1,3 @@ -import 'module-alias/register'; import { NestFactory } from '@nestjs/core'; import { EventProcessorModule } from './event-processor'; import { CallContractApprovedProcessorModule } from './call-contract-approved-processor/call-contract-approved.processor.module'; diff --git a/apps/mvx-event-processor/src/processors/gateway.processor.spec.ts b/apps/mvx-event-processor/src/processors/gateway.processor.spec.ts index 73f7a63..7e26a7a 100644 --- a/apps/mvx-event-processor/src/processors/gateway.processor.spec.ts +++ b/apps/mvx-event-processor/src/processors/gateway.processor.spec.ts @@ -2,12 +2,12 @@ import { ApiConfigService } from '@mvx-monorepo/common'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test } from '@nestjs/testing'; import { BinaryUtils } from '@multiversx/sdk-nestjs-common'; -import { Events } from '@mvx-monorepo/common/utils/event.enum'; +import { EventIdentifiers, Events } from '@mvx-monorepo/common/utils/event.enum'; import { ContractCallEventRepository } from '@mvx-monorepo/common/database/repository/contract-call-event.repository'; import { GatewayProcessor } from './gateway.processor'; import { NotifierEvent } from '../event-processor/types'; import { Address } from '@multiversx/sdk-core/out'; -import { ContractCallApprovedStatus, ContractCallEventStatus } from '@prisma/client'; +import { ContractCallApproved, ContractCallApprovedStatus, ContractCallEventStatus } from '@prisma/client'; import { GrpcService } from '@mvx-monorepo/common/grpc/grpc.service'; import { GatewayContract } from '@mvx-monorepo/common/contracts/gateway.contract'; import { ContractCallApprovedEvent, ContractCallEvent } from '@mvx-monorepo/common/contracts/entities/gateway-events'; @@ -79,6 +79,7 @@ describe('ContractCallProcessor', () => { gatewayContract.decodeContractCallEvent.mockReturnValue(contractCallEvent); gatewayContract.decodeContractCallApprovedEvent.mockReturnValue(contractCallApprovedEvent); + gatewayContract.decodeContractCallExecutedEvent.mockReturnValue(contractCallApprovedEvent.commandId); service = moduleRef.get(GatewayProcessor); }); @@ -111,7 +112,7 @@ describe('ContractCallProcessor', () => { const rawEvent: NotifierEvent = { txHash: 'txHash', address: 'mockGatewayAddress', - identifier: 'callContract', + identifier: EventIdentifiers.CALL_CONTRACT, data: data.toString('base64'), topics: [ BinaryUtils.base64Encode(Events.CONTRACT_CALL_EVENT), @@ -158,7 +159,7 @@ describe('ContractCallProcessor', () => { const rawEvent: NotifierEvent = { txHash: 'txHash', address: 'mockGatewayAddress', - identifier: 'execute', + identifier: EventIdentifiers.EXECUTE, data: '', topics: [ BinaryUtils.base64Encode(Events.CONTRACT_CALL_APPROVED_EVENT), @@ -214,4 +215,70 @@ describe('ContractCallProcessor', () => { expect(contractCallApprovedRepository.create).toHaveBeenCalledTimes(1); }); }); + + describe('handleContractCallExecutedEvent', () => { + const rawEvent: NotifierEvent = { + txHash: 'txHash', + address: 'mockGatewayAddress', + identifier: EventIdentifiers.VALIDATE_CONTRACT_CALL, + data: '', + topics: [ + BinaryUtils.base64Encode(Events.CONTRACT_CALL_EXECUTED_EVENT), + Buffer.from('0c38359b7a35c755573659d797afec315bb0e51374a056745abd9764715a15da', 'hex').toString('base64'), + ], + }; + + it('Should handle event update contract call approved', async () => { + const contractCallApproved: ContractCallApproved = { + commandId: '0c38359b7a35c755573659d797afec315bb0e51374a056745abd9764715a15da', + txHash: 'txHash', + status: ContractCallApprovedStatus.PENDING, + sourceAddress: 'sourceAddress', + sourceChain: 'ethereum', + contractAddress: 'erd1qqqqqqqqqqqqqpgqzqvm5ywqqf524efwrhr039tjs29w0qltkklsa05pk7', + payloadHash: 'ebc84cbd75ba5516bf45e7024a9e12bc3c5c880f73e3a5beca7ebba52b2867a7', + payload: Buffer.from('payload'), + retry: 0, + executeTxHash: null, + updatedAt: new Date(), + createdAt: new Date(), + }; + + contractCallApprovedRepository.findByCommandId.mockReturnValueOnce(Promise.resolve(contractCallApproved)); + + await service.handleEvent(rawEvent); + + expect(gatewayContract.decodeContractCallExecutedEvent).toHaveBeenCalledTimes(1); + expect(gatewayContract.decodeContractCallExecutedEvent).toHaveBeenCalledWith( + TransactionEvent.fromHttpResponse(rawEvent), + ); + expect(contractCallApprovedRepository.findByCommandId).toHaveBeenCalledTimes(1); + expect(contractCallApprovedRepository.findByCommandId).toHaveBeenCalledWith( + '0c38359b7a35c755573659d797afec315bb0e51374a056745abd9764715a15da', + ); + expect(contractCallApprovedRepository.updateManyStatusRetryExecuteTxHash).toHaveBeenCalledTimes(1); + expect(contractCallApprovedRepository.updateManyStatusRetryExecuteTxHash).toHaveBeenCalledWith([ + { + ...contractCallApproved, + status: ContractCallApprovedStatus.SUCCESS, + }, + ]); + }); + + it('Should handle event no contract call approved', async () => { + contractCallApprovedRepository.findByCommandId.mockReturnValueOnce(Promise.resolve(null)); + + await service.handleEvent(rawEvent); + + expect(gatewayContract.decodeContractCallExecutedEvent).toHaveBeenCalledTimes(1); + expect(gatewayContract.decodeContractCallExecutedEvent).toHaveBeenCalledWith( + TransactionEvent.fromHttpResponse(rawEvent), + ); + expect(contractCallApprovedRepository.findByCommandId).toHaveBeenCalledTimes(1); + expect(contractCallApprovedRepository.findByCommandId).toHaveBeenCalledWith( + '0c38359b7a35c755573659d797afec315bb0e51374a056745abd9764715a15da', + ); + expect(contractCallApprovedRepository.updateManyStatusRetryExecuteTxHash).not.toHaveBeenCalled(); + }); + }); }); diff --git a/apps/mvx-event-processor/src/processors/gateway.processor.ts b/apps/mvx-event-processor/src/processors/gateway.processor.ts index 018ed6e..13f7dd7 100644 --- a/apps/mvx-event-processor/src/processors/gateway.processor.ts +++ b/apps/mvx-event-processor/src/processors/gateway.processor.ts @@ -43,6 +43,12 @@ export class GatewayProcessor implements ProcessorInterface { return; } + + if (rawEvent.identifier === EventIdentifiers.VALIDATE_CONTRACT_CALL && eventName === Events.CONTRACT_CALL_EXECUTED_EVENT) { + await this.handleContractCallExecutedEvent(rawEvent); + + return; + } } private async handleContractCallEvent(rawEvent: NotifierEvent) { @@ -95,4 +101,18 @@ export class GatewayProcessor implements ProcessorInterface { throw new Error(`Couldn't save contract call approved to database for hash ${rawEvent.txHash}`); } } + + private async handleContractCallExecutedEvent(rawEvent: NotifierEvent) { + const commandId = this.gatewayContract.decodeContractCallExecutedEvent(TransactionEvent.fromHttpResponse(rawEvent)); + + const contractCallApproved = await this.contractCallApprovedRepository.findByCommandId(commandId); + + if (!contractCallApproved) { + return; + } + + contractCallApproved.status = ContractCallApprovedStatus.SUCCESS; + + await this.contractCallApprovedRepository.updateManyStatusRetryExecuteTxHash([contractCallApproved]); + } } diff --git a/libs/common/src/database/repository/contract-call-approved.repository.ts b/libs/common/src/database/repository/contract-call-approved.repository.ts index 2872dc7..8b9136b 100644 --- a/libs/common/src/database/repository/contract-call-approved.repository.ts +++ b/libs/common/src/database/repository/contract-call-approved.repository.ts @@ -55,6 +55,14 @@ export class ContractCallApprovedRepository { }); } + findByCommandId(commandId: string): Promise { + return this.prisma.contractCallApproved.findUnique({ + where: { + commandId: commandId, + }, + }); + } + async updateManyStatusRetryExecuteTxHash(entries: ContractCallApproved[]) { await this.prisma.$transaction( entries.map((data) => { From b538965c193dfe1ad9e47962390d1930d5622768 Mon Sep 17 00:00:00 2001 From: Rares <6453351+raresserban@users.noreply.github.com> Date: Tue, 12 Dec 2023 17:16:59 +0200 Subject: [PATCH 12/33] Add e2e test for call contract approved processor. --- .env.test | 18 ++ .github/workflows/lint.yml | 29 --- .github/workflows/test.yml | 46 ++++ ...all-contract-approved.processor.service.ts | 129 ++-------- .../call-contract-approved-processor/index.ts | 2 + apps/mvx-event-processor/src/main.ts | 2 +- ...ll-contract-approved.processor.e2e-spec.ts | 242 ++++++++++++++++++ libs/common/src/contracts/contracts.module.ts | 15 +- .../src/contracts/transactions.helper.ts | 51 ++++ .../contract-call-approved.repository.ts | 46 +--- package-lock.json | 64 ++--- package.json | 13 +- 12 files changed, 446 insertions(+), 211 deletions(-) create mode 100644 .env.test delete mode 100644 .github/workflows/lint.yml create mode 100644 .github/workflows/test.yml create mode 100644 apps/mvx-event-processor/src/call-contract-approved-processor/index.ts create mode 100644 apps/mvx-event-processor/test/call-contract-approved.processor.e2e-spec.ts create mode 100644 libs/common/src/contracts/transactions.helper.ts diff --git a/.env.test b/.env.test new file mode 100644 index 0000000..aff51af --- /dev/null +++ b/.env.test @@ -0,0 +1,18 @@ +DATABASE_URL=postgresql://root:password@localhost:5432/relayer_test + +API_URL=http://api.local +GATEWAY_URL=http://gateway.local +REDIS_URL=localhost + +EVENTS_NOTIFIER_URL=amqp://user:password@rabbitmq.local:5672 +EVENTS_NOTIFIER_QUEUE=events-2cf3b817 + +CONTRACT_GATEWAY=erd1qqqqqqqqqqqqqpgqvc7gdl0p4s97guh498wgz75k8sav6sjfjlwqh679jy +CONTRACT_GAS_SERVICE=erd1qqqqqqqqqqqqqpgqhe8t5jewej70zupmh44jurgn29psua5l2jps3ntjj3 + +AXELAR_API_URL=localhost:5000 + +SOURCE_CHAIN_NAME=multiversx-test + +# erd1fsk0cnaag2m78gunfddsvg0y042rf0maxxgz6kvm32kxcl25m0yq8s38vt +WALLET_MNEMONIC="fitness horror fluid six mutual ahead upon zone install stadium shuffle arrive caution flat slam machine wasp steel stand frog exist drink market absent" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml deleted file mode 100644 index 11d4b27..0000000 --- a/.github/workflows/lint.yml +++ /dev/null @@ -1,29 +0,0 @@ -# This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node -# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions - -name: ESLint - -on: - push: - branches: [main, development] - pull_request: - branches: [main, development] - -jobs: - build: - runs-on: ubuntu-latest - - strategy: - matrix: - node-version: [16.x] - # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ - - steps: - - uses: actions/checkout@v2 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v2 - with: - node-version: ${{ matrix.node-version }} - cache: 'npm' - - run: npm ci - - run: npm run lint \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..5a3bc0e --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,46 @@ +name: ESLint & Tests + +on: + push: + branches: [main, develop] + pull_request: + branches: [main, develop] + +jobs: + test: + runs-on: ubuntu-latest + services: + redis: + image: 'redis:alpine' + ports: + - 6379:6379 + postgres: + image: postgres:latest + ports: + - 5432:5432 + env: + POSTGRES_USER: user + POSTGRES_PASSWORD: password + POSTGRES_DB: relayer_test + + strategy: + matrix: + node-version: [18.x] + + steps: + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + - run: npm ci + - run: npm run lint + + - run: npm run test + + - run: npm install -g dotenv-cli + - run: npm run test:migrate + - run: npm run test:e2e +# env: +# REDIS_URL: localhost diff --git a/apps/mvx-event-processor/src/call-contract-approved-processor/call-contract-approved.processor.service.ts b/apps/mvx-event-processor/src/call-contract-approved-processor/call-contract-approved.processor.service.ts index 8f29bcf..9d51871 100644 --- a/apps/mvx-event-processor/src/call-contract-approved-processor/call-contract-approved.processor.service.ts +++ b/apps/mvx-event-processor/src/call-contract-approved-processor/call-contract-approved.processor.service.ts @@ -1,13 +1,9 @@ import { Inject, Injectable, Logger } from '@nestjs/common'; import { Cron } from '@nestjs/schedule'; import { Locker } from '@multiversx/sdk-nestjs-common'; -import { - ContractCallApprovedRepository, - MAX_NUMBER_OF_RETRIES, -} from '@mvx-monorepo/common/database/repository/contract-call-approved.repository'; +import { ContractCallApprovedRepository } from '@mvx-monorepo/common/database/repository/contract-call-approved.repository'; import { ProviderKeys } from '@mvx-monorepo/common/utils/provider.enum'; import { UserSigner } from '@multiversx/sdk-wallet/out'; -import { ProxyNetworkProvider } from '@multiversx/sdk-network-providers/out'; import { Address, BytesValue, @@ -18,92 +14,45 @@ import { Transaction, } from '@multiversx/sdk-core/out'; import { ContractCallApproved, ContractCallApprovedStatus } from '@prisma/client'; -import { GetOrSetCache } from '@mvx-monorepo/common/decorators/get.or.set.cache'; -import { CacheInfo } from '@mvx-monorepo/common'; +import { TransactionsHelper } from '@mvx-monorepo/common/contracts/transactions.helper'; + +// Support a max of 3 retries (mainly because some Interchain Token Service endpoints need to be called 3 times) +const MAX_NUMBER_OF_RETRIES: number = 3; @Injectable() export class CallContractApprovedProcessorService { private readonly logger: Logger; constructor( - private readonly callContractApprovedRepository: ContractCallApprovedRepository, + private readonly contractCallApprovedRepository: ContractCallApprovedRepository, @Inject(ProviderKeys.WALLET_SIGNER) private readonly walletSigner: UserSigner, - private readonly proxy: ProxyNetworkProvider, + private readonly transactionsHelper: TransactionsHelper, ) { this.logger = new Logger(CallContractApprovedProcessorService.name); } - // execute every 30 seconds, starting from second 0 - @Cron('0/30 * * * * *') - async processPendingCallContractApproved() { - await Locker.lock('processCallContractApproved', async () => { - this.logger.debug('Running processPendingCallContractApproved cron'); + @Cron('*/30 * * * * *') + async processPendingContractCallApproved() { + await Locker.lock('processPendingContractCallApproved', async () => { + this.logger.debug('Running processPendingContractCallApproved cron'); let accountNonce = null; - const chainId = await this.getChainId(); + const chainId = await this.transactionsHelper.getChainId(); let page = 0; let entries; - while ((entries = await this.callContractApprovedRepository.findPendingNoRetries(page))?.length) { + while ((entries = await this.contractCallApprovedRepository.findPending(page))?.length) { if (accountNonce === null) { - accountNonce = await this.getAccountNonce(); + accountNonce = await this.transactionsHelper.getAccountNonce(this.walletSigner.getAddress()); } this.logger.log(`Found ${entries.length} CallContractApproved transactions to execute`); const transactionsToSend = []; for (const contractCallApproved of entries) { - this.logger.debug( - `Trying to execute ContractCallApproved transaction with commandId ${contractCallApproved.commandId}`, - ); - - const transaction = await this.buildTransaction(contractCallApproved, accountNonce, chainId); - - accountNonce++; - - transactionsToSend.push(transaction); - - contractCallApproved.executeTxHash = transaction.getHash().toString(); - } - - const result = await this.sendTransactionsAndUpdateEntries(transactionsToSend); - - if (result) { - // Page is not modified if database records are updated - await this.callContractApprovedRepository.updateManyStatusRetryExecuteTxHash(entries); - } else { - page++; - } - } - }); - } - - // execute every 60 seconds, starting from second 15 (so it shouldn't intersect with the cronjob above) - @Cron('15/60 * * * * *') - async processRetryCallContractApproved() { - // Use same lock as above to make sure account nonce is handled correctly - await Locker.lock('processCallContractApproved', async () => { - this.logger.debug('Running processRetryCallContractApproved cron'); - - let accountNonce = null; - const chainId = await this.getChainId(); - - let page = 0; - let entries; - while ((entries = await this.callContractApprovedRepository.findPendingForRetry(page))?.length) { - if (accountNonce === null) { - accountNonce = await this.getAccountNonce(); - } - - this.logger.log(`Found ${entries.length} CallContractApproved transactions to retry execute`); - - const transactionsToSend = []; - for (const contractCallApproved of entries) { - contractCallApproved.retry += 1; - if (contractCallApproved.retry === MAX_NUMBER_OF_RETRIES) { this.logger.error( - `Could not execute ContractCallApproved transaction with commandId ${contractCallApproved.commandId}`, + `Could not execute ContractCallApproved transaction with commandId ${contractCallApproved.commandId} after ${contractCallApproved.retry} retries`, ); contractCallApproved.status = ContractCallApprovedStatus.FAILED; @@ -115,20 +64,21 @@ export class CallContractApprovedProcessorService { `Trying to execute ContractCallApproved transaction with commandId ${contractCallApproved.commandId}`, ); - const transaction = await this.buildTransaction(contractCallApproved, accountNonce, chainId); + const transaction = await this.buildExecuteTransaction(contractCallApproved, accountNonce, chainId); accountNonce++; transactionsToSend.push(transaction); contractCallApproved.executeTxHash = transaction.getHash().toString(); + contractCallApproved.retry += 1; } - const result = await this.sendTransactionsAndUpdateEntries(transactionsToSend); + const result = await this.transactionsHelper.sendTransactions(transactionsToSend); if (result) { // Page is not modified if database records are updated - await this.callContractApprovedRepository.updateManyStatusRetryExecuteTxHash(entries); + await this.contractCallApprovedRepository.updateManyStatusRetryExecuteTxHash(entries); } else { page++; } @@ -136,27 +86,13 @@ export class CallContractApprovedProcessorService { }); } - private async getAccountNonce(): Promise { - const accountOnNetwork = await this.proxy.getAccount(this.walletSigner.getAddress()); - - return accountOnNetwork.nonce; - } - - @GetOrSetCache(CacheInfo.ChainId) - private async getChainId(): Promise { - const result = await this.proxy.getNetworkConfig(); - - return result.ChainID; - } - - private async buildTransaction( + private async buildExecuteTransaction( contractCallApproved: ContractCallApproved, accountNonce: number, chainId: string, ): Promise { const contract = new SmartContract({ address: new Address(contractCallApproved.contractAddress) }); - // TODO: Check if this encoding is correct const args = [ new BytesValue(Buffer.from(contractCallApproved.commandId, 'hex')), new StringValue(contractCallApproved.sourceChain), @@ -173,7 +109,7 @@ export class CallContractApprovedProcessorService { .withChainID(chainId) .buildTransaction(); - const gas = await this.getTransactionGas(transaction, contractCallApproved.retry); + const gas = await this.transactionsHelper.getTransactionGas(transaction, contractCallApproved.retry); transaction.setGasLimit(gas); const signature = await this.walletSigner.sign(transaction.serializeForSigning()); @@ -181,27 +117,4 @@ export class CallContractApprovedProcessorService { return transaction; } - - private async sendTransactionsAndUpdateEntries(transactions: Transaction[]) { - try { - await this.proxy.sendTransactions(transactions); - - this.logger.log( - `Sent ${transactions.length} transactions to proxy: ${transactions.map((trans) => trans.getHash())}`, - ); - - return true; - } catch (e) { - this.logger.error(`Can not send CallContractApproved transactions to proxy... ${e}`); - - return false; - } - } - - // TODO: Check if this works properly - private async getTransactionGas(transaction: Transaction, retry: number): Promise { - const result = await this.proxy.doPostGeneric('transaction/cost', transaction.toSendable()); - - return (result.data.txGasUnits * (11 + retry * 2)) / 10; // add 10% extra gas initially, and more gas with each retry - } } diff --git a/apps/mvx-event-processor/src/call-contract-approved-processor/index.ts b/apps/mvx-event-processor/src/call-contract-approved-processor/index.ts new file mode 100644 index 0000000..dc3d2d4 --- /dev/null +++ b/apps/mvx-event-processor/src/call-contract-approved-processor/index.ts @@ -0,0 +1,2 @@ +export * from './call-contract-approved.processor.module'; +export * from './call-contract-approved.processor.service'; diff --git a/apps/mvx-event-processor/src/main.ts b/apps/mvx-event-processor/src/main.ts index debfb44..e27c15a 100644 --- a/apps/mvx-event-processor/src/main.ts +++ b/apps/mvx-event-processor/src/main.ts @@ -1,6 +1,6 @@ import { NestFactory } from '@nestjs/core'; import { EventProcessorModule } from './event-processor'; -import { CallContractApprovedProcessorModule } from './call-contract-approved-processor/call-contract-approved.processor.module'; +import { CallContractApprovedProcessorModule } from './call-contract-approved-processor'; async function bootstrap() { await NestFactory.createApplicationContext(EventProcessorModule); diff --git a/apps/mvx-event-processor/test/call-contract-approved.processor.e2e-spec.ts b/apps/mvx-event-processor/test/call-contract-approved.processor.e2e-spec.ts new file mode 100644 index 0000000..eb3e985 --- /dev/null +++ b/apps/mvx-event-processor/test/call-contract-approved.processor.e2e-spec.ts @@ -0,0 +1,242 @@ +import { Test } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import { AccountOnNetwork, NetworkConfig, ProxyNetworkProvider } from '@multiversx/sdk-network-providers/out'; +import { + CallContractApprovedProcessorModule, + CallContractApprovedProcessorService, +} from '../src/call-contract-approved-processor'; +import { ContractCallApprovedRepository } from '@mvx-monorepo/common/database/repository/contract-call-approved.repository'; +import { PrismaService } from '@mvx-monorepo/common/database/prisma.service'; +import { CacheService } from '@multiversx/sdk-nestjs-cache'; +import { CacheInfo } from '@mvx-monorepo/common'; +import { ContractCallApproved, ContractCallApprovedStatus } from '@prisma/client'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { Transaction } from '@multiversx/sdk-core/out'; +import { BinaryUtils } from '@multiversx/sdk-nestjs-common'; + +const WALLET_SIGNER_ADDRESS = 'erd1fsk0cnaag2m78gunfddsvg0y042rf0maxxgz6kvm32kxcl25m0yq8s38vt'; + +describe('CallContractApprovedProcessorService', () => { + let cacheService: CacheService; + let proxy: DeepMocked; + let prisma: PrismaService; + let contractCallApprovedRepository: ContractCallApprovedRepository; + + let service: CallContractApprovedProcessorService; + + let app: INestApplication; + + const resetCache = async () => { + await cacheService.deleteMany([CacheInfo.ChainId().key]); + }; + + beforeEach(async () => { + proxy = createMock(); + + const moduleRef = await Test.createTestingModule({ + imports: [CallContractApprovedProcessorModule], + }) + .overrideProvider(ProxyNetworkProvider) + .useValue(proxy) + .compile(); + + cacheService = await moduleRef.get(CacheService); + // proxy = await moduleRef.get(ProxyNetworkProvider); + prisma = await moduleRef.get(PrismaService); + contractCallApprovedRepository = await moduleRef.get(ContractCallApprovedRepository); + + service = await moduleRef.get(CallContractApprovedProcessorService); + + // Mock general calls + const networkConfig = new NetworkConfig(); + networkConfig.ChainID = 'test'; + proxy.getNetworkConfig.mockReturnValueOnce(Promise.resolve(networkConfig)); + proxy.getAccount.mockReturnValueOnce( + Promise.resolve( + new AccountOnNetwork({ + nonce: 1, + }), + ), + ); + proxy.doPostGeneric.mockImplementation((url: string, _: any): Promise => { + if (url === 'transaction/cost') { + return Promise.resolve({ + data: { + txGasUnits: 10_000_000, + }, + }); + } + + return Promise.resolve(null); + }); + + // Reset database & cache + await prisma.contractCallApproved.deleteMany(); + await resetCache(); + + app = moduleRef.createNestApplication(); + await app.init(); + }); + + afterEach(async () => { + await resetCache(); + await prisma.$disconnect(); + + await app.close(); + }); + + const createContractCallApproved = async ( + extraData: Partial = {}, + ): Promise => { + const result = await contractCallApprovedRepository.create({ + commandId: '0c38359b7a35c755573659d797afec315bb0e51374a056745abd9764715a15aa', + txHash: 'txHashA', + status: ContractCallApprovedStatus.PENDING, + sourceAddress: 'sourceAddress', + sourceChain: 'ethereum', + contractAddress: 'erd1qqqqqqqqqqqqqpgqzqvm5ywqqf524efwrhr039tjs29w0qltkklsa05pk7', + payloadHash: 'ebc84cbd75ba5516bf45e7024a9e12bc3c5c880f73e3a5beca7ebba52b2867a7', + payload: Buffer.from('payload'), + retry: 0, + executeTxHash: null, + updatedAt: new Date(), + createdAt: new Date(), + ...extraData, + }); + + if (!result) { + throw new Error('Can not create database entries'); + } + + return result; + }; + + const assertArgs = (transaction: Transaction, entry: ContractCallApproved) => { + const args = transaction.getData().toString().split('@'); + + expect(args[0]).toBe('execute'); + expect(args[1]).toBe(entry.commandId); + expect(args[2]).toBe(BinaryUtils.stringToHex(entry.sourceChain)); + expect(args[3]).toBe(BinaryUtils.stringToHex(entry.sourceAddress)); + expect(args[4]).toBe(entry.payload.toString('hex')); + }; + + it('Should send execute transaction two initial', async () => { + const originalFirstEntry = await createContractCallApproved(); + const originalSecondEntry = await createContractCallApproved({ + commandId: '0c38359b7a35c755573659d797afec315bb0e51374a056745abd9764715a15bb', + txHash: 'txHashB', + sourceChain: 'polygon', + sourceAddress: 'otherSourceAddress', + payload: Buffer.from('otherPayload'), + }); + + await service.processPendingContractCallApproved(); + + expect(proxy.getNetworkConfig).toHaveBeenCalledTimes(1); + expect(proxy.getAccount).toHaveBeenCalledTimes(1); + expect(proxy.doPostGeneric).toHaveBeenCalledTimes(2); + expect(proxy.sendTransactions).toHaveBeenCalledTimes(1); + + // Assert transactions data is correct + const transactions = proxy.sendTransactions.mock.lastCall?.[0] as Transaction[]; + expect(transactions).toHaveLength(2); + + expect(transactions[0].getGasLimit()).toBe(11_000_000); // 10% over 10_000_000 + expect(transactions[0].getNonce()).toBe(1); + expect(transactions[0].getChainID()).toBe('test'); + expect(transactions[0].getSender().bech32()).toBe(WALLET_SIGNER_ADDRESS); + assertArgs(transactions[0], originalFirstEntry); + + expect(transactions[1].getGasLimit()).toBe(11_000_000); + expect(transactions[1].getNonce()).toBe(2); + expect(transactions[1].getChainID()).toBe('test'); + expect(transactions[1].getSender().bech32()).toBe(WALLET_SIGNER_ADDRESS); + assertArgs(transactions[1], originalSecondEntry); + + // No contract call approved pending + expect(await contractCallApprovedRepository.findPending()).toEqual([]); + + // Expect entries in database updated + const firstEntry = await contractCallApprovedRepository.findByCommandId(originalFirstEntry.commandId); + expect(firstEntry).toEqual({ + ...originalFirstEntry, + retry: 1, + executeTxHash: 'dbb1d4ed062e8b71538567116b5360911d1fe43025f1cf1858a14666aa2c9fda', + updatedAt: expect.any(Date), + }); + + const secondEntry = await contractCallApprovedRepository.findByCommandId(originalSecondEntry.commandId); + expect(secondEntry).toEqual({ + ...originalSecondEntry, + retry: 1, + executeTxHash: 'cf1c10a09bf817e198bc18df08357c6ac7a666a3ea9f2b760f92762f1f591601', + updatedAt: expect.any(Date), + }); + }); + + it('Should send execute transaction retry one processed one failed', async () => { + // Entries will be processed + const originalFirstEntry = await createContractCallApproved({ + retry: 1, + updatedAt: new Date(new Date().getTime() - 60_500), + }); + const originalSecondEntry = await createContractCallApproved({ + commandId: '0c38359b7a35c755573659d797afec315bb0e51374a056745abd9764715a15bb', + txHash: 'txHashB', + sourceChain: 'polygon', + sourceAddress: 'otherSourceAddress', + payload: Buffer.from('otherPayload'), + retry: 3, + updatedAt: new Date(new Date().getTime() - 60_500), + }); + // Entry will not be processed (updated to early) + const originalThirdEntry = await createContractCallApproved({ + commandId: '0c38359b7a35c755573659d797afec315bb0e51374a056745abd9764715a15cc', + txHash: 'txHashC', + retry: 1, + }); + + await service.processPendingContractCallApproved(); + + expect(proxy.getNetworkConfig).toHaveBeenCalledTimes(1); + expect(proxy.getAccount).toHaveBeenCalledTimes(1); + expect(proxy.doPostGeneric).toHaveBeenCalledTimes(1); + expect(proxy.sendTransactions).toHaveBeenCalledTimes(1); + + // Assert transactions data is correct + const transactions = proxy.sendTransactions.mock.lastCall?.[0] as Transaction[]; + expect(transactions).toHaveLength(1); + + expect(transactions[0].getGasLimit()).toBe(13_000_000); // 10% + 20% over 10_000_000 + expect(transactions[0].getNonce()).toBe(1); + expect(transactions[0].getChainID()).toBe('test'); + expect(transactions[0].getSender().bech32()).toBe(WALLET_SIGNER_ADDRESS); + assertArgs(transactions[0], originalFirstEntry); + + // No contract call approved pending remained + expect(await contractCallApprovedRepository.findPending()).toEqual([]); + + // Expect entries in database updated + const firstEntry = await contractCallApprovedRepository.findByCommandId(originalFirstEntry.commandId); + expect(firstEntry).toEqual({ + ...originalFirstEntry, + retry: 2, + executeTxHash: 'fc08669e4eabdf452e43adf5705777f8a527f4d6f84df9bf90ae74b499371061', + updatedAt: expect.any(Date), + }); + + const secondEntry = await contractCallApprovedRepository.findByCommandId(originalSecondEntry.commandId); + expect(secondEntry).toEqual({ + ...originalSecondEntry, + status: ContractCallApprovedStatus.FAILED, + updatedAt: expect.any(Date), + }); + + // Was not updated + const thirdEntry = await contractCallApprovedRepository.findByCommandId(originalThirdEntry.commandId); + expect(thirdEntry).toEqual({ + ...originalThirdEntry, + }); + }); +}); diff --git a/libs/common/src/contracts/contracts.module.ts b/libs/common/src/contracts/contracts.module.ts index c1c2880..6712c73 100644 --- a/libs/common/src/contracts/contracts.module.ts +++ b/libs/common/src/contracts/contracts.module.ts @@ -1,6 +1,6 @@ import { Module } from '@nestjs/common'; import { GatewayContract } from './gateway.contract'; -import { ApiConfigService } from '@mvx-monorepo/common'; +import { ApiConfigService, DynamicModuleUtils } from '@mvx-monorepo/common'; import { ApiNetworkProvider, ProxyNetworkProvider } from '@multiversx/sdk-network-providers/out'; import { ResultsParser } from '@multiversx/sdk-core/out'; import { ContractLoader } from '@mvx-monorepo/common/contracts/contract.loader'; @@ -8,9 +8,10 @@ import { join } from 'path'; import { GasServiceContract } from '@mvx-monorepo/common/contracts/gas-service.contract'; import { ProviderKeys } from '@mvx-monorepo/common/utils/provider.enum'; import { Mnemonic, UserSigner } from '@multiversx/sdk-wallet/out'; +import { TransactionsHelper } from '@mvx-monorepo/common/contracts/transactions.helper'; @Module({ - imports: [], + imports: [DynamicModuleUtils.getCachingModule()], providers: [ { provide: ProxyNetworkProvider, @@ -77,7 +78,15 @@ import { Mnemonic, UserSigner } from '@multiversx/sdk-wallet/out'; }, inject: [ApiConfigService, ResultsParser], }, + TransactionsHelper, + ], + exports: [ + GatewayContract, + GasServiceContract, + ProviderKeys.WALLET_SIGNER, + ProxyNetworkProvider, + ApiNetworkProvider, + TransactionsHelper, ], - exports: [GatewayContract, GasServiceContract, ProviderKeys.WALLET_SIGNER, ProxyNetworkProvider, ApiNetworkProvider], }) export class ContractsModule {} diff --git a/libs/common/src/contracts/transactions.helper.ts b/libs/common/src/contracts/transactions.helper.ts new file mode 100644 index 0000000..3cacfda --- /dev/null +++ b/libs/common/src/contracts/transactions.helper.ts @@ -0,0 +1,51 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { ProxyNetworkProvider } from '@multiversx/sdk-network-providers/out'; +import { GetOrSetCache } from '@mvx-monorepo/common/decorators/get.or.set.cache'; +import { CacheInfo } from '@mvx-monorepo/common'; +import { Transaction } from '@multiversx/sdk-core/out'; +import { UserAddress } from '@multiversx/sdk-wallet/out/userAddress'; + +@Injectable() +export class TransactionsHelper { + private readonly logger: Logger; + + constructor(private readonly proxy: ProxyNetworkProvider) { + this.logger = new Logger(TransactionsHelper.name); + } + + @GetOrSetCache(CacheInfo.ChainId) + async getChainId(): Promise { + const result = await this.proxy.getNetworkConfig(); + + return result.ChainID; + } + + async getAccountNonce(address: UserAddress): Promise { + const accountOnNetwork = await this.proxy.getAccount(address); + + return accountOnNetwork.nonce; + } + + // TODO: Check if this works properly + async getTransactionGas(transaction: Transaction, retry: number): Promise { + const result = await this.proxy.doPostGeneric('transaction/cost', transaction.toSendable()); + + return (result.data.txGasUnits * (11 + retry * 2)) / 10; // add 10% extra gas initially, and more gas with each retry + } + + async sendTransactions(transactions: Transaction[]) { + try { + await this.proxy.sendTransactions(transactions); + + this.logger.log( + `Sent ${transactions.length} transactions to proxy: ${transactions.map((trans) => trans.getHash())}`, + ); + + return true; + } catch (e) { + this.logger.error(`Can not send transactions to proxy... ${e}`); + + return false; + } + } +} diff --git a/libs/common/src/database/repository/contract-call-approved.repository.ts b/libs/common/src/database/repository/contract-call-approved.repository.ts index 8b9136b..43eae7b 100644 --- a/libs/common/src/database/repository/contract-call-approved.repository.ts +++ b/libs/common/src/database/repository/contract-call-approved.repository.ts @@ -2,9 +2,6 @@ import { Injectable } from '@nestjs/common'; import { PrismaService } from '@mvx-monorepo/common/database/prisma.service'; import { ContractCallApproved, ContractCallApprovedStatus, Prisma } from '@prisma/client'; -// Support a max of 3 retries (mainly because some Interchain Token Service endpoints need to be called 3 times) -export const MAX_NUMBER_OF_RETRIES: number = 3; - @Injectable() export class ContractCallApprovedRepository { constructor(private readonly prisma: PrismaService) {} @@ -15,41 +12,26 @@ export class ContractCallApprovedRepository { }); } - findPendingNoRetries(page: number = 0, take: number = 10): Promise { - return this.prisma.contractCallApproved.findMany({ - where: { - status: ContractCallApprovedStatus.PENDING, - retry: 0, - executeTxHash: null, - }, - orderBy: { - createdAt: 'desc', - }, - skip: page * take, - take, - }); - } - - findPendingForRetry(page: number = 0, take: number = 10): Promise { - // Last updated more than one minute ago + findPending(page: number = 0, take: number = 10): Promise { + // Last updated more than one minute ago, if retrying const lastUpdatedAt = new Date(new Date().getTime() - 60_000); return this.prisma.contractCallApproved.findMany({ where: { status: ContractCallApprovedStatus.PENDING, - retry: { - lt: MAX_NUMBER_OF_RETRIES, - }, - executeTxHash: { - not: null, - }, - updatedAt: { - lt: lastUpdatedAt, - }, - }, - orderBy: { - createdAt: 'desc', + OR: [ + { retry: 0 }, + { + updatedAt: { + lt: lastUpdatedAt, + }, + }, + ], }, + orderBy: [ + { retry: 'asc' }, // new entries have priority over older ones + { createdAt: 'asc' }, + ], skip: page * take, take, }); diff --git a/package-lock.json b/package-lock.json index 8943683..a669a56 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53,10 +53,10 @@ "@types/js-yaml": "^4.0.5", "@types/node": "^20.1.4", "@types/supertest": "^2.0.12", - "@typescript-eslint/eslint-plugin": "^5.59.5", - "@typescript-eslint/parser": "^5.59.5", - "eslint": "^8.40.0", - "eslint-config-prettier": "^8.8.0", + "@typescript-eslint/eslint-plugin": "^5.62.0", + "@typescript-eslint/parser": "^5.62.0", + "eslint": "^8.55.0", + "eslint-config-prettier": "^8.10.0", "eslint-plugin-prettier": "^4.2.1", "jest": "^29.7.0", "js-yaml": "^4.1.0", @@ -926,9 +926,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.3.tgz", - "integrity": "sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, "dependencies": { "ajv": "^6.12.4", @@ -971,9 +971,9 @@ "dev": true }, "node_modules/@eslint/js": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.54.0.tgz", - "integrity": "sha512-ut5V+D+fOoWPgGGNj83GGjnntO39xDy6DWxO0wb7Jp3DcMX0TfIqdzHF85VTQkerdyGmuuMD9AKAo5KiNlf/AQ==", + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.55.0.tgz", + "integrity": "sha512-qQfo2mxH5yVom1kacMtZZJFVdW+E70mqHMJvVg6WTLo+VBuQJ4TojZlfWBjK0ve5BdEeNAVxOsl/nvNMpJOaJA==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -5259,15 +5259,15 @@ } }, "node_modules/eslint": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.54.0.tgz", - "integrity": "sha512-NY0DfAkM8BIZDVl6PgSa1ttZbx3xHgJzSNJKYcQglem6CppHyMhRIQkBVSSMaSRnLhig3jsDbEzOjwCVt4AmmA==", + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.55.0.tgz", + "integrity": "sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdt+rCntju0xEH7teIABPwXpahftIaTdA==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.3", - "@eslint/js": "8.54.0", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.55.0", "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -6208,9 +6208,9 @@ "dev": true }, "node_modules/globals": { - "version": "13.23.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", - "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -11482,9 +11482,9 @@ "dev": true }, "@eslint/eslintrc": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.3.tgz", - "integrity": "sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, "requires": { "ajv": "^6.12.4", @@ -11519,9 +11519,9 @@ } }, "@eslint/js": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.54.0.tgz", - "integrity": "sha512-ut5V+D+fOoWPgGGNj83GGjnntO39xDy6DWxO0wb7Jp3DcMX0TfIqdzHF85VTQkerdyGmuuMD9AKAo5KiNlf/AQ==", + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.55.0.tgz", + "integrity": "sha512-qQfo2mxH5yVom1kacMtZZJFVdW+E70mqHMJvVg6WTLo+VBuQJ4TojZlfWBjK0ve5BdEeNAVxOsl/nvNMpJOaJA==", "dev": true }, "@golevelup/nestjs-common": { @@ -14768,15 +14768,15 @@ "dev": true }, "eslint": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.54.0.tgz", - "integrity": "sha512-NY0DfAkM8BIZDVl6PgSa1ttZbx3xHgJzSNJKYcQglem6CppHyMhRIQkBVSSMaSRnLhig3jsDbEzOjwCVt4AmmA==", + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.55.0.tgz", + "integrity": "sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdt+rCntju0xEH7teIABPwXpahftIaTdA==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.3", - "@eslint/js": "8.54.0", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.55.0", "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -15489,9 +15489,9 @@ "dev": true }, "globals": { - "version": "13.23.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", - "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "requires": { "type-fest": "^0.20.2" diff --git a/package.json b/package.json index 1351288..19168bc 100644 --- a/package.json +++ b/package.json @@ -22,9 +22,10 @@ "test:watch": "jest --watch", "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "jest --config ./apps/mvx-event-processor/test/jest-e2e.json", + "test:e2e": "dotenv -e .env.test -- jest --config ./apps/mvx-event-processor/test/jest-e2e.json --force-exit", "migrate": "prisma migrate dev", - "generate": "prisma generate" + "generate": "prisma generate", + "test:migrate": "dotenv -e .env.test -- prisma migrate deploy" }, "dependencies": { "@grpc/grpc-js": "^1.9.12", @@ -71,10 +72,10 @@ "@types/js-yaml": "^4.0.5", "@types/node": "^20.1.4", "@types/supertest": "^2.0.12", - "@typescript-eslint/eslint-plugin": "^5.59.5", - "@typescript-eslint/parser": "^5.59.5", - "eslint": "^8.40.0", - "eslint-config-prettier": "^8.8.0", + "@typescript-eslint/eslint-plugin": "^5.62.0", + "@typescript-eslint/parser": "^5.62.0", + "eslint": "^8.55.0", + "eslint-config-prettier": "^8.10.0", "eslint-plugin-prettier": "^4.2.1", "jest": "^29.7.0", "js-yaml": "^4.1.0", From 63a986dd85389173ad621d15725c9fc5ed89ee80 Mon Sep 17 00:00:00 2001 From: Rares <6453351+raresserban@users.noreply.github.com> Date: Wed, 13 Dec 2023 15:17:43 +0200 Subject: [PATCH 13/33] Create cron that subscribes to axelar events and sends transactions. --- .../approvals.processor.module.ts | 19 ++ .../approvals.processor.service.ts | 180 ++++++++++++++++++ .../entities/pending-transaction.ts | 4 + .../src/approvals-processor/index.ts | 2 + apps/axelar-event-processor/src/main.ts | 22 +-- .../src/processor/index.ts | 2 - .../processor/transaction.processor.module.ts | 10 - .../transaction.processor.service.ts | 40 ---- ...call-contract-approved.processor.module.ts | 2 +- ...all-contract-approved.processor.service.ts | 4 +- .../event-processor/event.processor.module.ts | 1 - .../event.processor.service.ts | 3 - .../src/processors/gateway.processor.ts | 4 +- libs/common/src/contracts/contracts.module.ts | 12 +- libs/common/src/contracts/gateway.contract.ts | 16 +- libs/common/src/contracts/index.ts | 4 + .../src/contracts/transactions.helper.ts | 32 +++- libs/common/src/decorators/index.ts | 1 + libs/common/src/grpc/grpc.service.ts | 14 +- libs/common/src/grpc/index.ts | 2 + libs/common/src/index.ts | 6 +- libs/common/src/pubsub/index.ts | 2 - .../src/pubsub/pub.sub.listener.controller.ts | 22 --- .../src/pubsub/pub.sub.listener.module.ts | 17 -- libs/common/src/utils/cache.info.ts | 17 +- libs/common/src/utils/dynamic.module.utils.ts | 22 ++- 26 files changed, 319 insertions(+), 141 deletions(-) create mode 100644 apps/axelar-event-processor/src/approvals-processor/approvals.processor.module.ts create mode 100644 apps/axelar-event-processor/src/approvals-processor/approvals.processor.service.ts create mode 100644 apps/axelar-event-processor/src/approvals-processor/entities/pending-transaction.ts create mode 100644 apps/axelar-event-processor/src/approvals-processor/index.ts delete mode 100644 apps/axelar-event-processor/src/processor/index.ts delete mode 100644 apps/axelar-event-processor/src/processor/transaction.processor.module.ts delete mode 100644 apps/axelar-event-processor/src/processor/transaction.processor.service.ts create mode 100644 libs/common/src/contracts/index.ts create mode 100644 libs/common/src/decorators/index.ts create mode 100644 libs/common/src/grpc/index.ts delete mode 100644 libs/common/src/pubsub/index.ts delete mode 100644 libs/common/src/pubsub/pub.sub.listener.controller.ts delete mode 100644 libs/common/src/pubsub/pub.sub.listener.module.ts diff --git a/apps/axelar-event-processor/src/approvals-processor/approvals.processor.module.ts b/apps/axelar-event-processor/src/approvals-processor/approvals.processor.module.ts new file mode 100644 index 0000000..51472ef --- /dev/null +++ b/apps/axelar-event-processor/src/approvals-processor/approvals.processor.module.ts @@ -0,0 +1,19 @@ +import { Module } from '@nestjs/common'; +import { ApiConfigModule, DynamicModuleUtils } from '@mvx-monorepo/common'; +import { ApprovalsProcessorService } from './approvals.processor.service'; +import { GrpcModule } from '@mvx-monorepo/common/grpc/grpc.module'; +import { ContractsModule } from '@mvx-monorepo/common/contracts/contracts.module'; +import { ScheduleModule } from '@nestjs/schedule'; + +@Module({ + imports: [ + ScheduleModule.forRoot(), + ApiConfigModule, + DynamicModuleUtils.getCacheModule(), + DynamicModuleUtils.getRedisCacheModule(), + GrpcModule, + ContractsModule, + ], + providers: [ApprovalsProcessorService], +}) +export class ApprovalsProcessorModule {} diff --git a/apps/axelar-event-processor/src/approvals-processor/approvals.processor.service.ts b/apps/axelar-event-processor/src/approvals-processor/approvals.processor.service.ts new file mode 100644 index 0000000..73142bc --- /dev/null +++ b/apps/axelar-event-processor/src/approvals-processor/approvals.processor.service.ts @@ -0,0 +1,180 @@ +import { Locker } from '@multiversx/sdk-nestjs-common'; +import { Inject, Injectable, Logger } from '@nestjs/common'; +import { Cron } from '@nestjs/schedule'; +import { GrpcService } from '@mvx-monorepo/common/grpc/grpc.service'; +import { CacheService, RedisCacheService } from '@multiversx/sdk-nestjs-cache'; +import { ApiConfigService, CacheInfo } from '@mvx-monorepo/common'; +import { Subscription } from 'rxjs'; +import { SubscribeToApprovalsResponse } from '@mvx-monorepo/common/grpc/entities/relayer'; +import { ProviderKeys } from '@mvx-monorepo/common/utils/provider.enum'; +import { UserSigner } from '@multiversx/sdk-wallet/out'; +import { TransactionsHelper } from '@mvx-monorepo/common/contracts/transactions.helper'; +import { GatewayContract } from '@mvx-monorepo/common/contracts/gateway.contract'; +import { PendingTransaction } from './entities/pending-transaction'; + +const MAX_NUMBER_OF_RETRIES = 3; + +@Injectable() +export class ApprovalsProcessorService { + private readonly logger: Logger; + private readonly sourceChain: string; + + private chainId: string = ''; + private approvalsSubscription: Subscription | null = null; + + constructor( + private readonly grpcService: GrpcService, + private readonly cacheService: CacheService, + private readonly redisCacheService: RedisCacheService, + @Inject(ProviderKeys.WALLET_SIGNER) private readonly walletSigner: UserSigner, + private readonly transactionsHelper: TransactionsHelper, + private readonly gatewayContract: GatewayContract, + apiConfigService: ApiConfigService, + ) { + this.logger = new Logger(ApprovalsProcessorService.name); + this.sourceChain = apiConfigService.getSourceChainName(); + } + + @Cron('*/30 * * * * *') + async handleNewApprovals() { + await Locker.lock('handleNewApprovals', async () => { + if (this.approvalsSubscription && !this.approvalsSubscription.closed) { + this.logger.log('GRPC approvals stream subscription is already running'); + + return; + } + + this.logger.log('Starting GRPC approvals stream subscription'); + + this.chainId = await this.transactionsHelper.getChainId(); + + const lastProcessedHeight = + (await this.cacheService.get(CacheInfo.StartProcessHeight().key)) || undefined; + + const observable = this.grpcService.subscribeToApprovals(this.sourceChain, lastProcessedHeight); + + const onComplete = () => { + this.logger.warn('Approvals stream subscription ended'); + + this.approvalsSubscription = null; + }; + const onError = (e: any) => { + this.logger.error(`Approvals stream subscription ended with error...`); + this.logger.error(e); + + this.approvalsSubscription = null; + }; + + // TODO: Test if this works as expected + this.approvalsSubscription = observable.subscribe({ + next: this.processMessage, + complete: onComplete, + error: onError, + }); + }); + } + + @Cron('*/6 * * * * *') + async handlePendingTransactions() { + await Locker.lock('pendingTransactions', async () => { + const keys = await this.redisCacheService.scan(CacheInfo.PendingTransaction('*').key); + for (const txHash of keys) { + const cachedValue = await this.cacheService.getRemote(txHash); + + await this.cacheService.deleteRemote(txHash); + + if (cachedValue === undefined) { + continue; + } + + const { executeData, retry } = cachedValue; + + const success = await this.transactionsHelper.awaitComplete(txHash); + + // Nothing to do on success + if (success) { + continue; + } + + if (retry === MAX_NUMBER_OF_RETRIES) { + this.logger.error(`Could not execute Gateway execute transaction with hash ${txHash} after ${retry} retries`); + + continue; + } + + try { + await this.executeTransaction(executeData, retry); + } catch (e) { + this.logger.error('Error while trying to retry Axelar Approvals response transaction...'); + this.logger.error(e); + + // Set value back in cache to be retried again (with same retry number) + await this.cacheService.setRemote(CacheInfo.PendingTransaction(txHash).key, { + executeData: executeData, + retry: retry, + }); + } + } + }); + } + + async processMessage(response: SubscribeToApprovalsResponse) { + this.logger.debug('Received Axelar Approvals response:'); + this.logger.debug(JSON.stringify(response)); + + try { + await this.executeTransaction(response.executeData); + } catch (e) { + this.logger.error('Error while processing Axelar Approvals response...'); + this.logger.error(e); + + // Set start process height to current block height + await this.cacheService.set( + CacheInfo.StartProcessHeight().key, + response.blockHeight, + CacheInfo.StartProcessHeight().ttl, + ); + + // Unsubscribe so processing stops at this event and is retried + (this.approvalsSubscription as Subscription).unsubscribe(); + + return; + } + + // Set start process height to next block height. + await this.cacheService.set( + CacheInfo.StartProcessHeight().key, + response.blockHeight + 1, + CacheInfo.StartProcessHeight().ttl, + ); + } + + private async executeTransaction(executeData: Uint8Array, retry: number = 0) { + this.logger.debug(`Trying to execute Gateway execute transaction with executeData:`); + this.logger.debug(executeData); + + // TODO: Check if it is fine to use the same wallet as in the CallContractApprovedProcessor + // and that no issues happen because of nonce + const accountNonce = await this.transactionsHelper.getAccountNonce(this.walletSigner.getAddress()); + + const transaction = this.gatewayContract.buildExecuteTransaction( + executeData, + accountNonce, + this.chainId, + this.walletSigner.getAddress(), + ); + + const gas = await this.transactionsHelper.getTransactionGas(transaction, retry); + transaction.setGasLimit(gas); + + const signature = await this.walletSigner.sign(transaction.serializeForSigning()); + transaction.applySignature(signature); + + const txHash = await this.transactionsHelper.sendTransaction(transaction); + + await this.cacheService.setRemote(CacheInfo.PendingTransaction(txHash).key, { + executeData: executeData, + retry: retry + 1, + }); + } +} diff --git a/apps/axelar-event-processor/src/approvals-processor/entities/pending-transaction.ts b/apps/axelar-event-processor/src/approvals-processor/entities/pending-transaction.ts new file mode 100644 index 0000000..3d3992a --- /dev/null +++ b/apps/axelar-event-processor/src/approvals-processor/entities/pending-transaction.ts @@ -0,0 +1,4 @@ +export interface PendingTransaction { + executeData: Uint8Array; + retry: number; +} diff --git a/apps/axelar-event-processor/src/approvals-processor/index.ts b/apps/axelar-event-processor/src/approvals-processor/index.ts new file mode 100644 index 0000000..02f7671 --- /dev/null +++ b/apps/axelar-event-processor/src/approvals-processor/index.ts @@ -0,0 +1,2 @@ +export * from './approvals.processor.module'; +export * from './approvals.processor.service'; diff --git a/apps/axelar-event-processor/src/main.ts b/apps/axelar-event-processor/src/main.ts index da802b9..1b9fe05 100644 --- a/apps/axelar-event-processor/src/main.ts +++ b/apps/axelar-event-processor/src/main.ts @@ -1,27 +1,9 @@ import 'module-alias/register'; import { NestFactory } from '@nestjs/core'; -import { TransactionProcessorModule } from './processor'; -import { ApiConfigService, PubSubListenerModule } from '@mvx-monorepo/common'; -import { MicroserviceOptions, Transport } from '@nestjs/microservices'; -import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; -import { join } from 'path'; -import { protobufPackage } from '@mvx-monorepo/common/grpc/entities/relayer'; +import { ApprovalsProcessorModule } from './approvals-processor'; async function bootstrap() { - const transactionProcessorApp = await NestFactory.createApplicationContext(TransactionProcessorModule); - const apiConfigService = transactionProcessorApp.get(ApiConfigService); - - const pubSubApp = await NestFactory.createMicroservice(PubSubListenerModule.forRoot(), { - transport: Transport.GRPC, - options: { - package: protobufPackage, - protoPath: join(__dirname, '../../../libs/common/src/assets/relayer.proto'), - url: apiConfigService.getAxelarApiUrl(), - }, - }); - pubSubApp.useLogger(pubSubApp.get(WINSTON_MODULE_NEST_PROVIDER)); - // eslint-disable-next-line @typescript-eslint/no-floating-promises - pubSubApp.listen(); + await NestFactory.createApplicationContext(ApprovalsProcessorModule); } // eslint-disable-next-line @typescript-eslint/no-floating-promises diff --git a/apps/axelar-event-processor/src/processor/index.ts b/apps/axelar-event-processor/src/processor/index.ts deleted file mode 100644 index 28f5a51..0000000 --- a/apps/axelar-event-processor/src/processor/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './transaction.processor.module'; -export * from './transaction.processor.service'; diff --git a/apps/axelar-event-processor/src/processor/transaction.processor.module.ts b/apps/axelar-event-processor/src/processor/transaction.processor.module.ts deleted file mode 100644 index a3f9b0f..0000000 --- a/apps/axelar-event-processor/src/processor/transaction.processor.module.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Module } from '@nestjs/common'; -import { ScheduleModule } from '@nestjs/schedule'; -import { ApiConfigModule, DynamicModuleUtils } from '@mvx-monorepo/common'; -import { TransactionProcessorService } from './transaction.processor.service'; - -@Module({ - imports: [ApiConfigModule, ScheduleModule.forRoot(), DynamicModuleUtils.getCachingModule()], - providers: [TransactionProcessorService], -}) -export class TransactionProcessorModule {} diff --git a/apps/axelar-event-processor/src/processor/transaction.processor.service.ts b/apps/axelar-event-processor/src/processor/transaction.processor.service.ts deleted file mode 100644 index b355bfe..0000000 --- a/apps/axelar-event-processor/src/processor/transaction.processor.service.ts +++ /dev/null @@ -1,40 +0,0 @@ - -import { CacheService } from "@multiversx/sdk-nestjs-cache"; -import { Locker } from "@multiversx/sdk-nestjs-common"; -import { TransactionProcessor } from "@multiversx/sdk-transaction-processor"; -import { Injectable, Logger } from "@nestjs/common"; -import { Cron } from "@nestjs/schedule"; -import { ApiConfigService, CacheInfo } from "@mvx-monorepo/common"; - -@Injectable() -export class TransactionProcessorService { - private transactionProcessor: TransactionProcessor = new TransactionProcessor(); - private readonly logger: Logger; - - constructor( - private readonly apiConfigService: ApiConfigService, - private readonly cacheService: CacheService - ) { - this.logger = new Logger(TransactionProcessorService.name); - } - - @Cron('*/1 * * * * *') - async handleNewTransactions() { - await Locker.lock('newTransactions', async () => { - await this.transactionProcessor.start({ - gatewayUrl: this.apiConfigService.getApiUrl(), - maxLookBehind: 100, - // eslint-disable-next-line require-await - onTransactionsReceived: async (shardId, nonce, transactions, statistics) => { - this.logger.log(`Received ${transactions.length} transactions on shard ${shardId} and nonce ${nonce}. Time left: ${statistics.secondsLeft}`); - }, - getLastProcessedNonce: async (shardId) => { - return await this.cacheService.getRemote(CacheInfo.LastProcessedNonce(shardId).key); - }, - setLastProcessedNonce: async (shardId, nonce) => { - await this.cacheService.setRemote(CacheInfo.LastProcessedNonce(shardId).key, nonce, CacheInfo.LastProcessedNonce(shardId).ttl); - }, - }); - }); - } -} diff --git a/apps/mvx-event-processor/src/call-contract-approved-processor/call-contract-approved.processor.module.ts b/apps/mvx-event-processor/src/call-contract-approved-processor/call-contract-approved.processor.module.ts index d8decf5..6f067df 100644 --- a/apps/mvx-event-processor/src/call-contract-approved-processor/call-contract-approved.processor.module.ts +++ b/apps/mvx-event-processor/src/call-contract-approved-processor/call-contract-approved.processor.module.ts @@ -7,7 +7,7 @@ import { CallContractApprovedProcessorService } from './call-contract-approved.p @Module({ imports: [ ScheduleModule.forRoot(), - DynamicModuleUtils.getCachingModule(), + DynamicModuleUtils.getCacheModule(), DatabaseModule, ContractsModule, ], diff --git a/apps/mvx-event-processor/src/call-contract-approved-processor/call-contract-approved.processor.service.ts b/apps/mvx-event-processor/src/call-contract-approved-processor/call-contract-approved.processor.service.ts index 9d51871..3efdec8 100644 --- a/apps/mvx-event-processor/src/call-contract-approved-processor/call-contract-approved.processor.service.ts +++ b/apps/mvx-event-processor/src/call-contract-approved-processor/call-contract-approved.processor.service.ts @@ -64,7 +64,7 @@ export class CallContractApprovedProcessorService { `Trying to execute ContractCallApproved transaction with commandId ${contractCallApproved.commandId}`, ); - const transaction = await this.buildExecuteTransaction(contractCallApproved, accountNonce, chainId); + const transaction = await this.buildAndSignExecuteTransaction(contractCallApproved, accountNonce, chainId); accountNonce++; @@ -86,7 +86,7 @@ export class CallContractApprovedProcessorService { }); } - private async buildExecuteTransaction( + private async buildAndSignExecuteTransaction( contractCallApproved: ContractCallApproved, accountNonce: number, chainId: string, diff --git a/apps/mvx-event-processor/src/event-processor/event.processor.module.ts b/apps/mvx-event-processor/src/event-processor/event.processor.module.ts index 79852cd..4bab530 100644 --- a/apps/mvx-event-processor/src/event-processor/event.processor.module.ts +++ b/apps/mvx-event-processor/src/event-processor/event.processor.module.ts @@ -17,6 +17,5 @@ import { ProcessorsModule } from '../processors'; ProcessorsModule, ], providers: [EventProcessorService], - exports: [EventProcessorService], }) export class EventProcessorModule {} diff --git a/apps/mvx-event-processor/src/event-processor/event.processor.service.ts b/apps/mvx-event-processor/src/event-processor/event.processor.service.ts index c9c83c7..0b15b22 100644 --- a/apps/mvx-event-processor/src/event-processor/event.processor.service.ts +++ b/apps/mvx-event-processor/src/event-processor/event.processor.service.ts @@ -43,9 +43,6 @@ export class EventProcessorService { } private async handleEvent(event: NotifierEvent) { - this.logger.debug('Received event from MultiversX:'); - this.logger.debug(JSON.stringify(event)); - if (event.address === this.contractGasService) { this.logger.debug('Received Gas Service event from MultiversX:'); this.logger.debug(JSON.stringify(event)); diff --git a/apps/mvx-event-processor/src/processors/gateway.processor.ts b/apps/mvx-event-processor/src/processors/gateway.processor.ts index 13f7dd7..957be5d 100644 --- a/apps/mvx-event-processor/src/processors/gateway.processor.ts +++ b/apps/mvx-event-processor/src/processors/gateway.processor.ts @@ -11,13 +11,13 @@ import { EventIdentifiers, Events } from '@mvx-monorepo/common/utils/event.enum' import { BinaryUtils } from '@multiversx/sdk-nestjs-common'; import { ContractCallApprovedRepository } from '@mvx-monorepo/common/database/repository/contract-call-approved.repository'; -// order/logIndex is unsupported since we can't easily get it in the relayer +// order/logIndex is unsupported since we can't easily get it in the relayer, // so we use a sufficiently large u32 value here instead const UNSUPPORTED_LOG_INDEX: number = 999_999; @Injectable() export class GatewayProcessor implements ProcessorInterface { - private sourceChain: string; + private readonly sourceChain: string; constructor( private readonly gatewayContract: GatewayContract, diff --git a/libs/common/src/contracts/contracts.module.ts b/libs/common/src/contracts/contracts.module.ts index 6712c73..f9a6364 100644 --- a/libs/common/src/contracts/contracts.module.ts +++ b/libs/common/src/contracts/contracts.module.ts @@ -1,17 +1,18 @@ import { Module } from '@nestjs/common'; import { GatewayContract } from './gateway.contract'; -import { ApiConfigService, DynamicModuleUtils } from '@mvx-monorepo/common'; import { ApiNetworkProvider, ProxyNetworkProvider } from '@multiversx/sdk-network-providers/out'; -import { ResultsParser } from '@multiversx/sdk-core/out'; +import { ResultsParser, TransactionWatcher } from '@multiversx/sdk-core/out'; import { ContractLoader } from '@mvx-monorepo/common/contracts/contract.loader'; import { join } from 'path'; import { GasServiceContract } from '@mvx-monorepo/common/contracts/gas-service.contract'; import { ProviderKeys } from '@mvx-monorepo/common/utils/provider.enum'; import { Mnemonic, UserSigner } from '@multiversx/sdk-wallet/out'; import { TransactionsHelper } from '@mvx-monorepo/common/contracts/transactions.helper'; +import { ApiConfigService } from '@mvx-monorepo/common/config'; +import { DynamicModuleUtils } from '@mvx-monorepo/common/utils'; @Module({ - imports: [DynamicModuleUtils.getCachingModule()], + imports: [DynamicModuleUtils.getCacheModule()], providers: [ { provide: ProxyNetworkProvider, @@ -35,6 +36,11 @@ import { TransactionsHelper } from '@mvx-monorepo/common/contracts/transactions. provide: ResultsParser, useValue: new ResultsParser(), }, + { + provide: TransactionWatcher, + useFactory: (api: ApiNetworkProvider) => new TransactionWatcher(api), // use api here not proxy since it returns proper transaction status + inject: [ApiNetworkProvider], + }, // { // provide: ContractQueryRunner, // useFactory: (api: ApiNetworkProvider) => new ContractQueryRunner(api), diff --git a/libs/common/src/contracts/gateway.contract.ts b/libs/common/src/contracts/gateway.contract.ts index 5c14b5f..cc9e5ea 100644 --- a/libs/common/src/contracts/gateway.contract.ts +++ b/libs/common/src/contracts/gateway.contract.ts @@ -1,4 +1,4 @@ -import { AbiRegistry, ResultsParser, SmartContract } from '@multiversx/sdk-core/out'; +import { AbiRegistry, BytesValue, IAddress, ResultsParser, SmartContract, Transaction } from '@multiversx/sdk-core/out'; import { Injectable, Logger } from '@nestjs/common'; import { Events } from '../utils/event.enum'; import { TransactionEvent } from '@multiversx/sdk-network-providers/out'; @@ -19,6 +19,20 @@ export class GatewayContract { this.logger = new Logger(GatewayContract.name); } + buildExecuteTransaction( + executeData: Uint8Array, + accountNonce: number, + chainId: string, + walletAddress: IAddress, + ): Transaction { + return this.smartContract.methodsExplicit + .execute([new BytesValue(Buffer.from(executeData))]) + .withSender(walletAddress) + .withNonce(accountNonce) + .withChainID(chainId) + .buildTransaction(); + } + decodeContractCallEvent(event: TransactionEvent): ContractCallEvent { const eventDefinition = this.abi.getEvent(Events.CONTRACT_CALL_EVENT); const outcome = this.resultsParser.parseEvent(event, eventDefinition); diff --git a/libs/common/src/contracts/index.ts b/libs/common/src/contracts/index.ts new file mode 100644 index 0000000..c4e7346 --- /dev/null +++ b/libs/common/src/contracts/index.ts @@ -0,0 +1,4 @@ +export * from './contracts.module'; +export * from './gas-service.contract'; +export * from './gateway.contract'; +export * from './transactions.helper'; diff --git a/libs/common/src/contracts/transactions.helper.ts b/libs/common/src/contracts/transactions.helper.ts index 3cacfda..a12bec6 100644 --- a/libs/common/src/contracts/transactions.helper.ts +++ b/libs/common/src/contracts/transactions.helper.ts @@ -1,15 +1,15 @@ import { Injectable, Logger } from '@nestjs/common'; import { ProxyNetworkProvider } from '@multiversx/sdk-network-providers/out'; import { GetOrSetCache } from '@mvx-monorepo/common/decorators/get.or.set.cache'; -import { CacheInfo } from '@mvx-monorepo/common'; -import { Transaction } from '@multiversx/sdk-core/out'; +import { Transaction, TransactionHash, TransactionWatcher } from '@multiversx/sdk-core/out'; import { UserAddress } from '@multiversx/sdk-wallet/out/userAddress'; +import { CacheInfo } from '@mvx-monorepo/common/utils'; @Injectable() export class TransactionsHelper { private readonly logger: Logger; - constructor(private readonly proxy: ProxyNetworkProvider) { + constructor(private readonly proxy: ProxyNetworkProvider, private readonly transactionWatcher: TransactionWatcher) { this.logger = new Logger(TransactionsHelper.name); } @@ -27,12 +27,26 @@ export class TransactionsHelper { } // TODO: Check if this works properly - async getTransactionGas(transaction: Transaction, retry: number): Promise { + async getTransactionGas(transaction: Transaction, retry: number = 0): Promise { const result = await this.proxy.doPostGeneric('transaction/cost', transaction.toSendable()); return (result.data.txGasUnits * (11 + retry * 2)) / 10; // add 10% extra gas initially, and more gas with each retry } + async sendTransaction(transaction: Transaction) { + try { + const hash = await this.proxy.sendTransaction(transaction); + + this.logger.log(`Sent transaction to proxy: ${transaction.getHash()}`); + + return hash; + } catch (e) { + this.logger.error(`Can not send transactions to proxy... ${e}`); + + throw e; + } + } + async sendTransactions(transactions: Transaction[]) { try { await this.proxy.sendTransactions(transactions); @@ -48,4 +62,14 @@ export class TransactionsHelper { return false; } } + + async awaitComplete(txHash: string) { + try { + const result = await this.transactionWatcher.awaitCompleted({ getHash: () => new TransactionHash(txHash) }); + + return !result.status.isFailed() && !result.status.isInvalid(); + } catch (e) { + return false; + } + } } diff --git a/libs/common/src/decorators/index.ts b/libs/common/src/decorators/index.ts new file mode 100644 index 0000000..116d0ad --- /dev/null +++ b/libs/common/src/decorators/index.ts @@ -0,0 +1 @@ +export * from './get.or.set.cache'; diff --git a/libs/common/src/grpc/grpc.service.ts b/libs/common/src/grpc/grpc.service.ts index 9144a35..3960900 100644 --- a/libs/common/src/grpc/grpc.service.ts +++ b/libs/common/src/grpc/grpc.service.ts @@ -2,8 +2,8 @@ import { Inject, Injectable, OnModuleInit } from '@nestjs/common'; import { ProviderKeys } from '@mvx-monorepo/common/utils/provider.enum'; import { ClientGrpc } from '@nestjs/microservices'; import { ContractCallEvent } from '@prisma/client'; -import { Relayer, VerifyRequest } from '@mvx-monorepo/common/grpc/entities/relayer'; -import { firstValueFrom, ReplaySubject } from 'rxjs'; +import { Relayer, SubscribeToApprovalsResponse, VerifyRequest } from '@mvx-monorepo/common/grpc/entities/relayer'; +import { firstValueFrom, Observable, ReplaySubject } from 'rxjs'; const RELAYER_SERVICE = 'Relayer'; @@ -45,4 +45,14 @@ export class GrpcService implements OnModuleInit { return Buffer.from(result.payload); } + + subscribeToApprovals( + chain: string, + startHeight?: number | undefined, + ): Observable { + return this.relayerService.subscribeToApprovals({ + chain, + startHeight, + }); + } } diff --git a/libs/common/src/grpc/index.ts b/libs/common/src/grpc/index.ts new file mode 100644 index 0000000..7506c0a --- /dev/null +++ b/libs/common/src/grpc/index.ts @@ -0,0 +1,2 @@ +export * from './grpc.module'; +export * from './grpc.service'; diff --git a/libs/common/src/index.ts b/libs/common/src/index.ts index ea10999..69b0706 100644 --- a/libs/common/src/index.ts +++ b/libs/common/src/index.ts @@ -1,4 +1,6 @@ -export * from './database'; -export * from './pubsub'; export * from './config'; +export * from './contracts'; +export * from './database'; +export * from './decorators'; +export * from './grpc'; export * from './utils'; diff --git a/libs/common/src/pubsub/index.ts b/libs/common/src/pubsub/index.ts deleted file mode 100644 index 624952c..0000000 --- a/libs/common/src/pubsub/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './pub.sub.listener.controller'; -export * from './pub.sub.listener.module'; diff --git a/libs/common/src/pubsub/pub.sub.listener.controller.ts b/libs/common/src/pubsub/pub.sub.listener.controller.ts deleted file mode 100644 index a57fa82..0000000 --- a/libs/common/src/pubsub/pub.sub.listener.controller.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { CacheService } from "@multiversx/sdk-nestjs-cache"; -import { Controller, Logger } from "@nestjs/common"; -import { EventPattern } from "@nestjs/microservices"; - -@Controller() -export class PubSubListenerController { - private logger: Logger; - - constructor( - private readonly cacheService: CacheService, - ) { - this.logger = new Logger(PubSubListenerController.name); - } - - @EventPattern('deleteCacheKeys') - async deleteCacheKey(keys: string[]) { - for (const key of keys) { - this.logger.log(`Deleting local cache key ${key}`); - await this.cacheService.deleteLocal(key); - } - } -} diff --git a/libs/common/src/pubsub/pub.sub.listener.module.ts b/libs/common/src/pubsub/pub.sub.listener.module.ts deleted file mode 100644 index 48ee2cd..0000000 --- a/libs/common/src/pubsub/pub.sub.listener.module.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { DynamicModule, Module } from '@nestjs/common'; -import { DynamicModuleUtils } from '@mvx-monorepo/common/utils/dynamic.module.utils'; -import { PubSubListenerController } from './pub.sub.listener.controller'; -import { LoggingModule } from '@multiversx/sdk-nestjs-common'; - -@Module({}) -export class PubSubListenerModule { - static forRoot(): DynamicModule { - return { - module: PubSubListenerModule, - imports: [LoggingModule, DynamicModuleUtils.getCachingModule()], - controllers: [PubSubListenerController], - providers: [DynamicModuleUtils.getPubSubService()], - exports: ['PUBSUB_SERVICE'], - }; - } -} diff --git a/libs/common/src/utils/cache.info.ts b/libs/common/src/utils/cache.info.ts index aa2a35b..36b8598 100644 --- a/libs/common/src/utils/cache.info.ts +++ b/libs/common/src/utils/cache.info.ts @@ -4,17 +4,24 @@ export class CacheInfo { key: string = ""; ttl: number = Constants.oneSecond() * 6; - static LastProcessedNonce(shardId: number): CacheInfo { + static ChainId(): CacheInfo { return { - key: `lastProcessedNonce:${shardId}`, - ttl: Constants.oneMonth(), + key: `chainId`, + ttl: Constants.oneWeek(), }; } - static ChainId(): CacheInfo { + static StartProcessHeight(): CacheInfo { return { - key: `chainId`, + key: `startProcessHeight`, ttl: Constants.oneWeek(), }; } + + static PendingTransaction(hash: string): CacheInfo { + return { + key: `pendingTransaction:${hash}`, + ttl: Constants.oneMinute() * 10, + }; + } } diff --git a/libs/common/src/utils/dynamic.module.utils.ts b/libs/common/src/utils/dynamic.module.utils.ts index c79801e..633bec1 100644 --- a/libs/common/src/utils/dynamic.module.utils.ts +++ b/libs/common/src/utils/dynamic.module.utils.ts @@ -1,10 +1,10 @@ -import { CacheModule, RedisCacheModuleOptions } from '@multiversx/sdk-nestjs-cache'; +import { CacheModule, RedisCacheModule, RedisCacheModuleOptions } from '@multiversx/sdk-nestjs-cache'; import { DynamicModule, Provider } from '@nestjs/common'; import { ClientOptions, ClientProxyFactory, Transport } from '@nestjs/microservices'; import { ApiConfigModule, ApiConfigService } from '../config'; export class DynamicModuleUtils { - static getCachingModule(): DynamicModule { + static getCacheModule(): DynamicModule { return CacheModule.forRootAsync({ imports: [ApiConfigModule], useFactory: (apiConfigService: ApiConfigService) => @@ -22,6 +22,24 @@ export class DynamicModuleUtils { }); } + static getRedisCacheModule(): DynamicModule { + return RedisCacheModule.forRootAsync({ + imports: [ApiConfigModule], + useFactory: (apiConfigService: ApiConfigService) => + new RedisCacheModuleOptions( + { + host: apiConfigService.getRedisUrl(), + port: apiConfigService.getRedisPort(), + }, + { + poolLimit: apiConfigService.getPoolLimit(), + processTtl: apiConfigService.getProcessTtl(), + }, + ), + inject: [ApiConfigService], + }); + } + static getPubSubService(): Provider { return { provide: 'PUBSUB_SERVICE', From 981e13c8f07dd694ec51af8a9a4991ad836cc550 Mon Sep 17 00:00:00 2001 From: Rares <6453351+raresserban@users.noreply.github.com> Date: Wed, 13 Dec 2023 17:15:51 +0200 Subject: [PATCH 14/33] Add test for approvals processor. --- .../approvals.processor.module.ts | 1 - .../approvals.processor.service.ts | 137 +++--- .../approvals.processor.spec.ts | 396 ++++++++++++++++++ .../entities/pending-transaction.ts | 1 + ...call-contract-approved.processor.module.ts | 27 +- .../src/contracts/transactions.helper.ts | 9 +- 6 files changed, 487 insertions(+), 84 deletions(-) create mode 100644 apps/axelar-event-processor/src/approvals-processor/approvals.processor.spec.ts diff --git a/apps/axelar-event-processor/src/approvals-processor/approvals.processor.module.ts b/apps/axelar-event-processor/src/approvals-processor/approvals.processor.module.ts index 51472ef..75b22ac 100644 --- a/apps/axelar-event-processor/src/approvals-processor/approvals.processor.module.ts +++ b/apps/axelar-event-processor/src/approvals-processor/approvals.processor.module.ts @@ -9,7 +9,6 @@ import { ScheduleModule } from '@nestjs/schedule'; imports: [ ScheduleModule.forRoot(), ApiConfigModule, - DynamicModuleUtils.getCacheModule(), DynamicModuleUtils.getRedisCacheModule(), GrpcModule, ContractsModule, diff --git a/apps/axelar-event-processor/src/approvals-processor/approvals.processor.service.ts b/apps/axelar-event-processor/src/approvals-processor/approvals.processor.service.ts index 73142bc..bd4a342 100644 --- a/apps/axelar-event-processor/src/approvals-processor/approvals.processor.service.ts +++ b/apps/axelar-event-processor/src/approvals-processor/approvals.processor.service.ts @@ -2,7 +2,7 @@ import { Locker } from '@multiversx/sdk-nestjs-common'; import { Inject, Injectable, Logger } from '@nestjs/common'; import { Cron } from '@nestjs/schedule'; import { GrpcService } from '@mvx-monorepo/common/grpc/grpc.service'; -import { CacheService, RedisCacheService } from '@multiversx/sdk-nestjs-cache'; +import { RedisCacheService } from '@multiversx/sdk-nestjs-cache'; import { ApiConfigService, CacheInfo } from '@mvx-monorepo/common'; import { Subscription } from 'rxjs'; import { SubscribeToApprovalsResponse } from '@mvx-monorepo/common/grpc/entities/relayer'; @@ -24,7 +24,6 @@ export class ApprovalsProcessorService { constructor( private readonly grpcService: GrpcService, - private readonly cacheService: CacheService, private readonly redisCacheService: RedisCacheService, @Inject(ProviderKeys.WALLET_SIGNER) private readonly walletSigner: UserSigner, private readonly transactionsHelper: TransactionsHelper, @@ -37,88 +36,95 @@ export class ApprovalsProcessorService { @Cron('*/30 * * * * *') async handleNewApprovals() { - await Locker.lock('handleNewApprovals', async () => { - if (this.approvalsSubscription && !this.approvalsSubscription.closed) { - this.logger.log('GRPC approvals stream subscription is already running'); + await Locker.lock('handleNewApprovals', this.handleNewApprovalsRaw.bind(this)); + } - return; - } + @Cron('*/6 * * * * *') + async handlePendingTransactions() { + await Locker.lock('pendingTransactions', this.handlePendingTransactionsRaw.bind(this)); + } - this.logger.log('Starting GRPC approvals stream subscription'); + async handleNewApprovalsRaw() { + if (this.approvalsSubscription && !this.approvalsSubscription.closed) { + this.logger.log('GRPC approvals stream subscription is already running'); - this.chainId = await this.transactionsHelper.getChainId(); + return; + } - const lastProcessedHeight = - (await this.cacheService.get(CacheInfo.StartProcessHeight().key)) || undefined; + this.logger.log('Starting GRPC approvals stream subscription'); - const observable = this.grpcService.subscribeToApprovals(this.sourceChain, lastProcessedHeight); + this.chainId = await this.transactionsHelper.getChainId(); - const onComplete = () => { - this.logger.warn('Approvals stream subscription ended'); + const lastProcessedHeight = + (await this.redisCacheService.get(CacheInfo.StartProcessHeight().key)) || undefined; - this.approvalsSubscription = null; - }; - const onError = (e: any) => { - this.logger.error(`Approvals stream subscription ended with error...`); - this.logger.error(e); + const observable = this.grpcService.subscribeToApprovals(this.sourceChain, lastProcessedHeight); + + const onComplete = () => { + this.logger.warn('Approvals stream subscription ended'); + + this.approvalsSubscription = null; + }; + const onError = (e: any) => { + this.logger.error(`Approvals stream subscription ended with error...`); + this.logger.error(e); - this.approvalsSubscription = null; - }; + this.approvalsSubscription = null; + }; - // TODO: Test if this works as expected - this.approvalsSubscription = observable.subscribe({ - next: this.processMessage, - complete: onComplete, - error: onError, - }); + // TODO: Test if this works as expected + this.approvalsSubscription = observable.subscribe({ + next: this.processMessage.bind(this), + complete: onComplete, + error: onError, }); } - @Cron('*/6 * * * * *') - async handlePendingTransactions() { - await Locker.lock('pendingTransactions', async () => { - const keys = await this.redisCacheService.scan(CacheInfo.PendingTransaction('*').key); - for (const txHash of keys) { - const cachedValue = await this.cacheService.getRemote(txHash); + async handlePendingTransactionsRaw() { + this.chainId = await this.transactionsHelper.getChainId(); - await this.cacheService.deleteRemote(txHash); + const keys = await this.redisCacheService.scan(CacheInfo.PendingTransaction('*').key); + for (const key of keys) { + const cachedValue = await this.redisCacheService.get(key); - if (cachedValue === undefined) { - continue; - } + await this.redisCacheService.delete(key); - const { executeData, retry } = cachedValue; + if (cachedValue === undefined) { + continue; + } - const success = await this.transactionsHelper.awaitComplete(txHash); + const { txHash, executeData, retry } = cachedValue; - // Nothing to do on success - if (success) { - continue; - } + const success = await this.transactionsHelper.awaitComplete(txHash); + + // Nothing to do on success + if (success) { + continue; + } - if (retry === MAX_NUMBER_OF_RETRIES) { - this.logger.error(`Could not execute Gateway execute transaction with hash ${txHash} after ${retry} retries`); + if (retry === MAX_NUMBER_OF_RETRIES) { + this.logger.error(`Could not execute Gateway execute transaction with hash ${txHash} after ${retry} retries`); - continue; - } + continue; + } - try { - await this.executeTransaction(executeData, retry); - } catch (e) { - this.logger.error('Error while trying to retry Axelar Approvals response transaction...'); - this.logger.error(e); + try { + await this.executeTransaction(executeData, retry); + } catch (e) { + this.logger.error('Error while trying to retry Axelar Approvals response transaction...'); + this.logger.error(e); - // Set value back in cache to be retried again (with same retry number) - await this.cacheService.setRemote(CacheInfo.PendingTransaction(txHash).key, { - executeData: executeData, - retry: retry, - }); - } + // Set value back in cache to be retried again (with same retry number) + await this.redisCacheService.set(CacheInfo.PendingTransaction(txHash).key, { + txHash, + executeData: executeData, + retry: retry, + }, CacheInfo.PendingTransaction(txHash).ttl); } - }); + } } - async processMessage(response: SubscribeToApprovalsResponse) { + private async processMessage(response: SubscribeToApprovalsResponse) { this.logger.debug('Received Axelar Approvals response:'); this.logger.debug(JSON.stringify(response)); @@ -129,20 +135,20 @@ export class ApprovalsProcessorService { this.logger.error(e); // Set start process height to current block height - await this.cacheService.set( + await this.redisCacheService.set( CacheInfo.StartProcessHeight().key, response.blockHeight, CacheInfo.StartProcessHeight().ttl, ); // Unsubscribe so processing stops at this event and is retried - (this.approvalsSubscription as Subscription).unsubscribe(); + this.approvalsSubscription?.unsubscribe(); return; } // Set start process height to next block height. - await this.cacheService.set( + await this.redisCacheService.set( CacheInfo.StartProcessHeight().key, response.blockHeight + 1, CacheInfo.StartProcessHeight().ttl, @@ -172,9 +178,10 @@ export class ApprovalsProcessorService { const txHash = await this.transactionsHelper.sendTransaction(transaction); - await this.cacheService.setRemote(CacheInfo.PendingTransaction(txHash).key, { + await this.redisCacheService.set(CacheInfo.PendingTransaction(txHash).key, { + txHash, executeData: executeData, retry: retry + 1, - }); + }, CacheInfo.PendingTransaction(txHash).ttl); } } diff --git a/apps/axelar-event-processor/src/approvals-processor/approvals.processor.spec.ts b/apps/axelar-event-processor/src/approvals-processor/approvals.processor.spec.ts new file mode 100644 index 0000000..2598b5a --- /dev/null +++ b/apps/axelar-event-processor/src/approvals-processor/approvals.processor.spec.ts @@ -0,0 +1,396 @@ +import { ApiConfigService, CacheInfo, TransactionsHelper } from '@mvx-monorepo/common'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { Test } from '@nestjs/testing'; +import { GrpcService } from '@mvx-monorepo/common/grpc/grpc.service'; +import { GatewayContract } from '@mvx-monorepo/common/contracts/gateway.contract'; +import { ApprovalsProcessorService } from './approvals.processor.service'; +import { RedisCacheService } from '@multiversx/sdk-nestjs-cache'; +import { UserSigner } from '@multiversx/sdk-wallet/out'; +import { ProviderKeys } from '@mvx-monorepo/common/utils/provider.enum'; +import { Subject } from 'rxjs'; +import { SubscribeToApprovalsResponse } from '@mvx-monorepo/common/grpc/entities/relayer'; +import { UserAddress } from '@multiversx/sdk-wallet/out/userAddress'; +import { Transaction } from '@multiversx/sdk-core/out'; + +describe('ApprovalsProcessorService', () => { + let grpcService: DeepMocked; + let redisCacheService: DeepMocked; + let walletSigner: DeepMocked; + let transactionsHelper: DeepMocked; + let gatewayContract: DeepMocked; + let apiConfigService: DeepMocked; + + let service: ApprovalsProcessorService; + + beforeEach(async () => { + grpcService = createMock(); + redisCacheService = createMock(); + walletSigner = createMock(); + transactionsHelper = createMock(); + gatewayContract = createMock(); + apiConfigService = createMock(); + + apiConfigService.getSourceChainName.mockReturnValue('multiversx-test'); + + const moduleRef = await Test.createTestingModule({ + providers: [ApprovalsProcessorService], + }) + .useMocker((token) => { + if (token === GrpcService) { + return grpcService; + } + + if (token === RedisCacheService) { + return redisCacheService; + } + + if (token === ProviderKeys.WALLET_SIGNER) { + return walletSigner; + } + + if (token === TransactionsHelper) { + return transactionsHelper; + } + + if (token === GatewayContract) { + return gatewayContract; + } + + if (token === ApiConfigService) { + return apiConfigService; + } + + return null; + }) + .compile(); + + apiConfigService.getSourceChainName.mockReturnValueOnce('multiversx-test'); + transactionsHelper.getChainId.mockReturnValue(Promise.resolve('test')); + redisCacheService.get.mockImplementation(() => { + return Promise.resolve(undefined); + }); + + service = moduleRef.get(ApprovalsProcessorService); + }); + + describe('handleNewApprovals', () => { + it('Should process message', async () => { + const observable = new Subject(); + grpcService.subscribeToApprovals.mockReturnValueOnce(observable); + + await service.handleNewApprovalsRaw(); + + // Calling again won't do anything since subscription is already active + await service.handleNewApprovalsRaw(); + + expect(transactionsHelper.getChainId).toHaveBeenCalledTimes(1); + expect(redisCacheService.get).toHaveBeenCalledTimes(1); + expect(grpcService.subscribeToApprovals).toHaveBeenCalledTimes(1); + expect(grpcService.subscribeToApprovals).toHaveBeenCalledWith('multiversx-test', undefined); + + const userAddress = UserAddress.fromBech32('erd1qqqqqqqqqqqqqpgqhe8t5jewej70zupmh44jurgn29psua5l2jps3ntjj3'); + const accountNonce = 0; + walletSigner.getAddress.mockReturnValueOnce(userAddress); + transactionsHelper.getAccountNonce.mockReturnValueOnce(Promise.resolve(accountNonce)); + + const transaction: DeepMocked = createMock(); + gatewayContract.buildExecuteTransaction.mockReturnValueOnce(transaction); + + transactionsHelper.getTransactionGas.mockReturnValueOnce(Promise.resolve(100_000_000)); + walletSigner.sign.mockReturnValueOnce(Promise.resolve(Buffer.from('signature'))); + transactionsHelper.sendTransaction.mockReturnValueOnce(Promise.resolve('txHash')); + + // Process a message + const message: SubscribeToApprovalsResponse = { + chain: 'multiversx-test', + executeData: Uint8Array.of(1, 2, 3, 4), + blockHeight: 1, + }; + observable.next(message); + + // Calling this won't do anything + observable.complete(); + + // Wait a bit so promises finish executing + await new Promise((resolve) => { + setTimeout(resolve, 500); + }); + + expect(transactionsHelper.getAccountNonce).toHaveBeenCalledTimes(1); + expect(transactionsHelper.getAccountNonce).toHaveBeenCalledWith(userAddress); + expect(gatewayContract.buildExecuteTransaction).toHaveBeenCalledTimes(1); + expect(gatewayContract.buildExecuteTransaction).toHaveBeenCalledWith( + message.executeData, + accountNonce, + 'test', + userAddress, + ); + expect(transactionsHelper.getTransactionGas).toHaveBeenCalledTimes(1); + expect(transactionsHelper.getTransactionGas).toHaveBeenCalledWith(transaction, 0); + expect(transaction.setGasLimit).toHaveBeenCalledTimes(1); + expect(transaction.setGasLimit).toHaveBeenCalledWith(100_000_000); + expect(transaction.applySignature).toHaveBeenCalledTimes(1); + expect(transaction.applySignature).toHaveBeenCalledWith(Buffer.from('signature')); + expect(transactionsHelper.sendTransaction).toHaveBeenCalledTimes(1); + expect(transactionsHelper.sendTransaction).toHaveBeenCalledWith(transaction); + + expect(redisCacheService.set).toHaveBeenCalledTimes(2); + expect(redisCacheService.set).toHaveBeenCalledWith( + CacheInfo.PendingTransaction('txHash').key, + { + txHash: 'txHash', + executeData: message.executeData, + retry: 1, + }, + CacheInfo.PendingTransaction('txHash').ttl, + ); + + expect(redisCacheService.set).toHaveBeenCalledWith( + CacheInfo.StartProcessHeight().key, + message.blockHeight + 1, // next block height + CacheInfo.StartProcessHeight().ttl, + ); + }); + + it('Should save current block height for retrying on error', async () => { + const observable = new Subject(); + grpcService.subscribeToApprovals.mockReturnValueOnce(observable); + + const userAddress = UserAddress.fromBech32('erd1qqqqqqqqqqqqqpgqhe8t5jewej70zupmh44jurgn29psua5l2jps3ntjj3'); + walletSigner.getAddress.mockReturnValueOnce(userAddress); + transactionsHelper.getAccountNonce.mockRejectedValueOnce(new Error('Network error')); + + await service.handleNewApprovalsRaw(); + // Process a message + const message: SubscribeToApprovalsResponse = { + chain: 'multiversx-test', + executeData: Uint8Array.of(1, 2, 3, 4), + blockHeight: 1, + }; + observable.next(message); + + // Wait a bit so promises finish executing + await new Promise((resolve) => { + setTimeout(resolve, 500); + }); + + expect(transactionsHelper.getAccountNonce).toHaveBeenCalledTimes(1); + expect(transactionsHelper.getAccountNonce).toHaveBeenCalledWith(userAddress); + + expect(redisCacheService.set).toHaveBeenCalledTimes(1); + expect(redisCacheService.set).toHaveBeenCalledWith( + CacheInfo.StartProcessHeight().key, + message.blockHeight, // same block height + CacheInfo.StartProcessHeight().ttl, + ); + + redisCacheService.get.mockImplementation(() => { + return Promise.resolve(1); + }); + + const newObservable = new Subject(); + grpcService.subscribeToApprovals.mockReturnValueOnce(newObservable); + + // Will re-initialize the subscription with same block height + await service.handleNewApprovalsRaw(); + + expect(transactionsHelper.getChainId).toHaveBeenCalledTimes(2); + expect(redisCacheService.get).toHaveBeenCalledTimes(2); + expect(grpcService.subscribeToApprovals).toHaveBeenCalledTimes(2); + expect(grpcService.subscribeToApprovals).toHaveBeenCalledWith('multiversx-test', 1); + }); + + it('Should reinitialize subscription on complete or on error', async () => { + const observable = new Subject(); + grpcService.subscribeToApprovals.mockReturnValueOnce(observable); + + await service.handleNewApprovalsRaw(); + + observable.complete(); + + const newObservable = new Subject(); + grpcService.subscribeToApprovals.mockReturnValueOnce(newObservable); + + await service.handleNewApprovalsRaw(); + + expect(transactionsHelper.getChainId).toHaveBeenCalledTimes(2); + expect(redisCacheService.get).toHaveBeenCalledTimes(2); + expect(grpcService.subscribeToApprovals).toHaveBeenCalledTimes(2); + + newObservable.error(new Error('Network error')); + + const newNewObservable = new Subject(); + grpcService.subscribeToApprovals.mockReturnValueOnce(newNewObservable); + + await service.handleNewApprovalsRaw(); + + expect(transactionsHelper.getChainId).toHaveBeenCalledTimes(3); + expect(redisCacheService.get).toHaveBeenCalledTimes(3); + expect(grpcService.subscribeToApprovals).toHaveBeenCalledTimes(3); + }); + }); + + describe('handlePendingTransactions', () => { + it('Should handle undefined', async () => { + const key = CacheInfo.PendingTransaction('txHashUndefined').key; + + redisCacheService.scan.mockReturnValueOnce( + Promise.resolve([key]), + ); + redisCacheService.get.mockReturnValueOnce(Promise.resolve(undefined)); + + await service.handlePendingTransactionsRaw(); + + expect(redisCacheService.scan).toHaveBeenCalledTimes(1); + expect(redisCacheService.get).toHaveBeenCalledTimes(1); + expect(redisCacheService.get).toHaveBeenCalledWith(key); + expect(redisCacheService.delete).toHaveBeenCalledTimes(1); + expect(redisCacheService.delete).toHaveBeenCalledWith(key); + expect(transactionsHelper.awaitComplete).not.toHaveBeenCalled(); + }); + + it('Should handle success', async () => { + const key = CacheInfo.PendingTransaction('txHashComplete').key; + + redisCacheService.scan.mockReturnValueOnce(Promise.resolve([key])); + redisCacheService.get.mockReturnValueOnce( + Promise.resolve({ + txHash: 'txHashComplete', + executeData: Uint8Array.of(1, 2, 3, 4), + retry: 1, + }), + ); + transactionsHelper.awaitComplete.mockReturnValueOnce(Promise.resolve(true)); + + await service.handlePendingTransactionsRaw(); + + expect(redisCacheService.scan).toHaveBeenCalledTimes(1); + expect(redisCacheService.get).toHaveBeenCalledTimes(1); + expect(redisCacheService.get).toHaveBeenCalledWith(key); + expect(redisCacheService.delete).toHaveBeenCalledTimes(1); + expect(redisCacheService.delete).toHaveBeenCalledWith(key); + expect(transactionsHelper.awaitComplete).toHaveBeenCalledTimes(1); + expect(transactionsHelper.awaitComplete).toHaveBeenCalledWith('txHashComplete'); + expect(transactionsHelper.getAccountNonce).not.toHaveBeenCalled(); + }); + + it('Should handle retry', async () => { + const key = CacheInfo.PendingTransaction('txHashComplete').key; + const executeData = Uint8Array.of(1, 2, 3, 4); + + redisCacheService.scan.mockReturnValueOnce(Promise.resolve([key])); + redisCacheService.get.mockReturnValueOnce( + Promise.resolve({ + txHash: 'txHashComplete', + executeData, + retry: 1, + }), + ); + transactionsHelper.awaitComplete.mockReturnValueOnce(Promise.resolve(false)); + + const userAddress = UserAddress.fromBech32('erd1qqqqqqqqqqqqqpgqhe8t5jewej70zupmh44jurgn29psua5l2jps3ntjj3'); + const accountNonce = 0; + walletSigner.getAddress.mockReturnValueOnce(userAddress); + transactionsHelper.getAccountNonce.mockReturnValueOnce(Promise.resolve(accountNonce)); + + const transaction: DeepMocked = createMock(); + gatewayContract.buildExecuteTransaction.mockReturnValueOnce(transaction); + + transactionsHelper.getTransactionGas.mockReturnValueOnce(Promise.resolve(100_000_000)); + walletSigner.sign.mockReturnValueOnce(Promise.resolve(Buffer.from('signature'))); + transactionsHelper.sendTransaction.mockReturnValueOnce(Promise.resolve('txHash')); + + await service.handlePendingTransactionsRaw(); + + expect(transactionsHelper.awaitComplete).toHaveBeenCalledTimes(1); + expect(transactionsHelper.awaitComplete).toHaveBeenCalledWith('txHashComplete'); + + expect(transactionsHelper.getAccountNonce).toHaveBeenCalledTimes(1); + expect(transactionsHelper.getAccountNonce).toHaveBeenCalledWith(userAddress); + expect(gatewayContract.buildExecuteTransaction).toHaveBeenCalledTimes(1); + expect(gatewayContract.buildExecuteTransaction).toHaveBeenCalledWith( + executeData, + accountNonce, + 'test', + userAddress, + ); + expect(transactionsHelper.getTransactionGas).toHaveBeenCalledTimes(1); + expect(transactionsHelper.getTransactionGas).toHaveBeenCalledWith(transaction, 1); + expect(transaction.setGasLimit).toHaveBeenCalledTimes(1); + expect(transaction.setGasLimit).toHaveBeenCalledWith(100_000_000); + expect(transaction.applySignature).toHaveBeenCalledTimes(1); + expect(transaction.applySignature).toHaveBeenCalledWith(Buffer.from('signature')); + expect(transactionsHelper.sendTransaction).toHaveBeenCalledTimes(1); + expect(transactionsHelper.sendTransaction).toHaveBeenCalledWith(transaction); + + expect(redisCacheService.set).toHaveBeenCalledTimes(1); + expect(redisCacheService.set).toHaveBeenCalledWith( + CacheInfo.PendingTransaction('txHash').key, + { + txHash: 'txHash', + executeData, + retry: 2, + }, + CacheInfo.PendingTransaction('txHash').ttl, + ); + }); + + it('Should not handle final retry', async () => { + const key = CacheInfo.PendingTransaction('txHashComplete').key; + const executeData = Uint8Array.of(1, 2, 3, 4); + + redisCacheService.scan.mockReturnValueOnce(Promise.resolve([key])); + redisCacheService.get.mockReturnValueOnce( + Promise.resolve({ + txHash: 'txHashComplete', + executeData, + retry: 3, + }), + ); + transactionsHelper.awaitComplete.mockReturnValueOnce(Promise.resolve(false)); + + await service.handlePendingTransactionsRaw(); + + expect(transactionsHelper.awaitComplete).toHaveBeenCalledTimes(1); + expect(transactionsHelper.awaitComplete).toHaveBeenCalledWith('txHashComplete'); + expect(transactionsHelper.getAccountNonce).not.toHaveBeenCalled(); + }); + + it('Should handle retry error', async () => { + const key = CacheInfo.PendingTransaction('txHashComplete').key; + const executeData = Uint8Array.of(1, 2, 3, 4); + + redisCacheService.scan.mockReturnValueOnce(Promise.resolve([key])); + redisCacheService.get.mockReturnValueOnce( + Promise.resolve({ + txHash: 'txHashComplete', + executeData, + retry: 1, + }), + ); + transactionsHelper.awaitComplete.mockReturnValueOnce(Promise.resolve(false)); + + const userAddress = UserAddress.fromBech32('erd1qqqqqqqqqqqqqpgqhe8t5jewej70zupmh44jurgn29psua5l2jps3ntjj3'); + walletSigner.getAddress.mockReturnValueOnce(userAddress); + transactionsHelper.getAccountNonce.mockRejectedValueOnce(new Error('Network error')); + + await service.handlePendingTransactionsRaw(); + + expect(transactionsHelper.awaitComplete).toHaveBeenCalledTimes(1); + expect(transactionsHelper.awaitComplete).toHaveBeenCalledWith('txHashComplete'); + + expect(transactionsHelper.getAccountNonce).toHaveBeenCalledTimes(1); + expect(transactionsHelper.getAccountNonce).toHaveBeenCalledWith(userAddress); + expect(redisCacheService.set).toHaveBeenCalledTimes(1); + expect(redisCacheService.set).toHaveBeenCalledWith( + CacheInfo.PendingTransaction('txHashComplete').key, + { + txHash: 'txHashComplete', + executeData, + retry: 1, + }, + CacheInfo.PendingTransaction('txHashComplete').ttl, + ); + }); + }); +}); diff --git a/apps/axelar-event-processor/src/approvals-processor/entities/pending-transaction.ts b/apps/axelar-event-processor/src/approvals-processor/entities/pending-transaction.ts index 3d3992a..1c85a41 100644 --- a/apps/axelar-event-processor/src/approvals-processor/entities/pending-transaction.ts +++ b/apps/axelar-event-processor/src/approvals-processor/entities/pending-transaction.ts @@ -1,4 +1,5 @@ export interface PendingTransaction { + txHash: string; executeData: Uint8Array; retry: number; } diff --git a/apps/mvx-event-processor/src/call-contract-approved-processor/call-contract-approved.processor.module.ts b/apps/mvx-event-processor/src/call-contract-approved-processor/call-contract-approved.processor.module.ts index 6f067df..4303081 100644 --- a/apps/mvx-event-processor/src/call-contract-approved-processor/call-contract-approved.processor.module.ts +++ b/apps/mvx-event-processor/src/call-contract-approved-processor/call-contract-approved.processor.module.ts @@ -1,16 +1,11 @@ -import { Module } from '@nestjs/common'; -import { ScheduleModule } from '@nestjs/schedule'; -import { DatabaseModule, DynamicModuleUtils } from '@mvx-monorepo/common'; -import { ContractsModule } from '@mvx-monorepo/common/contracts/contracts.module'; -import { CallContractApprovedProcessorService } from './call-contract-approved.processor.service'; - -@Module({ - imports: [ - ScheduleModule.forRoot(), - DynamicModuleUtils.getCacheModule(), - DatabaseModule, - ContractsModule, - ], - providers: [CallContractApprovedProcessorService], -}) -export class CallContractApprovedProcessorModule {} +import { Module } from '@nestjs/common'; +import { ScheduleModule } from '@nestjs/schedule'; +import { DatabaseModule } from '@mvx-monorepo/common'; +import { ContractsModule } from '@mvx-monorepo/common/contracts/contracts.module'; +import { CallContractApprovedProcessorService } from './call-contract-approved.processor.service'; + +@Module({ + imports: [ScheduleModule.forRoot(), DatabaseModule, ContractsModule], + providers: [CallContractApprovedProcessorService], +}) +export class CallContractApprovedProcessorModule {} diff --git a/libs/common/src/contracts/transactions.helper.ts b/libs/common/src/contracts/transactions.helper.ts index a12bec6..04f259c 100644 --- a/libs/common/src/contracts/transactions.helper.ts +++ b/libs/common/src/contracts/transactions.helper.ts @@ -41,7 +41,8 @@ export class TransactionsHelper { return hash; } catch (e) { - this.logger.error(`Can not send transactions to proxy... ${e}`); + this.logger.error(`Can not send transaction to proxy...`); + this.logger.error(e); throw e; } @@ -57,7 +58,8 @@ export class TransactionsHelper { return true; } catch (e) { - this.logger.error(`Can not send transactions to proxy... ${e}`); + this.logger.error(`Can not send transactions to proxy...`); + this.logger.error(e); return false; } @@ -69,6 +71,9 @@ export class TransactionsHelper { return !result.status.isFailed() && !result.status.isInvalid(); } catch (e) { + this.logger.error(`Can not await transaction completed`); + this.logger.error(e); + return false; } } From 066322ef098ad15e7ce11059e315c2518c9c07d3 Mon Sep 17 00:00:00 2001 From: Rares <6453351+raresserban@users.noreply.github.com> Date: Wed, 13 Dec 2023 17:26:45 +0200 Subject: [PATCH 15/33] Add chain id as env var for simplicity. --- .env.example | 2 +- .env.test | 2 +- ...all-contract-approved.processor.service.ts | 11 +++++--- ...ll-contract-approved.processor.e2e-spec.ts | 7 +---- libs/common/src/config/api.config.service.ts | 15 ++++++---- .../src/contracts/transactions.helper.ts | 9 ------ .../common/src/decorators/get.or.set.cache.ts | 28 ------------------- libs/common/src/utils/constants.enum.ts | 2 ++ 8 files changed, 22 insertions(+), 54 deletions(-) delete mode 100644 libs/common/src/decorators/get.or.set.cache.ts diff --git a/.env.example b/.env.example index 75b68e4..9e4c924 100644 --- a/.env.example +++ b/.env.example @@ -12,6 +12,6 @@ CONTRACT_GAS_SERVICE= AXELAR_API_URL= -SOURCE_CHAIN_NAME=multiversx-D +CHAIN_ID=D WALLET_MNEMONIC= diff --git a/.env.test b/.env.test index aff51af..e2881a6 100644 --- a/.env.test +++ b/.env.test @@ -12,7 +12,7 @@ CONTRACT_GAS_SERVICE=erd1qqqqqqqqqqqqqpgqhe8t5jewej70zupmh44jurgn29psua5l2jps3nt AXELAR_API_URL=localhost:5000 -SOURCE_CHAIN_NAME=multiversx-test +CHAIN_ID=test # erd1fsk0cnaag2m78gunfddsvg0y042rf0maxxgz6kvm32kxcl25m0yq8s38vt WALLET_MNEMONIC="fitness horror fluid six mutual ahead upon zone install stadium shuffle arrive caution flat slam machine wasp steel stand frog exist drink market absent" diff --git a/apps/mvx-event-processor/src/call-contract-approved-processor/call-contract-approved.processor.service.ts b/apps/mvx-event-processor/src/call-contract-approved-processor/call-contract-approved.processor.service.ts index 9d51871..768a374 100644 --- a/apps/mvx-event-processor/src/call-contract-approved-processor/call-contract-approved.processor.service.ts +++ b/apps/mvx-event-processor/src/call-contract-approved-processor/call-contract-approved.processor.service.ts @@ -15,6 +15,7 @@ import { } from '@multiversx/sdk-core/out'; import { ContractCallApproved, ContractCallApprovedStatus } from '@prisma/client'; import { TransactionsHelper } from '@mvx-monorepo/common/contracts/transactions.helper'; +import { ApiConfigService } from '@mvx-monorepo/common'; // Support a max of 3 retries (mainly because some Interchain Token Service endpoints need to be called 3 times) const MAX_NUMBER_OF_RETRIES: number = 3; @@ -23,12 +24,16 @@ const MAX_NUMBER_OF_RETRIES: number = 3; export class CallContractApprovedProcessorService { private readonly logger: Logger; + private readonly chainId: string; + constructor( private readonly contractCallApprovedRepository: ContractCallApprovedRepository, @Inject(ProviderKeys.WALLET_SIGNER) private readonly walletSigner: UserSigner, private readonly transactionsHelper: TransactionsHelper, + apiConfigService: ApiConfigService, ) { this.logger = new Logger(CallContractApprovedProcessorService.name); + this.chainId = apiConfigService.getChainId(); } @Cron('*/30 * * * * *') @@ -37,7 +42,6 @@ export class CallContractApprovedProcessorService { this.logger.debug('Running processPendingContractCallApproved cron'); let accountNonce = null; - const chainId = await this.transactionsHelper.getChainId(); let page = 0; let entries; @@ -64,7 +68,7 @@ export class CallContractApprovedProcessorService { `Trying to execute ContractCallApproved transaction with commandId ${contractCallApproved.commandId}`, ); - const transaction = await this.buildExecuteTransaction(contractCallApproved, accountNonce, chainId); + const transaction = await this.buildExecuteTransaction(contractCallApproved, accountNonce); accountNonce++; @@ -89,7 +93,6 @@ export class CallContractApprovedProcessorService { private async buildExecuteTransaction( contractCallApproved: ContractCallApproved, accountNonce: number, - chainId: string, ): Promise { const contract = new SmartContract({ address: new Address(contractCallApproved.contractAddress) }); @@ -106,7 +109,7 @@ export class CallContractApprovedProcessorService { .withSender(this.walletSigner.getAddress()) .withNonce(accountNonce) // .withValue() // TODO: Handle ITS transactions where EGLD value needs to be sent for deploying ESDT token - .withChainID(chainId) + .withChainID(this.chainId) .buildTransaction(); const gas = await this.transactionsHelper.getTransactionGas(transaction, contractCallApproved.retry); diff --git a/apps/mvx-event-processor/test/call-contract-approved.processor.e2e-spec.ts b/apps/mvx-event-processor/test/call-contract-approved.processor.e2e-spec.ts index eb3e985..203ef0d 100644 --- a/apps/mvx-event-processor/test/call-contract-approved.processor.e2e-spec.ts +++ b/apps/mvx-event-processor/test/call-contract-approved.processor.e2e-spec.ts @@ -1,6 +1,6 @@ import { Test } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; -import { AccountOnNetwork, NetworkConfig, ProxyNetworkProvider } from '@multiversx/sdk-network-providers/out'; +import { AccountOnNetwork, ProxyNetworkProvider } from '@multiversx/sdk-network-providers/out'; import { CallContractApprovedProcessorModule, CallContractApprovedProcessorService, @@ -48,9 +48,6 @@ describe('CallContractApprovedProcessorService', () => { service = await moduleRef.get(CallContractApprovedProcessorService); // Mock general calls - const networkConfig = new NetworkConfig(); - networkConfig.ChainID = 'test'; - proxy.getNetworkConfig.mockReturnValueOnce(Promise.resolve(networkConfig)); proxy.getAccount.mockReturnValueOnce( Promise.resolve( new AccountOnNetwork({ @@ -133,7 +130,6 @@ describe('CallContractApprovedProcessorService', () => { await service.processPendingContractCallApproved(); - expect(proxy.getNetworkConfig).toHaveBeenCalledTimes(1); expect(proxy.getAccount).toHaveBeenCalledTimes(1); expect(proxy.doPostGeneric).toHaveBeenCalledTimes(2); expect(proxy.sendTransactions).toHaveBeenCalledTimes(1); @@ -199,7 +195,6 @@ describe('CallContractApprovedProcessorService', () => { await service.processPendingContractCallApproved(); - expect(proxy.getNetworkConfig).toHaveBeenCalledTimes(1); expect(proxy.getAccount).toHaveBeenCalledTimes(1); expect(proxy.doPostGeneric).toHaveBeenCalledTimes(1); expect(proxy.sendTransactions).toHaveBeenCalledTimes(1); diff --git a/libs/common/src/config/api.config.service.ts b/libs/common/src/config/api.config.service.ts index 7ff5d5b..e5e1c07 100644 --- a/libs/common/src/config/api.config.service.ts +++ b/libs/common/src/config/api.config.service.ts @@ -1,6 +1,7 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { EVENTS_NOTIFIER_QUEUE } from '../../../../config/configuration'; +import { CONSTANTS } from '@mvx-monorepo/common/utils/constants.enum'; @Injectable() export class ApiConfigService { @@ -84,13 +85,17 @@ export class ApiConfigService { return axelarApiUrl; } - getSourceChainName(): string { - const sourceChainName = this.configService.get('SOURCE_CHAIN_NAME'); - if (!sourceChainName) { - throw new Error('No Axelar API url present'); + getChainId(): string { + const chainId = this.configService.get('CHAIN_ID'); + if (!chainId) { + throw new Error('No Chain Id present'); } - return sourceChainName; + return chainId; + } + + getSourceChainName(): string { + return CONSTANTS.SOURCE_CHAIN_NAME_SUFFIX + this.getChainId(); } getWalletMnemonic(): string { diff --git a/libs/common/src/contracts/transactions.helper.ts b/libs/common/src/contracts/transactions.helper.ts index 3cacfda..53ebbb6 100644 --- a/libs/common/src/contracts/transactions.helper.ts +++ b/libs/common/src/contracts/transactions.helper.ts @@ -1,7 +1,5 @@ import { Injectable, Logger } from '@nestjs/common'; import { ProxyNetworkProvider } from '@multiversx/sdk-network-providers/out'; -import { GetOrSetCache } from '@mvx-monorepo/common/decorators/get.or.set.cache'; -import { CacheInfo } from '@mvx-monorepo/common'; import { Transaction } from '@multiversx/sdk-core/out'; import { UserAddress } from '@multiversx/sdk-wallet/out/userAddress'; @@ -13,13 +11,6 @@ export class TransactionsHelper { this.logger = new Logger(TransactionsHelper.name); } - @GetOrSetCache(CacheInfo.ChainId) - async getChainId(): Promise { - const result = await this.proxy.getNetworkConfig(); - - return result.ChainID; - } - async getAccountNonce(address: UserAddress): Promise { const accountOnNetwork = await this.proxy.getAccount(address); diff --git a/libs/common/src/decorators/get.or.set.cache.ts b/libs/common/src/decorators/get.or.set.cache.ts deleted file mode 100644 index ae50b60..0000000 --- a/libs/common/src/decorators/get.or.set.cache.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { CacheService } from '@multiversx/sdk-nestjs-cache'; -import { Inject } from '@nestjs/common'; -import { CacheInfo } from '@mvx-monorepo/common'; - -export function GetOrSetCache(cacheInfoFunc: (...args: any[]) => CacheInfo) { - const injectCachingService = Inject(CacheService); - - return ( - target: any, - _key: string | symbol, - descriptor: PropertyDescriptor, - ) => { - injectCachingService(target, 'cachingService'); - - const childMethod = descriptor.value; - - descriptor.value = async function (...args: any[]) { - const { key, ttl } = cacheInfoFunc(...args); - - const cachingService: CacheService = (this as any).cachingService; - - const funcValue = () => childMethod.apply(this, args); - return await cachingService.getOrSet(key, funcValue, ttl); - }; - - return descriptor; - }; -} diff --git a/libs/common/src/utils/constants.enum.ts b/libs/common/src/utils/constants.enum.ts index 3691476..311e83b 100644 --- a/libs/common/src/utils/constants.enum.ts +++ b/libs/common/src/utils/constants.enum.ts @@ -1,3 +1,5 @@ export enum CONSTANTS { EGLD_IDENTIFIER = 'EGLD', + + SOURCE_CHAIN_NAME_SUFFIX = 'multiversx-', } From 62d3ef954d377c2b8a06423d1fdb11e2025803cf Mon Sep 17 00:00:00 2001 From: Rares <6453351+raresserban@users.noreply.github.com> Date: Wed, 13 Dec 2023 17:26:45 +0200 Subject: [PATCH 16/33] Add chain id as env var for simplicity. --- .env.example | 2 +- .env.test | 2 +- .github/workflows/test.yml | 2 -- ...all-contract-approved.processor.service.ts | 11 +++++--- ...ll-contract-approved.processor.e2e-spec.ts | 7 +---- libs/common/src/config/api.config.service.ts | 15 ++++++---- .../src/contracts/transactions.helper.ts | 9 ------ .../common/src/decorators/get.or.set.cache.ts | 28 ------------------- libs/common/src/utils/constants.enum.ts | 2 ++ 9 files changed, 22 insertions(+), 56 deletions(-) delete mode 100644 libs/common/src/decorators/get.or.set.cache.ts diff --git a/.env.example b/.env.example index 75b68e4..9e4c924 100644 --- a/.env.example +++ b/.env.example @@ -12,6 +12,6 @@ CONTRACT_GAS_SERVICE= AXELAR_API_URL= -SOURCE_CHAIN_NAME=multiversx-D +CHAIN_ID=D WALLET_MNEMONIC= diff --git a/.env.test b/.env.test index aff51af..e2881a6 100644 --- a/.env.test +++ b/.env.test @@ -12,7 +12,7 @@ CONTRACT_GAS_SERVICE=erd1qqqqqqqqqqqqqpgqhe8t5jewej70zupmh44jurgn29psua5l2jps3nt AXELAR_API_URL=localhost:5000 -SOURCE_CHAIN_NAME=multiversx-test +CHAIN_ID=test # erd1fsk0cnaag2m78gunfddsvg0y042rf0maxxgz6kvm32kxcl25m0yq8s38vt WALLET_MNEMONIC="fitness horror fluid six mutual ahead upon zone install stadium shuffle arrive caution flat slam machine wasp steel stand frog exist drink market absent" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5a3bc0e..b350467 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -42,5 +42,3 @@ jobs: - run: npm install -g dotenv-cli - run: npm run test:migrate - run: npm run test:e2e -# env: -# REDIS_URL: localhost diff --git a/apps/mvx-event-processor/src/call-contract-approved-processor/call-contract-approved.processor.service.ts b/apps/mvx-event-processor/src/call-contract-approved-processor/call-contract-approved.processor.service.ts index 9d51871..768a374 100644 --- a/apps/mvx-event-processor/src/call-contract-approved-processor/call-contract-approved.processor.service.ts +++ b/apps/mvx-event-processor/src/call-contract-approved-processor/call-contract-approved.processor.service.ts @@ -15,6 +15,7 @@ import { } from '@multiversx/sdk-core/out'; import { ContractCallApproved, ContractCallApprovedStatus } from '@prisma/client'; import { TransactionsHelper } from '@mvx-monorepo/common/contracts/transactions.helper'; +import { ApiConfigService } from '@mvx-monorepo/common'; // Support a max of 3 retries (mainly because some Interchain Token Service endpoints need to be called 3 times) const MAX_NUMBER_OF_RETRIES: number = 3; @@ -23,12 +24,16 @@ const MAX_NUMBER_OF_RETRIES: number = 3; export class CallContractApprovedProcessorService { private readonly logger: Logger; + private readonly chainId: string; + constructor( private readonly contractCallApprovedRepository: ContractCallApprovedRepository, @Inject(ProviderKeys.WALLET_SIGNER) private readonly walletSigner: UserSigner, private readonly transactionsHelper: TransactionsHelper, + apiConfigService: ApiConfigService, ) { this.logger = new Logger(CallContractApprovedProcessorService.name); + this.chainId = apiConfigService.getChainId(); } @Cron('*/30 * * * * *') @@ -37,7 +42,6 @@ export class CallContractApprovedProcessorService { this.logger.debug('Running processPendingContractCallApproved cron'); let accountNonce = null; - const chainId = await this.transactionsHelper.getChainId(); let page = 0; let entries; @@ -64,7 +68,7 @@ export class CallContractApprovedProcessorService { `Trying to execute ContractCallApproved transaction with commandId ${contractCallApproved.commandId}`, ); - const transaction = await this.buildExecuteTransaction(contractCallApproved, accountNonce, chainId); + const transaction = await this.buildExecuteTransaction(contractCallApproved, accountNonce); accountNonce++; @@ -89,7 +93,6 @@ export class CallContractApprovedProcessorService { private async buildExecuteTransaction( contractCallApproved: ContractCallApproved, accountNonce: number, - chainId: string, ): Promise { const contract = new SmartContract({ address: new Address(contractCallApproved.contractAddress) }); @@ -106,7 +109,7 @@ export class CallContractApprovedProcessorService { .withSender(this.walletSigner.getAddress()) .withNonce(accountNonce) // .withValue() // TODO: Handle ITS transactions where EGLD value needs to be sent for deploying ESDT token - .withChainID(chainId) + .withChainID(this.chainId) .buildTransaction(); const gas = await this.transactionsHelper.getTransactionGas(transaction, contractCallApproved.retry); diff --git a/apps/mvx-event-processor/test/call-contract-approved.processor.e2e-spec.ts b/apps/mvx-event-processor/test/call-contract-approved.processor.e2e-spec.ts index eb3e985..203ef0d 100644 --- a/apps/mvx-event-processor/test/call-contract-approved.processor.e2e-spec.ts +++ b/apps/mvx-event-processor/test/call-contract-approved.processor.e2e-spec.ts @@ -1,6 +1,6 @@ import { Test } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; -import { AccountOnNetwork, NetworkConfig, ProxyNetworkProvider } from '@multiversx/sdk-network-providers/out'; +import { AccountOnNetwork, ProxyNetworkProvider } from '@multiversx/sdk-network-providers/out'; import { CallContractApprovedProcessorModule, CallContractApprovedProcessorService, @@ -48,9 +48,6 @@ describe('CallContractApprovedProcessorService', () => { service = await moduleRef.get(CallContractApprovedProcessorService); // Mock general calls - const networkConfig = new NetworkConfig(); - networkConfig.ChainID = 'test'; - proxy.getNetworkConfig.mockReturnValueOnce(Promise.resolve(networkConfig)); proxy.getAccount.mockReturnValueOnce( Promise.resolve( new AccountOnNetwork({ @@ -133,7 +130,6 @@ describe('CallContractApprovedProcessorService', () => { await service.processPendingContractCallApproved(); - expect(proxy.getNetworkConfig).toHaveBeenCalledTimes(1); expect(proxy.getAccount).toHaveBeenCalledTimes(1); expect(proxy.doPostGeneric).toHaveBeenCalledTimes(2); expect(proxy.sendTransactions).toHaveBeenCalledTimes(1); @@ -199,7 +195,6 @@ describe('CallContractApprovedProcessorService', () => { await service.processPendingContractCallApproved(); - expect(proxy.getNetworkConfig).toHaveBeenCalledTimes(1); expect(proxy.getAccount).toHaveBeenCalledTimes(1); expect(proxy.doPostGeneric).toHaveBeenCalledTimes(1); expect(proxy.sendTransactions).toHaveBeenCalledTimes(1); diff --git a/libs/common/src/config/api.config.service.ts b/libs/common/src/config/api.config.service.ts index 7ff5d5b..e5e1c07 100644 --- a/libs/common/src/config/api.config.service.ts +++ b/libs/common/src/config/api.config.service.ts @@ -1,6 +1,7 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { EVENTS_NOTIFIER_QUEUE } from '../../../../config/configuration'; +import { CONSTANTS } from '@mvx-monorepo/common/utils/constants.enum'; @Injectable() export class ApiConfigService { @@ -84,13 +85,17 @@ export class ApiConfigService { return axelarApiUrl; } - getSourceChainName(): string { - const sourceChainName = this.configService.get('SOURCE_CHAIN_NAME'); - if (!sourceChainName) { - throw new Error('No Axelar API url present'); + getChainId(): string { + const chainId = this.configService.get('CHAIN_ID'); + if (!chainId) { + throw new Error('No Chain Id present'); } - return sourceChainName; + return chainId; + } + + getSourceChainName(): string { + return CONSTANTS.SOURCE_CHAIN_NAME_SUFFIX + this.getChainId(); } getWalletMnemonic(): string { diff --git a/libs/common/src/contracts/transactions.helper.ts b/libs/common/src/contracts/transactions.helper.ts index 3cacfda..53ebbb6 100644 --- a/libs/common/src/contracts/transactions.helper.ts +++ b/libs/common/src/contracts/transactions.helper.ts @@ -1,7 +1,5 @@ import { Injectable, Logger } from '@nestjs/common'; import { ProxyNetworkProvider } from '@multiversx/sdk-network-providers/out'; -import { GetOrSetCache } from '@mvx-monorepo/common/decorators/get.or.set.cache'; -import { CacheInfo } from '@mvx-monorepo/common'; import { Transaction } from '@multiversx/sdk-core/out'; import { UserAddress } from '@multiversx/sdk-wallet/out/userAddress'; @@ -13,13 +11,6 @@ export class TransactionsHelper { this.logger = new Logger(TransactionsHelper.name); } - @GetOrSetCache(CacheInfo.ChainId) - async getChainId(): Promise { - const result = await this.proxy.getNetworkConfig(); - - return result.ChainID; - } - async getAccountNonce(address: UserAddress): Promise { const accountOnNetwork = await this.proxy.getAccount(address); diff --git a/libs/common/src/decorators/get.or.set.cache.ts b/libs/common/src/decorators/get.or.set.cache.ts deleted file mode 100644 index ae50b60..0000000 --- a/libs/common/src/decorators/get.or.set.cache.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { CacheService } from '@multiversx/sdk-nestjs-cache'; -import { Inject } from '@nestjs/common'; -import { CacheInfo } from '@mvx-monorepo/common'; - -export function GetOrSetCache(cacheInfoFunc: (...args: any[]) => CacheInfo) { - const injectCachingService = Inject(CacheService); - - return ( - target: any, - _key: string | symbol, - descriptor: PropertyDescriptor, - ) => { - injectCachingService(target, 'cachingService'); - - const childMethod = descriptor.value; - - descriptor.value = async function (...args: any[]) { - const { key, ttl } = cacheInfoFunc(...args); - - const cachingService: CacheService = (this as any).cachingService; - - const funcValue = () => childMethod.apply(this, args); - return await cachingService.getOrSet(key, funcValue, ttl); - }; - - return descriptor; - }; -} diff --git a/libs/common/src/utils/constants.enum.ts b/libs/common/src/utils/constants.enum.ts index 3691476..311e83b 100644 --- a/libs/common/src/utils/constants.enum.ts +++ b/libs/common/src/utils/constants.enum.ts @@ -1,3 +1,5 @@ export enum CONSTANTS { EGLD_IDENTIFIER = 'EGLD', + + SOURCE_CHAIN_NAME_SUFFIX = 'multiversx-', } From a279f14f935b957c9ca8b44a3dfd13c3c749cf26 Mon Sep 17 00:00:00 2001 From: Rares <6453351+raresserban@users.noreply.github.com> Date: Thu, 14 Dec 2023 10:25:55 +0200 Subject: [PATCH 17/33] Fix pipeline. --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b350467..b354c5c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,7 +19,7 @@ jobs: ports: - 5432:5432 env: - POSTGRES_USER: user + POSTGRES_USER: root POSTGRES_PASSWORD: password POSTGRES_DB: relayer_test From ac39ba38ee98180ca0556ddbb1a313e0f7da6277 Mon Sep 17 00:00:00 2001 From: Rares <6453351+raresserban@users.noreply.github.com> Date: Thu, 14 Dec 2023 12:57:18 +0200 Subject: [PATCH 18/33] Create basic cron for collecting gas service fees. --- .env.example | 2 + .env.test | 2 + .../src/gas-checker/gas-checker.module.ts | 10 ++ .../gas-checker/gas-checker.service.spec.ts | 18 +++ .../src/gas-checker/gas-checker.service.ts | 94 ++++++++++++++ apps/mvx-event-processor/src/main.ts | 3 + libs/common/src/assets/wegld-swap.abi.json | 118 ++++++++++++++++++ libs/common/src/config/api.config.service.ts | 9 ++ libs/common/src/contracts/contracts.module.ts | 13 ++ .../src/contracts/gas-service.contract.ts | 28 +++-- .../src/contracts/wegld-swap.contract.ts | 35 ++++++ .../common/src/decorators/get.or.set.cache.ts | 28 +++++ libs/common/src/utils/cache.info.ts | 7 ++ libs/common/src/utils/gas.info.ts | 15 +++ 14 files changed, 374 insertions(+), 8 deletions(-) create mode 100644 apps/mvx-event-processor/src/gas-checker/gas-checker.module.ts create mode 100644 apps/mvx-event-processor/src/gas-checker/gas-checker.service.spec.ts create mode 100644 apps/mvx-event-processor/src/gas-checker/gas-checker.service.ts create mode 100644 libs/common/src/assets/wegld-swap.abi.json create mode 100644 libs/common/src/contracts/wegld-swap.contract.ts create mode 100644 libs/common/src/decorators/get.or.set.cache.ts create mode 100644 libs/common/src/utils/gas.info.ts diff --git a/.env.example b/.env.example index 9e4c924..c5c8c18 100644 --- a/.env.example +++ b/.env.example @@ -10,6 +10,8 @@ EVENTS_NOTIFIER_QUEUE=queue CONTRACT_GATEWAY= CONTRACT_GAS_SERVICE= +CONTRACT_WEGLD_SWAP=erd1qqqqqqqqqqqqqpgqpv09kfzry5y4sj05udcngesat07umyj70n4sa2c0rp + AXELAR_API_URL= CHAIN_ID=D diff --git a/.env.test b/.env.test index e2881a6..e72f536 100644 --- a/.env.test +++ b/.env.test @@ -10,6 +10,8 @@ EVENTS_NOTIFIER_QUEUE=events-2cf3b817 CONTRACT_GATEWAY=erd1qqqqqqqqqqqqqpgqvc7gdl0p4s97guh498wgz75k8sav6sjfjlwqh679jy CONTRACT_GAS_SERVICE=erd1qqqqqqqqqqqqqpgqhe8t5jewej70zupmh44jurgn29psua5l2jps3ntjj3 +CONTRACT_WEGLD_SWAP=erd1qqqqqqqqqqqqqpgqhe8t5jewej70zupmh44jurgn29psua5l2jps3ntjj3 + AXELAR_API_URL=localhost:5000 CHAIN_ID=test diff --git a/apps/mvx-event-processor/src/gas-checker/gas-checker.module.ts b/apps/mvx-event-processor/src/gas-checker/gas-checker.module.ts new file mode 100644 index 0000000..387e399 --- /dev/null +++ b/apps/mvx-event-processor/src/gas-checker/gas-checker.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { ScheduleModule } from '@nestjs/schedule'; +import { ContractsModule } from '@mvx-monorepo/common/contracts/contracts.module'; +import { GasCheckerService } from './gas-checker.service'; + +@Module({ + imports: [ScheduleModule.forRoot(), ContractsModule], + providers: [GasCheckerService], +}) +export class GasCheckerModule {} diff --git a/apps/mvx-event-processor/src/gas-checker/gas-checker.service.spec.ts b/apps/mvx-event-processor/src/gas-checker/gas-checker.service.spec.ts new file mode 100644 index 0000000..f9bf5ad --- /dev/null +++ b/apps/mvx-event-processor/src/gas-checker/gas-checker.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { GasCheckerService } from './gas-checker.service'; + +describe('GasCheckerService', () => { + let service: GasCheckerService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [GasCheckerService], + }).compile(); + + service = module.get(GasCheckerService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/apps/mvx-event-processor/src/gas-checker/gas-checker.service.ts b/apps/mvx-event-processor/src/gas-checker/gas-checker.service.ts new file mode 100644 index 0000000..60951c0 --- /dev/null +++ b/apps/mvx-event-processor/src/gas-checker/gas-checker.service.ts @@ -0,0 +1,94 @@ +import { Inject, Injectable, Logger } from '@nestjs/common'; +import { Cron, CronExpression } from '@nestjs/schedule'; +import { ProviderKeys } from '@mvx-monorepo/common/utils/provider.enum'; +import { UserSigner } from '@multiversx/sdk-wallet/out'; +import { TransactionsHelper } from '@mvx-monorepo/common/contracts/transactions.helper'; +import { ApiConfigService, CacheInfo } from '@mvx-monorepo/common'; +import { Locker } from '@multiversx/sdk-nestjs-common'; +import { ApiNetworkProvider } from '@multiversx/sdk-network-providers/out'; +import { Address } from '@multiversx/sdk-core/out'; +import { CONSTANTS } from '@mvx-monorepo/common/utils/constants.enum'; +import { GetOrSetCache } from '@mvx-monorepo/common/decorators/get.or.set.cache'; +import { WegldSwapContract } from '@mvx-monorepo/common/contracts/wegld-swap.contract'; +import { FungibleTokenOfAccountOnNetwork } from '@multiversx/sdk-network-providers/out/tokens'; +import BigNumber from 'bignumber.js'; +import { GasServiceContract } from '@mvx-monorepo/common/contracts/gas-service.contract'; + +const EGLD_THRESHOLD = new BigNumber('100_000_000_000_000_000'); // 0.1 EGLD + +@Injectable() +export class GasCheckerService { + private readonly logger: Logger; + + private readonly contractGasService: string; + private readonly chainId: string; + + constructor( + @Inject(ProviderKeys.WALLET_SIGNER) private readonly walletSigner: UserSigner, + private readonly transactionsHelper: TransactionsHelper, + private readonly api: ApiNetworkProvider, + private readonly wegldSwapContract: WegldSwapContract, + private readonly gasServiceContract: GasServiceContract, + apiConfigService: ApiConfigService, + ) { + this.logger = new Logger(GasCheckerService.name); + this.contractGasService = apiConfigService.getContractGasService(); + this.chainId = apiConfigService.getChainId(); + } + + @Cron(CronExpression.EVERY_30_MINUTES) + async checkGasServiceAndWallet() { + await Locker.lock('checkGasServiceAndWallet', async () => { + this.logger.debug('Running checkGasServiceAndWallet cron'); + + const tokens = await this.getGasServiceEgldAndWegld(); + const toClaim = tokens.filter((token) => token.balance.gte(EGLD_THRESHOLD)); + + if (toClaim.length) { + await this.collectFees(toClaim); + } + }); + } + + private async getGasServiceEgldAndWegld(): Promise { + const address = Address.fromBech32(this.contractGasService); + const account = await this.api.getAccount(address); + + const tokens: FungibleTokenOfAccountOnNetwork[] = []; + + tokens.push({ + identifier: CONSTANTS.EGLD_IDENTIFIER, + balance: account.balance, + rawResponse: {}, + }); + + const wegldTokenId = await this.getWegldTokenId(); + + const token = await this.api.getFungibleTokenOfAccount(address, wegldTokenId); + + tokens.push(token); + + return tokens; + } + + @GetOrSetCache(CacheInfo.WegldTokenId) + private async getWegldTokenId(): Promise { + return await this.wegldSwapContract.getWrappedEgldTokenId(); + } + + private async collectFees(toClaim: FungibleTokenOfAccountOnNetwork[]) { + const accountNonce = await this.transactionsHelper.getAccountNonce(this.walletSigner.getAddress()); + + const transaction = this.gasServiceContract.collectFees( + this.walletSigner.getAddress(), + toClaim.map(token => token.identifier), + toClaim.map(token => token.balance), + accountNonce, + this.chainId, + ); + + await this.transactionsHelper.sendTransactions([transaction]); + + + } +} diff --git a/apps/mvx-event-processor/src/main.ts b/apps/mvx-event-processor/src/main.ts index e27c15a..05959fe 100644 --- a/apps/mvx-event-processor/src/main.ts +++ b/apps/mvx-event-processor/src/main.ts @@ -1,10 +1,13 @@ import { NestFactory } from '@nestjs/core'; import { EventProcessorModule } from './event-processor'; import { CallContractApprovedProcessorModule } from './call-contract-approved-processor'; +import { GasCheckerModule } from './gas-checker/gas-checker.module'; async function bootstrap() { + // TODO: Probably these should be refactor under the same module await NestFactory.createApplicationContext(EventProcessorModule); await NestFactory.createApplicationContext(CallContractApprovedProcessorModule); + await NestFactory.createApplicationContext(GasCheckerModule); } // eslint-disable-next-line @typescript-eslint/no-floating-promises diff --git a/libs/common/src/assets/wegld-swap.abi.json b/libs/common/src/assets/wegld-swap.abi.json new file mode 100644 index 0000000..21b6156 --- /dev/null +++ b/libs/common/src/assets/wegld-swap.abi.json @@ -0,0 +1,118 @@ +{ + "buildInfo": { + "rustc": { + "version": "1.63.0-nightly", + "commitHash": "e6a4afc3af2d2a53f91fc8a77bdfe94bea375b29", + "commitDate": "2022-05-20", + "channel": "Nightly", + "short": "rustc 1.63.0-nightly (e6a4afc3a 2022-05-20)" + }, + "contractCrate": { + "name": "wegld-swap", + "version": "0.0.0" + }, + "framework": { + "name": "elrond-wasm", + "version": "0.34.1" + } + }, + "name": "EgldEsdtSwap", + "constructor": { + "inputs": [ + { + "name": "wrapped_egld_token_id", + "type": "TokenIdentifier" + } + ], + "outputs": [] + }, + "endpoints": [ + { + "name": "wrapEgld", + "mutability": "mutable", + "payableInTokens": [ + "EGLD" + ], + "inputs": [], + "outputs": [ + { + "type": "EsdtTokenPayment" + } + ] + }, + { + "name": "unwrapEgld", + "mutability": "mutable", + "payableInTokens": [ + "*" + ], + "inputs": [], + "outputs": [] + }, + { + "name": "getLockedEgldBalance", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "BigUint" + } + ] + }, + { + "name": "getWrappedEgldTokenId", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "TokenIdentifier" + } + ] + }, + { + "name": "isPaused", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "bool" + } + ] + }, + { + "name": "pause", + "onlyOwner": true, + "mutability": "mutable", + "inputs": [], + "outputs": [] + }, + { + "name": "unpause", + "onlyOwner": true, + "mutability": "mutable", + "inputs": [], + "outputs": [] + } + ], + "events": [], + "hasCallback": false, + "types": { + "EsdtTokenPayment": { + "type": "struct", + "fields": [ + { + "name": "token_identifier", + "type": "TokenIdentifier" + }, + { + "name": "token_nonce", + "type": "u64" + }, + { + "name": "amount", + "type": "BigUint" + } + ] + } + } +} diff --git a/libs/common/src/config/api.config.service.ts b/libs/common/src/config/api.config.service.ts index e5e1c07..cf08252 100644 --- a/libs/common/src/config/api.config.service.ts +++ b/libs/common/src/config/api.config.service.ts @@ -76,6 +76,15 @@ export class ApiConfigService { return contractGasService; } + getContractWegldSwap(): string { + const contractWegldSwap = this.configService.get('CONTRACT_WEGLD_SWAP'); + if (!contractWegldSwap) { + throw new Error('No Contract Wegld Swap present'); + } + + return contractWegldSwap; + } + getAxelarApiUrl(): string { const axelarApiUrl = this.configService.get('AXELAR_API_URL'); if (!axelarApiUrl) { diff --git a/libs/common/src/contracts/contracts.module.ts b/libs/common/src/contracts/contracts.module.ts index 6712c73..d9177b8 100644 --- a/libs/common/src/contracts/contracts.module.ts +++ b/libs/common/src/contracts/contracts.module.ts @@ -9,6 +9,7 @@ import { GasServiceContract } from '@mvx-monorepo/common/contracts/gas-service.c import { ProviderKeys } from '@mvx-monorepo/common/utils/provider.enum'; import { Mnemonic, UserSigner } from '@multiversx/sdk-wallet/out'; import { TransactionsHelper } from '@mvx-monorepo/common/contracts/transactions.helper'; +import { WegldSwapContract } from '@mvx-monorepo/common/contracts/wegld-swap.contract'; @Module({ imports: [DynamicModuleUtils.getCachingModule()], @@ -69,6 +70,17 @@ import { TransactionsHelper } from '@mvx-monorepo/common/contracts/transactions. }, inject: [ApiConfigService, ResultsParser], }, + { + provide: WegldSwapContract, + useFactory: async (apiConfigService: ApiConfigService, resultsParser: ResultsParser, proxy: ProxyNetworkProvider) => { + const contractLoader = new ContractLoader(join(__dirname, '../assets/wegld-swap.abi.json')); + + const smartContract = await contractLoader.getContract(apiConfigService.getContractWegldSwap()); + + return new WegldSwapContract(smartContract, resultsParser, proxy); + }, + inject: [ApiConfigService, ResultsParser, ProxyNetworkProvider], + }, { provide: ProviderKeys.WALLET_SIGNER, useFactory: (apiConfigService: ApiConfigService) => { @@ -83,6 +95,7 @@ import { TransactionsHelper } from '@mvx-monorepo/common/contracts/transactions. exports: [ GatewayContract, GasServiceContract, + WegldSwapContract, ProviderKeys.WALLET_SIGNER, ProxyNetworkProvider, ApiNetworkProvider, diff --git a/libs/common/src/contracts/gas-service.contract.ts b/libs/common/src/contracts/gas-service.contract.ts index 0694a37..b5cfca8 100644 --- a/libs/common/src/contracts/gas-service.contract.ts +++ b/libs/common/src/contracts/gas-service.contract.ts @@ -1,5 +1,5 @@ -import { AbiRegistry, ResultsParser, SmartContract } from '@multiversx/sdk-core/out'; -import { Injectable, Logger } from '@nestjs/common'; +import { AbiRegistry, IAddress, ResultsParser, SmartContract, Transaction } from '@multiversx/sdk-core/out'; +import { Injectable } from '@nestjs/common'; import { Events } from '../utils/event.enum'; import { TransactionEvent } from '@multiversx/sdk-network-providers/out'; import { @@ -9,19 +9,31 @@ import { } from '@mvx-monorepo/common/contracts/entities/gas-service-events'; import { CONSTANTS } from '@mvx-monorepo/common/utils/constants.enum'; import { DecodingUtils } from '@mvx-monorepo/common/utils/decoding.utils'; +import BigNumber from 'bignumber.js'; +import { GasInfo } from '@mvx-monorepo/common/utils/gas.info'; @Injectable() export class GasServiceContract { - // @ts-ignore - private readonly logger: Logger; - constructor( - // @ts-ignore private readonly smartContract: SmartContract, private readonly abi: AbiRegistry, private readonly resultsParser: ResultsParser, - ) { - this.logger = new Logger(GasServiceContract.name); + ) {} + + collectFees( + sender: IAddress, + tokens: string[], + amounts: BigNumber[], + accountNonce: number, + chainId: string, + ): Transaction { + return this.smartContract.methods + .collectFees([sender, tokens, amounts]) + .withSender(sender) + .withNonce(accountNonce) + .withGasLimit(GasInfo.CollectFeesBase.value + GasInfo.CollectFeesExtra.value * tokens.length) + .withChainID(chainId) + .buildTransaction(); } decodeGasPaidForContractCallEvent(event: TransactionEvent): GasPaidForContractCallEvent { diff --git a/libs/common/src/contracts/wegld-swap.contract.ts b/libs/common/src/contracts/wegld-swap.contract.ts new file mode 100644 index 0000000..decf314 --- /dev/null +++ b/libs/common/src/contracts/wegld-swap.contract.ts @@ -0,0 +1,35 @@ +import { Injectable } from '@nestjs/common'; +import { IAddress, ResultsParser, SmartContract, TokenTransfer, Transaction } from '@multiversx/sdk-core/out'; +import { GasInfo } from '@mvx-monorepo/common/utils/gas.info'; +import BigNumber from 'bignumber.js'; +import { ProxyNetworkProvider } from '@multiversx/sdk-network-providers/out'; + +@Injectable() +export class WegldSwapContract { + constructor( + private readonly smartContract: SmartContract, + private readonly resultsParser: ResultsParser, + private readonly proxy: ProxyNetworkProvider, + ) {} + + unwrapEgld(token: string, amount: BigNumber, sender: IAddress, accountNonce: number, chainId: string): Transaction { + return this.smartContract.methodsExplicit + .unwrapEgld() + .withSender(sender) + .withNonce(accountNonce) + .withSingleESDTTransfer(TokenTransfer.fungibleFromBigInteger(token, amount)) + .withGasLimit(GasInfo.UnwrapEgld.value) + .withChainID(chainId) + .buildTransaction(); + } + + async getWrappedEgldTokenId(): Promise { + const interaction = this.smartContract.methods.getWrappedEgldTokenId([]); + const query = interaction.check().buildQuery(); + const response = await this.proxy.queryContract(query); + + const { firstValue: tokenId } = this.resultsParser.parseQueryResponse(response, interaction.getEndpoint()); + + return tokenId?.valueOf().toString() ?? ''; + } +} diff --git a/libs/common/src/decorators/get.or.set.cache.ts b/libs/common/src/decorators/get.or.set.cache.ts new file mode 100644 index 0000000..a15b01a --- /dev/null +++ b/libs/common/src/decorators/get.or.set.cache.ts @@ -0,0 +1,28 @@ +import { Inject } from '@nestjs/common'; +import { CacheService } from '@multiversx/sdk-nestjs-cache'; +import { CacheInfo } from '@mvx-monorepo/common'; + +export function GetOrSetCache(cacheInfoFunc: (...args: any[]) => CacheInfo) { + const injectCacheService = Inject(CacheService); + + return ( + target: any, + _key: string | symbol, + descriptor: PropertyDescriptor, + ) => { + injectCacheService(target, 'cacheService'); + + const childMethod = descriptor.value; + + descriptor.value = async function (...args: any[]) { + const { key, ttl } = cacheInfoFunc(...args); + + const cachingService: CacheService = (this as any).cacheService; + + const funcValue = () => childMethod.apply(this, args); + return await cachingService.getOrSet(key, funcValue, ttl); + }; + + return descriptor; + }; +} diff --git a/libs/common/src/utils/cache.info.ts b/libs/common/src/utils/cache.info.ts index aa2a35b..2071bf9 100644 --- a/libs/common/src/utils/cache.info.ts +++ b/libs/common/src/utils/cache.info.ts @@ -17,4 +17,11 @@ export class CacheInfo { ttl: Constants.oneWeek(), }; } + + static WegldTokenId(): CacheInfo { + return { + key: `wegldTokenId`, + ttl: Constants.oneWeek(), + }; + } } diff --git a/libs/common/src/utils/gas.info.ts b/libs/common/src/utils/gas.info.ts new file mode 100644 index 0000000..49aa750 --- /dev/null +++ b/libs/common/src/utils/gas.info.ts @@ -0,0 +1,15 @@ +export class GasInfo { + public value: number = 0; + + static UnwrapEgld: GasInfo = { + value: 5_000_000, + }; + + static CollectFeesBase: GasInfo = { + value: 5_000_000, + }; + + static CollectFeesExtra: GasInfo = { + value: 1_000_000, + }; +} From 80a049fa219b59ce5c8e58955baf0e379a795bf1 Mon Sep 17 00:00:00 2001 From: Rares <6453351+raresserban@users.noreply.github.com> Date: Thu, 14 Dec 2023 16:05:42 +0200 Subject: [PATCH 19/33] Working gas checker cron and refactoring. --- .../approvals.processor.service.ts | 46 +++--- .../approvals.processor.spec.ts | 61 +++---- apps/axelar-event-processor/src/main.ts | 1 - ...all-contract-approved.processor.service.ts | 7 +- .../gas-checker/gas-checker.service.spec.ts | 82 +++++++++- .../src/gas-checker/gas-checker.service.ts | 153 +++++++++++++----- apps/mvx-event-processor/src/main.ts | 9 +- .../src/mvx-event-processor.module.ts | 9 ++ .../src/contracts/gas-service.contract.ts | 18 +-- libs/common/src/contracts/gateway.contract.ts | 11 +- libs/common/src/contracts/index.ts | 1 + .../src/contracts/transactions.helper.ts | 31 +++- .../src/contracts/wegld-swap.contract.ts | 6 +- 13 files changed, 287 insertions(+), 148 deletions(-) create mode 100644 apps/mvx-event-processor/src/mvx-event-processor.module.ts diff --git a/apps/axelar-event-processor/src/approvals-processor/approvals.processor.service.ts b/apps/axelar-event-processor/src/approvals-processor/approvals.processor.service.ts index 6151669..666504c 100644 --- a/apps/axelar-event-processor/src/approvals-processor/approvals.processor.service.ts +++ b/apps/axelar-event-processor/src/approvals-processor/approvals.processor.service.ts @@ -18,7 +18,6 @@ const MAX_NUMBER_OF_RETRIES = 3; export class ApprovalsProcessorService { private readonly logger: Logger; private readonly sourceChain: string; - private readonly chainId: string; private approvalsSubscription: Subscription | null = null; @@ -32,7 +31,6 @@ export class ApprovalsProcessorService { ) { this.logger = new Logger(ApprovalsProcessorService.name); this.sourceChain = apiConfigService.getSourceChainName(); - this.chainId = apiConfigService.getChainId(); } @Cron('*/30 * * * * *') @@ -112,11 +110,15 @@ export class ApprovalsProcessorService { this.logger.error(e); // Set value back in cache to be retried again (with same retry number) - await this.redisCacheService.set(CacheInfo.PendingTransaction(txHash).key, { - txHash, - executeData: executeData, - retry: retry, - }, CacheInfo.PendingTransaction(txHash).ttl); + await this.redisCacheService.set( + CacheInfo.PendingTransaction(txHash).key, + { + txHash, + executeData: executeData, + retry: retry, + }, + CacheInfo.PendingTransaction(txHash).ttl, + ); } } } @@ -156,29 +158,21 @@ export class ApprovalsProcessorService { this.logger.debug(`Trying to execute Gateway execute transaction with executeData:`); this.logger.debug(executeData); - // TODO: Check if it is fine to use the same wallet as in the CallContractApprovedProcessor - // and that no issues happen because of nonce - const accountNonce = await this.transactionsHelper.getAccountNonce(this.walletSigner.getAddress()); - - const transaction = this.gatewayContract.buildExecuteTransaction( - executeData, - accountNonce, - this.chainId, - this.walletSigner.getAddress(), - ); + const transaction = this.gatewayContract.buildExecuteTransaction(executeData, this.walletSigner.getAddress()); const gas = await this.transactionsHelper.getTransactionGas(transaction, retry); transaction.setGasLimit(gas); - const signature = await this.walletSigner.sign(transaction.serializeForSigning()); - transaction.applySignature(signature); + const txHash = await this.transactionsHelper.signAndSendTransaction(transaction, this.walletSigner); - const txHash = await this.transactionsHelper.sendTransaction(transaction); - - await this.redisCacheService.set(CacheInfo.PendingTransaction(txHash).key, { - txHash, - executeData: executeData, - retry: retry + 1, - }, CacheInfo.PendingTransaction(txHash).ttl); + await this.redisCacheService.set( + CacheInfo.PendingTransaction(txHash).key, + { + txHash, + executeData: executeData, + retry: retry + 1, + }, + CacheInfo.PendingTransaction(txHash).ttl, + ); } } diff --git a/apps/axelar-event-processor/src/approvals-processor/approvals.processor.spec.ts b/apps/axelar-event-processor/src/approvals-processor/approvals.processor.spec.ts index 7baf712..d19b3d7 100644 --- a/apps/axelar-event-processor/src/approvals-processor/approvals.processor.spec.ts +++ b/apps/axelar-event-processor/src/approvals-processor/approvals.processor.spec.ts @@ -88,16 +88,13 @@ describe('ApprovalsProcessorService', () => { expect(grpcService.subscribeToApprovals).toHaveBeenCalledWith('multiversx-test', undefined); const userAddress = UserAddress.fromBech32('erd1qqqqqqqqqqqqqpgqhe8t5jewej70zupmh44jurgn29psua5l2jps3ntjj3'); - const accountNonce = 0; walletSigner.getAddress.mockReturnValueOnce(userAddress); - transactionsHelper.getAccountNonce.mockReturnValueOnce(Promise.resolve(accountNonce)); const transaction: DeepMocked = createMock(); gatewayContract.buildExecuteTransaction.mockReturnValueOnce(transaction); transactionsHelper.getTransactionGas.mockReturnValueOnce(Promise.resolve(100_000_000)); - walletSigner.sign.mockReturnValueOnce(Promise.resolve(Buffer.from('signature'))); - transactionsHelper.sendTransaction.mockReturnValueOnce(Promise.resolve('txHash')); + transactionsHelper.signAndSendTransaction.mockReturnValueOnce(Promise.resolve('txHash')); // Process a message const message: SubscribeToApprovalsResponse = { @@ -115,23 +112,14 @@ describe('ApprovalsProcessorService', () => { setTimeout(resolve, 500); }); - expect(transactionsHelper.getAccountNonce).toHaveBeenCalledTimes(1); - expect(transactionsHelper.getAccountNonce).toHaveBeenCalledWith(userAddress); expect(gatewayContract.buildExecuteTransaction).toHaveBeenCalledTimes(1); - expect(gatewayContract.buildExecuteTransaction).toHaveBeenCalledWith( - message.executeData, - accountNonce, - 'test', - userAddress, - ); + expect(gatewayContract.buildExecuteTransaction).toHaveBeenCalledWith(message.executeData, userAddress); expect(transactionsHelper.getTransactionGas).toHaveBeenCalledTimes(1); expect(transactionsHelper.getTransactionGas).toHaveBeenCalledWith(transaction, 0); expect(transaction.setGasLimit).toHaveBeenCalledTimes(1); expect(transaction.setGasLimit).toHaveBeenCalledWith(100_000_000); - expect(transaction.applySignature).toHaveBeenCalledTimes(1); - expect(transaction.applySignature).toHaveBeenCalledWith(Buffer.from('signature')); - expect(transactionsHelper.sendTransaction).toHaveBeenCalledTimes(1); - expect(transactionsHelper.sendTransaction).toHaveBeenCalledWith(transaction); + expect(transactionsHelper.signAndSendTransaction).toHaveBeenCalledTimes(1); + expect(transactionsHelper.signAndSendTransaction).toHaveBeenCalledWith(transaction, walletSigner); expect(redisCacheService.set).toHaveBeenCalledTimes(2); expect(redisCacheService.set).toHaveBeenCalledWith( @@ -157,7 +145,9 @@ describe('ApprovalsProcessorService', () => { const userAddress = UserAddress.fromBech32('erd1qqqqqqqqqqqqqpgqhe8t5jewej70zupmh44jurgn29psua5l2jps3ntjj3'); walletSigner.getAddress.mockReturnValueOnce(userAddress); - transactionsHelper.getAccountNonce.mockRejectedValueOnce(new Error('Network error')); + const transaction: DeepMocked = createMock(); + gatewayContract.buildExecuteTransaction.mockReturnValueOnce(transaction); + transactionsHelper.getTransactionGas.mockRejectedValueOnce(new Error('Network error')); await service.handleNewApprovalsRaw(); // Process a message @@ -173,8 +163,8 @@ describe('ApprovalsProcessorService', () => { setTimeout(resolve, 500); }); - expect(transactionsHelper.getAccountNonce).toHaveBeenCalledTimes(1); - expect(transactionsHelper.getAccountNonce).toHaveBeenCalledWith(userAddress); + expect(transactionsHelper.getTransactionGas).toHaveBeenCalledTimes(1); + expect(transactionsHelper.getTransactionGas).toHaveBeenCalledWith(transaction, 0); expect(redisCacheService.set).toHaveBeenCalledTimes(1); expect(redisCacheService.set).toHaveBeenCalledWith( @@ -230,9 +220,7 @@ describe('ApprovalsProcessorService', () => { it('Should handle undefined', async () => { const key = CacheInfo.PendingTransaction('txHashUndefined').key; - redisCacheService.scan.mockReturnValueOnce( - Promise.resolve([key]), - ); + redisCacheService.scan.mockReturnValueOnce(Promise.resolve([key])); redisCacheService.get.mockReturnValueOnce(Promise.resolve(undefined)); await service.handlePendingTransactionsRaw(); @@ -267,7 +255,7 @@ describe('ApprovalsProcessorService', () => { expect(redisCacheService.delete).toHaveBeenCalledWith(key); expect(transactionsHelper.awaitComplete).toHaveBeenCalledTimes(1); expect(transactionsHelper.awaitComplete).toHaveBeenCalledWith('txHashComplete'); - expect(transactionsHelper.getAccountNonce).not.toHaveBeenCalled(); + expect(transactionsHelper.getTransactionGas).not.toHaveBeenCalled(); }); it('Should handle retry', async () => { @@ -285,39 +273,30 @@ describe('ApprovalsProcessorService', () => { transactionsHelper.awaitComplete.mockReturnValueOnce(Promise.resolve(false)); const userAddress = UserAddress.fromBech32('erd1qqqqqqqqqqqqqpgqhe8t5jewej70zupmh44jurgn29psua5l2jps3ntjj3'); - const accountNonce = 0; walletSigner.getAddress.mockReturnValueOnce(userAddress); - transactionsHelper.getAccountNonce.mockReturnValueOnce(Promise.resolve(accountNonce)); const transaction: DeepMocked = createMock(); gatewayContract.buildExecuteTransaction.mockReturnValueOnce(transaction); transactionsHelper.getTransactionGas.mockReturnValueOnce(Promise.resolve(100_000_000)); - walletSigner.sign.mockReturnValueOnce(Promise.resolve(Buffer.from('signature'))); - transactionsHelper.sendTransaction.mockReturnValueOnce(Promise.resolve('txHash')); + transactionsHelper.signAndSendTransaction.mockReturnValueOnce(Promise.resolve('txHash')); await service.handlePendingTransactionsRaw(); expect(transactionsHelper.awaitComplete).toHaveBeenCalledTimes(1); expect(transactionsHelper.awaitComplete).toHaveBeenCalledWith('txHashComplete'); - expect(transactionsHelper.getAccountNonce).toHaveBeenCalledTimes(1); - expect(transactionsHelper.getAccountNonce).toHaveBeenCalledWith(userAddress); expect(gatewayContract.buildExecuteTransaction).toHaveBeenCalledTimes(1); expect(gatewayContract.buildExecuteTransaction).toHaveBeenCalledWith( executeData, - accountNonce, - 'test', userAddress, ); expect(transactionsHelper.getTransactionGas).toHaveBeenCalledTimes(1); expect(transactionsHelper.getTransactionGas).toHaveBeenCalledWith(transaction, 1); expect(transaction.setGasLimit).toHaveBeenCalledTimes(1); expect(transaction.setGasLimit).toHaveBeenCalledWith(100_000_000); - expect(transaction.applySignature).toHaveBeenCalledTimes(1); - expect(transaction.applySignature).toHaveBeenCalledWith(Buffer.from('signature')); - expect(transactionsHelper.sendTransaction).toHaveBeenCalledTimes(1); - expect(transactionsHelper.sendTransaction).toHaveBeenCalledWith(transaction); + expect(transactionsHelper.signAndSendTransaction).toHaveBeenCalledTimes(1); + expect(transactionsHelper.signAndSendTransaction).toHaveBeenCalledWith(transaction, walletSigner); expect(redisCacheService.set).toHaveBeenCalledTimes(1); expect(redisCacheService.set).toHaveBeenCalledWith( @@ -349,7 +328,7 @@ describe('ApprovalsProcessorService', () => { expect(transactionsHelper.awaitComplete).toHaveBeenCalledTimes(1); expect(transactionsHelper.awaitComplete).toHaveBeenCalledWith('txHashComplete'); - expect(transactionsHelper.getAccountNonce).not.toHaveBeenCalled(); + expect(transactionsHelper.getTransactionGas).not.toHaveBeenCalled(); }); it('Should handle retry error', async () => { @@ -368,15 +347,19 @@ describe('ApprovalsProcessorService', () => { const userAddress = UserAddress.fromBech32('erd1qqqqqqqqqqqqqpgqhe8t5jewej70zupmh44jurgn29psua5l2jps3ntjj3'); walletSigner.getAddress.mockReturnValueOnce(userAddress); - transactionsHelper.getAccountNonce.mockRejectedValueOnce(new Error('Network error')); + + const transaction: DeepMocked = createMock(); + gatewayContract.buildExecuteTransaction.mockReturnValueOnce(transaction); + + transactionsHelper.getTransactionGas.mockRejectedValueOnce(new Error('Network error')); await service.handlePendingTransactionsRaw(); expect(transactionsHelper.awaitComplete).toHaveBeenCalledTimes(1); expect(transactionsHelper.awaitComplete).toHaveBeenCalledWith('txHashComplete'); - expect(transactionsHelper.getAccountNonce).toHaveBeenCalledTimes(1); - expect(transactionsHelper.getAccountNonce).toHaveBeenCalledWith(userAddress); + expect(transactionsHelper.getTransactionGas).toHaveBeenCalledTimes(1); + expect(transactionsHelper.getTransactionGas).toHaveBeenCalledWith(transaction, 1); expect(redisCacheService.set).toHaveBeenCalledTimes(1); expect(redisCacheService.set).toHaveBeenCalledWith( CacheInfo.PendingTransaction('txHashComplete').key, diff --git a/apps/axelar-event-processor/src/main.ts b/apps/axelar-event-processor/src/main.ts index 1b9fe05..039c8ed 100644 --- a/apps/axelar-event-processor/src/main.ts +++ b/apps/axelar-event-processor/src/main.ts @@ -1,4 +1,3 @@ -import 'module-alias/register'; import { NestFactory } from '@nestjs/core'; import { ApprovalsProcessorModule } from './approvals-processor'; diff --git a/apps/mvx-event-processor/src/call-contract-approved-processor/call-contract-approved.processor.service.ts b/apps/mvx-event-processor/src/call-contract-approved-processor/call-contract-approved.processor.service.ts index d31fc4d..f57e0ae 100644 --- a/apps/mvx-event-processor/src/call-contract-approved-processor/call-contract-approved.processor.service.ts +++ b/apps/mvx-event-processor/src/call-contract-approved-processor/call-contract-approved.processor.service.ts @@ -84,6 +84,8 @@ export class CallContractApprovedProcessorService { // Page is not modified if database records are updated await this.contractCallApprovedRepository.updateManyStatusRetryExecuteTxHash(entries); } else { + accountNonce = null; // re-retrieve account nonce + page++; } } @@ -112,7 +114,10 @@ export class CallContractApprovedProcessorService { .withChainID(this.chainId) .buildTransaction(); - const gas = await this.transactionsHelper.getTransactionGas(transaction, contractCallApproved.retry); + const gas = await this.transactionsHelper.getTransactionGas( + transaction, + contractCallApproved.retry, + ); transaction.setGasLimit(gas); const signature = await this.walletSigner.sign(transaction.serializeForSigning()); diff --git a/apps/mvx-event-processor/src/gas-checker/gas-checker.service.spec.ts b/apps/mvx-event-processor/src/gas-checker/gas-checker.service.spec.ts index f9bf5ad..27853dd 100644 --- a/apps/mvx-event-processor/src/gas-checker/gas-checker.service.spec.ts +++ b/apps/mvx-event-processor/src/gas-checker/gas-checker.service.spec.ts @@ -1,18 +1,92 @@ import { Test, TestingModule } from '@nestjs/testing'; import { GasCheckerService } from './gas-checker.service'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { CacheInfo, GasServiceContract, TransactionsHelper, WegldSwapContract } from '@mvx-monorepo/common'; +import { UserSigner } from '@multiversx/sdk-wallet/out'; +import { ApiNetworkProvider } from '@multiversx/sdk-network-providers/out'; +import { ProviderKeys } from '@mvx-monorepo/common/utils/provider.enum'; +import { Address } from '@multiversx/sdk-core/out'; +import { CacheService } from '@multiversx/sdk-nestjs-cache'; +import { UserAddress } from '@multiversx/sdk-wallet/out/userAddress'; describe('GasCheckerService', () => { + const gasServiceAddress = 'erd1qqqqqqqqqqqqqpgqhe8t5jewej70zupmh44jurgn29psua5l2jps3ntjj3'; + const userSignerAddress = 'erd1fsk0cnaag2m78gunfddsvg0y042rf0maxxgz6kvm32kxcl25m0yq8s38vt'; + + let walletSigner: DeepMocked; + let transactionsHelper: DeepMocked; + let api: DeepMocked; + let wegldSwapContract: DeepMocked; + let gasServiceContract: DeepMocked; + let cacheService: DeepMocked; + let service: GasCheckerService; beforeEach(async () => { + walletSigner = createMock(); + transactionsHelper = createMock(); + api = createMock(); + wegldSwapContract = createMock(); + gasServiceContract = createMock(); + cacheService = createMock(); + const module: TestingModule = await Test.createTestingModule({ - providers: [GasCheckerService], - }).compile(); + providers: [ + GasCheckerService, + { + provide: CacheService, + useValue: cacheService, + }, + ], + }) + .useMocker((token) => { + if (token === ProviderKeys.WALLET_SIGNER) { + return walletSigner; + } + + if (token === TransactionsHelper) { + return transactionsHelper; + } + + if (token === ApiNetworkProvider) { + return api; + } + + if (token === WegldSwapContract) { + return wegldSwapContract; + } + + if (token === GasServiceContract) { + return gasServiceContract; + } + + return null; + }) + .compile(); + + gasServiceContract.getContractAddress.mockReturnValue(Address.fromBech32(gasServiceAddress)); + cacheService.getOrSet.mockImplementation((key) => { + if (key === CacheInfo.WegldTokenId().key) { + return Promise.resolve('WEGLD-123456'); + } + + return Promise.resolve(undefined); + }); + const userAddress = UserAddress.fromBech32(userSignerAddress); + walletSigner.getAddress.mockReturnValue(userAddress); service = module.get(GasCheckerService); }); - it('should be defined', () => { - expect(service).toBeDefined(); + it('Should check gas service fees and wallet tokens error', async () => { + api.getAccount.mockRejectedValue(new Error('Invalid account')); + + await service.checkGasServiceAndWalletRaw(); + + expect(gasServiceContract.getContractAddress).toHaveBeenCalledTimes(2); + expect(api.getAccount).toHaveBeenCalledTimes(2); + expect(api.getAccount).toHaveBeenCalledWith(Address.fromBech32(gasServiceAddress)); + expect(api.getAccount).toHaveBeenCalledWith(UserAddress.fromBech32(userSignerAddress)); + expect(api.getFungibleTokenOfAccount).not.toHaveBeenCalled(); }); }); diff --git a/apps/mvx-event-processor/src/gas-checker/gas-checker.service.ts b/apps/mvx-event-processor/src/gas-checker/gas-checker.service.ts index 60951c0..802c6bc 100644 --- a/apps/mvx-event-processor/src/gas-checker/gas-checker.service.ts +++ b/apps/mvx-event-processor/src/gas-checker/gas-checker.service.ts @@ -3,92 +3,161 @@ import { Cron, CronExpression } from '@nestjs/schedule'; import { ProviderKeys } from '@mvx-monorepo/common/utils/provider.enum'; import { UserSigner } from '@multiversx/sdk-wallet/out'; import { TransactionsHelper } from '@mvx-monorepo/common/contracts/transactions.helper'; -import { ApiConfigService, CacheInfo } from '@mvx-monorepo/common'; +import { CacheInfo } from '@mvx-monorepo/common'; import { Locker } from '@multiversx/sdk-nestjs-common'; import { ApiNetworkProvider } from '@multiversx/sdk-network-providers/out'; -import { Address } from '@multiversx/sdk-core/out'; import { CONSTANTS } from '@mvx-monorepo/common/utils/constants.enum'; import { GetOrSetCache } from '@mvx-monorepo/common/decorators/get.or.set.cache'; import { WegldSwapContract } from '@mvx-monorepo/common/contracts/wegld-swap.contract'; import { FungibleTokenOfAccountOnNetwork } from '@multiversx/sdk-network-providers/out/tokens'; import BigNumber from 'bignumber.js'; import { GasServiceContract } from '@mvx-monorepo/common/contracts/gas-service.contract'; +import { IAddress } from '@multiversx/sdk-network-providers/out/interface'; -const EGLD_THRESHOLD = new BigNumber('100_000_000_000_000_000'); // 0.1 EGLD +const EGLD_COLLECT_THRESHOLD = new BigNumber('200000000000000000'); // 0.2 EGLD +const EGLD_LOW_ERROR_THRESHOLD = new BigNumber('100000000000000000'); // 0.1 EGLD @Injectable() export class GasCheckerService { private readonly logger: Logger; - private readonly contractGasService: string; - private readonly chainId: string; - constructor( @Inject(ProviderKeys.WALLET_SIGNER) private readonly walletSigner: UserSigner, private readonly transactionsHelper: TransactionsHelper, private readonly api: ApiNetworkProvider, private readonly wegldSwapContract: WegldSwapContract, private readonly gasServiceContract: GasServiceContract, - apiConfigService: ApiConfigService, ) { this.logger = new Logger(GasCheckerService.name); - this.contractGasService = apiConfigService.getContractGasService(); - this.chainId = apiConfigService.getChainId(); } - @Cron(CronExpression.EVERY_30_MINUTES) + @Cron(CronExpression.EVERY_HOUR) async checkGasServiceAndWallet() { await Locker.lock('checkGasServiceAndWallet', async () => { - this.logger.debug('Running checkGasServiceAndWallet cron'); - - const tokens = await this.getGasServiceEgldAndWegld(); - const toClaim = tokens.filter((token) => token.balance.gte(EGLD_THRESHOLD)); - - if (toClaim.length) { - await this.collectFees(toClaim); - } + await this.checkGasServiceAndWalletRaw(); }); } - private async getGasServiceEgldAndWegld(): Promise { - const address = Address.fromBech32(this.contractGasService); - const account = await this.api.getAccount(address); + async checkGasServiceAndWalletRaw() { + this.logger.debug('Running checkGasServiceAndWallet cron'); - const tokens: FungibleTokenOfAccountOnNetwork[] = []; + this.logger.log( + `Checking gas service fees with address ${this.gasServiceContract.getContractAddress().bech32()}`, + ); - tokens.push({ - identifier: CONSTANTS.EGLD_IDENTIFIER, - balance: account.balance, - rawResponse: {}, - }); + // First check gas service fees and collect them if necessary + try { + await this.checkGasServiceFees(); - const wegldTokenId = await this.getWegldTokenId(); + this.logger.log('Checked gas service fees successfully'); + } catch (e) { + this.logger.error('Error while trying to collect fees...'); + this.logger.error(e); + } - const token = await this.api.getFungibleTokenOfAccount(address, wegldTokenId); + this.logger.log(`Checking wallet signer balance with address ${this.walletSigner.getAddress().bech32()}`); - tokens.push(token); + try { + await this.checkWalletTokens(); - return tokens; + this.logger.log('Checked wallet signer balance successfully'); + } catch (e) { + this.logger.error('Error while checking wallet signer balance...'); + this.logger.error(e); + } } - @GetOrSetCache(CacheInfo.WegldTokenId) - private async getWegldTokenId(): Promise { - return await this.wegldSwapContract.getWrappedEgldTokenId(); - } + private async checkGasServiceFees() { + const tokens = await this.getAccountEgldAndWegld(this.gasServiceContract.getContractAddress()); + const tokensToCollect = Object.values(tokens).filter((token) => token.balance.gte(EGLD_COLLECT_THRESHOLD)); + + if (!tokensToCollect.length) { + this.logger.log('No fees to collect currently'); - private async collectFees(toClaim: FungibleTokenOfAccountOnNetwork[]) { - const accountNonce = await this.transactionsHelper.getAccountNonce(this.walletSigner.getAddress()); + return; + } + + this.logger.log( + 'Trying to collect fees from gas service for: ' + + tokensToCollect.map((token) => `${token.identifier} - ${token.balance}`), + ); const transaction = this.gasServiceContract.collectFees( this.walletSigner.getAddress(), - toClaim.map(token => token.identifier), - toClaim.map(token => token.balance), - accountNonce, - this.chainId, + tokensToCollect.map((token) => token.identifier), + tokensToCollect.map((token) => token.balance), ); - await this.transactionsHelper.sendTransactions([transaction]); + const txHash = await this.transactionsHelper.signAndSendTransaction(transaction, this.walletSigner); + + const success = await this.transactionsHelper.awaitComplete(txHash); + + if (!success) { + throw new Error(`Error while executing transaction ${txHash}`); + } + + this.logger.log(`Successfully collected fees from gas service with transaction: ${txHash}!`); + } + private async checkWalletTokens() { + const tokens = await this.getAccountEgldAndWegld(this.walletSigner.getAddress()); + if (tokens.wegldToken.balance.gte(EGLD_COLLECT_THRESHOLD)) { + this.logger.log(`Trying to convert ${tokens.wegldToken.balance} wegld token to egld for wallet`); + + const wegld = tokens.wegldToken; + + const transaction = this.wegldSwapContract.unwrapEgld( + wegld.identifier, + wegld.balance, + this.walletSigner.getAddress(), + ); + + const txHash = await this.transactionsHelper.signAndSendTransaction(transaction, this.walletSigner); + + const success = await this.transactionsHelper.awaitComplete(txHash); + + if (!success) { + throw new Error(`Error while executing unwrap egld transaction ${txHash}`); + } + + this.logger.log('Successfully converted wegld token to egld for wallet'); + } + + if (tokens.egldToken.balance.lt(EGLD_LOW_ERROR_THRESHOLD)) { + this.logger.error('Low balance for signer wallet! Consider manually topping up EGLD!'); + } + } + + private async getAccountEgldAndWegld( + address: IAddress, + ): Promise<{ egldToken: FungibleTokenOfAccountOnNetwork; wegldToken: FungibleTokenOfAccountOnNetwork }> { + const account = await this.api.getAccount(address); + const egldToken = { + identifier: CONSTANTS.EGLD_IDENTIFIER, + balance: account.balance, + rawResponse: {}, + }; + + const wegldTokenId = await this.getWegldTokenId(); + let wegldToken; + try { + wegldToken = await this.api.getFungibleTokenOfAccount(address, wegldTokenId); + } catch (e) { + this.logger.warn(`Could not get wegld balance for ${address.bech32()}`); + + wegldToken = { + identifier: wegldTokenId, + balance: new BigNumber(0), + rawResponse: {}, + }; + } + + return { egldToken, wegldToken }; + } + + @GetOrSetCache(CacheInfo.WegldTokenId) + private async getWegldTokenId(): Promise { + return await this.wegldSwapContract.getWrappedEgldTokenId(); } } diff --git a/apps/mvx-event-processor/src/main.ts b/apps/mvx-event-processor/src/main.ts index 05959fe..be78a66 100644 --- a/apps/mvx-event-processor/src/main.ts +++ b/apps/mvx-event-processor/src/main.ts @@ -1,13 +1,8 @@ import { NestFactory } from '@nestjs/core'; -import { EventProcessorModule } from './event-processor'; -import { CallContractApprovedProcessorModule } from './call-contract-approved-processor'; -import { GasCheckerModule } from './gas-checker/gas-checker.module'; +import { MvxEventProcessorModule } from './mvx-event-processor.module'; async function bootstrap() { - // TODO: Probably these should be refactor under the same module - await NestFactory.createApplicationContext(EventProcessorModule); - await NestFactory.createApplicationContext(CallContractApprovedProcessorModule); - await NestFactory.createApplicationContext(GasCheckerModule); + await NestFactory.createApplicationContext(MvxEventProcessorModule); } // eslint-disable-next-line @typescript-eslint/no-floating-promises diff --git a/apps/mvx-event-processor/src/mvx-event-processor.module.ts b/apps/mvx-event-processor/src/mvx-event-processor.module.ts new file mode 100644 index 0000000..0046acc --- /dev/null +++ b/apps/mvx-event-processor/src/mvx-event-processor.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { EventProcessorModule } from './event-processor'; +import { CallContractApprovedProcessorModule } from './call-contract-approved-processor'; +import { GasCheckerModule } from './gas-checker/gas-checker.module'; + +@Module({ + imports: [EventProcessorModule, CallContractApprovedProcessorModule, GasCheckerModule], +}) +export class MvxEventProcessorModule {} diff --git a/libs/common/src/contracts/gas-service.contract.ts b/libs/common/src/contracts/gas-service.contract.ts index b5cfca8..4b21bed 100644 --- a/libs/common/src/contracts/gas-service.contract.ts +++ b/libs/common/src/contracts/gas-service.contract.ts @@ -20,19 +20,11 @@ export class GasServiceContract { private readonly resultsParser: ResultsParser, ) {} - collectFees( - sender: IAddress, - tokens: string[], - amounts: BigNumber[], - accountNonce: number, - chainId: string, - ): Transaction { + collectFees(sender: IAddress, tokens: string[], amounts: BigNumber[]): Transaction { return this.smartContract.methods - .collectFees([sender, tokens, amounts]) - .withSender(sender) - .withNonce(accountNonce) + .collectFees([sender.bech32(), tokens, amounts]) .withGasLimit(GasInfo.CollectFeesBase.value + GasInfo.CollectFeesExtra.value * tokens.length) - .withChainID(chainId) + .withSender(sender) .buildTransaction(); } @@ -116,4 +108,8 @@ export class GasServiceContract { }, }; } + + getContractAddress(): IAddress { + return this.smartContract.getAddress(); + } } diff --git a/libs/common/src/contracts/gateway.contract.ts b/libs/common/src/contracts/gateway.contract.ts index cc9e5ea..c8864ab 100644 --- a/libs/common/src/contracts/gateway.contract.ts +++ b/libs/common/src/contracts/gateway.contract.ts @@ -19,17 +19,10 @@ export class GatewayContract { this.logger = new Logger(GatewayContract.name); } - buildExecuteTransaction( - executeData: Uint8Array, - accountNonce: number, - chainId: string, - walletAddress: IAddress, - ): Transaction { + buildExecuteTransaction(executeData: Uint8Array, sender: IAddress): Transaction { return this.smartContract.methodsExplicit .execute([new BytesValue(Buffer.from(executeData))]) - .withSender(walletAddress) - .withNonce(accountNonce) - .withChainID(chainId) + .withSender(sender) .buildTransaction(); } diff --git a/libs/common/src/contracts/index.ts b/libs/common/src/contracts/index.ts index c4e7346..54d4fda 100644 --- a/libs/common/src/contracts/index.ts +++ b/libs/common/src/contracts/index.ts @@ -2,3 +2,4 @@ export * from './contracts.module'; export * from './gas-service.contract'; export * from './gateway.contract'; export * from './transactions.helper'; +export * from './wegld-swap.contract'; diff --git a/libs/common/src/contracts/transactions.helper.ts b/libs/common/src/contracts/transactions.helper.ts index 66341f1..b385706 100644 --- a/libs/common/src/contracts/transactions.helper.ts +++ b/libs/common/src/contracts/transactions.helper.ts @@ -2,13 +2,23 @@ import { Injectable, Logger } from '@nestjs/common'; import { ProxyNetworkProvider } from '@multiversx/sdk-network-providers/out'; import { Transaction, TransactionHash, TransactionWatcher } from '@multiversx/sdk-core/out'; import { UserAddress } from '@multiversx/sdk-wallet/out/userAddress'; +import { UserSigner } from '@multiversx/sdk-wallet/out'; +import { ApiConfigService } from '@mvx-monorepo/common/config'; @Injectable() export class TransactionsHelper { private readonly logger: Logger; - constructor(private readonly proxy: ProxyNetworkProvider, private readonly transactionWatcher: TransactionWatcher) { + private readonly chainId: string; + + constructor( + private readonly proxy: ProxyNetworkProvider, + private readonly transactionWatcher: TransactionWatcher, + apiConfigService: ApiConfigService, + ) { this.logger = new Logger(TransactionsHelper.name); + + this.chainId = apiConfigService.getChainId(); } async getAccountNonce(address: UserAddress): Promise { @@ -18,21 +28,34 @@ export class TransactionsHelper { } // TODO: Check if this works properly - async getTransactionGas(transaction: Transaction, retry: number = 0): Promise { + async getTransactionGas(transaction: Transaction, retry: number): Promise { + transaction.setChainID(this.chainId); + const result = await this.proxy.doPostGeneric('transaction/cost', transaction.toSendable()); return (result.data.txGasUnits * (11 + retry * 2)) / 10; // add 10% extra gas initially, and more gas with each retry } - async sendTransaction(transaction: Transaction) { + async signAndSendTransaction(transaction: Transaction, signer: UserSigner) { try { + // TODO: Check if it is fine to use the same wallet as in the CallContractApprovedProcessor + // and that no issues happen because of nonce + const accountNonce = await this.getAccountNonce(signer.getAddress()); + + transaction.setNonce(accountNonce); + transaction.setSender(signer.getAddress()); + transaction.setChainID(this.chainId); + + const signature = await signer.sign(transaction.serializeForSigning()); + transaction.applySignature(signature); + const hash = await this.proxy.sendTransaction(transaction); this.logger.log(`Sent transaction to proxy: ${transaction.getHash()}`); return hash; } catch (e) { - this.logger.error(`Can not send transaction to proxy...`); + this.logger.error(`Can not sign or send transaction to proxy...`); this.logger.error(e); throw e; diff --git a/libs/common/src/contracts/wegld-swap.contract.ts b/libs/common/src/contracts/wegld-swap.contract.ts index decf314..a7c06b8 100644 --- a/libs/common/src/contracts/wegld-swap.contract.ts +++ b/libs/common/src/contracts/wegld-swap.contract.ts @@ -12,14 +12,12 @@ export class WegldSwapContract { private readonly proxy: ProxyNetworkProvider, ) {} - unwrapEgld(token: string, amount: BigNumber, sender: IAddress, accountNonce: number, chainId: string): Transaction { + unwrapEgld(token: string, amount: BigNumber, sender: IAddress): Transaction { return this.smartContract.methodsExplicit .unwrapEgld() - .withSender(sender) - .withNonce(accountNonce) .withSingleESDTTransfer(TokenTransfer.fungibleFromBigInteger(token, amount)) .withGasLimit(GasInfo.UnwrapEgld.value) - .withChainID(chainId) + .withSender(sender) .buildTransaction(); } From b6258756ac7e86c622065fddfe9ee890bbe7bdef Mon Sep 17 00:00:00 2001 From: Rares <6453351+raresserban@users.noreply.github.com> Date: Fri, 15 Dec 2023 12:01:17 +0200 Subject: [PATCH 20/33] Add gas checker test. --- .../gas-checker/gas-checker.service.spec.ts | 174 +++++++++++++++++- .../src/gas-checker/gas-checker.service.ts | 11 +- 2 files changed, 171 insertions(+), 14 deletions(-) diff --git a/apps/mvx-event-processor/src/gas-checker/gas-checker.service.spec.ts b/apps/mvx-event-processor/src/gas-checker/gas-checker.service.spec.ts index 27853dd..f6e902c 100644 --- a/apps/mvx-event-processor/src/gas-checker/gas-checker.service.spec.ts +++ b/apps/mvx-event-processor/src/gas-checker/gas-checker.service.spec.ts @@ -3,15 +3,16 @@ import { GasCheckerService } from './gas-checker.service'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { CacheInfo, GasServiceContract, TransactionsHelper, WegldSwapContract } from '@mvx-monorepo/common'; import { UserSigner } from '@multiversx/sdk-wallet/out'; -import { ApiNetworkProvider } from '@multiversx/sdk-network-providers/out'; +import { AccountOnNetwork, ApiNetworkProvider } from '@multiversx/sdk-network-providers/out'; import { ProviderKeys } from '@mvx-monorepo/common/utils/provider.enum'; -import { Address } from '@multiversx/sdk-core/out'; +import { Address, Transaction } from '@multiversx/sdk-core/out'; import { CacheService } from '@multiversx/sdk-nestjs-cache'; import { UserAddress } from '@multiversx/sdk-wallet/out/userAddress'; +import BigNumber from 'bignumber.js'; describe('GasCheckerService', () => { - const gasServiceAddress = 'erd1qqqqqqqqqqqqqpgqhe8t5jewej70zupmh44jurgn29psua5l2jps3ntjj3'; - const userSignerAddress = 'erd1fsk0cnaag2m78gunfddsvg0y042rf0maxxgz6kvm32kxcl25m0yq8s38vt'; + const gasServiceAddress = Address.fromBech32('erd1qqqqqqqqqqqqqpgqhe8t5jewej70zupmh44jurgn29psua5l2jps3ntjj3'); + const userSignerAddress = UserAddress.fromBech32('erd1fsk0cnaag2m78gunfddsvg0y042rf0maxxgz6kvm32kxcl25m0yq8s38vt'); let walletSigner: DeepMocked; let transactionsHelper: DeepMocked; @@ -64,7 +65,7 @@ describe('GasCheckerService', () => { }) .compile(); - gasServiceContract.getContractAddress.mockReturnValue(Address.fromBech32(gasServiceAddress)); + gasServiceContract.getContractAddress.mockReturnValue(gasServiceAddress); cacheService.getOrSet.mockImplementation((key) => { if (key === CacheInfo.WegldTokenId().key) { return Promise.resolve('WEGLD-123456'); @@ -72,8 +73,7 @@ describe('GasCheckerService', () => { return Promise.resolve(undefined); }); - const userAddress = UserAddress.fromBech32(userSignerAddress); - walletSigner.getAddress.mockReturnValue(userAddress); + walletSigner.getAddress.mockReturnValue(userSignerAddress); service = module.get(GasCheckerService); }); @@ -85,8 +85,164 @@ describe('GasCheckerService', () => { expect(gasServiceContract.getContractAddress).toHaveBeenCalledTimes(2); expect(api.getAccount).toHaveBeenCalledTimes(2); - expect(api.getAccount).toHaveBeenCalledWith(Address.fromBech32(gasServiceAddress)); - expect(api.getAccount).toHaveBeenCalledWith(UserAddress.fromBech32(userSignerAddress)); + expect(api.getAccount).toHaveBeenCalledWith(gasServiceAddress); + expect(api.getAccount).toHaveBeenCalledWith(userSignerAddress); expect(api.getFungibleTokenOfAccount).not.toHaveBeenCalled(); }); + + describe('checkGasServiceFees', () => { + it('Should check gas service fees no fees to collect', async () => { + api.getAccount.mockImplementation((address) => { + if (address !== gasServiceAddress) { + throw new Error('Invalid account'); + } + + return Promise.resolve(new AccountOnNetwork({ balance: new BigNumber('1000') })); + }); + api.getFungibleTokenOfAccount.mockReturnValueOnce( + Promise.resolve({ + identifier: 'WEGLD-123456', + balance: new BigNumber('2000'), + rawResponse: {}, + }), + ); + + await service.checkGasServiceAndWalletRaw(); + + expect(gasServiceContract.getContractAddress).toHaveBeenCalledTimes(2); + expect(api.getAccount).toHaveBeenCalledTimes(2); + expect(api.getAccount).toHaveBeenCalledWith(gasServiceAddress); + expect(api.getAccount).toHaveBeenCalledWith(userSignerAddress); + expect(api.getFungibleTokenOfAccount).toHaveBeenCalledTimes(1); + expect(api.getFungibleTokenOfAccount).toHaveBeenCalledWith(gasServiceAddress, 'WEGLD-123456'); + expect(gasServiceContract.collectFees).not.toHaveBeenCalled(); + expect(wegldSwapContract.unwrapEgld).not.toHaveBeenCalled(); + }); + + const checkGasServiceFeesComplete = async (success: boolean) => { + api.getAccount.mockImplementation((address) => { + if (address !== gasServiceAddress) { + throw new Error('Invalid account'); + } + + return Promise.resolve(new AccountOnNetwork({ balance: new BigNumber('200000000000000000') })); + }); + api.getFungibleTokenOfAccount.mockRejectedValue(new Error('No wegld token for address')); + + const transaction: DeepMocked = createMock(); + gasServiceContract.collectFees.mockReturnValueOnce(transaction); + transactionsHelper.signAndSendTransaction.mockReturnValueOnce(Promise.resolve('txHash')); + transactionsHelper.awaitComplete.mockReturnValueOnce(Promise.resolve(success)); + + await service.checkGasServiceAndWalletRaw(); + + expect(gasServiceContract.getContractAddress).toHaveBeenCalledTimes(2); + expect(api.getAccount).toHaveBeenCalledTimes(2); + expect(api.getAccount).toHaveBeenCalledWith(gasServiceAddress); + expect(api.getAccount).toHaveBeenCalledWith(userSignerAddress); + expect(api.getFungibleTokenOfAccount).toHaveBeenCalledTimes(1); + expect(api.getFungibleTokenOfAccount).toHaveBeenCalledWith(gasServiceAddress, 'WEGLD-123456'); + expect(gasServiceContract.collectFees).toHaveBeenCalledTimes(1); + expect(gasServiceContract.collectFees).toHaveBeenCalledWith( + userSignerAddress, + ['EGLD'], + [new BigNumber('200000000000000000')], + ); + expect(transactionsHelper.signAndSendTransaction).toHaveBeenCalledTimes(1); + expect(transactionsHelper.signAndSendTransaction).toHaveBeenCalledWith(transaction, walletSigner); + expect(transactionsHelper.awaitComplete).toHaveBeenCalledTimes(1); + expect(transactionsHelper.awaitComplete).toHaveBeenCalledWith('txHash'); + expect(wegldSwapContract.unwrapEgld).not.toHaveBeenCalled(); + }; + + it('Should check gas service fees collect fees complete error', async () => { + await checkGasServiceFeesComplete(false); + }); + + it('Should check gas service fees collect fees complete success', async () => { + await checkGasServiceFeesComplete(true); + }); + }); + + describe('checkWalletTokens', () => { + it('Should check wallet tokens low balance', async () => { + api.getAccount.mockImplementation((address) => { + if (address !== userSignerAddress) { + throw new Error('Invalid account'); + } + + return Promise.resolve(new AccountOnNetwork({ balance: new BigNumber('1000') })); + }); + api.getFungibleTokenOfAccount.mockRejectedValue(new Error('No wegld token for address')); + + await service.checkGasServiceAndWalletRaw(); + + expect(gasServiceContract.getContractAddress).toHaveBeenCalledTimes(2); + expect(api.getAccount).toHaveBeenCalledTimes(2); + expect(api.getAccount).toHaveBeenCalledWith(gasServiceAddress); + expect(api.getAccount).toHaveBeenCalledWith(userSignerAddress); + expect(api.getFungibleTokenOfAccount).toHaveBeenCalledTimes(1); + expect(api.getFungibleTokenOfAccount).toHaveBeenCalledWith(userSignerAddress, 'WEGLD-123456'); + expect(gasServiceContract.collectFees).not.toHaveBeenCalled(); + expect(wegldSwapContract.unwrapEgld).not.toHaveBeenCalled(); + + // The low balance just logs an error, which can not be tested currently + }); + + const checkWalletTokensUnwrapComplete = async (complete: boolean) => { + api.getAccount.mockImplementation((address) => { + if (address !== userSignerAddress) { + throw new Error('Invalid account'); + } + + return Promise.resolve(new AccountOnNetwork({ balance: new BigNumber('100000000000000000') })); + }); + api.getFungibleTokenOfAccount.mockReturnValueOnce( + Promise.resolve({ + identifier: 'WEGLD-123456', + balance: new BigNumber('200000000000000000'), + rawResponse: {}, + }), + ); + + const transaction: DeepMocked = createMock(); + wegldSwapContract.unwrapEgld.mockReturnValueOnce(transaction); + transactionsHelper.signAndSendTransaction.mockReturnValueOnce(Promise.resolve('txHash')); + transactionsHelper.awaitComplete.mockReturnValueOnce(Promise.resolve(complete)); + + await service.checkGasServiceAndWalletRaw(); + + expect(gasServiceContract.getContractAddress).toHaveBeenCalledTimes(2); + expect(api.getAccount).toHaveBeenCalled(); + expect(api.getAccount).toHaveBeenCalledWith(gasServiceAddress); + expect(api.getAccount).toHaveBeenCalledWith(userSignerAddress); + expect(api.getFungibleTokenOfAccount).toHaveBeenCalledTimes(1); + expect(api.getFungibleTokenOfAccount).toHaveBeenCalledWith(userSignerAddress, 'WEGLD-123456'); + expect(wegldSwapContract.unwrapEgld).toHaveBeenCalledTimes(1); + expect(wegldSwapContract.unwrapEgld).toHaveBeenCalledWith( + 'WEGLD-123456', + new BigNumber('200000000000000000'), + userSignerAddress, + ); + expect(transactionsHelper.signAndSendTransaction).toHaveBeenCalledTimes(1); + expect(transactionsHelper.signAndSendTransaction).toHaveBeenCalledWith(transaction, walletSigner); + expect(transactionsHelper.awaitComplete).toHaveBeenCalledTimes(1); + expect(transactionsHelper.awaitComplete).toHaveBeenCalledWith('txHash'); + expect(gasServiceContract.collectFees).not.toHaveBeenCalled(); + }; + + it('Should check wallet tokens unwrap complete error', async () => { + await checkWalletTokensUnwrapComplete(false); + + // Only called 2 times (one for gas service and one for wallet) + expect(api.getAccount).toHaveBeenCalledTimes(2); + }); + + it('Should check wallet tokens unwrap complete success', async () => { + await checkWalletTokensUnwrapComplete(true); + + // Called 3 times, one more time for wallet + expect(api.getAccount).toHaveBeenCalledTimes(3); + }); + }); }); diff --git a/apps/mvx-event-processor/src/gas-checker/gas-checker.service.ts b/apps/mvx-event-processor/src/gas-checker/gas-checker.service.ts index 802c6bc..05904b1 100644 --- a/apps/mvx-event-processor/src/gas-checker/gas-checker.service.ts +++ b/apps/mvx-event-processor/src/gas-checker/gas-checker.service.ts @@ -41,9 +41,7 @@ export class GasCheckerService { async checkGasServiceAndWalletRaw() { this.logger.debug('Running checkGasServiceAndWallet cron'); - this.logger.log( - `Checking gas service fees with address ${this.gasServiceContract.getContractAddress().bech32()}`, - ); + this.logger.log(`Checking gas service fees with address ${this.gasServiceContract.getContractAddress().bech32()}`); // First check gas service fees and collect them if necessary try { @@ -122,6 +120,9 @@ export class GasCheckerService { } this.logger.log('Successfully converted wegld token to egld for wallet'); + + // Retrieve new EGLD balance + tokens.egldToken.balance = (await this.api.getAccount(this.walletSigner.getAddress())).balance; } if (tokens.egldToken.balance.lt(EGLD_LOW_ERROR_THRESHOLD)) { @@ -133,14 +134,14 @@ export class GasCheckerService { address: IAddress, ): Promise<{ egldToken: FungibleTokenOfAccountOnNetwork; wegldToken: FungibleTokenOfAccountOnNetwork }> { const account = await this.api.getAccount(address); - const egldToken = { + const egldToken: FungibleTokenOfAccountOnNetwork = { identifier: CONSTANTS.EGLD_IDENTIFIER, balance: account.balance, rawResponse: {}, }; const wegldTokenId = await this.getWegldTokenId(); - let wegldToken; + let wegldToken: FungibleTokenOfAccountOnNetwork; try { wegldToken = await this.api.getFungibleTokenOfAccount(address, wegldTokenId); } catch (e) { From 80046b3eaf61ee91b89e939324dc5991f60641a0 Mon Sep 17 00:00:00 2001 From: Rares <6453351+raress96@users.noreply.github.com> Date: Mon, 22 Jan 2024 16:00:26 +0200 Subject: [PATCH 21/33] Change unsupported log index value to 0. --- .../src/processors/gateway.processor.spec.ts | 4 ++-- .../mvx-event-processor/src/processors/gateway.processor.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/mvx-event-processor/src/processors/gateway.processor.spec.ts b/apps/mvx-event-processor/src/processors/gateway.processor.spec.ts index 7e26a7a..d4a53f6 100644 --- a/apps/mvx-event-processor/src/processors/gateway.processor.spec.ts +++ b/apps/mvx-event-processor/src/processors/gateway.processor.spec.ts @@ -129,9 +129,9 @@ describe('ContractCallProcessor', () => { expect(gatewayContract.decodeContractCallEvent).toHaveBeenCalledWith(TransactionEvent.fromHttpResponse(rawEvent)); expect(contractCallEventRepository.create).toHaveBeenCalledTimes(1); expect(contractCallEventRepository.create).toHaveBeenCalledWith({ - id: 'multiversx-test:txHash:999999', + id: 'multiversx-test:txHash:0', txHash: 'txHash', - eventIndex: 999999, + eventIndex: 0, status: ContractCallEventStatus.PENDING, sourceAddress: 'erd1qqqqqqqqqqqqqpgqzqvm5ywqqf524efwrhr039tjs29w0qltkklsa05pk7', sourceChain: 'multiversx-test', diff --git a/apps/mvx-event-processor/src/processors/gateway.processor.ts b/apps/mvx-event-processor/src/processors/gateway.processor.ts index 957be5d..3946b0f 100644 --- a/apps/mvx-event-processor/src/processors/gateway.processor.ts +++ b/apps/mvx-event-processor/src/processors/gateway.processor.ts @@ -11,9 +11,9 @@ import { EventIdentifiers, Events } from '@mvx-monorepo/common/utils/event.enum' import { BinaryUtils } from '@multiversx/sdk-nestjs-common'; import { ContractCallApprovedRepository } from '@mvx-monorepo/common/database/repository/contract-call-approved.repository'; -// order/logIndex is unsupported since we can't easily get it in the relayer, -// so we use a sufficiently large u32 value here instead -const UNSUPPORTED_LOG_INDEX: number = 999_999; +// order/logIndex is unsupported since we can't easily get it in the relayer, so we use 0 by default +// this means that only one cross chain call is supported for now (the first appropriate call found in transaction logs) +const UNSUPPORTED_LOG_INDEX: number = 0; @Injectable() export class GatewayProcessor implements ProcessorInterface { From 072f460b24838cce519f6353e48322bb5fb94ec6 Mon Sep 17 00:00:00 2001 From: Rares <6453351+raress96@users.noreply.github.com> Date: Mon, 22 Jan 2024 16:30:59 +0200 Subject: [PATCH 22/33] Start working on special handling for its transactions. --- ...all-contract-approved.processor.service.ts | 10 + libs/common/src/assets/gas-service.abi.json | 223 +--- libs/common/src/assets/gateway.abi.json | 17 +- .../assets/interchain-token-service.abi.json | 1106 +++++++++++++++++ libs/common/src/config/api.config.service.ts | 9 + libs/common/src/contracts/contracts.module.ts | 13 + libs/common/src/contracts/its.contract.ts | 11 + 7 files changed, 1189 insertions(+), 200 deletions(-) create mode 100644 libs/common/src/assets/interchain-token-service.abi.json create mode 100644 libs/common/src/contracts/its.contract.ts diff --git a/apps/mvx-event-processor/src/call-contract-approved-processor/call-contract-approved.processor.service.ts b/apps/mvx-event-processor/src/call-contract-approved-processor/call-contract-approved.processor.service.ts index f57e0ae..9bbea44 100644 --- a/apps/mvx-event-processor/src/call-contract-approved-processor/call-contract-approved.processor.service.ts +++ b/apps/mvx-event-processor/src/call-contract-approved-processor/call-contract-approved.processor.service.ts @@ -16,6 +16,7 @@ import { import { ContractCallApproved, ContractCallApprovedStatus } from '@prisma/client'; import { TransactionsHelper } from '@mvx-monorepo/common/contracts/transactions.helper'; import { ApiConfigService } from '@mvx-monorepo/common'; +import { ItsContract } from '@mvx-monorepo/common/contracts/its.contract'; // Support a max of 3 retries (mainly because some Interchain Token Service endpoints need to be called 3 times) const MAX_NUMBER_OF_RETRIES: number = 3; @@ -25,15 +26,18 @@ export class CallContractApprovedProcessorService { private readonly logger: Logger; private readonly chainId: string; + private readonly contractItsAddress: string; constructor( private readonly contractCallApprovedRepository: ContractCallApprovedRepository, @Inject(ProviderKeys.WALLET_SIGNER) private readonly walletSigner: UserSigner, private readonly transactionsHelper: TransactionsHelper, + private readonly itsContract: ItsContract, apiConfigService: ApiConfigService, ) { this.logger = new Logger(CallContractApprovedProcessorService.name); this.chainId = apiConfigService.getChainId(); + this.contractItsAddress = apiConfigService.getContractIts(); } @Cron('*/30 * * * * *') @@ -96,6 +100,12 @@ export class CallContractApprovedProcessorService { contractCallApproved: ContractCallApproved, accountNonce: number, ): Promise { + if (contractCallApproved.contractAddress === this.contractItsAddress) { + await this. + + return; + } + const contract = new SmartContract({ address: new Address(contractCallApproved.contractAddress) }); const args = [ diff --git a/libs/common/src/assets/gas-service.abi.json b/libs/common/src/assets/gas-service.abi.json index f26dd93..19d8ad0 100644 --- a/libs/common/src/assets/gas-service.abi.json +++ b/libs/common/src/assets/gas-service.abi.json @@ -1,11 +1,11 @@ { "buildInfo": { "rustc": { - "version": "1.71.0-nightly", - "commitHash": "a2b1646c597329d0a25efa3889b66650f65de1de", - "commitDate": "2023-05-25", + "version": "1.76.0-nightly", + "commitHash": "d86d65bbc19b928387f68427fcc3a0da498d8a19", + "commitDate": "2023-12-10", "channel": "Nightly", - "short": "rustc 1.71.0-nightly (a2b1646c5 2023-05-25)" + "short": "rustc 1.76.0-nightly (d86d65bbc 2023-12-10)" }, "contractCrate": { "name": "gas-service", @@ -13,7 +13,7 @@ }, "framework": { "name": "multiversx-sc", - "version": "0.43.5" + "version": "0.46.1" } }, "name": "GasService", @@ -28,33 +28,13 @@ }, "endpoints": [ { - "name": "payGasForContractCall", + "name": "upgrade", "mutability": "mutable", - "payableInTokens": [ - "*" - ], - "inputs": [ - { - "name": "destination_chain", - "type": "bytes" - }, - { - "name": "destination_address", - "type": "bytes" - }, - { - "name": "payload", - "type": "bytes" - }, - { - "name": "refund_address", - "type": "Address" - } - ], + "inputs": [], "outputs": [] }, { - "name": "payGasForContractCallWithToken", + "name": "payGasForContractCall", "mutability": "mutable", "payableInTokens": [ "*" @@ -72,14 +52,6 @@ "name": "payload", "type": "bytes" }, - { - "name": "symbol", - "type": "bytes" - }, - { - "name": "amount", - "type": "BigUint" - }, { "name": "refund_address", "type": "Address" @@ -114,41 +86,7 @@ "outputs": [] }, { - "name": "payNativeGasForContractCallWithToken", - "mutability": "mutable", - "payableInTokens": [ - "EGLD" - ], - "inputs": [ - { - "name": "destination_chain", - "type": "bytes" - }, - { - "name": "destination_address", - "type": "bytes" - }, - { - "name": "payload", - "type": "bytes" - }, - { - "name": "symbol", - "type": "bytes" - }, - { - "name": "amount", - "type": "BigUint" - }, - { - "name": "refund_address", - "type": "Address" - } - ], - "outputs": [] - }, - { - "name": "payGasForExpressCallWithToken", + "name": "payGasForExpressCall", "mutability": "mutable", "payableInTokens": [ "*" @@ -166,14 +104,6 @@ "name": "payload", "type": "bytes" }, - { - "name": "symbol", - "type": "bytes" - }, - { - "name": "amount", - "type": "BigUint" - }, { "name": "refund_address", "type": "Address" @@ -182,7 +112,7 @@ "outputs": [] }, { - "name": "payNativeGasForExpressCallWithToken", + "name": "payNativeGasForExpressCall", "mutability": "mutable", "payableInTokens": [ "EGLD" @@ -200,14 +130,6 @@ "name": "payload", "type": "bytes" }, - { - "name": "symbol", - "type": "bytes" - }, - { - "name": "amount", - "type": "BigUint" - }, { "name": "refund_address", "type": "Address" @@ -322,7 +244,8 @@ "multi_arg": true } ], - "outputs": [] + "outputs": [], + "allow_multiple_var_args": true }, { "name": "refund", @@ -351,6 +274,17 @@ ], "outputs": [] }, + { + "name": "setGasCollector", + "mutability": "mutable", + "inputs": [ + { + "name": "gas_collector", + "type": "Address" + } + ], + "outputs": [] + }, { "name": "gas_collector", "mutability": "readonly", @@ -387,30 +321,6 @@ } ] }, - { - "identifier": "gas_paid_for_contract_call_with_token_event", - "inputs": [ - { - "name": "sender", - "type": "Address", - "indexed": true - }, - { - "name": "destination_chain", - "type": "bytes", - "indexed": true - }, - { - "name": "destination_contract_address", - "type": "bytes", - "indexed": true - }, - { - "name": "data", - "type": "GasPaidForContractCallWithTokenData" - } - ] - }, { "identifier": "native_gas_paid_for_contract_call_event", "inputs": [ @@ -436,31 +346,7 @@ ] }, { - "identifier": "native_gas_paid_for_contract_call_with_token_event", - "inputs": [ - { - "name": "sender", - "type": "Address", - "indexed": true - }, - { - "name": "destination_chain", - "type": "bytes", - "indexed": true - }, - { - "name": "destination_contract_address", - "type": "bytes", - "indexed": true - }, - { - "name": "data", - "type": "NativeGasPaidForContractCallWithTokenData" - } - ] - }, - { - "identifier": "gas_paid_for_express_call_with_token_event", + "identifier": "gas_paid_for_express_call", "inputs": [ { "name": "sender", @@ -479,12 +365,12 @@ }, { "name": "data", - "type": "GasPaidForContractCallWithTokenData" + "type": "GasPaidForContractCallData" } ] }, { - "identifier": "native_gas_paid_for_express_call_with_token_event", + "identifier": "native_gas_paid_for_express_call", "inputs": [ { "name": "sender", @@ -503,7 +389,7 @@ }, { "name": "data", - "type": "NativeGasPaidForContractCallWithTokenData" + "type": "NativeGasPaidForContractCallData" } ] }, @@ -603,6 +489,7 @@ ] } ], + "esdtAttributes": [], "hasCallback": false, "types": { "AddGasData": { @@ -656,35 +543,6 @@ } ] }, - "GasPaidForContractCallWithTokenData": { - "type": "struct", - "fields": [ - { - "name": "hash", - "type": "array32" - }, - { - "name": "symbol", - "type": "bytes" - }, - { - "name": "amount", - "type": "BigUint" - }, - { - "name": "gas_token", - "type": "TokenIdentifier" - }, - { - "name": "gas_fee_amount", - "type": "BigUint" - }, - { - "name": "refund_address", - "type": "Address" - } - ] - }, "NativeGasPaidForContractCallData": { "type": "struct", "fields": [ @@ -702,31 +560,6 @@ } ] }, - "NativeGasPaidForContractCallWithTokenData": { - "type": "struct", - "fields": [ - { - "name": "hash", - "type": "array32" - }, - { - "name": "symbol", - "type": "bytes" - }, - { - "name": "amount", - "type": "BigUint" - }, - { - "name": "value", - "type": "BigUint" - }, - { - "name": "refund_address", - "type": "Address" - } - ] - }, "RefundedData": { "type": "struct", "fields": [ diff --git a/libs/common/src/assets/gateway.abi.json b/libs/common/src/assets/gateway.abi.json index 8da5f98..712cb5d 100644 --- a/libs/common/src/assets/gateway.abi.json +++ b/libs/common/src/assets/gateway.abi.json @@ -1,11 +1,11 @@ { "buildInfo": { "rustc": { - "version": "1.71.0-nightly", - "commitHash": "a2b1646c597329d0a25efa3889b66650f65de1de", - "commitDate": "2023-05-25", + "version": "1.76.0-nightly", + "commitHash": "d86d65bbc19b928387f68427fcc3a0da498d8a19", + "commitDate": "2023-12-10", "channel": "Nightly", - "short": "rustc 1.71.0-nightly (a2b1646c5 2023-05-25)" + "short": "rustc 1.76.0-nightly (d86d65bbc 2023-12-10)" }, "contractCrate": { "name": "gateway", @@ -13,7 +13,7 @@ }, "framework": { "name": "multiversx-sc", - "version": "0.43.4" + "version": "0.46.1" } }, "name": "Gateway", @@ -31,6 +31,12 @@ "outputs": [] }, "endpoints": [ + { + "name": "upgrade", + "mutability": "mutable", + "inputs": [], + "outputs": [] + }, { "name": "callContract", "mutability": "mutable", @@ -240,6 +246,7 @@ ] } ], + "esdtAttributes": [], "hasCallback": false, "types": { "ContractCallData": { diff --git a/libs/common/src/assets/interchain-token-service.abi.json b/libs/common/src/assets/interchain-token-service.abi.json new file mode 100644 index 0000000..91f71e3 --- /dev/null +++ b/libs/common/src/assets/interchain-token-service.abi.json @@ -0,0 +1,1106 @@ +{ + "buildInfo": { + "rustc": { + "version": "1.76.0-nightly", + "commitHash": "d86d65bbc19b928387f68427fcc3a0da498d8a19", + "commitDate": "2023-12-10", + "channel": "Nightly", + "short": "rustc 1.76.0-nightly (d86d65bbc 2023-12-10)" + }, + "contractCrate": { + "name": "interchain-token-service", + "version": "0.0.0" + }, + "framework": { + "name": "multiversx-sc", + "version": "0.46.1" + } + }, + "name": "InterchainTokenServiceContract", + "constructor": { + "inputs": [ + { + "name": "gateway", + "type": "Address" + }, + { + "name": "gas_service", + "type": "Address" + }, + { + "name": "token_manager_implementation", + "type": "Address" + }, + { + "name": "operator", + "type": "Address" + }, + { + "name": "chain_name", + "type": "bytes" + }, + { + "name": "trusted_chain_names", + "type": "counted-variadic", + "multi_arg": true + }, + { + "name": "trusted_addresses", + "type": "counted-variadic", + "multi_arg": true + } + ], + "outputs": [] + }, + "endpoints": [ + { + "name": "upgrade", + "mutability": "mutable", + "inputs": [], + "outputs": [] + }, + { + "name": "setInterchainTokenFactory", + "onlyOwner": true, + "mutability": "mutable", + "inputs": [ + { + "name": "interchain_token_factory", + "type": "Address" + } + ], + "outputs": [] + }, + { + "docs": [ + "User Functions" + ], + "name": "deployTokenManager", + "mutability": "mutable", + "payableInTokens": [ + "EGLD" + ], + "inputs": [ + { + "name": "salt", + "type": "array32" + }, + { + "name": "destination_chain", + "type": "bytes" + }, + { + "name": "token_manager_type", + "type": "TokenManagerType" + }, + { + "name": "params", + "type": "bytes" + } + ], + "outputs": [ + { + "type": "array32" + } + ] + }, + { + "name": "deployInterchainToken", + "mutability": "mutable", + "payableInTokens": [ + "EGLD" + ], + "inputs": [ + { + "name": "salt", + "type": "array32" + }, + { + "name": "destination_chain", + "type": "bytes" + }, + { + "name": "name", + "type": "bytes" + }, + { + "name": "symbol", + "type": "bytes" + }, + { + "name": "decimals", + "type": "u8" + }, + { + "name": "minter", + "type": "bytes" + } + ], + "outputs": [ + { + "type": "array32" + } + ] + }, + { + "name": "expressExecute", + "mutability": "mutable", + "payableInTokens": [ + "*" + ], + "inputs": [ + { + "name": "command_id", + "type": "array32" + }, + { + "name": "source_chain", + "type": "bytes" + }, + { + "name": "source_address", + "type": "bytes" + }, + { + "name": "payload", + "type": "bytes" + } + ], + "outputs": [] + }, + { + "name": "interchainTransfer", + "mutability": "mutable", + "payableInTokens": [ + "*" + ], + "inputs": [ + { + "name": "token_id", + "type": "array32" + }, + { + "name": "destination_chain", + "type": "bytes" + }, + { + "name": "destination_address", + "type": "bytes" + }, + { + "name": "metadata", + "type": "bytes" + }, + { + "name": "gas_value", + "type": "BigUint" + } + ], + "outputs": [] + }, + { + "name": "callContractWithInterchainToken", + "mutability": "mutable", + "payableInTokens": [ + "*" + ], + "inputs": [ + { + "name": "token_id", + "type": "array32" + }, + { + "name": "destination_chain", + "type": "bytes" + }, + { + "name": "destination_address", + "type": "bytes" + }, + { + "name": "data", + "type": "bytes" + }, + { + "name": "gas_value", + "type": "BigUint" + } + ], + "outputs": [] + }, + { + "docs": [ + "Owner functions" + ], + "name": "setFlowLimits", + "mutability": "mutable", + "inputs": [ + { + "name": "token_ids", + "type": "counted-variadic>", + "multi_arg": true + }, + { + "name": "flow_limits", + "type": "counted-variadic", + "multi_arg": true + } + ], + "outputs": [], + "allow_multiple_var_args": true + }, + { + "docs": [ + "Internal Functions" + ], + "name": "execute", + "mutability": "mutable", + "payableInTokens": [ + "EGLD" + ], + "inputs": [ + { + "name": "command_id", + "type": "array32" + }, + { + "name": "source_chain", + "type": "bytes" + }, + { + "name": "source_address", + "type": "bytes" + }, + { + "name": "payload", + "type": "bytes" + } + ], + "outputs": [] + }, + { + "name": "interchainTokenId", + "mutability": "readonly", + "inputs": [ + { + "name": "sender", + "type": "Address" + }, + { + "name": "salt", + "type": "array32" + } + ], + "outputs": [ + { + "type": "array32" + } + ] + }, + { + "name": "contractCallValue", + "mutability": "readonly", + "inputs": [ + { + "name": "source_chain", + "type": "bytes" + }, + { + "name": "source_address", + "type": "bytes" + }, + { + "name": "payload", + "type": "bytes" + } + ], + "outputs": [ + { + "type": "EgldOrEsdtTokenIdentifier" + }, + { + "type": "BigUint" + } + ] + }, + { + "name": "interchainTokenFactory", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "Address" + } + ] + }, + { + "name": "chainNameHash", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "array32" + } + ] + }, + { + "name": "transferOperatorship", + "mutability": "mutable", + "inputs": [ + { + "name": "operator", + "type": "Address" + } + ], + "outputs": [] + }, + { + "name": "proposeOperatorship", + "mutability": "mutable", + "inputs": [ + { + "name": "operator", + "type": "Address" + } + ], + "outputs": [] + }, + { + "name": "acceptOperatorship", + "mutability": "mutable", + "inputs": [ + { + "name": "from_operator", + "type": "Address" + } + ], + "outputs": [] + }, + { + "name": "isOperator", + "mutability": "readonly", + "inputs": [ + { + "name": "address", + "type": "Address" + } + ], + "outputs": [ + { + "type": "bool" + } + ] + }, + { + "name": "getAccountRoles", + "mutability": "readonly", + "inputs": [ + { + "name": "address", + "type": "Address" + } + ], + "outputs": [ + { + "type": "u8" + } + ] + }, + { + "name": "getProposedRoles", + "mutability": "readonly", + "inputs": [ + { + "name": "from_address", + "type": "Address" + }, + { + "name": "to_address", + "type": "Address" + } + ], + "outputs": [ + { + "type": "u8" + } + ] + }, + { + "name": "setTrustedAddress", + "onlyOwner": true, + "mutability": "mutable", + "inputs": [ + { + "name": "chain", + "type": "bytes" + }, + { + "name": "address", + "type": "bytes" + } + ], + "outputs": [] + }, + { + "name": "removeTrustedAddress", + "onlyOwner": true, + "mutability": "mutable", + "inputs": [ + { + "name": "source_chain", + "type": "bytes" + } + ], + "outputs": [] + }, + { + "name": "chainName", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "bytes" + } + ] + }, + { + "name": "trustedAddress", + "mutability": "readonly", + "inputs": [ + { + "name": "chain_name", + "type": "bytes" + } + ], + "outputs": [ + { + "type": "bytes" + } + ] + }, + { + "name": "trustedAddressHash", + "mutability": "readonly", + "inputs": [ + { + "name": "chain_name", + "type": "bytes" + } + ], + "outputs": [ + { + "type": "array32" + } + ] + }, + { + "name": "flowLimit", + "mutability": "readonly", + "inputs": [ + { + "name": "token_id", + "type": "array32" + } + ], + "outputs": [ + { + "type": "BigUint" + } + ] + }, + { + "name": "flowOutAmount", + "mutability": "readonly", + "inputs": [ + { + "name": "token_id", + "type": "array32" + } + ], + "outputs": [ + { + "type": "BigUint" + } + ] + }, + { + "name": "flowInAmount", + "mutability": "readonly", + "inputs": [ + { + "name": "token_id", + "type": "array32" + } + ], + "outputs": [ + { + "type": "BigUint" + } + ] + }, + { + "name": "validTokenManagerAddress", + "mutability": "readonly", + "inputs": [ + { + "name": "token_id", + "type": "array32" + } + ], + "outputs": [ + { + "type": "Address" + } + ] + }, + { + "name": "validTokenIdentifier", + "mutability": "readonly", + "inputs": [ + { + "name": "token_id", + "type": "array32" + } + ], + "outputs": [ + { + "type": "EgldOrEsdtTokenIdentifier" + } + ] + }, + { + "name": "invalidTokenManagerAddress", + "mutability": "readonly", + "inputs": [ + { + "name": "token_id", + "type": "array32" + } + ], + "outputs": [ + { + "type": "Address" + } + ] + }, + { + "name": "gateway", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "Address" + } + ] + }, + { + "name": "gasService", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "Address" + } + ] + }, + { + "name": "tokenManagerAddress", + "mutability": "readonly", + "inputs": [ + { + "name": "token_id", + "type": "array32" + } + ], + "outputs": [ + { + "type": "Address" + } + ] + }, + { + "name": "tokenManagerImplementation", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "Address" + } + ] + }, + { + "name": "pause", + "onlyOwner": true, + "mutability": "mutable", + "inputs": [], + "outputs": [] + }, + { + "name": "unpause", + "onlyOwner": true, + "mutability": "mutable", + "inputs": [], + "outputs": [] + }, + { + "name": "isPaused", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "bool" + } + ] + } + ], + "events": [ + { + "identifier": "roles_proposed_event", + "inputs": [ + { + "name": "from_address", + "type": "Address", + "indexed": true + }, + { + "name": "to_address", + "type": "Address", + "indexed": true + }, + { + "name": "roles", + "type": "u8" + } + ] + }, + { + "identifier": "roles_added_event", + "inputs": [ + { + "name": "address", + "type": "Address", + "indexed": true + }, + { + "name": "roles", + "type": "u8" + } + ] + }, + { + "identifier": "roles_removed_event", + "inputs": [ + { + "name": "address", + "type": "Address", + "indexed": true + }, + { + "name": "roles", + "type": "u8" + } + ] + }, + { + "identifier": "trusted_address_added_event", + "inputs": [ + { + "name": "source_chain", + "type": "bytes", + "indexed": true + }, + { + "name": "source_address", + "type": "bytes" + } + ] + }, + { + "identifier": "trusted_address_removed_event", + "inputs": [ + { + "name": "source_chain", + "type": "bytes", + "indexed": true + } + ] + }, + { + "identifier": "token_manager_deployed_event", + "inputs": [ + { + "name": "token_id", + "type": "array32", + "indexed": true + }, + { + "name": "data", + "type": "TokenManagerDeployedEventData" + } + ] + }, + { + "identifier": "interchain_token_deployment_started_event", + "inputs": [ + { + "name": "token_id", + "type": "array32", + "indexed": true + }, + { + "name": "data", + "type": "InterchainTokenDeploymentStartedEventData" + } + ] + }, + { + "identifier": "interchain_token_id_claimed_event", + "inputs": [ + { + "name": "token_id", + "type": "array32", + "indexed": true + }, + { + "name": "deployer", + "type": "Address", + "indexed": true + }, + { + "name": "data", + "type": "array32" + } + ] + }, + { + "identifier": "token_manager_deployment_started_event", + "inputs": [ + { + "name": "token_id", + "type": "array32", + "indexed": true + }, + { + "name": "data", + "type": "TokenManagerDeploymentStartedEventData" + } + ] + }, + { + "identifier": "standardized_token_deployed_event", + "inputs": [ + { + "name": "token_id", + "type": "array32", + "indexed": true + }, + { + "name": "minter", + "type": "Address", + "indexed": true + }, + { + "name": "data", + "type": "StandardizedTokenDeployedEventData" + } + ] + }, + { + "identifier": "interchain_transfer_event", + "inputs": [ + { + "name": "token_id", + "type": "array32", + "indexed": true + }, + { + "name": "source_address", + "type": "Address", + "indexed": true + }, + { + "name": "data_hash", + "type": "array32", + "indexed": true + }, + { + "name": "data", + "type": "InterchainTransferEventData" + } + ] + }, + { + "identifier": "interchain_transfer_received_event", + "inputs": [ + { + "name": "command_id", + "type": "array32", + "indexed": true + }, + { + "name": "token_id", + "type": "array32", + "indexed": true + }, + { + "name": "source_chain", + "type": "bytes", + "indexed": true + }, + { + "name": "source_address", + "type": "bytes", + "indexed": true + }, + { + "name": "destination_address", + "type": "Address", + "indexed": true + }, + { + "name": "data_hash", + "type": "array32", + "indexed": true + }, + { + "name": "amount", + "type": "BigUint" + } + ] + }, + { + "identifier": "execute_with_interchain_token_success_event", + "inputs": [ + { + "name": "command_id", + "type": "array32", + "indexed": true + } + ] + }, + { + "identifier": "execute_with_interchain_token_failed_event", + "inputs": [ + { + "name": "command_id", + "type": "array32", + "indexed": true + } + ] + }, + { + "identifier": "express_executed_event", + "inputs": [ + { + "name": "command_id", + "type": "array32", + "indexed": true + }, + { + "name": "source_chain", + "type": "bytes", + "indexed": true + }, + { + "name": "source_address", + "type": "bytes", + "indexed": true + }, + { + "name": "payload_hash", + "type": "array32", + "indexed": true + }, + { + "name": "express_executor", + "type": "Address" + } + ] + }, + { + "identifier": "express_execution_fulfilled_event", + "inputs": [ + { + "name": "command_id", + "type": "array32", + "indexed": true + }, + { + "name": "source_chain", + "type": "bytes", + "indexed": true + }, + { + "name": "source_address", + "type": "bytes", + "indexed": true + }, + { + "name": "payload_hash", + "type": "array32", + "indexed": true + }, + { + "name": "express_executor", + "type": "Address" + } + ] + }, + { + "identifier": "express_execute_with_interchain_token_success_event", + "inputs": [ + { + "name": "command_id", + "type": "array32", + "indexed": true + }, + { + "name": "express_executor", + "type": "Address", + "indexed": true + } + ] + }, + { + "identifier": "express_execute_with_interchain_token_failed_event", + "inputs": [ + { + "name": "command_id", + "type": "array32", + "indexed": true + }, + { + "name": "express_executor", + "type": "Address", + "indexed": true + } + ] + } + ], + "esdtAttributes": [], + "hasCallback": true, + "types": { + "InterchainTokenDeploymentStartedEventData": { + "type": "struct", + "fields": [ + { + "name": "name", + "type": "bytes" + }, + { + "name": "symbol", + "type": "bytes" + }, + { + "name": "decimals", + "type": "u8" + }, + { + "name": "minter", + "type": "bytes" + }, + { + "name": "destination_chain", + "type": "bytes" + } + ] + }, + "InterchainTransferEventData": { + "type": "struct", + "fields": [ + { + "name": "destination_chain", + "type": "bytes" + }, + { + "name": "destination_address", + "type": "bytes" + }, + { + "name": "amount", + "type": "BigUint" + } + ] + }, + "StandardizedTokenDeployedEventData": { + "type": "struct", + "fields": [ + { + "name": "name", + "type": "bytes" + }, + { + "name": "symbol", + "type": "bytes" + }, + { + "name": "decimals", + "type": "u8" + }, + { + "name": "mint_amount", + "type": "BigUint" + }, + { + "name": "mint_to", + "type": "Address" + } + ] + }, + "TokenManagerDeployedEventData": { + "type": "struct", + "fields": [ + { + "name": "token_manager", + "type": "Address" + }, + { + "name": "token_manager_type", + "type": "TokenManagerType" + }, + { + "name": "params", + "type": "bytes" + } + ] + }, + "TokenManagerDeploymentStartedEventData": { + "type": "struct", + "fields": [ + { + "name": "destination_chain", + "type": "bytes" + }, + { + "name": "token_manager_type", + "type": "TokenManagerType" + }, + { + "name": "params", + "type": "bytes" + } + ] + }, + "TokenManagerType": { + "type": "enum", + "variants": [ + { + "name": "MintBurn", + "discriminant": 0 + }, + { + "name": "MintBurnFrom", + "discriminant": 1 + }, + { + "name": "LockUnlock", + "discriminant": 2 + }, + { + "name": "LockUnlockFee", + "discriminant": 3 + } + ] + } + } +} diff --git a/libs/common/src/config/api.config.service.ts b/libs/common/src/config/api.config.service.ts index cf08252..db26ef6 100644 --- a/libs/common/src/config/api.config.service.ts +++ b/libs/common/src/config/api.config.service.ts @@ -76,6 +76,15 @@ export class ApiConfigService { return contractGasService; } + getContractIts(): string { + const contractIts = this.configService.get('CONTRACT_ITS'); + if (!contractIts) { + throw new Error('No Contract ITS present'); + } + + return contractIts; + } + getContractWegldSwap(): string { const contractWegldSwap = this.configService.get('CONTRACT_WEGLD_SWAP'); if (!contractWegldSwap) { diff --git a/libs/common/src/contracts/contracts.module.ts b/libs/common/src/contracts/contracts.module.ts index 6673cc0..be02786 100644 --- a/libs/common/src/contracts/contracts.module.ts +++ b/libs/common/src/contracts/contracts.module.ts @@ -11,6 +11,7 @@ import { TransactionsHelper } from '@mvx-monorepo/common/contracts/transactions. import { WegldSwapContract } from '@mvx-monorepo/common/contracts/wegld-swap.contract'; import { ApiConfigService } from '@mvx-monorepo/common/config'; import { DynamicModuleUtils } from '@mvx-monorepo/common/utils'; +import { ItsContract } from '@mvx-monorepo/common/contracts/its.contract'; @Module({ imports: [DynamicModuleUtils.getCacheModule()], @@ -76,6 +77,18 @@ import { DynamicModuleUtils } from '@mvx-monorepo/common/utils'; }, inject: [ApiConfigService, ResultsParser], }, + { + provide: ItsContract, + useFactory: async (apiConfigService: ApiConfigService, resultsParser: ResultsParser) => { + const contractLoader = new ContractLoader(join(__dirname, '../assets/interchain-token-service.abi.json')); + + const smartContract = await contractLoader.getContract(apiConfigService.getContractIts()); + const abi = await contractLoader.getAbiRegistry(apiConfigService.getContractIts()); + + return new ItsContract(smartContract, abi, resultsParser); + }, + inject: [ApiConfigService, ResultsParser], + }, { provide: WegldSwapContract, useFactory: async ( diff --git a/libs/common/src/contracts/its.contract.ts b/libs/common/src/contracts/its.contract.ts new file mode 100644 index 0000000..c48b8be --- /dev/null +++ b/libs/common/src/contracts/its.contract.ts @@ -0,0 +1,11 @@ +import { AbiRegistry, IAddress, ResultsParser, SmartContract } from '@multiversx/sdk-core/out'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class ItsContract { + constructor( + private readonly smartContract: SmartContract, + private readonly _abi: AbiRegistry, + private readonly _resultsParser: ResultsParser, + ) {} +} From 36eede3c18fe0cc2c940cccac7d8fcff84b1cbe8 Mon Sep 17 00:00:00 2001 From: Rares <6453351+raress96@users.noreply.github.com> Date: Tue, 23 Jan 2024 12:42:17 +0200 Subject: [PATCH 23/33] Handle special its transactions that need to send egld. --- .env.test | 1 + ...all-contract-approved.processor.service.ts | 64 ++++--- .../src/processors/gateway.processor.spec.ts | 8 +- .../src/processors/gateway.processor.ts | 3 +- ...ll-contract-approved.processor.e2e-spec.ts | 170 +++++++++++++++++- libs/common/src/contracts/contracts.module.ts | 8 +- libs/common/src/contracts/its.contract.ts | 43 ++++- .../src/contracts/transactions.helper.ts | 4 + .../contract-call-approved.repository.ts | 3 +- package-lock.json | 163 +++++++++++++++++ package.json | 1 + .../migration.sql | 2 + prisma/schema.prisma | 1 + 13 files changed, 429 insertions(+), 42 deletions(-) create mode 100644 prisma/migrations/20240123071151_add_success_times_to_contract_call_approved/migration.sql diff --git a/.env.test b/.env.test index e72f536..bb93c02 100644 --- a/.env.test +++ b/.env.test @@ -9,6 +9,7 @@ EVENTS_NOTIFIER_QUEUE=events-2cf3b817 CONTRACT_GATEWAY=erd1qqqqqqqqqqqqqpgqvc7gdl0p4s97guh498wgz75k8sav6sjfjlwqh679jy CONTRACT_GAS_SERVICE=erd1qqqqqqqqqqqqqpgqhe8t5jewej70zupmh44jurgn29psua5l2jps3ntjj3 +CONTRACT_ITS=erd1qqqqqqqqqqqqqpgq97wezxw6l7lgg7k9rxvycrz66vn92ksh2tssxwf7ep CONTRACT_WEGLD_SWAP=erd1qqqqqqqqqqqqqpgqhe8t5jewej70zupmh44jurgn29psua5l2jps3ntjj3 diff --git a/apps/mvx-event-processor/src/call-contract-approved-processor/call-contract-approved.processor.service.ts b/apps/mvx-event-processor/src/call-contract-approved-processor/call-contract-approved.processor.service.ts index 9bbea44..eff9f7a 100644 --- a/apps/mvx-event-processor/src/call-contract-approved-processor/call-contract-approved.processor.service.ts +++ b/apps/mvx-event-processor/src/call-contract-approved-processor/call-contract-approved.processor.service.ts @@ -18,7 +18,7 @@ import { TransactionsHelper } from '@mvx-monorepo/common/contracts/transactions. import { ApiConfigService } from '@mvx-monorepo/common'; import { ItsContract } from '@mvx-monorepo/common/contracts/its.contract'; -// Support a max of 3 retries (mainly because some Interchain Token Service endpoints need to be called 3 times) +// Support a max of 3 retries (mainly because some Interchain Token Service endpoints need to be called 2 times) const MAX_NUMBER_OF_RETRIES: number = 3; @Injectable() @@ -86,9 +86,10 @@ export class CallContractApprovedProcessorService { if (result) { // Page is not modified if database records are updated - await this.contractCallApprovedRepository.updateManyStatusRetryExecuteTxHash(entries); + await this.contractCallApprovedRepository.updateManyPartial(entries); } else { - accountNonce = null; // re-retrieve account nonce + // re-retrieve account nonce in case sendTransactions failed because of nonce error + accountNonce = null; page++; } @@ -100,34 +101,15 @@ export class CallContractApprovedProcessorService { contractCallApproved: ContractCallApproved, accountNonce: number, ): Promise { - if (contractCallApproved.contractAddress === this.contractItsAddress) { - await this. - - return; - } - - const contract = new SmartContract({ address: new Address(contractCallApproved.contractAddress) }); - - const args = [ - new BytesValue(Buffer.from(contractCallApproved.commandId, 'hex')), - new StringValue(contractCallApproved.sourceChain), - new StringValue(contractCallApproved.sourceAddress), - new BytesValue(contractCallApproved.payload), - ]; - - const interaction = new Interaction(contract, new ContractFunction('execute'), args); + const interaction = await this.buildExecuteInteraction(contractCallApproved); const transaction = interaction .withSender(this.walletSigner.getAddress()) .withNonce(accountNonce) - // .withValue() // TODO: Handle ITS transactions where EGLD value needs to be sent for deploying ESDT token .withChainID(this.chainId) .buildTransaction(); - const gas = await this.transactionsHelper.getTransactionGas( - transaction, - contractCallApproved.retry, - ); + const gas = await this.transactionsHelper.getTransactionGas(transaction, contractCallApproved.retry); transaction.setGasLimit(gas); const signature = await this.walletSigner.sign(transaction.serializeForSigning()); @@ -135,4 +117,38 @@ export class CallContractApprovedProcessorService { return transaction; } + + private async buildExecuteInteraction(contractCallApproved: ContractCallApproved) { + const commandId = Buffer.from(contractCallApproved.commandId, 'hex'); + + if (contractCallApproved.contractAddress !== this.contractItsAddress) { + const contract = new SmartContract({ address: new Address(contractCallApproved.contractAddress) }); + + const args = [ + new BytesValue(commandId), + new StringValue(contractCallApproved.sourceChain), + new StringValue(contractCallApproved.sourceAddress), + new BytesValue(contractCallApproved.payload), + ]; + + return new Interaction(contract, new ContractFunction('execute'), args); + } + + // In case first transaction exists for ITS, wait for it to complete and mark it as successful if necessary + if (contractCallApproved.executeTxHash && !contractCallApproved.successTimes) { + const success = await this.transactionsHelper.awaitComplete(contractCallApproved.executeTxHash); + + if (success) { + contractCallApproved.successTimes = 1; + } + } + + return this.itsContract.execute( + commandId, + contractCallApproved.sourceChain, + contractCallApproved.sourceAddress, + contractCallApproved.payload, + contractCallApproved.successTimes || 0, + ); + } } diff --git a/apps/mvx-event-processor/src/processors/gateway.processor.spec.ts b/apps/mvx-event-processor/src/processors/gateway.processor.spec.ts index d4a53f6..61256a1 100644 --- a/apps/mvx-event-processor/src/processors/gateway.processor.spec.ts +++ b/apps/mvx-event-processor/src/processors/gateway.processor.spec.ts @@ -242,6 +242,7 @@ describe('ContractCallProcessor', () => { executeTxHash: null, updatedAt: new Date(), createdAt: new Date(), + successTimes: null, }; contractCallApprovedRepository.findByCommandId.mockReturnValueOnce(Promise.resolve(contractCallApproved)); @@ -256,11 +257,12 @@ describe('ContractCallProcessor', () => { expect(contractCallApprovedRepository.findByCommandId).toHaveBeenCalledWith( '0c38359b7a35c755573659d797afec315bb0e51374a056745abd9764715a15da', ); - expect(contractCallApprovedRepository.updateManyStatusRetryExecuteTxHash).toHaveBeenCalledTimes(1); - expect(contractCallApprovedRepository.updateManyStatusRetryExecuteTxHash).toHaveBeenCalledWith([ + expect(contractCallApprovedRepository.updateManyPartial).toHaveBeenCalledTimes(1); + expect(contractCallApprovedRepository.updateManyPartial).toHaveBeenCalledWith([ { ...contractCallApproved, status: ContractCallApprovedStatus.SUCCESS, + successTimes: 1, }, ]); }); @@ -278,7 +280,7 @@ describe('ContractCallProcessor', () => { expect(contractCallApprovedRepository.findByCommandId).toHaveBeenCalledWith( '0c38359b7a35c755573659d797afec315bb0e51374a056745abd9764715a15da', ); - expect(contractCallApprovedRepository.updateManyStatusRetryExecuteTxHash).not.toHaveBeenCalled(); + expect(contractCallApprovedRepository.updateManyPartial).not.toHaveBeenCalled(); }); }); }); diff --git a/apps/mvx-event-processor/src/processors/gateway.processor.ts b/apps/mvx-event-processor/src/processors/gateway.processor.ts index 3946b0f..f4f4837 100644 --- a/apps/mvx-event-processor/src/processors/gateway.processor.ts +++ b/apps/mvx-event-processor/src/processors/gateway.processor.ts @@ -112,7 +112,8 @@ export class GatewayProcessor implements ProcessorInterface { } contractCallApproved.status = ContractCallApprovedStatus.SUCCESS; + contractCallApproved.successTimes = (contractCallApproved.successTimes || 0) + 1; - await this.contractCallApprovedRepository.updateManyStatusRetryExecuteTxHash([contractCallApproved]); + await this.contractCallApprovedRepository.updateManyPartial([contractCallApproved]); } } diff --git a/apps/mvx-event-processor/test/call-contract-approved.processor.e2e-spec.ts b/apps/mvx-event-processor/test/call-contract-approved.processor.e2e-spec.ts index 203ef0d..05926dc 100644 --- a/apps/mvx-event-processor/test/call-contract-approved.processor.e2e-spec.ts +++ b/apps/mvx-event-processor/test/call-contract-approved.processor.e2e-spec.ts @@ -1,6 +1,6 @@ import { Test } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; -import { AccountOnNetwork, ProxyNetworkProvider } from '@multiversx/sdk-network-providers/out'; +import { AccountOnNetwork, ProxyNetworkProvider, TransactionStatus } from '@multiversx/sdk-network-providers/out'; import { CallContractApprovedProcessorModule, CallContractApprovedProcessorService, @@ -11,14 +11,16 @@ import { CacheService } from '@multiversx/sdk-nestjs-cache'; import { CacheInfo } from '@mvx-monorepo/common'; import { ContractCallApproved, ContractCallApprovedStatus } from '@prisma/client'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { Transaction } from '@multiversx/sdk-core/out'; +import { Transaction, TransactionWatcher } from '@multiversx/sdk-core/out'; import { BinaryUtils } from '@multiversx/sdk-nestjs-common'; +import { AbiCoder } from 'ethers'; const WALLET_SIGNER_ADDRESS = 'erd1fsk0cnaag2m78gunfddsvg0y042rf0maxxgz6kvm32kxcl25m0yq8s38vt'; describe('CallContractApprovedProcessorService', () => { let cacheService: CacheService; let proxy: DeepMocked; + let transactionWatcher: DeepMocked; let prisma: PrismaService; let contractCallApprovedRepository: ContractCallApprovedRepository; @@ -32,12 +34,15 @@ describe('CallContractApprovedProcessorService', () => { beforeEach(async () => { proxy = createMock(); + transactionWatcher = createMock(); const moduleRef = await Test.createTestingModule({ imports: [CallContractApprovedProcessorModule], }) .overrideProvider(ProxyNetworkProvider) .useValue(proxy) + .overrideProvider(TransactionWatcher) + .useValue(transactionWatcher) .compile(); cacheService = await moduleRef.get(CacheService); @@ -186,7 +191,7 @@ describe('CallContractApprovedProcessorService', () => { retry: 3, updatedAt: new Date(new Date().getTime() - 60_500), }); - // Entry will not be processed (updated to early) + // Entry will not be processed (updated too early) const originalThirdEntry = await createContractCallApproved({ commandId: '0c38359b7a35c755573659d797afec315bb0e51374a056745abd9764715a15cc', txHash: 'txHashC', @@ -234,4 +239,163 @@ describe('CallContractApprovedProcessorService', () => { ...originalThirdEntry, }); }); + + describe('ITS execute', () => { + const contractAddress = 'erd1qqqqqqqqqqqqqpgq97wezxw6l7lgg7k9rxvycrz66vn92ksh2tssxwf7ep'; + + it('Should send execute transaction one deploy interchain token one other', async () => { + const originalItsExecuteOther = await createContractCallApproved({ + contractAddress, + payload: Buffer.from(AbiCoder.defaultAbiCoder().encode(['uint256'], [0]).substring(2), 'hex'), + }); + const originalItsExecute = await createContractCallApproved({ + contractAddress, + commandId: '0c38359b7a35c755573659d797afec315bb0e51374a056745abd9764715a15bb', + txHash: 'txHashB', + sourceChain: 'polygon', + sourceAddress: 'otherSourceAddress', + payload: Buffer.from(AbiCoder.defaultAbiCoder().encode(['uint256'], [1]).substring(2), 'hex'), + }); + + await service.processPendingContractCallApproved(); + + expect(proxy.getAccount).toHaveBeenCalledTimes(1); + expect(proxy.doPostGeneric).toHaveBeenCalledTimes(2); + expect(proxy.sendTransactions).toHaveBeenCalledTimes(1); + + // Assert transactions data is correct + const transactions = proxy.sendTransactions.mock.lastCall?.[0] as Transaction[]; + expect(transactions).toHaveLength(2); + + expect(transactions[0].getGasLimit()).toBe(11_000_000); // 10% over 10_000_000 + expect(transactions[0].getNonce()).toBe(1); + expect(transactions[0].getChainID()).toBe('test'); + expect(transactions[0].getSender().bech32()).toBe(WALLET_SIGNER_ADDRESS); + assertArgs(transactions[0], originalItsExecuteOther); + expect(transactions[0].getValue()).toBe('0'); // assert sent with value 0 + + expect(transactions[1].getGasLimit()).toBe(11_000_000); + expect(transactions[1].getNonce()).toBe(2); + expect(transactions[1].getChainID()).toBe('test'); + expect(transactions[1].getSender().bech32()).toBe(WALLET_SIGNER_ADDRESS); + assertArgs(transactions[1], originalItsExecute); + expect(transactions[1].getValue()).toBe('0'); // assert sent with value 0 + + // No contract call approved pending + expect(await contractCallApprovedRepository.findPending()).toEqual([]); + + // Expect entries in database updated + const itsExecuteOther = await contractCallApprovedRepository.findByCommandId(originalItsExecuteOther.commandId); + expect(itsExecuteOther).toEqual({ + ...originalItsExecuteOther, + retry: 1, + executeTxHash: '2795b8489921528a63a317ab6241e2b63f42fba3ac7f3821a524d771a55c2f1b', + updatedAt: expect.any(Date), + successTimes: null, + }); + + const itsExecute = await contractCallApprovedRepository.findByCommandId(originalItsExecute.commandId); + expect(itsExecute).toEqual({ + ...originalItsExecute, + retry: 1, + executeTxHash: '9206c0fad5d91eef0802311b2baea2d6c91677da8a2fa6cc8ebc2d4a7c5892b4', + updatedAt: expect.any(Date), + successTimes: null, + }); + }); + + it('Should send execute transaction deploy interchain token 2 times', async () => { + const originalItsExecute = await createContractCallApproved({ + contractAddress, + commandId: '0c38359b7a35c755573659d797afec315bb0e51374a056745abd9764715a15bb', + txHash: 'txHashB', + sourceChain: 'polygon', + sourceAddress: 'otherSourceAddress', + payload: Buffer.from(AbiCoder.defaultAbiCoder().encode(['uint256'], [1]).substring(2), 'hex'), + }); + + await service.processPendingContractCallApproved(); + + expect(proxy.getAccount).toHaveBeenCalledTimes(1); + expect(proxy.doPostGeneric).toHaveBeenCalledTimes(1); + expect(proxy.sendTransactions).toHaveBeenCalledTimes(1); + + // Assert transactions data is correct + let transactions = proxy.sendTransactions.mock.lastCall?.[0] as Transaction[]; + expect(transactions).toHaveLength(1); + + expect(transactions[0].getGasLimit()).toBe(11_000_000); + expect(transactions[0].getNonce()).toBe(1); + expect(transactions[0].getChainID()).toBe('test'); + expect(transactions[0].getSender().bech32()).toBe(WALLET_SIGNER_ADDRESS); + assertArgs(transactions[0], originalItsExecute); + expect(transactions[0].getValue()).toBe('0'); // assert sent with no value 1st time + + // No contract call approved pending + expect(await contractCallApprovedRepository.findPending()).toEqual([]); + + // @ts-ignore + let itsExecute: ContractCallApproved = await contractCallApprovedRepository.findByCommandId( + originalItsExecute.commandId, + ); + expect(itsExecute).toEqual({ + ...originalItsExecute, + retry: 1, + executeTxHash: 'af0848face1fa76874752bbc9fab1928b33e08ff646471cab3d0fa91a6506a51', + updatedAt: expect.any(Date), + successTimes: null, + }); + + // Mark as last updated more than 1 minute ago + itsExecute.updatedAt = new Date(new Date().getTime() - 60_500); + await prisma.contractCallApproved.update({ where: { commandId: itsExecute.commandId }, data: itsExecute }); + + // Mock 1st transaction executed successfully + transactionWatcher.awaitCompleted.mockReturnValueOnce( + // @ts-ignore + Promise.resolve({ + ...transactions[0], + status: new TransactionStatus('success'), + }), + ); + + // Process transaction 2nd time + await service.processPendingContractCallApproved(); + + transactions = proxy.sendTransactions.mock.lastCall?.[0] as Transaction[]; + expect(transactions).toHaveLength(1); + expect(transactions[0].getValue()).toBe('50000000000000000'); // assert sent with value 2nd time + + // @ts-ignore + itsExecute = await contractCallApprovedRepository.findByCommandId(originalItsExecute.commandId); + expect(itsExecute).toEqual({ + ...originalItsExecute, + retry: 2, + executeTxHash: '36a71e24554303f6b734143ad90f939b57018f8c05f8abaa63e23950f899ce56', + updatedAt: expect.any(Date), + successTimes: 1, + }); + + // Mark as last updated more than 1 minute ago + itsExecute.updatedAt = new Date(new Date().getTime() - 60_500); + await prisma.contractCallApproved.update({ where: { commandId: itsExecute.commandId }, data: itsExecute }); + + // Process transaction 3rd time will retry + await service.processPendingContractCallApproved(); + + transactions = proxy.sendTransactions.mock.lastCall?.[0] as Transaction[]; + expect(transactions).toHaveLength(1); + expect(transactions[0].getValue()).toBe('50000000000000000'); // assert sent with value + + // @ts-ignore + itsExecute = await contractCallApprovedRepository.findByCommandId(originalItsExecute.commandId); + expect(itsExecute).toEqual({ + ...originalItsExecute, + retry: 3, + executeTxHash: 'e072d88e869e51a261e4a48aea1abb6f62a1f69c8af6fc3740d26e57b5e0a2bb', + updatedAt: expect.any(Date), + successTimes: 1, + }); + }); + }); }); diff --git a/libs/common/src/contracts/contracts.module.ts b/libs/common/src/contracts/contracts.module.ts index be02786..cd3a2b4 100644 --- a/libs/common/src/contracts/contracts.module.ts +++ b/libs/common/src/contracts/contracts.module.ts @@ -79,15 +79,14 @@ import { ItsContract } from '@mvx-monorepo/common/contracts/its.contract'; }, { provide: ItsContract, - useFactory: async (apiConfigService: ApiConfigService, resultsParser: ResultsParser) => { + useFactory: async (apiConfigService: ApiConfigService) => { const contractLoader = new ContractLoader(join(__dirname, '../assets/interchain-token-service.abi.json')); const smartContract = await contractLoader.getContract(apiConfigService.getContractIts()); - const abi = await contractLoader.getAbiRegistry(apiConfigService.getContractIts()); - return new ItsContract(smartContract, abi, resultsParser); + return new ItsContract(smartContract); }, - inject: [ApiConfigService, ResultsParser], + inject: [ApiConfigService], }, { provide: WegldSwapContract, @@ -118,6 +117,7 @@ import { ItsContract } from '@mvx-monorepo/common/contracts/its.contract'; exports: [ GatewayContract, GasServiceContract, + ItsContract, WegldSwapContract, ProviderKeys.WALLET_SIGNER, ProxyNetworkProvider, diff --git a/libs/common/src/contracts/its.contract.ts b/libs/common/src/contracts/its.contract.ts index c48b8be..caf2787 100644 --- a/libs/common/src/contracts/its.contract.ts +++ b/libs/common/src/contracts/its.contract.ts @@ -1,11 +1,42 @@ -import { AbiRegistry, IAddress, ResultsParser, SmartContract } from '@multiversx/sdk-core/out'; +import { BytesValue, Interaction, SmartContract, TokenTransfer } from '@multiversx/sdk-core/out'; import { Injectable } from '@nestjs/common'; +import { AbiCoder } from 'ethers'; + +const MESSAGE_TYPE_DEPLOY_INTERCHAIN_TOKEN = 1; + +const DEFAULT_ESDT_ISSUE_COST = 50000000000000000; // 0.05 EGLD @Injectable() export class ItsContract { - constructor( - private readonly smartContract: SmartContract, - private readonly _abi: AbiRegistry, - private readonly _resultsParser: ResultsParser, - ) {} + constructor(private readonly smartContract: SmartContract) {} + + execute( + commandId: Buffer, + sourceChain: string, + sourceAddress: string, + payload: Buffer, + executedTimes: number, + ): Interaction { + const messageType = this.decodeExecutePayloadMessageType(payload); + + const interaction = this.smartContract.methods.execute([ + new BytesValue(commandId), + sourceChain, + sourceAddress, + payload, + ]); + + // The second time this transaction is executed it needs to contain and EGLD transfer for issuing ESDT + if (messageType === MESSAGE_TYPE_DEPLOY_INTERCHAIN_TOKEN && executedTimes === 1) { + interaction.withValue(TokenTransfer.egldFromBigInteger(DEFAULT_ESDT_ISSUE_COST)); + } + + return interaction; + } + + private decodeExecutePayloadMessageType(payload: Buffer): number { + const result = AbiCoder.defaultAbiCoder().decode(['uint256'], payload); + + return Number(result[0]); + } } diff --git a/libs/common/src/contracts/transactions.helper.ts b/libs/common/src/contracts/transactions.helper.ts index b385706..e0f879a 100644 --- a/libs/common/src/contracts/transactions.helper.ts +++ b/libs/common/src/contracts/transactions.helper.ts @@ -63,6 +63,10 @@ export class TransactionsHelper { } async sendTransactions(transactions: Transaction[]) { + if (!transactions.length) { + return true; + } + try { await this.proxy.sendTransactions(transactions); diff --git a/libs/common/src/database/repository/contract-call-approved.repository.ts b/libs/common/src/database/repository/contract-call-approved.repository.ts index 43eae7b..24800b5 100644 --- a/libs/common/src/database/repository/contract-call-approved.repository.ts +++ b/libs/common/src/database/repository/contract-call-approved.repository.ts @@ -45,7 +45,7 @@ export class ContractCallApprovedRepository { }); } - async updateManyStatusRetryExecuteTxHash(entries: ContractCallApproved[]) { + async updateManyPartial(entries: ContractCallApproved[]) { await this.prisma.$transaction( entries.map((data) => { return this.prisma.contractCallApproved.update({ @@ -56,6 +56,7 @@ export class ContractCallApprovedRepository { status: data.status, retry: data.retry, executeTxHash: data.executeTxHash, + successTimes: data.successTimes, }, }); }), diff --git a/package-lock.json b/package-lock.json index a669a56..9877bc2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,6 +33,7 @@ "bull": "^4.10.4", "cache-manager": "^5.2.1", "cron": "^3.1.6", + "ethers": "^6.10.0", "ioredis": "^5.3.2", "module-alias": "^2.2.3", "nest-winston": "^1.9.2", @@ -80,6 +81,11 @@ "node": ">=0.10.0" } }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.0.tgz", + "integrity": "sha512-nA9XHtlAkYfJxY7bce8DcN7eKxWWCWkU+1GR9d+U6MbNpfwQp8TI7vqOsBsMcHoT4mBu2kypKoSKnghEzOOq5Q==" + }, "node_modules/@ampproject/remapping": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", @@ -2696,6 +2702,28 @@ "optional": true, "peer": true }, + "node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves/node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@noble/ed25519": { "version": "1.7.3", "resolved": "https://registry.npmjs.org/@noble/ed25519/-/ed25519-1.7.3.tgz", @@ -3546,6 +3574,11 @@ "node": ">=0.4.0" } }, + "node_modules/aes-js": { + "version": "4.0.0-beta.5", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", + "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==" + }, "node_modules/agentkeepalive": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", @@ -5530,6 +5563,74 @@ "node": ">= 0.6" } }, + "node_modules/ethers": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.10.0.tgz", + "integrity": "sha512-nMNwYHzs6V1FR3Y4cdfxSQmNgZsRj1RiTU25JwvnJLmyzw9z3SKxNc2XKDuiXXo/v9ds5Mp9m6HBabgYQQ26tA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/ethers-io/" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@adraffy/ens-normalize": "1.10.0", + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.2", + "@types/node": "18.15.13", + "aes-js": "4.0.0-beta.5", + "tslib": "2.4.0", + "ws": "8.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ethers/node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ethers/node_modules/@types/node": { + "version": "18.15.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.13.tgz", + "integrity": "sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q==" + }, + "node_modules/ethers/node_modules/tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + }, + "node_modules/ethers/node_modules/ws": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", + "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -10846,6 +10947,11 @@ "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", "dev": true }, + "@adraffy/ens-normalize": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.0.tgz", + "integrity": "sha512-nA9XHtlAkYfJxY7bce8DcN7eKxWWCWkU+1GR9d+U6MbNpfwQp8TI7vqOsBsMcHoT4mBu2kypKoSKnghEzOOq5Q==" + }, "@ampproject/remapping": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", @@ -12749,6 +12855,21 @@ } } }, + "@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "requires": { + "@noble/hashes": "1.3.2" + }, + "dependencies": { + "@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==" + } + } + }, "@noble/ed25519": { "version": "1.7.3", "resolved": "https://registry.npmjs.org/@noble/ed25519/-/ed25519-1.7.3.tgz", @@ -13450,6 +13571,11 @@ "integrity": "sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA==", "dev": true }, + "aes-js": { + "version": "4.0.0-beta.5", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", + "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==" + }, "agentkeepalive": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", @@ -14960,6 +15086,43 @@ "optional": true, "peer": true }, + "ethers": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.10.0.tgz", + "integrity": "sha512-nMNwYHzs6V1FR3Y4cdfxSQmNgZsRj1RiTU25JwvnJLmyzw9z3SKxNc2XKDuiXXo/v9ds5Mp9m6HBabgYQQ26tA==", + "requires": { + "@adraffy/ens-normalize": "1.10.0", + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.2", + "@types/node": "18.15.13", + "aes-js": "4.0.0-beta.5", + "tslib": "2.4.0", + "ws": "8.5.0" + }, + "dependencies": { + "@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==" + }, + "@types/node": { + "version": "18.15.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.13.tgz", + "integrity": "sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q==" + }, + "tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + }, + "ws": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", + "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", + "requires": {} + } + } + }, "events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", diff --git a/package.json b/package.json index 19168bc..e1adcca 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "bull": "^4.10.4", "cache-manager": "^5.2.1", "cron": "^3.1.6", + "ethers": "^6.10.0", "ioredis": "^5.3.2", "module-alias": "^2.2.3", "nest-winston": "^1.9.2", diff --git a/prisma/migrations/20240123071151_add_success_times_to_contract_call_approved/migration.sql b/prisma/migrations/20240123071151_add_success_times_to_contract_call_approved/migration.sql new file mode 100644 index 0000000..101bdb4 --- /dev/null +++ b/prisma/migrations/20240123071151_add_success_times_to_contract_call_approved/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "ContractCallApproved" ADD COLUMN "successTimes" SMALLINT; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 46ed028..f6ae4f5 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -74,6 +74,7 @@ model ContractCallApproved { retry Int @db.SmallInt createdAt DateTime @default(now()) @db.Timestamp(6) updatedAt DateTime @default(now()) @updatedAt @db.Timestamp(6) + successTimes Int? @db.SmallInt } enum ContractCallApprovedStatus { From 0ab7b2942e1fde168e663c2b1acecf5963f95745 Mon Sep 17 00:00:00 2001 From: Rares <6453351+raress96@users.noreply.github.com> Date: Mon, 29 Jan 2024 17:32:52 +0200 Subject: [PATCH 24/33] Bug fixes and new proto file. --- .../src/processors/gateway.processor.ts | 3 +- .../src/assets/google/api/annotations.proto | 31 ++ libs/common/src/assets/google/api/http.proto | 379 ++++++++++++++++++ libs/common/src/assets/relayer.proto | 69 +++- .../contract-call-event.repository.ts | 19 +- libs/common/src/grpc/entities/relayer.ts | 56 ++- .../migration.sql | 5 + prisma/schema.prisma | 4 +- 8 files changed, 535 insertions(+), 31 deletions(-) create mode 100644 libs/common/src/assets/google/api/annotations.proto create mode 100644 libs/common/src/assets/google/api/http.proto create mode 100644 prisma/migrations/20240129153240_remove_unecessary_unique/migration.sql diff --git a/apps/mvx-event-processor/src/processors/gateway.processor.ts b/apps/mvx-event-processor/src/processors/gateway.processor.ts index 3946b0f..ccce550 100644 --- a/apps/mvx-event-processor/src/processors/gateway.processor.ts +++ b/apps/mvx-event-processor/src/processors/gateway.processor.ts @@ -67,8 +67,9 @@ export class GatewayProcessor implements ProcessorInterface { payload: event.data.payload, }); + // A duplicate might exist in the database, so we can skip creation in this case if (!contractCallEvent) { - throw new Error(`Couldn't save contract call event to database for hash ${rawEvent.txHash}`); + return; } // TODO: Should this be batched instead and have this in a separate cronjob? diff --git a/libs/common/src/assets/google/api/annotations.proto b/libs/common/src/assets/google/api/annotations.proto new file mode 100644 index 0000000..d18aeb5 --- /dev/null +++ b/libs/common/src/assets/google/api/annotations.proto @@ -0,0 +1,31 @@ +// Copyright 2015 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.api; + +import "google/api/http.proto"; +import "google/protobuf/descriptor.proto"; + +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "AnnotationsProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + +extend google.protobuf.MethodOptions { + // See `HttpRule`. + HttpRule http = 72295728; +} diff --git a/libs/common/src/assets/google/api/http.proto b/libs/common/src/assets/google/api/http.proto new file mode 100644 index 0000000..1633f90 --- /dev/null +++ b/libs/common/src/assets/google/api/http.proto @@ -0,0 +1,379 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.api; + +option cc_enable_arenas = true; +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "HttpProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + +// Defines the HTTP configuration for an API service. It contains a list of +// [HttpRule][google.api.HttpRule], each specifying the mapping of an RPC method +// to one or more HTTP REST API methods. +message Http { + // A list of HTTP configuration rules that apply to individual API methods. + // + // **NOTE:** All service configuration rules follow "last one wins" order. + repeated HttpRule rules = 1; + + // When set to true, URL path parameters will be fully URI-decoded except in + // cases of single segment matches in reserved expansion, where "%2F" will be + // left encoded. + // + // The default behavior is to not decode RFC 6570 reserved characters in multi + // segment matches. + bool fully_decode_reserved_expansion = 2; +} + +// # gRPC Transcoding +// +// gRPC Transcoding is a feature for mapping between a gRPC method and one or +// more HTTP REST endpoints. It allows developers to build a single API service +// that supports both gRPC APIs and REST APIs. Many systems, including [Google +// APIs](https://github.com/googleapis/googleapis), +// [Cloud Endpoints](https://cloud.google.com/endpoints), [gRPC +// Gateway](https://github.com/grpc-ecosystem/grpc-gateway), +// and [Envoy](https://github.com/envoyproxy/envoy) proxy support this feature +// and use it for large scale production services. +// +// `HttpRule` defines the schema of the gRPC/REST mapping. The mapping specifies +// how different portions of the gRPC request message are mapped to the URL +// path, URL query parameters, and HTTP request body. It also controls how the +// gRPC response message is mapped to the HTTP response body. `HttpRule` is +// typically specified as an `google.api.http` annotation on the gRPC method. +// +// Each mapping specifies a URL path template and an HTTP method. The path +// template may refer to one or more fields in the gRPC request message, as long +// as each field is a non-repeated field with a primitive (non-message) type. +// The path template controls how fields of the request message are mapped to +// the URL path. +// +// Example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get: "/v1/{name=messages/*}" +// }; +// } +// } +// message GetMessageRequest { +// string name = 1; // Mapped to URL path. +// } +// message Message { +// string text = 1; // The resource content. +// } +// +// This enables an HTTP REST to gRPC mapping as below: +// +// HTTP | gRPC +// -----|----- +// `GET /v1/messages/123456` | `GetMessage(name: "messages/123456")` +// +// Any fields in the request message which are not bound by the path template +// automatically become HTTP query parameters if there is no HTTP request body. +// For example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get:"/v1/messages/{message_id}" +// }; +// } +// } +// message GetMessageRequest { +// message SubMessage { +// string subfield = 1; +// } +// string message_id = 1; // Mapped to URL path. +// int64 revision = 2; // Mapped to URL query parameter `revision`. +// SubMessage sub = 3; // Mapped to URL query parameter `sub.subfield`. +// } +// +// This enables a HTTP JSON to RPC mapping as below: +// +// HTTP | gRPC +// -----|----- +// `GET /v1/messages/123456?revision=2&sub.subfield=foo` | +// `GetMessage(message_id: "123456" revision: 2 sub: SubMessage(subfield: +// "foo"))` +// +// Note that fields which are mapped to URL query parameters must have a +// primitive type or a repeated primitive type or a non-repeated message type. +// In the case of a repeated type, the parameter can be repeated in the URL +// as `...?param=A¶m=B`. In the case of a message type, each field of the +// message is mapped to a separate parameter, such as +// `...?foo.a=A&foo.b=B&foo.c=C`. +// +// For HTTP methods that allow a request body, the `body` field +// specifies the mapping. Consider a REST update method on the +// message resource collection: +// +// service Messaging { +// rpc UpdateMessage(UpdateMessageRequest) returns (Message) { +// option (google.api.http) = { +// patch: "/v1/messages/{message_id}" +// body: "message" +// }; +// } +// } +// message UpdateMessageRequest { +// string message_id = 1; // mapped to the URL +// Message message = 2; // mapped to the body +// } +// +// The following HTTP JSON to RPC mapping is enabled, where the +// representation of the JSON in the request body is determined by +// protos JSON encoding: +// +// HTTP | gRPC +// -----|----- +// `PATCH /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: +// "123456" message { text: "Hi!" })` +// +// The special name `*` can be used in the body mapping to define that +// every field not bound by the path template should be mapped to the +// request body. This enables the following alternative definition of +// the update method: +// +// service Messaging { +// rpc UpdateMessage(Message) returns (Message) { +// option (google.api.http) = { +// patch: "/v1/messages/{message_id}" +// body: "*" +// }; +// } +// } +// message Message { +// string message_id = 1; +// string text = 2; +// } +// +// +// The following HTTP JSON to RPC mapping is enabled: +// +// HTTP | gRPC +// -----|----- +// `PATCH /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: +// "123456" text: "Hi!")` +// +// Note that when using `*` in the body mapping, it is not possible to +// have HTTP parameters, as all fields not bound by the path end in +// the body. This makes this option more rarely used in practice when +// defining REST APIs. The common usage of `*` is in custom methods +// which don't use the URL at all for transferring data. +// +// It is possible to define multiple HTTP methods for one RPC by using +// the `additional_bindings` option. Example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get: "/v1/messages/{message_id}" +// additional_bindings { +// get: "/v1/users/{user_id}/messages/{message_id}" +// } +// }; +// } +// } +// message GetMessageRequest { +// string message_id = 1; +// string user_id = 2; +// } +// +// This enables the following two alternative HTTP JSON to RPC mappings: +// +// HTTP | gRPC +// -----|----- +// `GET /v1/messages/123456` | `GetMessage(message_id: "123456")` +// `GET /v1/users/me/messages/123456` | `GetMessage(user_id: "me" message_id: +// "123456")` +// +// ## Rules for HTTP mapping +// +// 1. Leaf request fields (recursive expansion nested messages in the request +// message) are classified into three categories: +// - Fields referred by the path template. They are passed via the URL path. +// - Fields referred by the [HttpRule.body][google.api.HttpRule.body]. They +// are passed via the HTTP +// request body. +// - All other fields are passed via the URL query parameters, and the +// parameter name is the field path in the request message. A repeated +// field can be represented as multiple query parameters under the same +// name. +// 2. If [HttpRule.body][google.api.HttpRule.body] is "*", there is no URL +// query parameter, all fields +// are passed via URL path and HTTP request body. +// 3. If [HttpRule.body][google.api.HttpRule.body] is omitted, there is no HTTP +// request body, all +// fields are passed via URL path and URL query parameters. +// +// ### Path template syntax +// +// Template = "/" Segments [ Verb ] ; +// Segments = Segment { "/" Segment } ; +// Segment = "*" | "**" | LITERAL | Variable ; +// Variable = "{" FieldPath [ "=" Segments ] "}" ; +// FieldPath = IDENT { "." IDENT } ; +// Verb = ":" LITERAL ; +// +// The syntax `*` matches a single URL path segment. The syntax `**` matches +// zero or more URL path segments, which must be the last part of the URL path +// except the `Verb`. +// +// The syntax `Variable` matches part of the URL path as specified by its +// template. A variable template must not contain other variables. If a variable +// matches a single path segment, its template may be omitted, e.g. `{var}` +// is equivalent to `{var=*}`. +// +// The syntax `LITERAL` matches literal text in the URL path. If the `LITERAL` +// contains any reserved character, such characters should be percent-encoded +// before the matching. +// +// If a variable contains exactly one path segment, such as `"{var}"` or +// `"{var=*}"`, when such a variable is expanded into a URL path on the client +// side, all characters except `[-_.~0-9a-zA-Z]` are percent-encoded. The +// server side does the reverse decoding. Such variables show up in the +// [Discovery +// Document](https://developers.google.com/discovery/v1/reference/apis) as +// `{var}`. +// +// If a variable contains multiple path segments, such as `"{var=foo/*}"` +// or `"{var=**}"`, when such a variable is expanded into a URL path on the +// client side, all characters except `[-_.~/0-9a-zA-Z]` are percent-encoded. +// The server side does the reverse decoding, except "%2F" and "%2f" are left +// unchanged. Such variables show up in the +// [Discovery +// Document](https://developers.google.com/discovery/v1/reference/apis) as +// `{+var}`. +// +// ## Using gRPC API Service Configuration +// +// gRPC API Service Configuration (service config) is a configuration language +// for configuring a gRPC service to become a user-facing product. The +// service config is simply the YAML representation of the `google.api.Service` +// proto message. +// +// As an alternative to annotating your proto file, you can configure gRPC +// transcoding in your service config YAML files. You do this by specifying a +// `HttpRule` that maps the gRPC method to a REST endpoint, achieving the same +// effect as the proto annotation. This can be particularly useful if you +// have a proto that is reused in multiple services. Note that any transcoding +// specified in the service config will override any matching transcoding +// configuration in the proto. +// +// Example: +// +// http: +// rules: +// # Selects a gRPC method and applies HttpRule to it. +// - selector: example.v1.Messaging.GetMessage +// get: /v1/messages/{message_id}/{sub.subfield} +// +// ## Special notes +// +// When gRPC Transcoding is used to map a gRPC to JSON REST endpoints, the +// proto to JSON conversion must follow the [proto3 +// specification](https://developers.google.com/protocol-buffers/docs/proto3#json). +// +// While the single segment variable follows the semantics of +// [RFC 6570](https://tools.ietf.org/html/rfc6570) Section 3.2.2 Simple String +// Expansion, the multi segment variable **does not** follow RFC 6570 Section +// 3.2.3 Reserved Expansion. The reason is that the Reserved Expansion +// does not expand special characters like `?` and `#`, which would lead +// to invalid URLs. As the result, gRPC Transcoding uses a custom encoding +// for multi segment variables. +// +// The path variables **must not** refer to any repeated or mapped field, +// because client libraries are not capable of handling such variable expansion. +// +// The path variables **must not** capture the leading "/" character. The reason +// is that the most common use case "{var}" does not capture the leading "/" +// character. For consistency, all path variables must share the same behavior. +// +// Repeated message fields must not be mapped to URL query parameters, because +// no client library can support such complicated mapping. +// +// If an API needs to use a JSON array for request or response body, it can map +// the request or response body to a repeated field. However, some gRPC +// Transcoding implementations may not support this feature. +message HttpRule { + // Selects a method to which this rule applies. + // + // Refer to [selector][google.api.DocumentationRule.selector] for syntax + // details. + string selector = 1; + + // Determines the URL pattern is matched by this rules. This pattern can be + // used with any of the {get|put|post|delete|patch} methods. A custom method + // can be defined using the 'custom' field. + oneof pattern { + // Maps to HTTP GET. Used for listing and getting information about + // resources. + string get = 2; + + // Maps to HTTP PUT. Used for replacing a resource. + string put = 3; + + // Maps to HTTP POST. Used for creating a resource or performing an action. + string post = 4; + + // Maps to HTTP DELETE. Used for deleting a resource. + string delete = 5; + + // Maps to HTTP PATCH. Used for updating a resource. + string patch = 6; + + // The custom pattern is used for specifying an HTTP method that is not + // included in the `pattern` field, such as HEAD, or "*" to leave the + // HTTP method unspecified for this rule. The wild-card rule is useful + // for services that provide content to Web (HTML) clients. + CustomHttpPattern custom = 8; + } + + // The name of the request field whose value is mapped to the HTTP request + // body, or `*` for mapping all request fields not captured by the path + // pattern to the HTTP body, or omitted for not having any HTTP request body. + // + // NOTE: the referred field must be present at the top-level of the request + // message type. + string body = 7; + + // Optional. The name of the response field whose value is mapped to the HTTP + // response body. When omitted, the entire response message will be used + // as the HTTP response body. + // + // NOTE: The referred field must be present at the top-level of the response + // message type. + string response_body = 12; + + // Additional HTTP bindings for the selector. Nested bindings must + // not contain an `additional_bindings` field themselves (that is, + // the nesting may only be one level deep). + repeated HttpRule additional_bindings = 11; +} + +// A custom pattern is used for defining custom HTTP verb. +message CustomHttpPattern { + // The name of this custom HTTP verb. + string kind = 1; + + // The path matched by this custom verb. + string path = 2; +} diff --git a/libs/common/src/assets/relayer.proto b/libs/common/src/assets/relayer.proto index cb8ab4c..588a363 100644 --- a/libs/common/src/assets/relayer.proto +++ b/libs/common/src/assets/relayer.proto @@ -1,29 +1,30 @@ syntax = "proto3"; -package axelar.relayer.v1beta1; +import "google/api/annotations.proto"; service Relayer{ rpc Verify(stream VerifyRequest) returns (stream VerifyResponse); - rpc GetPayload(GetPayloadRequest) returns (GetPayloadResponse); + rpc GetPayload(GetPayloadRequest) returns (GetPayloadResponse) { + option (google.api.http) = { + get: "/v1beta1/payload/{hash}" + }; + } rpc SubscribeToApprovals(SubscribeToApprovalsRequest) returns (stream SubscribeToApprovalsResponse); -} - -message VerifyRequest{ - Message message = 1; -} - -message VerifyResponse{ - Message message = 1; - bool success = 3; + rpc SubscribeToWasmEvents(SubscribeToWasmEventsRequest) returns (stream SubscribeToWasmEventsResponse); + rpc Broadcast(BroadcastRequest) returns (BroadcastResponse) { + option (google.api.http) = { + post: "/v1beta1/broadcast" + body: "*" + }; + } } message Message{ string id = 1; // the unique identifier with which the message can be looked up on the source chain string source_chain = 2; - string source_address = 3; + string source_address= 3; string destination_chain = 4; string destination_address = 5; bytes payload = 6; - // when we have a better idea of the requirement, we can add an additional optional field here to facilitate verification proofs } message GetPayloadRequest{ @@ -44,3 +45,45 @@ message SubscribeToApprovalsResponse{ bytes execute_data = 2; uint64 block_height = 3; } + +message VerifyRequest{ + Message message = 1; +} + +message VerifyResponse{ + Message message = 1; + bool success = 3; +} + +message SubscribeToWasmEventsRequest{ + optional uint64 start_height = 1; +} + +message SubscribeToWasmEventsResponse{ + string type = 1; + repeated Attribute attributes = 2; + uint64 height = 3; +} + +message Attribute{ + string key = 1; + string value = 2; +} + +message BroadcastRequest { + string address = 1; + bytes payload = 2; +} + +message BroadcastResponse { + Receipt receipt = 1; +} + +message Receipt { + string error = 1; + int64 block_height = 2; + int64 gas_used = 3; + int64 gas_wanted = 4; + string tx_hash = 5; + string tx_response_log = 6; +} diff --git a/libs/common/src/database/repository/contract-call-event.repository.ts b/libs/common/src/database/repository/contract-call-event.repository.ts index d41facd..d70baca 100644 --- a/libs/common/src/database/repository/contract-call-event.repository.ts +++ b/libs/common/src/database/repository/contract-call-event.repository.ts @@ -7,10 +7,21 @@ import { ContractCallEventWithGasPaid } from '@mvx-monorepo/common/database/enti export class ContractCallEventRepository { constructor(private readonly prisma: PrismaService) {} - create(data: Prisma.ContractCallEventCreateInput): Promise { - return this.prisma.contractCallEvent.create({ - data, - }); + async create(data: Prisma.ContractCallEventCreateInput): Promise { + try { + return await this.prisma.contractCallEvent.create({ + data, + }); + } catch (e) { + if (e instanceof Prisma.PrismaClientKnownRequestError) { + // Unique constraint fails + if (e.code === 'P2002') { + return null; + } + } + + throw e; + } } findWithoutGasPaid(gasPaid: Prisma.GasPaidCreateInput): Promise { diff --git a/libs/common/src/grpc/entities/relayer.ts b/libs/common/src/grpc/entities/relayer.ts index 2289a7e..b24af21 100644 --- a/libs/common/src/grpc/entities/relayer.ts +++ b/libs/common/src/grpc/entities/relayer.ts @@ -1,16 +1,7 @@ /* eslint-disable */ import { Observable } from "rxjs"; -export const protobufPackage = "axelar.relayer.v1beta1"; - -export interface VerifyRequest { - message: Message | undefined; -} - -export interface VerifyResponse { - message: Message | undefined; - success: boolean; -} +export const protobufPackage = ""; export interface Message { /** the unique identifier with which the message can be looked up on the source chain */ @@ -19,7 +10,6 @@ export interface Message { sourceAddress: string; destinationChain: string; destinationAddress: string; - /** when we have a better idea of the requirement, we can add an additional optional field here to facilitate verification proofs */ payload: Uint8Array; } @@ -43,8 +33,52 @@ export interface SubscribeToApprovalsResponse { blockHeight: number; } +export interface VerifyRequest { + message: Message | undefined; +} + +export interface VerifyResponse { + message: Message | undefined; + success: boolean; +} + +export interface SubscribeToWasmEventsRequest { + startHeight?: number | undefined; +} + +export interface SubscribeToWasmEventsResponse { + type: string; + attributes: Attribute[]; + height: number; +} + +export interface Attribute { + key: string; + value: string; +} + +export interface BroadcastRequest { + address: string; + payload: Uint8Array; +} + +export interface BroadcastResponse { + receipt: Receipt | undefined; +} + +export interface Receipt { + error: string; + blockHeight: number; + gasUsed: number; + gasWanted: number; + txHash: string; + txResponseLog: string; +} + export interface Relayer { verify(request: Observable): Observable; getPayload(request: GetPayloadRequest): Promise; subscribeToApprovals(request: SubscribeToApprovalsRequest): Observable; + subscribeToWasmEvents(request: SubscribeToWasmEventsRequest): Observable; + broadcast(request: BroadcastRequest): Promise; } diff --git a/prisma/migrations/20240129153240_remove_unecessary_unique/migration.sql b/prisma/migrations/20240129153240_remove_unecessary_unique/migration.sql new file mode 100644 index 0000000..842a2ee --- /dev/null +++ b/prisma/migrations/20240129153240_remove_unecessary_unique/migration.sql @@ -0,0 +1,5 @@ +-- DropIndex +DROP INDEX "ContractCallApproved_txHash_key"; + +-- DropIndex +DROP INDEX "ContractCallEvent_txHash_key"; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 46ed028..9f381dc 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -12,7 +12,7 @@ datasource db { model ContractCallEvent { id String @id @db.VarChar(255) // should be formatted as [source_chain]:[unique identifier]:[log index], i.e. Ethereum:0x74ac0205b1f8f51023942856145182f0e6fdd41ccb2c8058bf2d89fc67564d56:0 - txHash String @unique @db.VarChar(64) + txHash String @db.VarChar(64) eventIndex Int @db.SmallInt status ContractCallEventStatus sourceAddress String @db.VarChar(62) @@ -63,7 +63,7 @@ enum GasPaidStatus { model ContractCallApproved { commandId String @id @db.VarChar(64) - txHash String @unique @db.VarChar(64) + txHash String @db.VarChar(64) status ContractCallApprovedStatus sourceAddress String @db.VarChar(255) sourceChain String @db.VarChar(255) From 17afec979eea00da7df73841fc50fd5d235dae1a Mon Sep 17 00:00:00 2001 From: Rares <6453351+raress96@users.noreply.github.com> Date: Tue, 30 Jan 2024 11:51:19 +0200 Subject: [PATCH 25/33] Basic support for processing operatorship transferred event and improvements. --- .../approvals.processor.service.ts | 2 +- .../approvals.processor.spec.ts | 26 +-- .../gas-checker/gas-checker.service.spec.ts | 12 +- .../src/gas-checker/gas-checker.service.ts | 4 +- .../src/processors/gateway.processor.spec.ts | 46 ++++- .../src/processors/gateway.processor.ts | 33 +++- libs/common/src/assets/auth.abi.json | 162 ++++++++++++++++++ libs/common/src/contracts/auth.contract.ts | 21 +++ libs/common/src/contracts/contract.loader.ts | 26 +-- libs/common/src/contracts/contracts.module.ts | 43 +++-- .../src/contracts/entities/auth-types.ts | 7 + .../src/contracts/gateway.contract.spec.ts | 40 ++++- libs/common/src/contracts/gateway.contract.ts | 20 ++- .../src/contracts/transactions.helper.ts | 4 +- .../contract-call-approved.repository.ts | 11 ++ .../contract-call-event.repository.ts | 4 +- libs/common/src/grpc/grpc.service.ts | 34 +++- libs/common/src/utils/event.enum.ts | 1 + .../migration.sql | 11 ++ prisma/schema.prisma | 2 +- 20 files changed, 429 insertions(+), 80 deletions(-) create mode 100644 libs/common/src/assets/auth.abi.json create mode 100644 libs/common/src/contracts/auth.contract.ts create mode 100644 libs/common/src/contracts/entities/auth-types.ts create mode 100644 prisma/migrations/20240130085712_contract_call_event_unique_constraint/migration.sql diff --git a/apps/axelar-event-processor/src/approvals-processor/approvals.processor.service.ts b/apps/axelar-event-processor/src/approvals-processor/approvals.processor.service.ts index 666504c..5881aa5 100644 --- a/apps/axelar-event-processor/src/approvals-processor/approvals.processor.service.ts +++ b/apps/axelar-event-processor/src/approvals-processor/approvals.processor.service.ts @@ -90,7 +90,7 @@ export class ApprovalsProcessorService { const { txHash, executeData, retry } = cachedValue; - const success = await this.transactionsHelper.awaitComplete(txHash); + const success = await this.transactionsHelper.awaitSuccess(txHash); // Nothing to do on success if (success) { diff --git a/apps/axelar-event-processor/src/approvals-processor/approvals.processor.spec.ts b/apps/axelar-event-processor/src/approvals-processor/approvals.processor.spec.ts index d19b3d7..3b61aa3 100644 --- a/apps/axelar-event-processor/src/approvals-processor/approvals.processor.spec.ts +++ b/apps/axelar-event-processor/src/approvals-processor/approvals.processor.spec.ts @@ -230,7 +230,7 @@ describe('ApprovalsProcessorService', () => { expect(redisCacheService.get).toHaveBeenCalledWith(key); expect(redisCacheService.delete).toHaveBeenCalledTimes(1); expect(redisCacheService.delete).toHaveBeenCalledWith(key); - expect(transactionsHelper.awaitComplete).not.toHaveBeenCalled(); + expect(transactionsHelper.awaitSuccess).not.toHaveBeenCalled(); }); it('Should handle success', async () => { @@ -244,7 +244,7 @@ describe('ApprovalsProcessorService', () => { retry: 1, }), ); - transactionsHelper.awaitComplete.mockReturnValueOnce(Promise.resolve(true)); + transactionsHelper.awaitSuccess.mockReturnValueOnce(Promise.resolve(true)); await service.handlePendingTransactionsRaw(); @@ -253,8 +253,8 @@ describe('ApprovalsProcessorService', () => { expect(redisCacheService.get).toHaveBeenCalledWith(key); expect(redisCacheService.delete).toHaveBeenCalledTimes(1); expect(redisCacheService.delete).toHaveBeenCalledWith(key); - expect(transactionsHelper.awaitComplete).toHaveBeenCalledTimes(1); - expect(transactionsHelper.awaitComplete).toHaveBeenCalledWith('txHashComplete'); + expect(transactionsHelper.awaitSuccess).toHaveBeenCalledTimes(1); + expect(transactionsHelper.awaitSuccess).toHaveBeenCalledWith('txHashComplete'); expect(transactionsHelper.getTransactionGas).not.toHaveBeenCalled(); }); @@ -270,7 +270,7 @@ describe('ApprovalsProcessorService', () => { retry: 1, }), ); - transactionsHelper.awaitComplete.mockReturnValueOnce(Promise.resolve(false)); + transactionsHelper.awaitSuccess.mockReturnValueOnce(Promise.resolve(false)); const userAddress = UserAddress.fromBech32('erd1qqqqqqqqqqqqqpgqhe8t5jewej70zupmh44jurgn29psua5l2jps3ntjj3'); walletSigner.getAddress.mockReturnValueOnce(userAddress); @@ -283,8 +283,8 @@ describe('ApprovalsProcessorService', () => { await service.handlePendingTransactionsRaw(); - expect(transactionsHelper.awaitComplete).toHaveBeenCalledTimes(1); - expect(transactionsHelper.awaitComplete).toHaveBeenCalledWith('txHashComplete'); + expect(transactionsHelper.awaitSuccess).toHaveBeenCalledTimes(1); + expect(transactionsHelper.awaitSuccess).toHaveBeenCalledWith('txHashComplete'); expect(gatewayContract.buildExecuteTransaction).toHaveBeenCalledTimes(1); expect(gatewayContract.buildExecuteTransaction).toHaveBeenCalledWith( @@ -322,12 +322,12 @@ describe('ApprovalsProcessorService', () => { retry: 3, }), ); - transactionsHelper.awaitComplete.mockReturnValueOnce(Promise.resolve(false)); + transactionsHelper.awaitSuccess.mockReturnValueOnce(Promise.resolve(false)); await service.handlePendingTransactionsRaw(); - expect(transactionsHelper.awaitComplete).toHaveBeenCalledTimes(1); - expect(transactionsHelper.awaitComplete).toHaveBeenCalledWith('txHashComplete'); + expect(transactionsHelper.awaitSuccess).toHaveBeenCalledTimes(1); + expect(transactionsHelper.awaitSuccess).toHaveBeenCalledWith('txHashComplete'); expect(transactionsHelper.getTransactionGas).not.toHaveBeenCalled(); }); @@ -343,7 +343,7 @@ describe('ApprovalsProcessorService', () => { retry: 1, }), ); - transactionsHelper.awaitComplete.mockReturnValueOnce(Promise.resolve(false)); + transactionsHelper.awaitSuccess.mockReturnValueOnce(Promise.resolve(false)); const userAddress = UserAddress.fromBech32('erd1qqqqqqqqqqqqqpgqhe8t5jewej70zupmh44jurgn29psua5l2jps3ntjj3'); walletSigner.getAddress.mockReturnValueOnce(userAddress); @@ -355,8 +355,8 @@ describe('ApprovalsProcessorService', () => { await service.handlePendingTransactionsRaw(); - expect(transactionsHelper.awaitComplete).toHaveBeenCalledTimes(1); - expect(transactionsHelper.awaitComplete).toHaveBeenCalledWith('txHashComplete'); + expect(transactionsHelper.awaitSuccess).toHaveBeenCalledTimes(1); + expect(transactionsHelper.awaitSuccess).toHaveBeenCalledWith('txHashComplete'); expect(transactionsHelper.getTransactionGas).toHaveBeenCalledTimes(1); expect(transactionsHelper.getTransactionGas).toHaveBeenCalledWith(transaction, 1); diff --git a/apps/mvx-event-processor/src/gas-checker/gas-checker.service.spec.ts b/apps/mvx-event-processor/src/gas-checker/gas-checker.service.spec.ts index f6e902c..971ba89 100644 --- a/apps/mvx-event-processor/src/gas-checker/gas-checker.service.spec.ts +++ b/apps/mvx-event-processor/src/gas-checker/gas-checker.service.spec.ts @@ -132,7 +132,7 @@ describe('GasCheckerService', () => { const transaction: DeepMocked = createMock(); gasServiceContract.collectFees.mockReturnValueOnce(transaction); transactionsHelper.signAndSendTransaction.mockReturnValueOnce(Promise.resolve('txHash')); - transactionsHelper.awaitComplete.mockReturnValueOnce(Promise.resolve(success)); + transactionsHelper.awaitSuccess.mockReturnValueOnce(Promise.resolve(success)); await service.checkGasServiceAndWalletRaw(); @@ -150,8 +150,8 @@ describe('GasCheckerService', () => { ); expect(transactionsHelper.signAndSendTransaction).toHaveBeenCalledTimes(1); expect(transactionsHelper.signAndSendTransaction).toHaveBeenCalledWith(transaction, walletSigner); - expect(transactionsHelper.awaitComplete).toHaveBeenCalledTimes(1); - expect(transactionsHelper.awaitComplete).toHaveBeenCalledWith('txHash'); + expect(transactionsHelper.awaitSuccess).toHaveBeenCalledTimes(1); + expect(transactionsHelper.awaitSuccess).toHaveBeenCalledWith('txHash'); expect(wegldSwapContract.unwrapEgld).not.toHaveBeenCalled(); }; @@ -208,7 +208,7 @@ describe('GasCheckerService', () => { const transaction: DeepMocked = createMock(); wegldSwapContract.unwrapEgld.mockReturnValueOnce(transaction); transactionsHelper.signAndSendTransaction.mockReturnValueOnce(Promise.resolve('txHash')); - transactionsHelper.awaitComplete.mockReturnValueOnce(Promise.resolve(complete)); + transactionsHelper.awaitSuccess.mockReturnValueOnce(Promise.resolve(complete)); await service.checkGasServiceAndWalletRaw(); @@ -226,8 +226,8 @@ describe('GasCheckerService', () => { ); expect(transactionsHelper.signAndSendTransaction).toHaveBeenCalledTimes(1); expect(transactionsHelper.signAndSendTransaction).toHaveBeenCalledWith(transaction, walletSigner); - expect(transactionsHelper.awaitComplete).toHaveBeenCalledTimes(1); - expect(transactionsHelper.awaitComplete).toHaveBeenCalledWith('txHash'); + expect(transactionsHelper.awaitSuccess).toHaveBeenCalledTimes(1); + expect(transactionsHelper.awaitSuccess).toHaveBeenCalledWith('txHash'); expect(gasServiceContract.collectFees).not.toHaveBeenCalled(); }; diff --git a/apps/mvx-event-processor/src/gas-checker/gas-checker.service.ts b/apps/mvx-event-processor/src/gas-checker/gas-checker.service.ts index 05904b1..6472e7a 100644 --- a/apps/mvx-event-processor/src/gas-checker/gas-checker.service.ts +++ b/apps/mvx-event-processor/src/gas-checker/gas-checker.service.ts @@ -88,7 +88,7 @@ export class GasCheckerService { const txHash = await this.transactionsHelper.signAndSendTransaction(transaction, this.walletSigner); - const success = await this.transactionsHelper.awaitComplete(txHash); + const success = await this.transactionsHelper.awaitSuccess(txHash); if (!success) { throw new Error(`Error while executing transaction ${txHash}`); @@ -113,7 +113,7 @@ export class GasCheckerService { const txHash = await this.transactionsHelper.signAndSendTransaction(transaction, this.walletSigner); - const success = await this.transactionsHelper.awaitComplete(txHash); + const success = await this.transactionsHelper.awaitSuccess(txHash); if (!success) { throw new Error(`Error while executing unwrap egld transaction ${txHash}`); diff --git a/apps/mvx-event-processor/src/processors/gateway.processor.spec.ts b/apps/mvx-event-processor/src/processors/gateway.processor.spec.ts index d4a53f6..46de1ba 100644 --- a/apps/mvx-event-processor/src/processors/gateway.processor.spec.ts +++ b/apps/mvx-event-processor/src/processors/gateway.processor.spec.ts @@ -143,9 +143,20 @@ describe('ContractCallProcessor', () => { expect(grpcService.verify).toHaveBeenCalledTimes(1); }); - it('Should throw error can not save in database', async () => { + it('Should not handle duplicate', async () => { contractCallEventRepository.create.mockReturnValueOnce(Promise.resolve(null)); + await service.handleEvent(rawEvent); + + expect(gatewayContract.decodeContractCallEvent).toHaveBeenCalledTimes(1); + expect(gatewayContract.decodeContractCallEvent).toHaveBeenCalledWith(TransactionEvent.fromHttpResponse(rawEvent)); + expect(contractCallEventRepository.create).toHaveBeenCalledTimes(1); + expect(grpcService.verify).not.toHaveBeenCalled(); + }); + + it('Should throw error can not save in database', async () => { + contractCallEventRepository.create.mockRejectedValue(new Error('Can not save in database')); + await expect(service.handleEvent(rawEvent)).rejects.toThrow(); expect(gatewayContract.decodeContractCallEvent).toHaveBeenCalledTimes(1); @@ -216,6 +227,27 @@ describe('ContractCallProcessor', () => { }); }); + describe('handleOperatorshipTransferredEvent', () => { + const rawEvent: NotifierEvent = { + txHash: 'txHash', + address: 'mockGatewayAddress', + identifier: EventIdentifiers.EXECUTE, + data: Buffer.from( + '000000018049d639e5a6980d1cd2392abcce41029cda74a1563523a202f09641cc2618f80000000100000001020000000102', + 'hex', + ).toString('base64'), + topics: [BinaryUtils.base64Encode(Events.OPERATORSHIP_TRANSFERRED_EVENT)], + }; + + it('Should handle event', async () => { + await service.handleEvent(rawEvent); + + expect(gatewayContract.decodeOperatorshipTransferredEvent).toHaveBeenCalledTimes(1); + expect(gatewayContract.decodeOperatorshipTransferredEvent).toHaveBeenCalledWith(TransactionEvent.fromHttpResponse(rawEvent)); + expect(grpcService.verifyWorkerSet).toHaveBeenCalledTimes(1); + }); + }); + describe('handleContractCallExecutedEvent', () => { const rawEvent: NotifierEvent = { txHash: 'txHash', @@ -256,13 +288,11 @@ describe('ContractCallProcessor', () => { expect(contractCallApprovedRepository.findByCommandId).toHaveBeenCalledWith( '0c38359b7a35c755573659d797afec315bb0e51374a056745abd9764715a15da', ); - expect(contractCallApprovedRepository.updateManyStatusRetryExecuteTxHash).toHaveBeenCalledTimes(1); - expect(contractCallApprovedRepository.updateManyStatusRetryExecuteTxHash).toHaveBeenCalledWith([ - { - ...contractCallApproved, - status: ContractCallApprovedStatus.SUCCESS, - }, - ]); + expect(contractCallApprovedRepository.updateStatus).toHaveBeenCalledTimes(1); + expect(contractCallApprovedRepository.updateStatus).toHaveBeenCalledWith({ + ...contractCallApproved, + status: ContractCallApprovedStatus.SUCCESS, + }); }); it('Should handle event no contract call approved', async () => { diff --git a/apps/mvx-event-processor/src/processors/gateway.processor.ts b/apps/mvx-event-processor/src/processors/gateway.processor.ts index ccce550..f3ebe86 100644 --- a/apps/mvx-event-processor/src/processors/gateway.processor.ts +++ b/apps/mvx-event-processor/src/processors/gateway.processor.ts @@ -38,13 +38,20 @@ export class GatewayProcessor implements ProcessorInterface { return; } - if (rawEvent.identifier === EventIdentifiers.EXECUTE && eventName === Events.CONTRACT_CALL_APPROVED_EVENT) { - await this.handleContractCallApprovedEvent(rawEvent); + if (rawEvent.identifier === EventIdentifiers.EXECUTE) { + if (eventName === Events.CONTRACT_CALL_APPROVED_EVENT) { + await this.handleContractCallApprovedEvent(rawEvent); + } else if (eventName === Events.OPERATORSHIP_TRANSFERRED_EVENT) { + await this.handleOperatorshipTransferredEvent(rawEvent); + } return; } - if (rawEvent.identifier === EventIdentifiers.VALIDATE_CONTRACT_CALL && eventName === Events.CONTRACT_CALL_EXECUTED_EVENT) { + if ( + rawEvent.identifier === EventIdentifiers.VALIDATE_CONTRACT_CALL && + eventName === Events.CONTRACT_CALL_EXECUTED_EVENT + ) { await this.handleContractCallExecutedEvent(rawEvent); return; @@ -54,8 +61,9 @@ export class GatewayProcessor implements ProcessorInterface { private async handleContractCallEvent(rawEvent: NotifierEvent) { const event = this.gatewayContract.decodeContractCallEvent(TransactionEvent.fromHttpResponse(rawEvent)); + const id = `${this.sourceChain}:${rawEvent.txHash}:${UNSUPPORTED_LOG_INDEX}`; const contractCallEvent = await this.contractCallEventRepository.create({ - id: `${this.sourceChain}:${rawEvent.txHash}:${UNSUPPORTED_LOG_INDEX}`, + id, txHash: rawEvent.txHash, eventIndex: UNSUPPORTED_LOG_INDEX, status: ContractCallEventStatus.PENDING, @@ -103,6 +111,21 @@ export class GatewayProcessor implements ProcessorInterface { } } + private async handleOperatorshipTransferredEvent(rawEvent: NotifierEvent) { + const trasnsferData = this.gatewayContract.decodeOperatorshipTransferredEvent( + TransactionEvent.fromHttpResponse(rawEvent), + ); + + const id = `${this.sourceChain}:${rawEvent.txHash}:${UNSUPPORTED_LOG_INDEX}`; + + await this.grpcService.verifyWorkerSet( + id, + trasnsferData.newOperators, + trasnsferData.newWeights, + trasnsferData.newThreshold, + ); + } + private async handleContractCallExecutedEvent(rawEvent: NotifierEvent) { const commandId = this.gatewayContract.decodeContractCallExecutedEvent(TransactionEvent.fromHttpResponse(rawEvent)); @@ -114,6 +137,6 @@ export class GatewayProcessor implements ProcessorInterface { contractCallApproved.status = ContractCallApprovedStatus.SUCCESS; - await this.contractCallApprovedRepository.updateManyStatusRetryExecuteTxHash([contractCallApproved]); + await this.contractCallApprovedRepository.updateStatus(contractCallApproved); } } diff --git a/libs/common/src/assets/auth.abi.json b/libs/common/src/assets/auth.abi.json new file mode 100644 index 0000000..537dd11 --- /dev/null +++ b/libs/common/src/assets/auth.abi.json @@ -0,0 +1,162 @@ +{ + "buildInfo": { + "rustc": { + "version": "1.76.0-nightly", + "commitHash": "d86d65bbc19b928387f68427fcc3a0da498d8a19", + "commitDate": "2023-12-10", + "channel": "Nightly", + "short": "rustc 1.76.0-nightly (d86d65bbc 2023-12-10)" + }, + "contractCrate": { + "name": "auth", + "version": "0.0.0" + }, + "framework": { + "name": "multiversx-sc", + "version": "0.46.1" + } + }, + "name": "Auth", + "constructor": { + "inputs": [ + { + "name": "recent_operators", + "type": "variadic", + "multi_arg": true + } + ], + "outputs": [] + }, + "endpoints": [ + { + "name": "upgrade", + "mutability": "mutable", + "inputs": [], + "outputs": [] + }, + { + "name": "validateProof", + "mutability": "mutable", + "inputs": [ + { + "name": "message_hash", + "type": "array32" + }, + { + "name": "proof_data", + "type": "ProofData" + } + ], + "outputs": [ + { + "type": "bool" + } + ] + }, + { + "name": "transferOperatorship", + "onlyOwner": true, + "mutability": "mutable", + "inputs": [ + { + "name": "transfer_data", + "type": "TransferData" + } + ], + "outputs": [] + }, + { + "name": "epoch_for_hash", + "mutability": "readonly", + "inputs": [ + { + "name": "hash", + "type": "array32" + } + ], + "outputs": [ + { + "type": "u64" + } + ] + }, + { + "name": "hash_for_epoch", + "mutability": "readonly", + "inputs": [ + { + "name": "epoch", + "type": "u64" + } + ], + "outputs": [ + { + "type": "array32" + } + ] + }, + { + "name": "current_epoch", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "u64" + } + ] + } + ], + "events": [ + { + "identifier": "operatorship_transferred_event", + "inputs": [ + { + "name": "data", + "type": "TransferData" + } + ] + } + ], + "esdtAttributes": [], + "hasCallback": false, + "types": { + "ProofData": { + "type": "struct", + "fields": [ + { + "name": "operators", + "type": "List>" + }, + { + "name": "weights", + "type": "List" + }, + { + "name": "threshold", + "type": "BigUint" + }, + { + "name": "signatures", + "type": "List>" + } + ] + }, + "TransferData": { + "type": "struct", + "fields": [ + { + "name": "new_operators", + "type": "List>" + }, + { + "name": "new_weights", + "type": "List" + }, + { + "name": "new_threshold", + "type": "BigUint" + } + ] + } + } +} diff --git a/libs/common/src/contracts/auth.contract.ts b/libs/common/src/contracts/auth.contract.ts new file mode 100644 index 0000000..aa5f173 --- /dev/null +++ b/libs/common/src/contracts/auth.contract.ts @@ -0,0 +1,21 @@ +import { AbiRegistry, BinaryCodec } from '@multiversx/sdk-core/out'; +import { Injectable } from '@nestjs/common'; +import { DecodingUtils } from '@mvx-monorepo/common/utils/decoding.utils'; +import { TransferData } from '@mvx-monorepo/common/contracts/entities/auth-types'; +import BigNumber from 'bignumber.js'; + +@Injectable() +export class AuthContract { + constructor(private readonly abi: AbiRegistry, private readonly binaryCodec: BinaryCodec) {} + + decodeTransferData(params: Buffer): TransferData { + const structType = this.abi.getStruct('TransferData'); + const outcome = this.binaryCodec.decodeTopLevel(params, structType).valueOf(); + + return { + newOperators: outcome.new_operators.map((operator: BigNumber[]) => DecodingUtils.decodeKeccak256Hash(operator)), + newWeights: outcome.new_weights, + newThreshold: outcome.new_threshold, + }; + } +} diff --git a/libs/common/src/contracts/contract.loader.ts b/libs/common/src/contracts/contract.loader.ts index 1616f4c..2f8d648 100644 --- a/libs/common/src/contracts/contract.loader.ts +++ b/libs/common/src/contracts/contract.loader.ts @@ -14,12 +14,9 @@ export class ContractLoader { this.logger = new Logger(ContractLoader.name); } - private async load(contractAddress: string): Promise { + private async loadContract(contractAddress: string): Promise { try { - const jsonContent: string = await fs.promises.readFile(this.abiPath, { encoding: 'utf8' }); - const json = JSON.parse(jsonContent); - - this.abiRegistry = AbiRegistry.create(json); + await this.loadAbiRegistry(); return new SmartContract({ address: new Address(contractAddress), @@ -33,18 +30,27 @@ export class ContractLoader { } } + private async loadAbiRegistry() { + if (this.abiRegistry) { + return; + } + + const jsonContent: string = await fs.promises.readFile(this.abiPath, { encoding: 'utf8' }); + const json = JSON.parse(jsonContent); + + this.abiRegistry = AbiRegistry.create(json); + } + async getContract(contractAddress: string): Promise { if (!this.contract) { - this.contract = await this.load(contractAddress); + this.contract = await this.loadContract(contractAddress); } return this.contract; } - async getAbiRegistry(contractAddress: string): Promise { - if (!this.abiRegistry) { - await this.load(contractAddress); - } + async getAbiRegistry(): Promise { + await this.loadAbiRegistry(); return this.abiRegistry as AbiRegistry; } diff --git a/libs/common/src/contracts/contracts.module.ts b/libs/common/src/contracts/contracts.module.ts index 6673cc0..24a8f12 100644 --- a/libs/common/src/contracts/contracts.module.ts +++ b/libs/common/src/contracts/contracts.module.ts @@ -1,7 +1,7 @@ import { Module } from '@nestjs/common'; import { GatewayContract } from './gateway.contract'; import { ApiNetworkProvider, ProxyNetworkProvider } from '@multiversx/sdk-network-providers/out'; -import { ResultsParser, TransactionWatcher } from '@multiversx/sdk-core/out'; +import { BinaryCodec, ResultsParser, TransactionWatcher } from '@multiversx/sdk-core/out'; import { ContractLoader } from '@mvx-monorepo/common/contracts/contract.loader'; import { join } from 'path'; import { GasServiceContract } from '@mvx-monorepo/common/contracts/gas-service.contract'; @@ -11,6 +11,7 @@ import { TransactionsHelper } from '@mvx-monorepo/common/contracts/transactions. import { WegldSwapContract } from '@mvx-monorepo/common/contracts/wegld-swap.contract'; import { ApiConfigService } from '@mvx-monorepo/common/config'; import { DynamicModuleUtils } from '@mvx-monorepo/common/utils'; +import { AuthContract } from '@mvx-monorepo/common/contracts/auth.contract'; @Module({ imports: [DynamicModuleUtils.getCacheModule()], @@ -37,32 +38,30 @@ import { DynamicModuleUtils } from '@mvx-monorepo/common/utils'; provide: ResultsParser, useValue: new ResultsParser(), }, + { + provide: BinaryCodec, + useValue: new BinaryCodec(), + }, { provide: TransactionWatcher, useFactory: (api: ApiNetworkProvider) => new TransactionWatcher(api), // use api here not proxy since it returns proper transaction status inject: [ApiNetworkProvider], }, - // { - // provide: ContractQueryRunner, - // useFactory: (api: ApiNetworkProvider) => new ContractQueryRunner(api), - // inject: [ApiNetworkProvider], - // }, - // { - // provide: ContractTransactionGenerator, - // useFactory: (api: ApiNetworkProvider) => new ContractTransactionGenerator(api), - // inject: [ApiNetworkProvider], - // }, { provide: GatewayContract, - useFactory: async (apiConfigService: ApiConfigService, resultsParser: ResultsParser) => { + useFactory: async ( + apiConfigService: ApiConfigService, + resultsParser: ResultsParser, + authContract: AuthContract, + ) => { const contractLoader = new ContractLoader(join(__dirname, '../assets/gateway.abi.json')); const smartContract = await contractLoader.getContract(apiConfigService.getContractGateway()); - const abi = await contractLoader.getAbiRegistry(apiConfigService.getContractGateway()); + const abi = await contractLoader.getAbiRegistry(); - return new GatewayContract(smartContract, abi, resultsParser); + return new GatewayContract(smartContract, abi, resultsParser, authContract); }, - inject: [ApiConfigService, ResultsParser], + inject: [ApiConfigService, ResultsParser, AuthContract], }, { provide: GasServiceContract, @@ -70,12 +69,23 @@ import { DynamicModuleUtils } from '@mvx-monorepo/common/utils'; const contractLoader = new ContractLoader(join(__dirname, '../assets/gas-service.abi.json')); const smartContract = await contractLoader.getContract(apiConfigService.getContractGasService()); - const abi = await contractLoader.getAbiRegistry(apiConfigService.getContractGasService()); + const abi = await contractLoader.getAbiRegistry(); return new GasServiceContract(smartContract, abi, resultsParser); }, inject: [ApiConfigService, ResultsParser], }, + { + provide: AuthContract, + useFactory: async (binaryCodec: BinaryCodec) => { + const contractLoader = new ContractLoader(join(__dirname, '../assets/auth.abi.json')); + + const abi = await contractLoader.getAbiRegistry(); + + return new AuthContract(abi, binaryCodec); + }, + inject: [ApiConfigService, ResultsParser], + }, { provide: WegldSwapContract, useFactory: async ( @@ -105,6 +115,7 @@ import { DynamicModuleUtils } from '@mvx-monorepo/common/utils'; exports: [ GatewayContract, GasServiceContract, + AuthContract, WegldSwapContract, ProviderKeys.WALLET_SIGNER, ProxyNetworkProvider, diff --git a/libs/common/src/contracts/entities/auth-types.ts b/libs/common/src/contracts/entities/auth-types.ts new file mode 100644 index 0000000..9cc14d7 --- /dev/null +++ b/libs/common/src/contracts/entities/auth-types.ts @@ -0,0 +1,7 @@ +import BigNumber from 'bignumber.js'; + +export interface TransferData { + newOperators: string[]; + newWeights: BigNumber[]; + newThreshold: BigNumber; +} diff --git a/libs/common/src/contracts/gateway.contract.spec.ts b/libs/common/src/contracts/gateway.contract.spec.ts index f342c1b..4746f11 100644 --- a/libs/common/src/contracts/gateway.contract.spec.ts +++ b/libs/common/src/contracts/gateway.contract.spec.ts @@ -2,24 +2,30 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test } from '@nestjs/testing'; import { BinaryUtils } from '@multiversx/sdk-nestjs-common'; import { Events } from '@mvx-monorepo/common/utils/event.enum'; -import { AbiRegistry, Address, ResultsParser, SmartContract } from '@multiversx/sdk-core/out'; +import { AbiRegistry, Address, BinaryCodec, ResultsParser, SmartContract } from '@multiversx/sdk-core/out'; import { GatewayContract } from '@mvx-monorepo/common/contracts/gateway.contract'; import { TransactionEvent } from '@multiversx/sdk-network-providers/out'; import { NotifierEvent } from '../../../../apps/mvx-event-processor/src/event-processor/types'; import gatewayAbi from '../assets/gateway.abi.json'; +import authAbi from '../assets/auth.abi.json'; +import { AuthContract } from '@mvx-monorepo/common/contracts/auth.contract'; +import BigNumber from 'bignumber.js'; +import { TransactionEventData } from '@multiversx/sdk-network-providers/out/transactionEvents'; describe('GatewayContract', () => { let smartContract: DeepMocked; let abi: AbiRegistry; let resultsParser: ResultsParser; + let authContract: AuthContract; let contract: GatewayContract; beforeEach(async () => { smartContract = createMock(); abi = AbiRegistry.create(gatewayAbi); // use real Gateway contract abi resultsParser = new ResultsParser(); + authContract = new AuthContract(AbiRegistry.create(authAbi), new BinaryCodec()); const moduleRef = await Test.createTestingModule({ providers: [GatewayContract], @@ -37,6 +43,10 @@ describe('GatewayContract', () => { return resultsParser; } + if (token === AuthContract) { + return authContract; + } + return null; }) .compile(); @@ -127,6 +137,34 @@ describe('GatewayContract', () => { }); }); + describe('decodeOperatorshipTransferredEvent', () => { + const rawEvent: NotifierEvent = { + txHash: 'txHash', + address: 'mockGatewayAddress', + identifier: 'execute', + data: Buffer.from( + '000000018049d639e5a6980d1cd2392abcce41029cda74a1563523a202f09641cc2618f80000000100000001020000000102', + 'hex', + ).toString('base64'), + topics: [], + }; + const event = TransactionEvent.fromHttpResponse(rawEvent); + + it('Should decode event', () => { + const result = contract.decodeOperatorshipTransferredEvent(event); + + expect(result.newOperators).toEqual(['8049d639e5a6980d1cd2392abcce41029cda74a1563523a202f09641cc2618f8']); + expect(result.newWeights).toEqual([new BigNumber('2')]); + expect(result.newThreshold).toEqual(new BigNumber('2')); + }); + + it('Should throw error while decoding', () => { + event.dataPayload = new TransactionEventData(new Buffer('')); + + expect(() => contract.decodeOperatorshipTransferredEvent(event)).toThrow(); + }); + }); + describe('decodeContractCallExecutedEvent', () => { const rawEvent: NotifierEvent = { txHash: 'txHash', diff --git a/libs/common/src/contracts/gateway.contract.ts b/libs/common/src/contracts/gateway.contract.ts index c8864ab..a3211e2 100644 --- a/libs/common/src/contracts/gateway.contract.ts +++ b/libs/common/src/contracts/gateway.contract.ts @@ -1,23 +1,20 @@ import { AbiRegistry, BytesValue, IAddress, ResultsParser, SmartContract, Transaction } from '@multiversx/sdk-core/out'; -import { Injectable, Logger } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { Events } from '../utils/event.enum'; import { TransactionEvent } from '@multiversx/sdk-network-providers/out'; import { ContractCallApprovedEvent, ContractCallEvent } from '@mvx-monorepo/common/contracts/entities/gateway-events'; import { DecodingUtils } from '@mvx-monorepo/common/utils/decoding.utils'; +import { TransferData } from '@mvx-monorepo/common/contracts/entities/auth-types'; +import { AuthContract } from '@mvx-monorepo/common/contracts/auth.contract'; @Injectable() export class GatewayContract { - // @ts-ignore - private readonly logger: Logger; - constructor( - // @ts-ignore private readonly smartContract: SmartContract, private readonly abi: AbiRegistry, private readonly resultsParser: ResultsParser, - ) { - this.logger = new Logger(GatewayContract.name); - } + private readonly authContract: AuthContract, + ) {} buildExecuteTransaction(executeData: Uint8Array, sender: IAddress): Transaction { return this.smartContract.methodsExplicit @@ -54,6 +51,13 @@ export class GatewayContract { }; } + decodeOperatorshipTransferredEvent(event: TransactionEvent): TransferData { + const eventDefinition = this.abi.getEvent(Events.OPERATORSHIP_TRANSFERRED_EVENT); + const outcome = this.resultsParser.parseEvent(event, eventDefinition); + + return this.authContract.decodeTransferData(outcome.params); + } + decodeContractCallExecutedEvent(event: TransactionEvent): string { const eventDefinition = this.abi.getEvent(Events.CONTRACT_CALL_EXECUTED_EVENT); const outcome = this.resultsParser.parseEvent(event, eventDefinition); diff --git a/libs/common/src/contracts/transactions.helper.ts b/libs/common/src/contracts/transactions.helper.ts index b385706..5fd24bb 100644 --- a/libs/common/src/contracts/transactions.helper.ts +++ b/libs/common/src/contracts/transactions.helper.ts @@ -79,13 +79,13 @@ export class TransactionsHelper { } } - async awaitComplete(txHash: string) { + async awaitSuccess(txHash: string) { try { const result = await this.transactionWatcher.awaitCompleted({ getHash: () => new TransactionHash(txHash) }); return !result.status.isFailed() && !result.status.isInvalid(); } catch (e) { - this.logger.error(`Can not await transaction completed`); + this.logger.error(`Can not await transaction success`); this.logger.error(e); return false; diff --git a/libs/common/src/database/repository/contract-call-approved.repository.ts b/libs/common/src/database/repository/contract-call-approved.repository.ts index 43eae7b..ff17b13 100644 --- a/libs/common/src/database/repository/contract-call-approved.repository.ts +++ b/libs/common/src/database/repository/contract-call-approved.repository.ts @@ -61,4 +61,15 @@ export class ContractCallApprovedRepository { }), ); } + + async updateStatus(data: ContractCallApproved) { + await this.prisma.contractCallApproved.update({ + where: { + commandId: data.commandId, + }, + data: { + status: data.status, + }, + }); + } } diff --git a/libs/common/src/database/repository/contract-call-event.repository.ts b/libs/common/src/database/repository/contract-call-event.repository.ts index d70baca..84898b4 100644 --- a/libs/common/src/database/repository/contract-call-event.repository.ts +++ b/libs/common/src/database/repository/contract-call-event.repository.ts @@ -40,11 +40,11 @@ export class ContractCallEventRepository { } findPending(txHash: string, eventIndex: number): Promise { - return this.prisma.contractCallEvent.findUnique({ + return this.prisma.contractCallEvent.findFirst({ where: { - status: ContractCallEventStatus.PENDING, txHash, eventIndex, + status: ContractCallEventStatus.PENDING, }, include: { gasPaidEntries: true, diff --git a/libs/common/src/grpc/grpc.service.ts b/libs/common/src/grpc/grpc.service.ts index 3960900..0020997 100644 --- a/libs/common/src/grpc/grpc.service.ts +++ b/libs/common/src/grpc/grpc.service.ts @@ -2,8 +2,14 @@ import { Inject, Injectable, OnModuleInit } from '@nestjs/common'; import { ProviderKeys } from '@mvx-monorepo/common/utils/provider.enum'; import { ClientGrpc } from '@nestjs/microservices'; import { ContractCallEvent } from '@prisma/client'; -import { Relayer, SubscribeToApprovalsResponse, VerifyRequest } from '@mvx-monorepo/common/grpc/entities/relayer'; +import { + BroadcastRequest, + Relayer, + SubscribeToApprovalsResponse, + VerifyRequest, +} from '@mvx-monorepo/common/grpc/entities/relayer'; import { firstValueFrom, Observable, ReplaySubject } from 'rxjs'; +import BigNumber from 'bignumber.js'; const RELAYER_SERVICE = 'Relayer'; @@ -46,13 +52,31 @@ export class GrpcService implements OnModuleInit { return Buffer.from(result.payload); } - subscribeToApprovals( - chain: string, - startHeight?: number | undefined, - ): Observable { + subscribeToApprovals(chain: string, startHeight?: number | undefined): Observable { return this.relayerService.subscribeToApprovals({ chain, startHeight, }); } + + async verifyWorkerSet(messageId: string, newOperators: string[], newWeights: BigNumber[], newThreshold: BigNumber) { + // TODO: This is probably not right... + const weightsByAddresses = newOperators.reduce((previousValue, operator, currentIndex) => { + previousValue.push({ operator, weight: newWeights[currentIndex] }); + + return previousValue; + }, []); + + const request: BroadcastRequest = { + address: 'TODO', + payload: Buffer.concat([ + Buffer.from(messageId, 'hex'), + Buffer.from(weightsByAddresses), + Buffer.from(newThreshold), + ]), + }; + + // TODO: Check with Axelar how it is best to handle this + await this.relayerService.broadcast(request); + } } diff --git a/libs/common/src/utils/event.enum.ts b/libs/common/src/utils/event.enum.ts index 10a1766..f47a602 100644 --- a/libs/common/src/utils/event.enum.ts +++ b/libs/common/src/utils/event.enum.ts @@ -7,6 +7,7 @@ export enum EventIdentifiers { export enum Events { CONTRACT_CALL_EVENT = 'contract_call_event', CONTRACT_CALL_APPROVED_EVENT = 'contract_call_approved_event', + OPERATORSHIP_TRANSFERRED_EVENT = 'operatorship_transferred_event', CONTRACT_CALL_EXECUTED_EVENT = 'contract_call_executed_event', GAS_PAID_FOR_CONTRACT_CALL_EVENT = 'gas_paid_for_contract_call_event', diff --git a/prisma/migrations/20240130085712_contract_call_event_unique_constraint/migration.sql b/prisma/migrations/20240130085712_contract_call_event_unique_constraint/migration.sql new file mode 100644 index 0000000..ed09d1e --- /dev/null +++ b/prisma/migrations/20240130085712_contract_call_event_unique_constraint/migration.sql @@ -0,0 +1,11 @@ +/* + Warnings: + + - A unique constraint covering the columns `[txHash,eventIndex]` on the table `ContractCallEvent` will be added. If there are existing duplicate values, this will fail. + +*/ +-- DropIndex +DROP INDEX "ContractCallEvent_txHash_eventIndex_idx"; + +-- CreateIndex +CREATE UNIQUE INDEX "ContractCallEvent_txHash_eventIndex_key" ON "ContractCallEvent"("txHash", "eventIndex"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 9f381dc..36a7165 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -26,7 +26,7 @@ model ContractCallEvent { updatedAt DateTime @default(now()) @updatedAt @db.Timestamp(6) gasPaidEntries GasPaid[] - @@index([txHash, eventIndex]) + @@unique([txHash, eventIndex]) @@index([sourceAddress, payloadHash]) } From cfb3e6c473b30614d31888955700c39ba0919dcf Mon Sep 17 00:00:00 2001 From: Rares <6453351+raress96@users.noreply.github.com> Date: Fri, 2 Feb 2024 14:53:43 +0200 Subject: [PATCH 26/33] Properly implement operatorship transferred event handling and grpc refactoring. --- .env.example | 3 + .env.test | 3 + .../contract-call-event.processor.module.ts | 10 ++++ .../contract-call-event.processor.service.ts | 57 +++++++++++++++++++ .../contract-call-event-processor/index.ts | 2 + .../src/mvx-event-processor.module.ts | 8 ++- .../processors/gas-service.processor.spec.ts | 6 +- .../src/processors/gas-service.processor.ts | 2 +- .../src/processors/gateway.processor.ts | 28 ++++++--- libs/common/src/config/api.config.service.ts | 9 +++ .../src/contracts/transactions.helper.ts | 2 +- .../contract-call-event.repository.ts | 31 +++++++++- libs/common/src/grpc/grpc.service.ts | 47 +++++++++------ 13 files changed, 177 insertions(+), 31 deletions(-) create mode 100644 apps/mvx-event-processor/src/contract-call-event-processor/contract-call-event.processor.module.ts create mode 100644 apps/mvx-event-processor/src/contract-call-event-processor/contract-call-event.processor.service.ts create mode 100644 apps/mvx-event-processor/src/contract-call-event-processor/index.ts diff --git a/.env.example b/.env.example index c5c8c18..306e8a5 100644 --- a/.env.example +++ b/.env.example @@ -9,9 +9,12 @@ EVENTS_NOTIFIER_QUEUE=queue CONTRACT_GATEWAY= CONTRACT_GAS_SERVICE= +CONTRACT_ITS= CONTRACT_WEGLD_SWAP=erd1qqqqqqqqqqqqqpgqpv09kfzry5y4sj05udcngesat07umyj70n4sa2c0rp +AXELAR_CONTRACT_VOTING_VERIFIER= + AXELAR_API_URL= CHAIN_ID=D diff --git a/.env.test b/.env.test index e72f536..17fd585 100644 --- a/.env.test +++ b/.env.test @@ -9,9 +9,12 @@ EVENTS_NOTIFIER_QUEUE=events-2cf3b817 CONTRACT_GATEWAY=erd1qqqqqqqqqqqqqpgqvc7gdl0p4s97guh498wgz75k8sav6sjfjlwqh679jy CONTRACT_GAS_SERVICE=erd1qqqqqqqqqqqqqpgqhe8t5jewej70zupmh44jurgn29psua5l2jps3ntjj3 +CONTRACT_ITS=erd1qqqqqqqqqqqqqpgqxwakt2g7u9atsnr03gqcgmhcv38pt7mkd94q6shuwt CONTRACT_WEGLD_SWAP=erd1qqqqqqqqqqqqqpgqhe8t5jewej70zupmh44jurgn29psua5l2jps3ntjj3 +AXELAR_CONTRACT_VOTING_VERIFIER=axelar1jjtc5zemkt9tn6nfvk78xl6f8svrrsvyqcdcca + AXELAR_API_URL=localhost:5000 CHAIN_ID=test diff --git a/apps/mvx-event-processor/src/contract-call-event-processor/contract-call-event.processor.module.ts b/apps/mvx-event-processor/src/contract-call-event-processor/contract-call-event.processor.module.ts new file mode 100644 index 0000000..d4724b1 --- /dev/null +++ b/apps/mvx-event-processor/src/contract-call-event-processor/contract-call-event.processor.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { ScheduleModule } from '@nestjs/schedule'; +import { DatabaseModule, GrpcModule } from '@mvx-monorepo/common'; +import { ContractCallEventProcessorService } from './contract-call-event.processor.service'; + +@Module({ + imports: [ScheduleModule.forRoot(), DatabaseModule, GrpcModule], + providers: [ContractCallEventProcessorService], +}) +export class ContractCallEventProcessorModule {} diff --git a/apps/mvx-event-processor/src/contract-call-event-processor/contract-call-event.processor.service.ts b/apps/mvx-event-processor/src/contract-call-event-processor/contract-call-event.processor.service.ts new file mode 100644 index 0000000..23abfb5 --- /dev/null +++ b/apps/mvx-event-processor/src/contract-call-event-processor/contract-call-event.processor.service.ts @@ -0,0 +1,57 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { Cron } from '@nestjs/schedule'; +import { Locker } from '@multiversx/sdk-nestjs-common'; +import { ContractCallEventStatus } from '@prisma/client'; +import { GrpcService } from '@mvx-monorepo/common'; +import { ContractCallEventRepository } from '@mvx-monorepo/common/database/repository/contract-call-event.repository'; +import { firstValueFrom } from 'rxjs'; + +@Injectable() +export class ContractCallEventProcessorService { + private readonly logger: Logger; + + constructor( + private readonly contractCallEventRepository: ContractCallEventRepository, + private readonly grpcService: GrpcService, + ) { + this.logger = new Logger(ContractCallEventProcessorService.name); + } + + @Cron('15 */2 * * * *') + async processPendingContractCallEvent() { + await Locker.lock('processPendingContractCallEvent', async () => { + this.logger.debug('Running processPendingContractCallEvent cron'); + + let page = 0; + let entries; + while ((entries = await this.contractCallEventRepository.findPending(page))?.length) { + this.logger.log(`Found ${entries.length} ContractCallEvent transactions to execute`); + + for (const contractCallEvent of entries) { + this.logger.debug(`Trying to verify ContractCallEvent with id ${contractCallEvent.id}`); + + try { + const value = await firstValueFrom(this.grpcService.verify(contractCallEvent)); + + if (value.success) { + contractCallEvent.status = ContractCallEventStatus.APPROVED; + } else { + contractCallEvent.status = ContractCallEventStatus.FAILED; + + this.logger.error(`Verify contract call event ${contractCallEvent.id} was not successful`); + } + } catch (e) { + this.logger.error(`Could not verify contract call event ${contractCallEvent.id}`); + this.logger.error(e); + + contractCallEvent.status = ContractCallEventStatus.FAILED; + } + + await this.contractCallEventRepository.updateStatus(contractCallEvent); + } + + page++; + } + }); + } +} diff --git a/apps/mvx-event-processor/src/contract-call-event-processor/index.ts b/apps/mvx-event-processor/src/contract-call-event-processor/index.ts new file mode 100644 index 0000000..5ecf6d3 --- /dev/null +++ b/apps/mvx-event-processor/src/contract-call-event-processor/index.ts @@ -0,0 +1,2 @@ +export * from './contract-call-event.processor.module'; +export * from './contract-call-event.processor.service'; diff --git a/apps/mvx-event-processor/src/mvx-event-processor.module.ts b/apps/mvx-event-processor/src/mvx-event-processor.module.ts index 0046acc..811da17 100644 --- a/apps/mvx-event-processor/src/mvx-event-processor.module.ts +++ b/apps/mvx-event-processor/src/mvx-event-processor.module.ts @@ -2,8 +2,14 @@ import { Module } from '@nestjs/common'; import { EventProcessorModule } from './event-processor'; import { CallContractApprovedProcessorModule } from './call-contract-approved-processor'; import { GasCheckerModule } from './gas-checker/gas-checker.module'; +import { ContractCallEventProcessorModule } from './contract-call-event-processor'; @Module({ - imports: [EventProcessorModule, CallContractApprovedProcessorModule, GasCheckerModule], + imports: [ + EventProcessorModule, + CallContractApprovedProcessorModule, + GasCheckerModule, + ContractCallEventProcessorModule, + ], }) export class MvxEventProcessorModule {} diff --git a/apps/mvx-event-processor/src/processors/gas-service.processor.spec.ts b/apps/mvx-event-processor/src/processors/gas-service.processor.spec.ts index fd8b9eb..af1f400 100644 --- a/apps/mvx-event-processor/src/processors/gas-service.processor.spec.ts +++ b/apps/mvx-event-processor/src/processors/gas-service.processor.spec.ts @@ -188,12 +188,12 @@ describe('GasServiceProcessor', () => { contractCallEvent: any = null, extraGasPaid: any = null, ) { - contractCallEventRepository.findPending.mockReturnValueOnce(Promise.resolve(contractCallEvent)); + contractCallEventRepository.findOnePending.mockReturnValueOnce(Promise.resolve(contractCallEvent)); await service.handleEvent(rawEvent); - expect(contractCallEventRepository.findPending).toHaveBeenCalledTimes(1); - expect(contractCallEventRepository.findPending).toHaveBeenCalledWith(event.txHash, event.logIndex); + expect(contractCallEventRepository.findOnePending).toHaveBeenCalledTimes(1); + expect(contractCallEventRepository.findOnePending).toHaveBeenCalledWith(event.txHash, event.logIndex); if (!extraGasPaid) { expect(gasPaidRepository.update).not.toHaveBeenCalled(); diff --git a/apps/mvx-event-processor/src/processors/gas-service.processor.ts b/apps/mvx-event-processor/src/processors/gas-service.processor.ts index aac9ae6..efa32d2 100644 --- a/apps/mvx-event-processor/src/processors/gas-service.processor.ts +++ b/apps/mvx-event-processor/src/processors/gas-service.processor.ts @@ -98,7 +98,7 @@ export class GasServiceProcessor implements ProcessorInterface { } async handleGasAddedEvents(event: GasAddedEvent, rawEventTxHash: string) { - const contractCallEvent = await this.contractCallEventRepository.findPending(event.txHash, event.logIndex); + const contractCallEvent = await this.contractCallEventRepository.findOnePending(event.txHash, event.logIndex); if (!contractCallEvent) { this.logger.warn('Received a GasAddedEvent but could find existing contract call entry'); diff --git a/apps/mvx-event-processor/src/processors/gateway.processor.ts b/apps/mvx-event-processor/src/processors/gateway.processor.ts index f3ebe86..6aae261 100644 --- a/apps/mvx-event-processor/src/processors/gateway.processor.ts +++ b/apps/mvx-event-processor/src/processors/gateway.processor.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; import { NotifierEvent } from '../event-processor/types'; import { GatewayContract } from '@mvx-monorepo/common/contracts/gateway.contract'; import { TransactionEvent } from '@multiversx/sdk-network-providers/out'; @@ -17,6 +17,7 @@ const UNSUPPORTED_LOG_INDEX: number = 0; @Injectable() export class GatewayProcessor implements ProcessorInterface { + private readonly logger: Logger; private readonly sourceChain: string; constructor( @@ -26,6 +27,7 @@ export class GatewayProcessor implements ProcessorInterface { private readonly grpcService: GrpcService, apiConfigService: ApiConfigService, ) { + this.logger = new Logger(GatewayProcessor.name); this.sourceChain = apiConfigService.getSourceChainName(); } @@ -80,13 +82,23 @@ export class GatewayProcessor implements ProcessorInterface { return; } - // TODO: Should this be batched instead and have this in a separate cronjob? - await this.grpcService.verify(contractCallEvent); - // TODO: We should mark here the message as successfull after sending to grpc - // Maybe this sending should be async in a cron? - // For now the ContractCallEvent in db will remain as PENDING if it was not successfully sent to the Relayer API - // Verify endpoint. After it was sent, it can be marked as APPROVED - // GasPaid will remain as PENDING status for now + // TODO: Test if this works correctly + this.grpcService.verify(contractCallEvent).subscribe({ + next: async (value) => { + if (value.success) { + contractCallEvent.status = ContractCallEventStatus.APPROVED; + + await this.contractCallEventRepository.updateStatus(contractCallEvent); + + return; + } + + this.logger.warn(`Verify contract call event ${id} was not successful. Will be retried.`); + }, + error: () => { + this.logger.warn(`Could not verify contract call event ${id}. Will be retried.`); + }, + }); } private async handleContractCallApprovedEvent(rawEvent: NotifierEvent) { diff --git a/libs/common/src/config/api.config.service.ts b/libs/common/src/config/api.config.service.ts index cf08252..dbc3a75 100644 --- a/libs/common/src/config/api.config.service.ts +++ b/libs/common/src/config/api.config.service.ts @@ -85,6 +85,15 @@ export class ApiConfigService { return contractWegldSwap; } + getAxelarContractVotingVerifier(): string { + const axelarContractVotingVerifier = this.configService.get('AXELAR_CONTRACT_VOTING_VERIFIER'); + if (!axelarContractVotingVerifier) { + throw new Error('No Axelar Contract Voting Verifier present'); + } + + return axelarContractVotingVerifier; + } + getAxelarApiUrl(): string { const axelarApiUrl = this.configService.get('AXELAR_API_URL'); if (!axelarApiUrl) { diff --git a/libs/common/src/contracts/transactions.helper.ts b/libs/common/src/contracts/transactions.helper.ts index 5fd24bb..6a36c52 100644 --- a/libs/common/src/contracts/transactions.helper.ts +++ b/libs/common/src/contracts/transactions.helper.ts @@ -27,7 +27,7 @@ export class TransactionsHelper { return accountOnNetwork.nonce; } - // TODO: Check if this works properly + // TODO: Test if this works correctly async getTransactionGas(transaction: Transaction, retry: number): Promise { transaction.setChainID(this.chainId); diff --git a/libs/common/src/database/repository/contract-call-event.repository.ts b/libs/common/src/database/repository/contract-call-event.repository.ts index 84898b4..b31f5d2 100644 --- a/libs/common/src/database/repository/contract-call-event.repository.ts +++ b/libs/common/src/database/repository/contract-call-event.repository.ts @@ -39,7 +39,7 @@ export class ContractCallEventRepository { }); } - findPending(txHash: string, eventIndex: number): Promise { + findOnePending(txHash: string, eventIndex: number): Promise { return this.prisma.contractCallEvent.findFirst({ where: { txHash, @@ -51,4 +51,33 @@ export class ContractCallEventRepository { }, }); } + + findPending(page: number = 0, take: number = 10): Promise { + // Last updated more than two minute ago, if retrying + const lastUpdatedAt = new Date(new Date().getTime() - 120_000); + + return this.prisma.contractCallEvent.findMany({ + where: { + status: ContractCallEventStatus.PENDING, + + updatedAt: { + lt: lastUpdatedAt, + }, + }, + orderBy: [{ createdAt: 'asc' }], + skip: page * take, + take, + }); + } + + async updateStatus(data: ContractCallEvent) { + await this.prisma.contractCallEvent.update({ + where: { + id: data.id, + }, + data: { + status: data.status, + }, + }); + } } diff --git a/libs/common/src/grpc/grpc.service.ts b/libs/common/src/grpc/grpc.service.ts index 0020997..ea35a47 100644 --- a/libs/common/src/grpc/grpc.service.ts +++ b/libs/common/src/grpc/grpc.service.ts @@ -8,23 +8,32 @@ import { SubscribeToApprovalsResponse, VerifyRequest, } from '@mvx-monorepo/common/grpc/entities/relayer'; -import { firstValueFrom, Observable, ReplaySubject } from 'rxjs'; +import { first, Observable, ReplaySubject, timeout } from 'rxjs'; import BigNumber from 'bignumber.js'; +import { ApiConfigService } from '@mvx-monorepo/common/config'; const RELAYER_SERVICE = 'Relayer'; +const VERIFY_TIMEOUT = 30_000; // TODO: Check if this timeout is enough + @Injectable() export class GrpcService implements OnModuleInit { // @ts-ignore private relayerService: Relayer; + private readonly axelarContractVotingVerifier: string; - constructor(@Inject(ProviderKeys.AXELAR_GRPC_CLIENT) private readonly client: ClientGrpc) {} + constructor( + @Inject(ProviderKeys.AXELAR_GRPC_CLIENT) private readonly client: ClientGrpc, + apiConfigService: ApiConfigService, + ) { + this.axelarContractVotingVerifier = apiConfigService.getAxelarContractVotingVerifier(); + } onModuleInit() { this.relayerService = this.client.getService(RELAYER_SERVICE); } - async verify(contractCallEvent: ContractCallEvent) { + verify(contractCallEvent: ContractCallEvent) { const replaySubject = new ReplaySubject(); replaySubject.next({ @@ -39,9 +48,7 @@ export class GrpcService implements OnModuleInit { }); replaySubject.complete(); - // TODO: Check if this works correctly - const result = this.relayerService.verify(replaySubject); - await firstValueFrom(result); + return this.relayerService.verify(replaySubject).pipe(first(), timeout(VERIFY_TIMEOUT)); } async getPayload(payloadHash: string): Promise { @@ -60,23 +67,31 @@ export class GrpcService implements OnModuleInit { } async verifyWorkerSet(messageId: string, newOperators: string[], newWeights: BigNumber[], newThreshold: BigNumber) { - // TODO: This is probably not right... - const weightsByAddresses = newOperators.reduce((previousValue, operator, currentIndex) => { - previousValue.push({ operator, weight: newWeights[currentIndex] }); + const weightsByAddresses: string[] = newOperators.reduce((previousValue, operator, currentIndex) => { + previousValue.push([operator, newWeights[currentIndex].toString()]); return previousValue; }, []); + // JSON format is used by CosmWasm contracts running on Axelar + const payload = Buffer.from( + JSON.stringify({ + verify_worker_set: { + message_id: messageId, + new_operators: { + weights_by_addresses: weightsByAddresses, + threshold: newThreshold.toString(), + }, + }, + }), + ); + const request: BroadcastRequest = { - address: 'TODO', - payload: Buffer.concat([ - Buffer.from(messageId, 'hex'), - Buffer.from(weightsByAddresses), - Buffer.from(newThreshold), - ]), + address: this.axelarContractVotingVerifier, + payload, }; - // TODO: Check with Axelar how it is best to handle this + // TODO: Should we add a retry mechanism here? await this.relayerService.broadcast(request); } } From a3782f22300c4507755080725d61ff02b42d92cf Mon Sep 17 00:00:00 2001 From: Rares <6453351+raress96@users.noreply.github.com> Date: Fri, 2 Feb 2024 15:44:41 +0200 Subject: [PATCH 27/33] Add test for call contract approved processor. --- .../src/processors/gateway.processor.spec.ts | 50 ++++- ...ll-contract-approved.processor.e2e-spec.ts | 1 - .../contract-call-event.processor.e2e-spec.ts | 176 ++++++++++++++++++ .../test/testGrpc.module.ts | 9 + 4 files changed, 233 insertions(+), 3 deletions(-) create mode 100644 apps/mvx-event-processor/test/contract-call-event.processor.e2e-spec.ts create mode 100644 apps/mvx-event-processor/test/testGrpc.module.ts diff --git a/apps/mvx-event-processor/src/processors/gateway.processor.spec.ts b/apps/mvx-event-processor/src/processors/gateway.processor.spec.ts index 46de1ba..49d5294 100644 --- a/apps/mvx-event-processor/src/processors/gateway.processor.spec.ts +++ b/apps/mvx-event-processor/src/processors/gateway.processor.spec.ts @@ -13,6 +13,8 @@ import { GatewayContract } from '@mvx-monorepo/common/contracts/gateway.contract import { ContractCallApprovedEvent, ContractCallEvent } from '@mvx-monorepo/common/contracts/entities/gateway-events'; import { TransactionEvent } from '@multiversx/sdk-network-providers/out'; import { ContractCallApprovedRepository } from '@mvx-monorepo/common/database/repository/contract-call-approved.repository'; +import { Subject } from 'rxjs'; +import { VerifyResponse } from '@mvx-monorepo/common/grpc/entities/relayer'; describe('ContractCallProcessor', () => { let gatewayContract: DeepMocked; @@ -122,7 +124,10 @@ describe('ContractCallProcessor', () => { ], }; - it('Should handle event', async () => { + it('Should handle event success', async () => { + const observable = new Subject(); + grpcService.verify.mockReturnValueOnce(observable); + await service.handleEvent(rawEvent); expect(gatewayContract.decodeContractCallEvent).toHaveBeenCalledTimes(1); @@ -141,6 +146,45 @@ describe('ContractCallProcessor', () => { payload: Buffer.from('payload'), }); expect(grpcService.verify).toHaveBeenCalledTimes(1); + + observable.next({ + message: undefined, + success: true, + }); + observable.complete(); + + expect(contractCallEventRepository.updateStatus).toHaveBeenCalledTimes(1); + expect(contractCallEventRepository.updateStatus).toHaveBeenCalledWith({ + id: 'multiversx-test:txHash:0', + txHash: 'txHash', + eventIndex: 0, + status: ContractCallEventStatus.APPROVED, + sourceAddress: 'erd1qqqqqqqqqqqqqpgqzqvm5ywqqf524efwrhr039tjs29w0qltkklsa05pk7', + sourceChain: 'multiversx-test', + destinationAddress: 'destinationAddress', + destinationChain: 'ethereum', + payloadHash: 'ebc84cbd75ba5516bf45e7024a9e12bc3c5c880f73e3a5beca7ebba52b2867a7', + payload: Buffer.from('payload'), + }); + }); + + it('Should handle error success', async () => { + const observable = new Subject(); + grpcService.verify.mockReturnValueOnce(observable); + + await service.handleEvent(rawEvent); + + expect(gatewayContract.decodeContractCallEvent).toHaveBeenCalledTimes(1); + expect(contractCallEventRepository.create).toHaveBeenCalledTimes(1); + expect(grpcService.verify).toHaveBeenCalledTimes(1); + + observable.next({ + message: undefined, + success: false, + }); + observable.complete(); + + expect(contractCallEventRepository.updateStatus).not.toHaveBeenCalled(); }); it('Should not handle duplicate', async () => { @@ -243,7 +287,9 @@ describe('ContractCallProcessor', () => { await service.handleEvent(rawEvent); expect(gatewayContract.decodeOperatorshipTransferredEvent).toHaveBeenCalledTimes(1); - expect(gatewayContract.decodeOperatorshipTransferredEvent).toHaveBeenCalledWith(TransactionEvent.fromHttpResponse(rawEvent)); + expect(gatewayContract.decodeOperatorshipTransferredEvent).toHaveBeenCalledWith( + TransactionEvent.fromHttpResponse(rawEvent), + ); expect(grpcService.verifyWorkerSet).toHaveBeenCalledTimes(1); }); }); diff --git a/apps/mvx-event-processor/test/call-contract-approved.processor.e2e-spec.ts b/apps/mvx-event-processor/test/call-contract-approved.processor.e2e-spec.ts index 203ef0d..09b87bf 100644 --- a/apps/mvx-event-processor/test/call-contract-approved.processor.e2e-spec.ts +++ b/apps/mvx-event-processor/test/call-contract-approved.processor.e2e-spec.ts @@ -41,7 +41,6 @@ describe('CallContractApprovedProcessorService', () => { .compile(); cacheService = await moduleRef.get(CacheService); - // proxy = await moduleRef.get(ProxyNetworkProvider); prisma = await moduleRef.get(PrismaService); contractCallApprovedRepository = await moduleRef.get(ContractCallApprovedRepository); diff --git a/apps/mvx-event-processor/test/contract-call-event.processor.e2e-spec.ts b/apps/mvx-event-processor/test/contract-call-event.processor.e2e-spec.ts new file mode 100644 index 0000000..ae0d144 --- /dev/null +++ b/apps/mvx-event-processor/test/contract-call-event.processor.e2e-spec.ts @@ -0,0 +1,176 @@ +import { Test } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import { PrismaService } from '@mvx-monorepo/common/database/prisma.service'; +import { ContractCallEventRepository } from '@mvx-monorepo/common/database/repository/contract-call-event.repository'; +import { + ContractCallEventProcessorModule, + ContractCallEventProcessorService, +} from '../src/contract-call-event-processor'; +import { ContractCallEvent, ContractCallEventStatus } from '@prisma/client'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { GrpcModule, GrpcService } from '@mvx-monorepo/common'; +import { Subject } from 'rxjs'; +import { VerifyResponse } from '@mvx-monorepo/common/grpc/entities/relayer'; +import { TestGrpcModule } from './testGrpc.module'; + +describe('ContractCallEventProcessorService', () => { + let prisma: PrismaService; + let grpcService: DeepMocked; + let contractCallEventRepository: ContractCallEventRepository; + + let service: ContractCallEventProcessorService; + + let app: INestApplication; + + beforeEach(async () => { + grpcService = createMock(); + + const moduleRef = await Test.createTestingModule({ + imports: [ContractCallEventProcessorModule], + }) + .overrideModule(GrpcModule) + .useModule(TestGrpcModule) + .overrideProvider(GrpcService) + .useValue(grpcService) + .compile(); + + prisma = await moduleRef.get(PrismaService); + contractCallEventRepository = await moduleRef.get(ContractCallEventRepository); + + service = await moduleRef.get(ContractCallEventProcessorService); + + // Reset database + await prisma.contractCallEvent.deleteMany(); + + app = moduleRef.createNestApplication(); + await app.init(); + }); + + afterEach(async () => { + await prisma.$disconnect(); + + await app.close(); + }); + + const createContractCallEvent = async (extraData: Partial = {}): Promise => { + const result = await contractCallEventRepository.create({ + id: 'multiversx:txHash:0', + eventIndex: 0, + txHash: 'txHashA', + status: ContractCallEventStatus.PENDING, + sourceChain: 'multiversx', + sourceAddress: 'erd1qqqqqqqqqqqqqpgqzqvm5ywqqf524efwrhr039tjs29w0qltkklsa05pk7', + destinationAddress: 'destinationAddress', + destinationChain: 'ethereum', + payloadHash: 'ebc84cbd75ba5516bf45e7024a9e12bc3c5c880f73e3a5beca7ebba52b2867a7', + payload: Buffer.from('payload'), + executeTxHash: null, + updatedAt: new Date(new Date().getTime() - 120_500), + createdAt: new Date(new Date().getTime() - 120_500), + ...extraData, + }); + + if (!result) { + throw new Error('Can not create database entries'); + } + + return result; + }; + + it('Should process pending contract call event success', async () => { + const originalEntry = await createContractCallEvent(); + + const observable = new Subject(); + grpcService.verify.mockReturnValueOnce(observable); + + // Publish to observable with a delay + setTimeout(() => { + observable.next({ + message: undefined, + success: true, + }); + observable.complete(); + }, 500); + + try { + await service.processPendingContractCallEvent(); + } catch (e) { + // Locker.lock throws error for some reason, ignore + } + + expect(await contractCallEventRepository.findPending()).toEqual([]); + + const firstEntry = await prisma.contractCallEvent.findUnique({ + where: { + id: originalEntry.id, + }, + }); + expect(firstEntry).toEqual({ + ...originalEntry, + status: ContractCallEventStatus.APPROVED, + updatedAt: expect.any(Date), + }); + }); + + it('Should process pending contract call event not success', async () => { + const originalEntry = await createContractCallEvent(); + + const observable = new Subject(); + grpcService.verify.mockReturnValueOnce(observable); + + // Publish to observable with a delay + setTimeout(() => { + observable.next({ + message: undefined, + success: false, + }); + observable.complete(); + }, 500); + + try { + await service.processPendingContractCallEvent(); + } catch (e) { + // Locker.lock throws error for some reason, ignore + } + + expect(await contractCallEventRepository.findPending()).toEqual([]); + + const firstEntry = await prisma.contractCallEvent.findUnique({ + where: { + id: originalEntry.id, + }, + }); + expect(firstEntry).toEqual({ + ...originalEntry, + status: ContractCallEventStatus.FAILED, + updatedAt: expect.any(Date), + }); + }); + + it('Should process pending contract call event failed', async () => { + const originalEntry = await createContractCallEvent(); + + const observable = new Subject(); + grpcService.verify.mockReturnValueOnce(observable); + observable.complete(); + + try { + await service.processPendingContractCallEvent(); + } catch (e) { + // Locker.lock throws error for some reason, ignore + } + + expect(await contractCallEventRepository.findPending()).toEqual([]); + + const firstEntry = await prisma.contractCallEvent.findUnique({ + where: { + id: originalEntry.id, + }, + }); + expect(firstEntry).toEqual({ + ...originalEntry, + status: ContractCallEventStatus.FAILED, + updatedAt: expect.any(Date), + }); + }); +}); diff --git a/apps/mvx-event-processor/test/testGrpc.module.ts b/apps/mvx-event-processor/test/testGrpc.module.ts new file mode 100644 index 0000000..ed77d92 --- /dev/null +++ b/apps/mvx-event-processor/test/testGrpc.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { GrpcService } from '@mvx-monorepo/common'; + +@Module({ + imports: [], + providers: [GrpcService], + exports: [GrpcService], +}) +export class TestGrpcModule {} From a09ed64a55f6a4d49ced5e76511098d3f7d20dd5 Mon Sep 17 00:00:00 2001 From: Rares <6453351+raress96@users.noreply.github.com> Date: Fri, 16 Feb 2024 12:25:11 +0200 Subject: [PATCH 28/33] Add prisma deploy script. --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index e1adcca..2ab12f3 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "test:e2e": "dotenv -e .env.test -- jest --config ./apps/mvx-event-processor/test/jest-e2e.json --force-exit", "migrate": "prisma migrate dev", "generate": "prisma generate", + "deploy": "prisma migrate deploy", "test:migrate": "dotenv -e .env.test -- prisma migrate deploy" }, "dependencies": { From 104f7f760c6a64e9acdc25b228a5d3564059e8f2 Mon Sep 17 00:00:00 2001 From: Rares <6453351+raress96@users.noreply.github.com> Date: Tue, 26 Mar 2024 12:19:29 +0200 Subject: [PATCH 29/33] Updates for new amplifier api grpc definition. --- README.md | 6 +- .../approvals.processor.service.ts | 28 ++---- .../approvals.processor.spec.ts | 15 ++-- .../contract-call-event.processor.service.ts | 8 +- .../src/processors/gateway.processor.spec.ts | 67 ++++++++++++-- .../src/processors/gateway.processor.ts | 51 ++++++++--- .../contract-call-event.processor.e2e-spec.ts | 9 +- libs/common/src/assets/amplifier.proto | 89 +++++++++++++++++++ libs/common/src/assets/relayer.proto | 89 ------------------- libs/common/src/config/api.config.service.ts | 5 -- .../entities/{relayer.ts => amplifier.ts} | 35 +++++--- libs/common/src/grpc/grpc.module.ts | 4 +- libs/common/src/grpc/grpc.service.ts | 28 +++--- libs/common/src/utils/constants.enum.ts | 2 +- nest-cli.json | 2 +- 15 files changed, 252 insertions(+), 186 deletions(-) create mode 100644 libs/common/src/assets/amplifier.proto delete mode 100644 libs/common/src/assets/relayer.proto rename libs/common/src/grpc/entities/{relayer.ts => amplifier.ts} (82%) diff --git a/README.md b/README.md index bfa9fc0..5535b11 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ -Axelar Relayer for MultiversX blockchain +Axelar Relayer for MultiversX blockchain. + +Based on Amplifier API Docs: https://bright-ambert-2bd.notion.site/Amplifier-API-Docs-EXTERNAL-7c56c143852147cd95b1c4a949121851 ## Quick start @@ -42,7 +44,7 @@ protoc --plugin=./node_modules/.bin/protoc-gen-ts_proto\ --ts_proto_out=./libs/common/src/grpc/entities\ --proto_path=./libs/common/src/assets\ --ts_proto_opt="$(IFS=, ; echo "${TS_ARGS[*]}")"\ - ./libs/common/src/assets/relayer.proto + ./libs/common/src/assets/amplifier.proto ``` Check out these resources for more information: diff --git a/apps/axelar-event-processor/src/approvals-processor/approvals.processor.service.ts b/apps/axelar-event-processor/src/approvals-processor/approvals.processor.service.ts index 5881aa5..1e81072 100644 --- a/apps/axelar-event-processor/src/approvals-processor/approvals.processor.service.ts +++ b/apps/axelar-event-processor/src/approvals-processor/approvals.processor.service.ts @@ -3,21 +3,21 @@ import { Inject, Injectable, Logger } from '@nestjs/common'; import { Cron } from '@nestjs/schedule'; import { GrpcService } from '@mvx-monorepo/common/grpc/grpc.service'; import { RedisCacheService } from '@multiversx/sdk-nestjs-cache'; -import { ApiConfigService, CacheInfo } from '@mvx-monorepo/common'; +import { CacheInfo } from '@mvx-monorepo/common'; import { Subscription } from 'rxjs'; -import { SubscribeToApprovalsResponse } from '@mvx-monorepo/common/grpc/entities/relayer'; +import { SubscribeToApprovalsResponse } from '@mvx-monorepo/common/grpc/entities/amplifier'; import { ProviderKeys } from '@mvx-monorepo/common/utils/provider.enum'; import { UserSigner } from '@multiversx/sdk-wallet/out'; import { TransactionsHelper } from '@mvx-monorepo/common/contracts/transactions.helper'; import { GatewayContract } from '@mvx-monorepo/common/contracts/gateway.contract'; import { PendingTransaction } from './entities/pending-transaction'; +import { CONSTANTS } from '@mvx-monorepo/common/utils/constants.enum'; const MAX_NUMBER_OF_RETRIES = 3; @Injectable() export class ApprovalsProcessorService { private readonly logger: Logger; - private readonly sourceChain: string; private approvalsSubscription: Subscription | null = null; @@ -27,10 +27,8 @@ export class ApprovalsProcessorService { @Inject(ProviderKeys.WALLET_SIGNER) private readonly walletSigner: UserSigner, private readonly transactionsHelper: TransactionsHelper, private readonly gatewayContract: GatewayContract, - apiConfigService: ApiConfigService, ) { this.logger = new Logger(ApprovalsProcessorService.name); - this.sourceChain = apiConfigService.getSourceChainName(); } @Cron('*/30 * * * * *') @@ -55,7 +53,7 @@ export class ApprovalsProcessorService { const lastProcessedHeight = (await this.redisCacheService.get(CacheInfo.StartProcessHeight().key)) || undefined; - const observable = this.grpcService.subscribeToApprovals(this.sourceChain, lastProcessedHeight); + const observable = this.grpcService.subscribeToApprovals(CONSTANTS.SOURCE_CHAIN_NAME, lastProcessedHeight); const onComplete = () => { this.logger.warn('Approvals stream subscription ended'); @@ -133,25 +131,17 @@ export class ApprovalsProcessorService { this.logger.error('Error while processing Axelar Approvals response...'); this.logger.error(e); - // Set start process height to current block height + // Unsubscribe so processing stops at this event and is retried + this.approvalsSubscription?.unsubscribe(); + } finally { + // Set start process height to this block height to not lose any progress in case of unexpected issues + // It is safe to retry Gateway execute transaction that were already executed since the contract supports this await this.redisCacheService.set( CacheInfo.StartProcessHeight().key, response.blockHeight, CacheInfo.StartProcessHeight().ttl, ); - - // Unsubscribe so processing stops at this event and is retried - this.approvalsSubscription?.unsubscribe(); - - return; } - - // Set start process height to next block height. - await this.redisCacheService.set( - CacheInfo.StartProcessHeight().key, - response.blockHeight + 1, - CacheInfo.StartProcessHeight().ttl, - ); } private async executeTransaction(executeData: Uint8Array, retry: number = 0) { diff --git a/apps/axelar-event-processor/src/approvals-processor/approvals.processor.spec.ts b/apps/axelar-event-processor/src/approvals-processor/approvals.processor.spec.ts index 3b61aa3..fe6daaf 100644 --- a/apps/axelar-event-processor/src/approvals-processor/approvals.processor.spec.ts +++ b/apps/axelar-event-processor/src/approvals-processor/approvals.processor.spec.ts @@ -8,7 +8,7 @@ import { RedisCacheService } from '@multiversx/sdk-nestjs-cache'; import { UserSigner } from '@multiversx/sdk-wallet/out'; import { ProviderKeys } from '@mvx-monorepo/common/utils/provider.enum'; import { Subject } from 'rxjs'; -import { SubscribeToApprovalsResponse } from '@mvx-monorepo/common/grpc/entities/relayer'; +import { SubscribeToApprovalsResponse } from '@mvx-monorepo/common/grpc/entities/amplifier'; import { UserAddress } from '@multiversx/sdk-wallet/out/userAddress'; import { Transaction } from '@multiversx/sdk-core/out'; @@ -30,8 +30,6 @@ describe('ApprovalsProcessorService', () => { gatewayContract = createMock(); apiConfigService = createMock(); - apiConfigService.getSourceChainName.mockReturnValue('multiversx-test'); - const moduleRef = await Test.createTestingModule({ providers: [ApprovalsProcessorService], }) @@ -64,7 +62,6 @@ describe('ApprovalsProcessorService', () => { }) .compile(); - apiConfigService.getSourceChainName.mockReturnValueOnce('multiversx-test'); apiConfigService.getChainId.mockReturnValue('test'); redisCacheService.get.mockImplementation(() => { return Promise.resolve(undefined); @@ -85,7 +82,7 @@ describe('ApprovalsProcessorService', () => { expect(redisCacheService.get).toHaveBeenCalledTimes(1); expect(grpcService.subscribeToApprovals).toHaveBeenCalledTimes(1); - expect(grpcService.subscribeToApprovals).toHaveBeenCalledWith('multiversx-test', undefined); + expect(grpcService.subscribeToApprovals).toHaveBeenCalledWith('multiversx', undefined); const userAddress = UserAddress.fromBech32('erd1qqqqqqqqqqqqqpgqhe8t5jewej70zupmh44jurgn29psua5l2jps3ntjj3'); walletSigner.getAddress.mockReturnValueOnce(userAddress); @@ -98,7 +95,7 @@ describe('ApprovalsProcessorService', () => { // Process a message const message: SubscribeToApprovalsResponse = { - chain: 'multiversx-test', + chain: 'multiversx', executeData: Uint8Array.of(1, 2, 3, 4), blockHeight: 1, }; @@ -134,7 +131,7 @@ describe('ApprovalsProcessorService', () => { expect(redisCacheService.set).toHaveBeenCalledWith( CacheInfo.StartProcessHeight().key, - message.blockHeight + 1, // next block height + message.blockHeight, CacheInfo.StartProcessHeight().ttl, ); }); @@ -152,7 +149,7 @@ describe('ApprovalsProcessorService', () => { await service.handleNewApprovalsRaw(); // Process a message const message: SubscribeToApprovalsResponse = { - chain: 'multiversx-test', + chain: 'multiversx', executeData: Uint8Array.of(1, 2, 3, 4), blockHeight: 1, }; @@ -185,7 +182,7 @@ describe('ApprovalsProcessorService', () => { expect(redisCacheService.get).toHaveBeenCalledTimes(2); expect(grpcService.subscribeToApprovals).toHaveBeenCalledTimes(2); - expect(grpcService.subscribeToApprovals).toHaveBeenCalledWith('multiversx-test', 1); + expect(grpcService.subscribeToApprovals).toHaveBeenCalledWith('multiversx', 1); }); it('Should reinitialize subscription on complete or on error', async () => { diff --git a/apps/mvx-event-processor/src/contract-call-event-processor/contract-call-event.processor.service.ts b/apps/mvx-event-processor/src/contract-call-event-processor/contract-call-event.processor.service.ts index 23abfb5..ac9b76e 100644 --- a/apps/mvx-event-processor/src/contract-call-event-processor/contract-call-event.processor.service.ts +++ b/apps/mvx-event-processor/src/contract-call-event-processor/contract-call-event.processor.service.ts @@ -31,14 +31,16 @@ export class ContractCallEventProcessorService { this.logger.debug(`Trying to verify ContractCallEvent with id ${contractCallEvent.id}`); try { - const value = await firstValueFrom(this.grpcService.verify(contractCallEvent)); + const response = await firstValueFrom(this.grpcService.verify(contractCallEvent)); - if (value.success) { + if (!response.error) { contractCallEvent.status = ContractCallEventStatus.APPROVED; } else { contractCallEvent.status = ContractCallEventStatus.FAILED; - this.logger.error(`Verify contract call event ${contractCallEvent.id} was not successful`); + this.logger.error( + `Verify contract call event ${contractCallEvent.id} was not successful. Got error code ${response.error.errorCode}`, + ); } } catch (e) { this.logger.error(`Could not verify contract call event ${contractCallEvent.id}`); diff --git a/apps/mvx-event-processor/src/processors/gateway.processor.spec.ts b/apps/mvx-event-processor/src/processors/gateway.processor.spec.ts index 7d09bec..14c2f41 100644 --- a/apps/mvx-event-processor/src/processors/gateway.processor.spec.ts +++ b/apps/mvx-event-processor/src/processors/gateway.processor.spec.ts @@ -14,7 +14,7 @@ import { ContractCallApprovedEvent, ContractCallEvent } from '@mvx-monorepo/comm import { TransactionEvent } from '@multiversx/sdk-network-providers/out'; import { ContractCallApprovedRepository } from '@mvx-monorepo/common/database/repository/contract-call-approved.repository'; import { Subject } from 'rxjs'; -import { VerifyResponse } from '@mvx-monorepo/common/grpc/entities/relayer'; +import { ErrorCode, VerifyResponse } from '@mvx-monorepo/common/grpc/entities/amplifier'; describe('ContractCallProcessor', () => { let gatewayContract: DeepMocked; @@ -49,8 +49,6 @@ describe('ContractCallProcessor', () => { grpcService = createMock(); apiConfigService = createMock(); - apiConfigService.getSourceChainName.mockReturnValue('multiversx-test'); - const moduleRef = await Test.createTestingModule({ providers: [GatewayProcessor], }) @@ -134,12 +132,12 @@ describe('ContractCallProcessor', () => { expect(gatewayContract.decodeContractCallEvent).toHaveBeenCalledWith(TransactionEvent.fromHttpResponse(rawEvent)); expect(contractCallEventRepository.create).toHaveBeenCalledTimes(1); expect(contractCallEventRepository.create).toHaveBeenCalledWith({ - id: 'multiversx-test:txHash:0', + id: 'multiversx:txHash:0', txHash: 'txHash', eventIndex: 0, status: ContractCallEventStatus.PENDING, sourceAddress: 'erd1qqqqqqqqqqqqqpgqzqvm5ywqqf524efwrhr039tjs29w0qltkklsa05pk7', - sourceChain: 'multiversx-test', + sourceChain: 'multiversx', destinationAddress: 'destinationAddress', destinationChain: 'ethereum', payloadHash: 'ebc84cbd75ba5516bf45e7024a9e12bc3c5c880f73e3a5beca7ebba52b2867a7', @@ -149,18 +147,18 @@ describe('ContractCallProcessor', () => { observable.next({ message: undefined, - success: true, + error: undefined, }); observable.complete(); expect(contractCallEventRepository.updateStatus).toHaveBeenCalledTimes(1); expect(contractCallEventRepository.updateStatus).toHaveBeenCalledWith({ - id: 'multiversx-test:txHash:0', + id: 'multiversx:txHash:0', txHash: 'txHash', eventIndex: 0, status: ContractCallEventStatus.APPROVED, sourceAddress: 'erd1qqqqqqqqqqqqqpgqzqvm5ywqqf524efwrhr039tjs29w0qltkklsa05pk7', - sourceChain: 'multiversx-test', + sourceChain: 'multiversx', destinationAddress: 'destinationAddress', destinationChain: 'ethereum', payloadHash: 'ebc84cbd75ba5516bf45e7024a9e12bc3c5c880f73e3a5beca7ebba52b2867a7', @@ -180,13 +178,50 @@ describe('ContractCallProcessor', () => { observable.next({ message: undefined, - success: false, + error: { + error: 'error', + errorCode: ErrorCode.VERIFICATION_FAILED, + }, }); observable.complete(); expect(contractCallEventRepository.updateStatus).not.toHaveBeenCalled(); }); + it('Should handle unrecoverable error', async () => { + const observable = new Subject(); + grpcService.verify.mockReturnValueOnce(observable); + + await service.handleEvent(rawEvent); + + expect(gatewayContract.decodeContractCallEvent).toHaveBeenCalledTimes(1); + expect(contractCallEventRepository.create).toHaveBeenCalledTimes(1); + expect(grpcService.verify).toHaveBeenCalledTimes(1); + + observable.next({ + message: undefined, + error: { + error: 'error', + errorCode: ErrorCode.FAILED_ON_CHAIN, + }, + }); + observable.complete(); + + expect(contractCallEventRepository.updateStatus).toHaveBeenCalledTimes(1); + expect(contractCallEventRepository.updateStatus).toHaveBeenCalledWith({ + id: 'multiversx:txHash:0', + txHash: 'txHash', + eventIndex: 0, + status: ContractCallEventStatus.FAILED, + sourceAddress: 'erd1qqqqqqqqqqqqqpgqzqvm5ywqqf524efwrhr039tjs29w0qltkklsa05pk7', + sourceChain: 'multiversx', + destinationAddress: 'destinationAddress', + destinationChain: 'ethereum', + payloadHash: 'ebc84cbd75ba5516bf45e7024a9e12bc3c5c880f73e3a5beca7ebba52b2867a7', + payload: Buffer.from('payload'), + }); + }); + it('Should not handle duplicate', async () => { contractCallEventRepository.create.mockReturnValueOnce(Promise.resolve(null)); @@ -292,6 +327,20 @@ describe('ContractCallProcessor', () => { ); expect(grpcService.verifyWorkerSet).toHaveBeenCalledTimes(1); }); + + it('Should handle event error', async () => { + grpcService.verifyWorkerSet.mockReturnValueOnce(Promise.resolve({ + success: false, + })); + + await service.handleEvent(rawEvent); + + expect(gatewayContract.decodeOperatorshipTransferredEvent).toHaveBeenCalledTimes(1); + expect(gatewayContract.decodeOperatorshipTransferredEvent).toHaveBeenCalledWith( + TransactionEvent.fromHttpResponse(rawEvent), + ); + expect(grpcService.verifyWorkerSet).toHaveBeenCalledTimes(1); + }); }); describe('handleContractCallExecutedEvent', () => { diff --git a/apps/mvx-event-processor/src/processors/gateway.processor.ts b/apps/mvx-event-processor/src/processors/gateway.processor.ts index 61303aa..49bd8cb 100644 --- a/apps/mvx-event-processor/src/processors/gateway.processor.ts +++ b/apps/mvx-event-processor/src/processors/gateway.processor.ts @@ -3,13 +3,16 @@ import { NotifierEvent } from '../event-processor/types'; import { GatewayContract } from '@mvx-monorepo/common/contracts/gateway.contract'; import { TransactionEvent } from '@multiversx/sdk-network-providers/out'; import { ContractCallEventRepository } from '@mvx-monorepo/common/database/repository/contract-call-event.repository'; -import { ApiConfigService } from '@mvx-monorepo/common'; import { ContractCallApprovedStatus, ContractCallEventStatus } from '@prisma/client'; import { GrpcService } from '@mvx-monorepo/common/grpc/grpc.service'; import { ProcessorInterface } from './entities/processor.interface'; import { EventIdentifiers, Events } from '@mvx-monorepo/common/utils/event.enum'; import { BinaryUtils } from '@multiversx/sdk-nestjs-common'; -import { ContractCallApprovedRepository } from '@mvx-monorepo/common/database/repository/contract-call-approved.repository'; +import { + ContractCallApprovedRepository, +} from '@mvx-monorepo/common/database/repository/contract-call-approved.repository'; +import { ErrorCode } from '@mvx-monorepo/common/grpc/entities/amplifier'; +import { CONSTANTS } from '@mvx-monorepo/common/utils/constants.enum'; // order/logIndex is unsupported since we can't easily get it in the relayer, so we use 0 by default // this means that only one cross chain call is supported for now (the first appropriate call found in transaction logs) @@ -18,17 +21,14 @@ const UNSUPPORTED_LOG_INDEX: number = 0; @Injectable() export class GatewayProcessor implements ProcessorInterface { private readonly logger: Logger; - private readonly sourceChain: string; constructor( private readonly gatewayContract: GatewayContract, private readonly contractCallEventRepository: ContractCallEventRepository, private readonly contractCallApprovedRepository: ContractCallApprovedRepository, private readonly grpcService: GrpcService, - apiConfigService: ApiConfigService, ) { this.logger = new Logger(GatewayProcessor.name); - this.sourceChain = apiConfigService.getSourceChainName(); } async handleEvent(rawEvent: NotifierEvent) { @@ -63,14 +63,14 @@ export class GatewayProcessor implements ProcessorInterface { private async handleContractCallEvent(rawEvent: NotifierEvent) { const event = this.gatewayContract.decodeContractCallEvent(TransactionEvent.fromHttpResponse(rawEvent)); - const id = `${this.sourceChain}:${rawEvent.txHash}:${UNSUPPORTED_LOG_INDEX}`; + const id = `${CONSTANTS.SOURCE_CHAIN_NAME}:${rawEvent.txHash}:${UNSUPPORTED_LOG_INDEX}`; const contractCallEvent = await this.contractCallEventRepository.create({ id, txHash: rawEvent.txHash, eventIndex: UNSUPPORTED_LOG_INDEX, status: ContractCallEventStatus.PENDING, sourceAddress: event.sender.bech32(), - sourceChain: this.sourceChain, + sourceChain: CONSTANTS.SOURCE_CHAIN_NAME, destinationAddress: event.destinationAddress, destinationChain: event.destinationChain, payloadHash: event.data.payloadHash, @@ -84,12 +84,22 @@ export class GatewayProcessor implements ProcessorInterface { // TODO: Test if this works correctly this.grpcService.verify(contractCallEvent).subscribe({ - next: async (value) => { - if (value.success) { + next: async (response) => { + if (!response.error) { contractCallEvent.status = ContractCallEventStatus.APPROVED; await this.contractCallEventRepository.updateStatus(contractCallEvent); + return; + } else if (response.error.errorCode === ErrorCode.FAILED_ON_CHAIN) { + this.logger.error( + `Verify contract call event ${id} was not successful. Will NOT be retried. Got error code ${response.error.errorCode}`, + ); + + contractCallEvent.status = ContractCallEventStatus.FAILED; + + await this.contractCallEventRepository.updateStatus(contractCallEvent); + return; } @@ -128,14 +138,33 @@ export class GatewayProcessor implements ProcessorInterface { TransactionEvent.fromHttpResponse(rawEvent), ); - const id = `${this.sourceChain}:${rawEvent.txHash}:${UNSUPPORTED_LOG_INDEX}`; + const id = `${CONSTANTS.SOURCE_CHAIN_NAME}:${rawEvent.txHash}:${UNSUPPORTED_LOG_INDEX}`; - await this.grpcService.verifyWorkerSet( + const response = await this.grpcService.verifyWorkerSet( id, trasnsferData.newOperators, trasnsferData.newWeights, trasnsferData.newThreshold, ); + + if (response.success) { + return; + } + + this.logger.warn(`Couldn't dispatch verifyWorkerSet ${id} to Amplifier API. Retrying...`); + + setTimeout(async () => { + const response = await this.grpcService.verifyWorkerSet( + id, + trasnsferData.newOperators, + trasnsferData.newWeights, + trasnsferData.newThreshold, + ); + + if (!response.success) { + this.logger.error(`Couldn't dispatch verifyWorkerSet ${id} to Amplifier API.`); + } + }, 60_000); } private async handleContractCallExecutedEvent(rawEvent: NotifierEvent) { diff --git a/apps/mvx-event-processor/test/contract-call-event.processor.e2e-spec.ts b/apps/mvx-event-processor/test/contract-call-event.processor.e2e-spec.ts index ae0d144..30d99f5 100644 --- a/apps/mvx-event-processor/test/contract-call-event.processor.e2e-spec.ts +++ b/apps/mvx-event-processor/test/contract-call-event.processor.e2e-spec.ts @@ -10,7 +10,7 @@ import { ContractCallEvent, ContractCallEventStatus } from '@prisma/client'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { GrpcModule, GrpcService } from '@mvx-monorepo/common'; import { Subject } from 'rxjs'; -import { VerifyResponse } from '@mvx-monorepo/common/grpc/entities/relayer'; +import { ErrorCode, VerifyResponse } from '@mvx-monorepo/common/grpc/entities/amplifier'; import { TestGrpcModule } from './testGrpc.module'; describe('ContractCallEventProcessorService', () => { @@ -87,7 +87,7 @@ describe('ContractCallEventProcessorService', () => { setTimeout(() => { observable.next({ message: undefined, - success: true, + error: undefined, }); observable.complete(); }, 500); @@ -122,7 +122,10 @@ describe('ContractCallEventProcessorService', () => { setTimeout(() => { observable.next({ message: undefined, - success: false, + error: { + error: 'error', + errorCode: ErrorCode.VERIFICATION_FAILED, + }, }); observable.complete(); }, 500); diff --git a/libs/common/src/assets/amplifier.proto b/libs/common/src/assets/amplifier.proto new file mode 100644 index 0000000..425477d --- /dev/null +++ b/libs/common/src/assets/amplifier.proto @@ -0,0 +1,89 @@ +syntax = "proto3"; +import "google/api/annotations.proto"; + +service Amplifier { + rpc Verify(stream VerifyRequest) returns (stream VerifyResponse); + rpc GetPayload(GetPayloadRequest) returns (GetPayloadResponse) { + option (google.api.http) = { + get : "/v1beta1/payload/{hash}" + }; + } + rpc SubscribeToApprovals(SubscribeToApprovalsRequest) + returns (stream SubscribeToApprovalsResponse); + rpc SubscribeToWasmEvents(SubscribeToWasmEventsRequest) + returns (stream SubscribeToWasmEventsResponse); + rpc Broadcast(BroadcastRequest) returns (BroadcastResponse) { + option (google.api.http) = { + post : "/v1beta1/broadcast" + body : "*" + }; + } +} + +message Message { + string id = 1; // the unique identifier with which the message can be looked + // up on the source chain + string source_chain = 2; + string source_address = 3; + string destination_chain = 4; + string destination_address = 5; + bytes payload = 6; +} + +message GetPayloadRequest { bytes hash = 1; } + +message GetPayloadResponse { bytes payload = 1; } + +message SubscribeToApprovalsRequest { + repeated string chains = 1; + optional uint64 start_height = 2; // can be used to replay events +} + +message SubscribeToApprovalsResponse { + string chain = 1; + bytes execute_data = 2; + uint64 block_height = 3; +} + +message VerifyRequest { Message message = 1; } + +message VerifyResponse { + Message message = 1; + optional Error error = 2; +} + +enum ErrorCode { + VERIFICATION_FAILED = 0; + INTERNAL_ERROR = 1; + AXELAR_NETWORK_ERROR = 2; + INSUFFICIENT_GAS = 3; + FAILED_ON_CHAIN = 4; + MESSAGE_NOT_FOUND = 5; +} + +message Error { + string error = 1; + ErrorCode error_code = 2; +} + +message SubscribeToWasmEventsRequest { optional uint64 start_height = 1; } + +message SubscribeToWasmEventsResponse { + string type = 1; + repeated Attribute attributes = 2; + uint64 height = 3; +} + +message Attribute { + string key = 1; + string value = 2; +} + +message BroadcastRequest { + string address = 1; + bytes payload = 2; +} + +message BroadcastResponse { + bool success = 1; +} diff --git a/libs/common/src/assets/relayer.proto b/libs/common/src/assets/relayer.proto deleted file mode 100644 index 588a363..0000000 --- a/libs/common/src/assets/relayer.proto +++ /dev/null @@ -1,89 +0,0 @@ -syntax = "proto3"; -import "google/api/annotations.proto"; - -service Relayer{ - rpc Verify(stream VerifyRequest) returns (stream VerifyResponse); - rpc GetPayload(GetPayloadRequest) returns (GetPayloadResponse) { - option (google.api.http) = { - get: "/v1beta1/payload/{hash}" - }; - } - rpc SubscribeToApprovals(SubscribeToApprovalsRequest) returns (stream SubscribeToApprovalsResponse); - rpc SubscribeToWasmEvents(SubscribeToWasmEventsRequest) returns (stream SubscribeToWasmEventsResponse); - rpc Broadcast(BroadcastRequest) returns (BroadcastResponse) { - option (google.api.http) = { - post: "/v1beta1/broadcast" - body: "*" - }; - } -} - -message Message{ - string id = 1; // the unique identifier with which the message can be looked up on the source chain - string source_chain = 2; - string source_address= 3; - string destination_chain = 4; - string destination_address = 5; - bytes payload = 6; -} - -message GetPayloadRequest{ - bytes hash = 1; -} - -message GetPayloadResponse{ - bytes payload = 1; -} - -message SubscribeToApprovalsRequest{ - string chain = 1; - optional uint64 start_height = 2; // can be used to replay events -} - -message SubscribeToApprovalsResponse{ - string chain = 1; - bytes execute_data = 2; - uint64 block_height = 3; -} - -message VerifyRequest{ - Message message = 1; -} - -message VerifyResponse{ - Message message = 1; - bool success = 3; -} - -message SubscribeToWasmEventsRequest{ - optional uint64 start_height = 1; -} - -message SubscribeToWasmEventsResponse{ - string type = 1; - repeated Attribute attributes = 2; - uint64 height = 3; -} - -message Attribute{ - string key = 1; - string value = 2; -} - -message BroadcastRequest { - string address = 1; - bytes payload = 2; -} - -message BroadcastResponse { - Receipt receipt = 1; -} - -message Receipt { - string error = 1; - int64 block_height = 2; - int64 gas_used = 3; - int64 gas_wanted = 4; - string tx_hash = 5; - string tx_response_log = 6; -} diff --git a/libs/common/src/config/api.config.service.ts b/libs/common/src/config/api.config.service.ts index d0e1a4c..fa44953 100644 --- a/libs/common/src/config/api.config.service.ts +++ b/libs/common/src/config/api.config.service.ts @@ -1,7 +1,6 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { EVENTS_NOTIFIER_QUEUE } from '../../../../config/configuration'; -import { CONSTANTS } from '@mvx-monorepo/common/utils/constants.enum'; @Injectable() export class ApiConfigService { @@ -121,10 +120,6 @@ export class ApiConfigService { return chainId; } - getSourceChainName(): string { - return CONSTANTS.SOURCE_CHAIN_NAME_SUFFIX + this.getChainId(); - } - getWalletMnemonic(): string { const walletMnemonic = this.configService.get('WALLET_MNEMONIC'); if (!walletMnemonic) { diff --git a/libs/common/src/grpc/entities/relayer.ts b/libs/common/src/grpc/entities/amplifier.ts similarity index 82% rename from libs/common/src/grpc/entities/relayer.ts rename to libs/common/src/grpc/entities/amplifier.ts index b24af21..137b73a 100644 --- a/libs/common/src/grpc/entities/relayer.ts +++ b/libs/common/src/grpc/entities/amplifier.ts @@ -3,9 +3,20 @@ import { Observable } from "rxjs"; export const protobufPackage = ""; +export enum ErrorCode { + VERIFICATION_FAILED = 0, + INTERNAL_ERROR = 1, + AXELAR_NETWORK_ERROR = 2, + INSUFFICIENT_GAS = 3, + FAILED_ON_CHAIN = 4, + MESSAGE_NOT_FOUND = 5, + UNRECOGNIZED = -1, +} + export interface Message { - /** the unique identifier with which the message can be looked up on the source chain */ + /** the unique identifier with which the message can be looked */ id: string; + /** up on the source chain */ sourceChain: string; sourceAddress: string; destinationChain: string; @@ -22,7 +33,7 @@ export interface GetPayloadResponse { } export interface SubscribeToApprovalsRequest { - chain: string; + chains: string[]; /** can be used to replay events */ startHeight?: number | undefined; } @@ -39,7 +50,12 @@ export interface VerifyRequest { export interface VerifyResponse { message: Message | undefined; - success: boolean; + error?: Error | undefined; +} + +export interface Error { + error: string; + errorCode: ErrorCode; } export interface SubscribeToWasmEventsRequest { @@ -63,19 +79,10 @@ export interface BroadcastRequest { } export interface BroadcastResponse { - receipt: Receipt | undefined; -} - -export interface Receipt { - error: string; - blockHeight: number; - gasUsed: number; - gasWanted: number; - txHash: string; - txResponseLog: string; + success: boolean; } -export interface Relayer { +export interface Amplifier { verify(request: Observable): Observable; getPayload(request: GetPayloadRequest): Promise; subscribeToApprovals(request: SubscribeToApprovalsRequest): Observable; diff --git a/libs/common/src/grpc/grpc.module.ts b/libs/common/src/grpc/grpc.module.ts index 58f092d..b15fa35 100644 --- a/libs/common/src/grpc/grpc.module.ts +++ b/libs/common/src/grpc/grpc.module.ts @@ -4,7 +4,7 @@ import { ApiConfigModule, ApiConfigService } from '@mvx-monorepo/common'; import { join } from 'path'; import { ProviderKeys } from '@mvx-monorepo/common/utils/provider.enum'; import { GrpcService } from '@mvx-monorepo/common/grpc/grpc.service'; -import { protobufPackage } from '@mvx-monorepo/common/grpc/entities/relayer'; +import { protobufPackage } from '@mvx-monorepo/common/grpc/entities/amplifier'; @Module({ imports: [ @@ -17,7 +17,7 @@ import { protobufPackage } from '@mvx-monorepo/common/grpc/entities/relayer'; transport: Transport.GRPC, options: { package: protobufPackage, - protoPath: join(__dirname, '../assets/relayer.proto'), + protoPath: join(__dirname, '../assets/amplifier.proto'), url: apiConfigService.getAxelarApiUrl(), }, }; diff --git a/libs/common/src/grpc/grpc.service.ts b/libs/common/src/grpc/grpc.service.ts index ea35a47..df24f24 100644 --- a/libs/common/src/grpc/grpc.service.ts +++ b/libs/common/src/grpc/grpc.service.ts @@ -2,24 +2,19 @@ import { Inject, Injectable, OnModuleInit } from '@nestjs/common'; import { ProviderKeys } from '@mvx-monorepo/common/utils/provider.enum'; import { ClientGrpc } from '@nestjs/microservices'; import { ContractCallEvent } from '@prisma/client'; -import { - BroadcastRequest, - Relayer, - SubscribeToApprovalsResponse, - VerifyRequest, -} from '@mvx-monorepo/common/grpc/entities/relayer'; +import { Amplifier, SubscribeToApprovalsResponse, VerifyRequest } from '@mvx-monorepo/common/grpc/entities/amplifier'; import { first, Observable, ReplaySubject, timeout } from 'rxjs'; import BigNumber from 'bignumber.js'; import { ApiConfigService } from '@mvx-monorepo/common/config'; -const RELAYER_SERVICE = 'Relayer'; +const AMPLIFIER_SERVICE = 'Amplifier'; const VERIFY_TIMEOUT = 30_000; // TODO: Check if this timeout is enough @Injectable() export class GrpcService implements OnModuleInit { // @ts-ignore - private relayerService: Relayer; + private amplifierService: Amplifier; private readonly axelarContractVotingVerifier: string; constructor( @@ -30,7 +25,7 @@ export class GrpcService implements OnModuleInit { } onModuleInit() { - this.relayerService = this.client.getService(RELAYER_SERVICE); + this.amplifierService = this.client.getService(AMPLIFIER_SERVICE); } verify(contractCallEvent: ContractCallEvent) { @@ -48,11 +43,11 @@ export class GrpcService implements OnModuleInit { }); replaySubject.complete(); - return this.relayerService.verify(replaySubject).pipe(first(), timeout(VERIFY_TIMEOUT)); + return this.amplifierService.verify(replaySubject).pipe(first(), timeout(VERIFY_TIMEOUT)); } async getPayload(payloadHash: string): Promise { - const result = await this.relayerService.getPayload({ + const result = await this.amplifierService.getPayload({ hash: Buffer.from(payloadHash, 'hex'), }); @@ -60,8 +55,8 @@ export class GrpcService implements OnModuleInit { } subscribeToApprovals(chain: string, startHeight?: number | undefined): Observable { - return this.relayerService.subscribeToApprovals({ - chain, + return this.amplifierService.subscribeToApprovals({ + chains: [chain], startHeight, }); } @@ -86,12 +81,9 @@ export class GrpcService implements OnModuleInit { }), ); - const request: BroadcastRequest = { + return await this.amplifierService.broadcast({ address: this.axelarContractVotingVerifier, payload, - }; - - // TODO: Should we add a retry mechanism here? - await this.relayerService.broadcast(request); + }); } } diff --git a/libs/common/src/utils/constants.enum.ts b/libs/common/src/utils/constants.enum.ts index 311e83b..d480286 100644 --- a/libs/common/src/utils/constants.enum.ts +++ b/libs/common/src/utils/constants.enum.ts @@ -1,5 +1,5 @@ export enum CONSTANTS { EGLD_IDENTIFIER = 'EGLD', - SOURCE_CHAIN_NAME_SUFFIX = 'multiversx-', + SOURCE_CHAIN_NAME = 'multiversx', } diff --git a/nest-cli.json b/nest-cli.json index bab760d..b707b4f 100644 --- a/nest-cli.json +++ b/nest-cli.json @@ -27,7 +27,7 @@ "tsConfigPath": "apps/axelar-event-processor/tsconfig.app.json", "assets": [ { - "include": "../axelar/relayer.proto", + "include": "../axelar/amplifier.proto", "outDir": "./dist/apps/axelar-event-processor/axelar" } ] From c0be0bf1ad47f535233ca3670e56ac6a0ee09154 Mon Sep 17 00:00:00 2001 From: Rares <6453351+raress96@users.noreply.github.com> Date: Fri, 5 Apr 2024 16:25:03 +0300 Subject: [PATCH 30/33] Update amplifier proto definition with proper package. --- libs/common/src/assets/amplifier.proto | 2 ++ libs/common/src/grpc/entities/amplifier.ts | 2 +- libs/common/src/utils/dynamic.module.utils.ts | 24 +------------------ 3 files changed, 4 insertions(+), 24 deletions(-) diff --git a/libs/common/src/assets/amplifier.proto b/libs/common/src/assets/amplifier.proto index 425477d..c030f3e 100644 --- a/libs/common/src/assets/amplifier.proto +++ b/libs/common/src/assets/amplifier.proto @@ -1,6 +1,8 @@ syntax = "proto3"; import "google/api/annotations.proto"; +package axelar.amplifier.v1beta1; + service Amplifier { rpc Verify(stream VerifyRequest) returns (stream VerifyResponse); rpc GetPayload(GetPayloadRequest) returns (GetPayloadResponse) { diff --git a/libs/common/src/grpc/entities/amplifier.ts b/libs/common/src/grpc/entities/amplifier.ts index 137b73a..d4ce138 100644 --- a/libs/common/src/grpc/entities/amplifier.ts +++ b/libs/common/src/grpc/entities/amplifier.ts @@ -1,7 +1,7 @@ /* eslint-disable */ import { Observable } from "rxjs"; -export const protobufPackage = ""; +export const protobufPackage = "axelar.amplifier.v1beta1"; export enum ErrorCode { VERIFICATION_FAILED = 0, diff --git a/libs/common/src/utils/dynamic.module.utils.ts b/libs/common/src/utils/dynamic.module.utils.ts index 633bec1..0ddeabf 100644 --- a/libs/common/src/utils/dynamic.module.utils.ts +++ b/libs/common/src/utils/dynamic.module.utils.ts @@ -1,6 +1,5 @@ import { CacheModule, RedisCacheModule, RedisCacheModuleOptions } from '@multiversx/sdk-nestjs-cache'; -import { DynamicModule, Provider } from '@nestjs/common'; -import { ClientOptions, ClientProxyFactory, Transport } from '@nestjs/microservices'; +import { DynamicModule } from '@nestjs/common'; import { ApiConfigModule, ApiConfigService } from '../config'; export class DynamicModuleUtils { @@ -39,25 +38,4 @@ export class DynamicModuleUtils { inject: [ApiConfigService], }); } - - static getPubSubService(): Provider { - return { - provide: 'PUBSUB_SERVICE', - useFactory: (apiConfigService: ApiConfigService) => { - const clientOptions: ClientOptions = { - transport: Transport.REDIS, - options: { - host: apiConfigService.getRedisUrl(), - port: 6379, - retryDelay: 1000, - retryAttempts: 10, - retryStrategy: () => 1000, - }, - }; - - return ClientProxyFactory.create(clientOptions); - }, - inject: [ApiConfigService], - }; - } } From 50bf8503619511aef8967c472d1fb2556f74ad52 Mon Sep 17 00:00:00 2001 From: Rares <6453351+raress96@users.noreply.github.com> Date: Mon, 8 Apr 2024 11:49:41 +0300 Subject: [PATCH 31/33] Update for correct proto file from amplifier examples repo. --- .env.example | 10 ++--- .../approvals.processor.service.ts | 8 +++- .../contract-call-event.processor.service.ts | 1 + .../src/processors/gateway.processor.spec.ts | 42 +++---------------- .../src/processors/gateway.processor.ts | 16 ++----- libs/common/src/assets/amplifier.proto | 11 +++-- libs/common/src/grpc/entities/amplifier.ts | 13 ++++-- 7 files changed, 36 insertions(+), 65 deletions(-) diff --git a/.env.example b/.env.example index 306e8a5..924fd70 100644 --- a/.env.example +++ b/.env.example @@ -7,15 +7,15 @@ REDIS_URL=127.0.0.1 EVENTS_NOTIFIER_URL=amqp://user:password@rabbitmq:5672 EVENTS_NOTIFIER_QUEUE=queue -CONTRACT_GATEWAY= -CONTRACT_GAS_SERVICE= -CONTRACT_ITS= +CONTRACT_GATEWAY=erd1qqqqqqqqqqqqqpgqhxy6dv9k5p3u4d6rawnwjyp0j3sunu9dkklspga3t9 +CONTRACT_GAS_SERVICE=erd1qqqqqqqqqqqqqpgqsrhknrwuvy606ar5l2kuaz4glgyyye9vkkls5ng86g +CONTRACT_ITS=erd1qqqqqqqqqqqqqpgqw08zahneragk9rnaujwe8qcyu84ehw2lkklsvca0jx CONTRACT_WEGLD_SWAP=erd1qqqqqqqqqqqqqpgqpv09kfzry5y4sj05udcngesat07umyj70n4sa2c0rp -AXELAR_CONTRACT_VOTING_VERIFIER= +AXELAR_CONTRACT_VOTING_VERIFIER=axelar1gajw625kz8el4ayk8fwpy7r6ew0m7zrg9jdd6grg85fle39shuxqwuaz2k -AXELAR_API_URL= +AXELAR_API_URL=devnet-amplifier-api.axelar.dev:11235 CHAIN_ID=D diff --git a/apps/axelar-event-processor/src/approvals-processor/approvals.processor.service.ts b/apps/axelar-event-processor/src/approvals-processor/approvals.processor.service.ts index 1e81072..4e9f91a 100644 --- a/apps/axelar-event-processor/src/approvals-processor/approvals.processor.service.ts +++ b/apps/axelar-event-processor/src/approvals-processor/approvals.processor.service.ts @@ -1,5 +1,5 @@ import { Locker } from '@multiversx/sdk-nestjs-common'; -import { Inject, Injectable, Logger } from '@nestjs/common'; +import { Inject, Injectable, Logger, OnModuleInit } from '@nestjs/common'; import { Cron } from '@nestjs/schedule'; import { GrpcService } from '@mvx-monorepo/common/grpc/grpc.service'; import { RedisCacheService } from '@multiversx/sdk-nestjs-cache'; @@ -16,7 +16,7 @@ import { CONSTANTS } from '@mvx-monorepo/common/utils/constants.enum'; const MAX_NUMBER_OF_RETRIES = 3; @Injectable() -export class ApprovalsProcessorService { +export class ApprovalsProcessorService implements OnModuleInit { private readonly logger: Logger; private approvalsSubscription: Subscription | null = null; @@ -31,6 +31,10 @@ export class ApprovalsProcessorService { this.logger = new Logger(ApprovalsProcessorService.name); } + async onModuleInit() { + await this.handleNewApprovalsRaw(); + } + @Cron('*/30 * * * * *') async handleNewApprovals() { await Locker.lock('handleNewApprovals', this.handleNewApprovalsRaw.bind(this)); diff --git a/apps/mvx-event-processor/src/contract-call-event-processor/contract-call-event.processor.service.ts b/apps/mvx-event-processor/src/contract-call-event-processor/contract-call-event.processor.service.ts index ac9b76e..94c1592 100644 --- a/apps/mvx-event-processor/src/contract-call-event-processor/contract-call-event.processor.service.ts +++ b/apps/mvx-event-processor/src/contract-call-event-processor/contract-call-event.processor.service.ts @@ -17,6 +17,7 @@ export class ContractCallEventProcessorService { this.logger = new Logger(ContractCallEventProcessorService.name); } + // Ofset at second 15 to not run at the same time as processPendingContractCallApproved @Cron('15 */2 * * * *') async processPendingContractCallEvent() { await Locker.lock('processPendingContractCallEvent', async () => { diff --git a/apps/mvx-event-processor/src/processors/gateway.processor.spec.ts b/apps/mvx-event-processor/src/processors/gateway.processor.spec.ts index 14c2f41..0dc25ad 100644 --- a/apps/mvx-event-processor/src/processors/gateway.processor.spec.ts +++ b/apps/mvx-event-processor/src/processors/gateway.processor.spec.ts @@ -188,40 +188,6 @@ describe('ContractCallProcessor', () => { expect(contractCallEventRepository.updateStatus).not.toHaveBeenCalled(); }); - it('Should handle unrecoverable error', async () => { - const observable = new Subject(); - grpcService.verify.mockReturnValueOnce(observable); - - await service.handleEvent(rawEvent); - - expect(gatewayContract.decodeContractCallEvent).toHaveBeenCalledTimes(1); - expect(contractCallEventRepository.create).toHaveBeenCalledTimes(1); - expect(grpcService.verify).toHaveBeenCalledTimes(1); - - observable.next({ - message: undefined, - error: { - error: 'error', - errorCode: ErrorCode.FAILED_ON_CHAIN, - }, - }); - observable.complete(); - - expect(contractCallEventRepository.updateStatus).toHaveBeenCalledTimes(1); - expect(contractCallEventRepository.updateStatus).toHaveBeenCalledWith({ - id: 'multiversx:txHash:0', - txHash: 'txHash', - eventIndex: 0, - status: ContractCallEventStatus.FAILED, - sourceAddress: 'erd1qqqqqqqqqqqqqpgqzqvm5ywqqf524efwrhr039tjs29w0qltkklsa05pk7', - sourceChain: 'multiversx', - destinationAddress: 'destinationAddress', - destinationChain: 'ethereum', - payloadHash: 'ebc84cbd75ba5516bf45e7024a9e12bc3c5c880f73e3a5beca7ebba52b2867a7', - payload: Buffer.from('payload'), - }); - }); - it('Should not handle duplicate', async () => { contractCallEventRepository.create.mockReturnValueOnce(Promise.resolve(null)); @@ -329,9 +295,11 @@ describe('ContractCallProcessor', () => { }); it('Should handle event error', async () => { - grpcService.verifyWorkerSet.mockReturnValueOnce(Promise.resolve({ - success: false, - })); + grpcService.verifyWorkerSet.mockReturnValueOnce( + Promise.resolve({ + result: false, + }), + ); await service.handleEvent(rawEvent); diff --git a/apps/mvx-event-processor/src/processors/gateway.processor.ts b/apps/mvx-event-processor/src/processors/gateway.processor.ts index 49bd8cb..937f6ea 100644 --- a/apps/mvx-event-processor/src/processors/gateway.processor.ts +++ b/apps/mvx-event-processor/src/processors/gateway.processor.ts @@ -11,7 +11,6 @@ import { BinaryUtils } from '@multiversx/sdk-nestjs-common'; import { ContractCallApprovedRepository, } from '@mvx-monorepo/common/database/repository/contract-call-approved.repository'; -import { ErrorCode } from '@mvx-monorepo/common/grpc/entities/amplifier'; import { CONSTANTS } from '@mvx-monorepo/common/utils/constants.enum'; // order/logIndex is unsupported since we can't easily get it in the relayer, so we use 0 by default @@ -90,16 +89,6 @@ export class GatewayProcessor implements ProcessorInterface { await this.contractCallEventRepository.updateStatus(contractCallEvent); - return; - } else if (response.error.errorCode === ErrorCode.FAILED_ON_CHAIN) { - this.logger.error( - `Verify contract call event ${id} was not successful. Will NOT be retried. Got error code ${response.error.errorCode}`, - ); - - contractCallEvent.status = ContractCallEventStatus.FAILED; - - await this.contractCallEventRepository.updateStatus(contractCallEvent); - return; } @@ -140,6 +129,7 @@ export class GatewayProcessor implements ProcessorInterface { const id = `${CONSTANTS.SOURCE_CHAIN_NAME}:${rawEvent.txHash}:${UNSUPPORTED_LOG_INDEX}`; + // TODO: Test that this works correctly const response = await this.grpcService.verifyWorkerSet( id, trasnsferData.newOperators, @@ -147,7 +137,7 @@ export class GatewayProcessor implements ProcessorInterface { trasnsferData.newThreshold, ); - if (response.success) { + if (response.result) { return; } @@ -161,7 +151,7 @@ export class GatewayProcessor implements ProcessorInterface { trasnsferData.newThreshold, ); - if (!response.success) { + if (!response.result) { this.logger.error(`Couldn't dispatch verifyWorkerSet ${id} to Amplifier API.`); } }, 60_000); diff --git a/libs/common/src/assets/amplifier.proto b/libs/common/src/assets/amplifier.proto index c030f3e..63dd248 100644 --- a/libs/common/src/assets/amplifier.proto +++ b/libs/common/src/assets/amplifier.proto @@ -1,7 +1,9 @@ syntax = "proto3"; +package axelar.amplifier.v1beta1; + import "google/api/annotations.proto"; -package axelar.amplifier.v1beta1; +option go_package = "github.com/axelarnetwork/axelar-eds/pkg/amplifier/server/api"; service Amplifier { rpc Verify(stream VerifyRequest) returns (stream VerifyResponse); @@ -30,6 +32,8 @@ message Message { string destination_chain = 4; string destination_address = 5; bytes payload = 6; + // when we have a better idea of the requirement, we can add an additional + // optional field here to facilitate verification proofs } message GetPayloadRequest { bytes hash = 1; } @@ -51,6 +55,7 @@ message VerifyRequest { Message message = 1; } message VerifyResponse { Message message = 1; + // bool success = 2; optional Error error = 2; } @@ -59,8 +64,6 @@ enum ErrorCode { INTERNAL_ERROR = 1; AXELAR_NETWORK_ERROR = 2; INSUFFICIENT_GAS = 3; - FAILED_ON_CHAIN = 4; - MESSAGE_NOT_FOUND = 5; } message Error { @@ -87,5 +90,5 @@ message BroadcastRequest { } message BroadcastResponse { - bool success = 1; + bool result = 1; } diff --git a/libs/common/src/grpc/entities/amplifier.ts b/libs/common/src/grpc/entities/amplifier.ts index d4ce138..38550b8 100644 --- a/libs/common/src/grpc/entities/amplifier.ts +++ b/libs/common/src/grpc/entities/amplifier.ts @@ -8,8 +8,6 @@ export enum ErrorCode { INTERNAL_ERROR = 1, AXELAR_NETWORK_ERROR = 2, INSUFFICIENT_GAS = 3, - FAILED_ON_CHAIN = 4, - MESSAGE_NOT_FOUND = 5, UNRECOGNIZED = -1, } @@ -21,6 +19,10 @@ export interface Message { sourceAddress: string; destinationChain: string; destinationAddress: string; + /** + * when we have a better idea of the requirement, we can add an additional + * optional field here to facilitate verification proofs + */ payload: Uint8Array; } @@ -49,7 +51,10 @@ export interface VerifyRequest { } export interface VerifyResponse { - message: Message | undefined; + message: + | Message + | undefined; + /** bool success = 2; */ error?: Error | undefined; } @@ -79,7 +84,7 @@ export interface BroadcastRequest { } export interface BroadcastResponse { - success: boolean; + result: boolean; } export interface Amplifier { From 4bb7d56e7b411fb1f4af653024446256726e16da Mon Sep 17 00:00:00 2001 From: Rares <6453351+raress96@users.noreply.github.com> Date: Tue, 9 Apr 2024 09:49:02 +0300 Subject: [PATCH 32/33] Update message id for amplifier. --- .../src/processors/gateway.processor.spec.ts | 4 ++-- apps/mvx-event-processor/src/processors/gateway.processor.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/mvx-event-processor/src/processors/gateway.processor.spec.ts b/apps/mvx-event-processor/src/processors/gateway.processor.spec.ts index 0dc25ad..dfb992e 100644 --- a/apps/mvx-event-processor/src/processors/gateway.processor.spec.ts +++ b/apps/mvx-event-processor/src/processors/gateway.processor.spec.ts @@ -132,7 +132,7 @@ describe('ContractCallProcessor', () => { expect(gatewayContract.decodeContractCallEvent).toHaveBeenCalledWith(TransactionEvent.fromHttpResponse(rawEvent)); expect(contractCallEventRepository.create).toHaveBeenCalledTimes(1); expect(contractCallEventRepository.create).toHaveBeenCalledWith({ - id: 'multiversx:txHash:0', + id: 'multiversx_txHash-0', txHash: 'txHash', eventIndex: 0, status: ContractCallEventStatus.PENDING, @@ -153,7 +153,7 @@ describe('ContractCallProcessor', () => { expect(contractCallEventRepository.updateStatus).toHaveBeenCalledTimes(1); expect(contractCallEventRepository.updateStatus).toHaveBeenCalledWith({ - id: 'multiversx:txHash:0', + id: 'multiversx_txHash-0', txHash: 'txHash', eventIndex: 0, status: ContractCallEventStatus.APPROVED, diff --git a/apps/mvx-event-processor/src/processors/gateway.processor.ts b/apps/mvx-event-processor/src/processors/gateway.processor.ts index 937f6ea..8462706 100644 --- a/apps/mvx-event-processor/src/processors/gateway.processor.ts +++ b/apps/mvx-event-processor/src/processors/gateway.processor.ts @@ -62,7 +62,7 @@ export class GatewayProcessor implements ProcessorInterface { private async handleContractCallEvent(rawEvent: NotifierEvent) { const event = this.gatewayContract.decodeContractCallEvent(TransactionEvent.fromHttpResponse(rawEvent)); - const id = `${CONSTANTS.SOURCE_CHAIN_NAME}:${rawEvent.txHash}:${UNSUPPORTED_LOG_INDEX}`; + const id = `${CONSTANTS.SOURCE_CHAIN_NAME}_${rawEvent.txHash}-${UNSUPPORTED_LOG_INDEX}`; const contractCallEvent = await this.contractCallEventRepository.create({ id, txHash: rawEvent.txHash, From 5e4911b79ea49232353e0869929bc18eea598266 Mon Sep 17 00:00:00 2001 From: Rares <6453351+raress96@users.noreply.github.com> Date: Wed, 10 Apr 2024 14:32:19 +0300 Subject: [PATCH 33/33] Small improvements for approvals processor and fix for send transactions edge case. --- .../approvals.processor.service.ts | 8 ++--- ...all-contract-approved.processor.service.ts | 8 +++-- ...ll-contract-approved.processor.e2e-spec.ts | 31 ++++++++++++++++++- .../src/contracts/transactions.helper.ts | 10 +++--- 4 files changed, 44 insertions(+), 13 deletions(-) diff --git a/apps/axelar-event-processor/src/approvals-processor/approvals.processor.service.ts b/apps/axelar-event-processor/src/approvals-processor/approvals.processor.service.ts index 4e9f91a..4fe51b5 100644 --- a/apps/axelar-event-processor/src/approvals-processor/approvals.processor.service.ts +++ b/apps/axelar-event-processor/src/approvals-processor/approvals.processor.service.ts @@ -54,10 +54,10 @@ export class ApprovalsProcessorService implements OnModuleInit { this.logger.log('Starting GRPC approvals stream subscription'); - const lastProcessedHeight = + const startProcessHeight = (await this.redisCacheService.get(CacheInfo.StartProcessHeight().key)) || undefined; - const observable = this.grpcService.subscribeToApprovals(CONSTANTS.SOURCE_CHAIN_NAME, lastProcessedHeight); + const observable = this.grpcService.subscribeToApprovals(CONSTANTS.SOURCE_CHAIN_NAME, startProcessHeight); const onComplete = () => { this.logger.warn('Approvals stream subscription ended'); @@ -74,8 +74,8 @@ export class ApprovalsProcessorService implements OnModuleInit { // TODO: Test if this works as expected this.approvalsSubscription = observable.subscribe({ next: this.processMessage.bind(this), - complete: onComplete, - error: onError, + complete: onComplete.bind(this), + error: onError.bind(this), }); } diff --git a/apps/mvx-event-processor/src/call-contract-approved-processor/call-contract-approved.processor.service.ts b/apps/mvx-event-processor/src/call-contract-approved-processor/call-contract-approved.processor.service.ts index 32bcefd..0783211 100644 --- a/apps/mvx-event-processor/src/call-contract-approved-processor/call-contract-approved.processor.service.ts +++ b/apps/mvx-event-processor/src/call-contract-approved-processor/call-contract-approved.processor.service.ts @@ -82,11 +82,13 @@ export class CallContractApprovedProcessorService { contractCallApproved.retry += 1; } - const result = await this.transactionsHelper.sendTransactions(transactionsToSend); + const hashes = await this.transactionsHelper.sendTransactions(transactionsToSend); + + if (hashes) { + const actuallySentEntries = entries.filter(entry => hashes.includes(entry.executeTxHash as string)); - if (result) { // Page is not modified if database records are updated - await this.contractCallApprovedRepository.updateManyPartial(entries); + await this.contractCallApprovedRepository.updateManyPartial(actuallySentEntries); } else { // re-retrieve account nonce in case sendTransactions failed because of nonce error accountNonce = null; diff --git a/apps/mvx-event-processor/test/call-contract-approved.processor.e2e-spec.ts b/apps/mvx-event-processor/test/call-contract-approved.processor.e2e-spec.ts index a4428c1..80dc87a 100644 --- a/apps/mvx-event-processor/test/call-contract-approved.processor.e2e-spec.ts +++ b/apps/mvx-event-processor/test/call-contract-approved.processor.e2e-spec.ts @@ -313,6 +313,10 @@ describe('CallContractApprovedProcessorService', () => { payload: Buffer.from(AbiCoder.defaultAbiCoder().encode(['uint256'], [1]).substring(2), 'hex'), }); + proxy.sendTransactions.mockReturnValueOnce(Promise.resolve([ + 'af0848face1fa76874752bbc9fab1928b33e08ff646471cab3d0fa91a6506a51', + ])); + await service.processPendingContractCallApproved(); expect(proxy.getAccount).toHaveBeenCalledTimes(1); @@ -358,6 +362,10 @@ describe('CallContractApprovedProcessorService', () => { }), ); + proxy.sendTransactions.mockReturnValueOnce(Promise.resolve([ + '36a71e24554303f6b734143ad90f939b57018f8c05f8abaa63e23950f899ce56', + ])); + // Process transaction 2nd time await service.processPendingContractCallApproved(); @@ -379,7 +387,28 @@ describe('CallContractApprovedProcessorService', () => { itsExecute.updatedAt = new Date(new Date().getTime() - 60_500); await prisma.contractCallApproved.update({ where: { commandId: itsExecute.commandId }, data: itsExecute }); - // Process transaction 3rd time will retry + // Process transaction 3rd time will retry transaction not sent + await service.processPendingContractCallApproved(); + + transactions = proxy.sendTransactions.mock.lastCall?.[0] as Transaction[]; + expect(transactions).toHaveLength(1); + expect(transactions[0].getValue()).toBe('50000000000000000'); // assert sent with value + + // @ts-ignore + itsExecute = await contractCallApprovedRepository.findByCommandId(originalItsExecute.commandId); + expect(itsExecute).toEqual({ + ...originalItsExecute, + retry: 2, + executeTxHash: '36a71e24554303f6b734143ad90f939b57018f8c05f8abaa63e23950f899ce56', + updatedAt: expect.any(Date), + successTimes: 1, + }); + + // Process transaction 3rd time will retry transaction sent + proxy.sendTransactions.mockReturnValueOnce(Promise.resolve([ + 'e072d88e869e51a261e4a48aea1abb6f62a1f69c8af6fc3740d26e57b5e0a2bb', + ])); + await service.processPendingContractCallApproved(); transactions = proxy.sendTransactions.mock.lastCall?.[0] as Transaction[]; diff --git a/libs/common/src/contracts/transactions.helper.ts b/libs/common/src/contracts/transactions.helper.ts index e73b6e2..8f997bb 100644 --- a/libs/common/src/contracts/transactions.helper.ts +++ b/libs/common/src/contracts/transactions.helper.ts @@ -64,22 +64,22 @@ export class TransactionsHelper { async sendTransactions(transactions: Transaction[]) { if (!transactions.length) { - return true; + return []; } try { - await this.proxy.sendTransactions(transactions); + const hashes = await this.proxy.sendTransactions(transactions); this.logger.log( - `Sent ${transactions.length} transactions to proxy: ${transactions.map((trans) => trans.getHash())}`, + `Sent ${transactions.length} transactions to proxy: ${hashes}`, ); - return true; + return hashes; } catch (e) { this.logger.error(`Can not send transactions to proxy...`); this.logger.error(e); - return false; + return null; } }