diff --git a/.github/workflows/auto-deploy-develop.yml b/.github/workflows/auto-deploy-develop.yml index b2e73d0..f0ab90d 100644 --- a/.github/workflows/auto-deploy-develop.yml +++ b/.github/workflows/auto-deploy-develop.yml @@ -11,12 +11,8 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-node@v2 with: - node-version: 16.15.0 + node-version: 18.15.0 - run: | echo "${{ vars.MINA_BRIDGE_BE_DEV }}" >> .env - docker-compose -p mina-bridge-dev-env up -d - yarn - yarn prebuild - yarn build - yarn migration:run - npx pm2 reload ecosystem.config.js + docker build . -t mina-bridge:1.0.0 + docker compose -f docker-compose.dev.yaml up -d diff --git a/.github/workflows/auto-deploy-test.yml b/.github/workflows/auto-deploy-test.yml index fdd11e4..1530649 100644 --- a/.github/workflows/auto-deploy-test.yml +++ b/.github/workflows/auto-deploy-test.yml @@ -3,7 +3,7 @@ run-name: ${{ github.actor }} is deploying test branch [test] 🌏 🚀 🛰️ on: push: branches: - - "test" + - "testing" jobs: DeployTest: runs-on: [self-hosted, mina-bridge-test] @@ -14,9 +14,5 @@ jobs: node-version: 16.15.0 - run: | echo "${{ vars.MINA_BRIDGE_BE_TEST }}" >> .env - docker-compose -p mina-bridge-test-env up -d - yarn - yarn prebuild - yarn build - yarn migration:run - npx pm2 reload ecosystem.config.js + docker build . -t mina-bridge:1.0.0 + docker compose -f docker-compose.dev.yaml up -d diff --git a/.gitignore b/.gitignore index d543a20..df98314 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,4 @@ postgresData #Docker mounted volumes /dumpData/* +.scannerwork/* \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 343051d..7bffd1b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM 590183806579.dkr.ecr.eu-north-1.amazonaws.com/node18-alpine:latest as build +FROM node:18-alpine As build WORKDIR /app COPY package*.json yarn.lock ./ @@ -6,7 +6,7 @@ RUN yarn COPY . . RUN yarn build -FROM 590183806579.dkr.ecr.eu-north-1.amazonaws.com/node18-alpine:latest +FROM node:18-alpine WORKDIR /app COPY --from=build /app/node_modules ./node_modules COPY --from=build /app/dist ./dist @@ -17,4 +17,4 @@ COPY --from=build /app/tsconfig.build.json ./tsconfig.build.json # RUN apk --no-cache add curl EXPOSE 3000 -#CMD ["sh", "-c", "yarn start"] +#CMD ["sh", "-c", "yarn start"] \ No newline at end of file diff --git a/docker-compose.dev.yaml b/docker-compose.dev.yaml new file mode 100644 index 0000000..337f7e4 --- /dev/null +++ b/docker-compose.dev.yaml @@ -0,0 +1,105 @@ +services: + api: + image: mina-bridge:1.0.0 + command: > + sh -c "npm run migration:run-dist && npm run start:prod;" + tty: true + restart: always + ports: + - ${PORT}:${PORT} + depends_on: + - postgres + networks: + - myNetwork + user: node + crawl-bridge-evm: + image: mina-bridge:1.0.0 + command: > + sh -c "npm run console crawl-eth-bridge-contract" + tty: true + restart: always + + depends_on: + - postgres + networks: + - myNetwork + user: node + sender-evm: + image: mina-bridge:1.0.0 + command: > + sh -c "npm run console sender-eth-bridge-unlock" + tty: true + restart: always + + depends_on: + - postgres + networks: + - myNetwork + user: node + crawl-bridge-mina: + image: mina-bridge:1.0.0 + command: > + sh -c "npm run console crawl-mina-bridge-contract" + tty: true + restart: always + + depends_on: + - postgres + networks: + - myNetwork + user: node + crawl-token-mina: + image: mina-bridge:1.0.0 + command: > + sh -c "npm run console crawl-mina-token-contract" + tty: true + restart: always + + depends_on: + - postgres + networks: + - myNetwork + user: node + sender-mina: + image: mina-bridge:1.0.0 + command: > + sh -c "npm run console sender-mina-bridge-unlock" + tty: true + restart: always + depends_on: + - postgres + networks: + - myNetwork + user: node + get-price-token: + image: mina-bridge:1.0.0 + command: > + sh -c "npm run console get-price-token" + tty: true + restart: always + depends_on: + - postgres + networks: + - myNetwork + user: node + postgres: + container_name: mina-bridge-${NODE_ENV}-postgres + image: postgres:15.3-alpine3.18 + ports: + - ${DB_PORT}:${DB_PORT} + volumes: + - postgresData:/var/lib/postgresql/data + command: -p ${DB_PORT} + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: mina-bridge + networks: + myNetwork: + + +volumes: + postgresData: +networks: + myNetwork: + name: minaBridgeNetwork${NODE_ENV} diff --git a/package-lock.json b/package-lock.json index 7eecf65..368c747 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51,7 +51,7 @@ "@trivago/prettier-plugin-sort-imports": "^4.3.0", "@types/express": "^4.17.17", "@types/jest": "^29.5.2", - "@types/node": "^20.3.1", + "@types/node": "^20.16.5", "@types/supertest": "^2.0.12", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", @@ -3244,12 +3244,11 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.12.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.6.tgz", - "integrity": "sha512-3KurE8taB8GCvZBPngVbp0lk5CKi8M9f9k1rsADh0Evdz5SzJ+Q+Hx9uHoFGsLnLnd1xmkDQr2hVhlA0Mn0lKQ==", - "license": "MIT", + "version": "20.16.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.5.tgz", + "integrity": "sha512-VwYCweNo3ERajwy0IUlqqcyZ8/A7Zwa9ZP3MnENWcB11AejO+tLy3pu850goUW2FC/IJMdZUfKpX/yxL1gymCA==", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.2" } }, "node_modules/@types/pbkdf2": { @@ -13059,10 +13058,9 @@ "license": "MIT" }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "license": "MIT" + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" }, "node_modules/universalify": { "version": "2.0.1", diff --git a/package.json b/package.json index e844d90..04b9531 100644 --- a/package.json +++ b/package.json @@ -21,9 +21,10 @@ "typeorm": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js", "migration:create": "ts-node -r tsconfig-paths/register src/database/migration-runner.ts", "migration:run": "npm run typeorm -- -d src/database/data-source.ts migration:run", + "migration:run-dist": "npm run typeorm -- -d dist/database/data-source.js migration:run", "migration:run:js": "node ./node_modules/typeorm/cli.js -d dist/database/data-source.js migration:run", "migration:revert": "npm run typeorm -- -d src/database/data-source.ts migration:revert", - "seed:run": "ts-node -r tsconfig-paths/register ./node_modules/typeorm-extension/bin/cli.cjs seed:run -d src/database/data-source.ts", + "seed:run": "ts-node -r tsconfig-paths/register ./node_modules/typeorm-extension/bin/cli.cjs seed:run -d dist/database/data-source.js", "console:dev": "ts-node -r tsconfig-paths/register src/console.ts", "console": "node dist/console.js" }, @@ -70,7 +71,7 @@ "@trivago/prettier-plugin-sort-imports": "^4.3.0", "@types/express": "^4.17.17", "@types/jest": "^29.5.2", - "@types/node": "^20.3.1", + "@types/node": "^20.16.5", "@types/supertest": "^2.0.12", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", diff --git a/src/app.service.ts b/src/app.service.ts index 1500e10..c140dca 100644 --- a/src/app.service.ts +++ b/src/app.service.ts @@ -4,7 +4,7 @@ import { Injectable } from '@nestjs/common'; export class AppService { getApiVersion() { return { - version: 'API V1.0.0', + version: 'v1.0.0', }; } } diff --git a/src/config/common.config.ts b/src/config/common.config.ts new file mode 100644 index 0000000..d1b244b --- /dev/null +++ b/src/config/common.config.ts @@ -0,0 +1,34 @@ +import { ConfigService } from '@nestjs/config'; + +import { ENetworkName } from '@constants/blockchain.constant'; +import { EEnvKey } from '@constants/env.constant'; + +import { RpcFactory } from '@shared/modules/web3/web3.module'; +import { ETHBridgeContract } from '@shared/modules/web3/web3.service'; + +async function createRpcService(configService: ConfigService) { + return await RpcFactory(configService); +} + +async function createRpcEthService(configService: ConfigService) { + return await RpcFactory(configService, ENetworkName.ETH); +} +function getEthBridgeAddress(configService: ConfigService) { + return configService.get(EEnvKey.ETH_BRIDGE_CONTRACT_ADDRESS); +} + +function getEthBridgeStartBlock(configService: ConfigService) { + return +configService.get(EEnvKey.ETH_BRIDGE_START_BLOCK); +} + +async function initializeEthContract(configService: ConfigService) { + const [rpcEthService, address, _startBlock] = await Promise.all([ + createRpcEthService(configService), + getEthBridgeAddress(configService), + getEthBridgeStartBlock(configService), + ]); + + // Instantiate the ETHBridgeContract with the resolved dependencies + return new ETHBridgeContract(rpcEthService, address, _startBlock); +} +export { createRpcService, createRpcEthService, getEthBridgeAddress, getEthBridgeStartBlock, initializeEthContract }; diff --git a/src/config/config.module.ts b/src/config/config.module.ts index c3a5478..c1ba58d 100644 --- a/src/config/config.module.ts +++ b/src/config/config.module.ts @@ -3,7 +3,7 @@ import { ConfigModule, ConfigService } from '@nestjs/config'; import { isNumber } from 'class-validator'; import * as Joi from 'joi'; -import { EEnvKey } from '@constants/env.constant'; +import { EEnvironments, EEnvKey } from '@constants/env.constant'; import redisConfig from './redis.config'; @@ -13,7 +13,9 @@ import redisConfig from './redis.config'; ConfigModule.forRoot({ isGlobal: true, validationSchema: Joi.object({ - [EEnvKey.NODE_ENV]: Joi.string().valid('local', 'dev', 'test', 'uat', 'production').default('dev'), + [EEnvKey.NODE_ENV]: Joi.string() + .valid(...Object.values(EEnvironments)) + .default(EEnvironments.DEV), [EEnvKey.PORT]: Joi.number().default(3000), [EEnvKey.TZ]: Joi.string().default('UTC'), [EEnvKey.GLOBAL_PREFIX]: Joi.string(), @@ -55,7 +57,7 @@ import redisConfig from './redis.config'; value[EEnvKey.ETH_BRIDGE_START_BLOCK] = isNumber(value[EEnvKey.ETH_BRIDGE_START_BLOCK]) ? value[EEnvKey.ETH_BRIDGE_START_BLOCK] : Number.MAX_SAFE_INTEGER; - value[EEnvKey.MINA_BRIDGE_START_BLOCK] = value[EEnvKey.MINA_BRIDGE_START_BLOCK]; + value[EEnvKey.MINA_BRIDGE_START_BLOCK] = Number(value[EEnvKey.MINA_BRIDGE_START_BLOCK]).valueOf(); value[EEnvKey.MINA_BRIDGE_RPC_OPTIONS] = value[EEnvKey.MINA_BRIDGE_RPC_OPTIONS].split(','); value[EEnvKey.ETH_BRIDGE_RPC_OPTIONS] = value[EEnvKey.ETH_BRIDGE_RPC_OPTIONS].split(','); value[EEnvKey.SIGNER_PRIVATE_KEY] = value[EEnvKey.SIGNER_PRIVATE_KEY].split(','); diff --git a/src/constants/api.constant.ts b/src/constants/api.constant.ts index 69842a2..2fa3f2d 100644 --- a/src/constants/api.constant.ts +++ b/src/constants/api.constant.ts @@ -1,4 +1,20 @@ +export const COMMOM_CONFIG_TIP = 0.5; +export const COMMON__CONFIG_DAILY_QUOTA = 500; + export enum EDirection { ASC = 'ASC', DESC = 'DESC', } + +export enum ERole { + MINA_ADMIN = 'Mina Admin', + EVM_ADMIN = 'EVM Admin', +} + +export enum EAsset { + ETH = 'ETH', + MINA = 'MINA', + WETH = 'WETH', +} + +export const JWT_TOKEN_EXPIRE_DURATION = '1d'; diff --git a/src/constants/blockchain.constant.ts b/src/constants/blockchain.constant.ts index c4cd8f6..ff49b9c 100644 --- a/src/constants/blockchain.constant.ts +++ b/src/constants/blockchain.constant.ts @@ -1,5 +1,6 @@ export const DEFAULT_DECIMAL_PLACES = 6; - +export const DEFAULT_ADDRESS_PREFIX = '0x'; +export const DECIMAL_BASE = 10; export enum ENetworkName { ETH = 'eth', MINA = 'mina', @@ -22,3 +23,8 @@ export enum ETokenPairStatus { ENABLE = 'enable', DISABLE = 'disable', } + +export enum EMinaChainEnviroment { + TESTNET = 'testnet', + MAINNET = 'mainnet', +} diff --git a/src/constants/env.constant.ts b/src/constants/env.constant.ts index fcdda95..150e6af 100644 --- a/src/constants/env.constant.ts +++ b/src/constants/env.constant.ts @@ -40,3 +40,11 @@ export enum EEnvKey { COINMARKET_URL = 'COINMARKET_URL', BASE_MINA_BRIDGE_FEE = 'BASE_MINA_BRIDGE_FEE', } + +export enum EEnvironments { + LOCAL = 'local', + DEV = 'dev', + TEST = 'test', + UAT = 'uat', + PRODUCTION = 'production', +} diff --git a/src/constants/service.constant.ts b/src/constants/service.constant.ts index 3b923c8..1bcbab1 100644 --- a/src/constants/service.constant.ts +++ b/src/constants/service.constant.ts @@ -1,4 +1 @@ -export const RPC_SERVICE_INJECT = 'RPC_SERVICE'; -export const RPC_ETH_SERVICE_INJECT = 'RPC_ETH_SERVICE'; -export const ETH_BRIDGE_ADDRESS_INJECT = 'ETH_BRIDGE_ADDRESS_INJECT'; -export const ETH_BRIDGE_START_BLOCK_INJECT = 'ETH_BRIDGE_START_BLOCK_INJECT'; +export const ASYNC_CONNECTION = 'ASYNC_CONNECTION'; diff --git a/src/core/base-repository.ts b/src/core/base-repository.ts index 8d4732d..684aeb9 100644 --- a/src/core/base-repository.ts +++ b/src/core/base-repository.ts @@ -1,5 +1,6 @@ import { Repository, SelectQueryBuilder } from 'typeorm'; +import { EDirection } from '@constants/api.constant'; import { ETableName } from '@constants/entity.constant'; import { IPagination } from '@shared/interfaces/pagination.interface'; @@ -30,8 +31,8 @@ export abstract class BaseRepository extends Repository { queryBuilder.skip((data.page - 1) * data.limit); } if (data.sortBy) { - if (!selections || (selections && selections.includes(`${this.alias}.${data.sortBy}`))) { - queryBuilder.orderBy(`${this.alias}.${data.sortBy}`, data.direction || 'ASC'); + if (!selections || selections?.includes(`${this.alias}.${data.sortBy}`)) { + queryBuilder.orderBy(`${this.alias}.${data.sortBy}`, data.direction || EDirection.ASC); } } return queryBuilder; @@ -55,8 +56,8 @@ export abstract class BaseRepository extends Repository { } if (data.sortBy) { - if (!selections || (selections && selections.includes(`${this.alias}.${data.sortBy}`))) { - queryBuilder.orderBy(`${this.alias}.${data.sortBy}`, data.direction || 'ASC'); + if (!selections || selections?.includes(`${this.alias}.${data.sortBy}`)) { + queryBuilder.orderBy(`${this.alias}.${data.sortBy}`, data.direction || EDirection.ASC); } } diff --git a/src/database/migrations/1702277564741-event_logs.ts b/src/database/migrations/1702277564741-event_logs.ts index c483496..ad085f3 100644 --- a/src/database/migrations/1702277564741-event_logs.ts +++ b/src/database/migrations/1702277564741-event_logs.ts @@ -1,6 +1,6 @@ import { MigrationInterface, QueryRunner, Table } from 'typeorm'; -import { EEventName, EEventStatus, ENetworkName } from '@constants/blockchain.constant'; +import { EEventName, EEventStatus } from '@constants/blockchain.constant'; export class EventLogs1702277564741 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { diff --git a/src/database/seeds/common_config.seed.ts b/src/database/seeds/common_config.seed.ts index 82f23c2..3f32ff2 100644 --- a/src/database/seeds/common_config.seed.ts +++ b/src/database/seeds/common_config.seed.ts @@ -2,6 +2,8 @@ import * as dotenv from 'dotenv'; import { DataSource } from 'typeorm'; import { Seeder, SeederFactoryManager } from 'typeorm-extension'; +import { COMMOM_CONFIG_TIP, COMMON__CONFIG_DAILY_QUOTA, EAsset } from '@constants/api.constant'; + import { CommonConfig } from '@modules/crawler/entities/common-config.entity'; export default class CommonConfigSeeder implements Seeder { @@ -10,9 +12,9 @@ export default class CommonConfigSeeder implements Seeder { const repository = dataSource.getRepository(CommonConfig); await repository.insert( new CommonConfig({ - tip: 0.5, - dailyQuota: 500, - asset: 'ETH', + tip: COMMOM_CONFIG_TIP, + dailyQuota: COMMON__CONFIG_DAILY_QUOTA, + asset: EAsset.ETH, }), ); } diff --git a/src/database/seeds/super_admin.seed.ts b/src/database/seeds/super_admin.seed.ts index a432d95..4187a5c 100644 --- a/src/database/seeds/super_admin.seed.ts +++ b/src/database/seeds/super_admin.seed.ts @@ -2,6 +2,8 @@ import * as dotenv from 'dotenv'; import { DataSource } from 'typeorm'; import { Seeder, SeederFactoryManager } from 'typeorm-extension'; +import { ERole } from '@constants/api.constant'; + import { User } from '@modules/users/entities/user.entity'; export default class SuperAdminSeeder implements Seeder { @@ -11,18 +13,18 @@ export default class SuperAdminSeeder implements Seeder { const listAdmin = [ { walletAddress: process.env.ADMIN_ADDRESS_EVM, - name: 'admin evm', + name: ERole.EVM_ADMIN, }, { walletAddress: process.env.ADMIN_ADDRESS_MINA, - name: 'admin mina', + name: ERole.MINA_ADMIN, }, ]; - for (let i = 0; i < listAdmin.length; i++) { + for (const admin of listAdmin) { const newUser = new User({ - walletAddress: listAdmin[i].walletAddress, - name: listAdmin[i].name, + walletAddress: admin.walletAddress, + name: admin.name, }); await repository.insert(newUser); } diff --git a/src/database/seeds/token_pairs.seed.ts b/src/database/seeds/token_pairs.seed.ts index d860587..0d42faa 100644 --- a/src/database/seeds/token_pairs.seed.ts +++ b/src/database/seeds/token_pairs.seed.ts @@ -2,6 +2,7 @@ import * as dotenv from 'dotenv'; import { DataSource } from 'typeorm'; import { Seeder, SeederFactoryManager } from 'typeorm-extension'; +import { EAsset } from '@constants/api.constant'; import { ENetworkName, ETokenPairStatus } from '@constants/blockchain.constant'; import { TokenPair } from '@modules/users/entities/tokenpair.entity'; @@ -14,8 +15,8 @@ export default class TokenPairsSeeder implements Seeder { { fromChain: ENetworkName.ETH, toChain: ENetworkName.MINA, - fromSymbol: 'ETH', - toSymbol: 'WETH', + fromSymbol: EAsset.ETH, + toSymbol: EAsset.WETH, fromAddress: process.env.ETH_TOKEN_BRIDGE_ADDRESS, toAddress: process.env.MINA_TOKEN_BRIDGE_ADDRESS, fromDecimal: 18, @@ -26,8 +27,8 @@ export default class TokenPairsSeeder implements Seeder { { fromChain: ENetworkName.MINA, toChain: ENetworkName.ETH, - fromSymbol: 'WETH', - toSymbol: 'ETH', + fromSymbol: EAsset.WETH, + toSymbol: EAsset.ETH, fromAddress: process.env.MINA_TOKEN_BRIDGE_ADDRESS, toAddress: process.env.ETH_TOKEN_BRIDGE_ADDRESS, fromDecimal: 9, @@ -36,18 +37,18 @@ export default class TokenPairsSeeder implements Seeder { toScAddress: process.env.ETH_BRIDGE_CONTRACT_ADDRESS, }, ]; - for (let i = 0; i < listToken.length; i++) { + for (const token of listToken) { const newToken = new TokenPair({ - fromChain: listToken[i].fromChain, - toChain: listToken[i].toChain, - fromSymbol: listToken[i].fromSymbol, - toSymbol: listToken[i].toSymbol, - fromAddress: listToken[i].fromAddress, - toAddress: listToken[i].toAddress, - fromDecimal: listToken[i].fromDecimal, - toDecimal: listToken[i].toDecimal, - fromScAddress: listToken[i].fromScAddress, - toScAddress: listToken[i].toScAddress, + fromChain: token.fromChain, + toChain: token.toChain, + fromSymbol: token.fromSymbol, + toSymbol: token.toSymbol, + fromAddress: token.fromAddress, + toAddress: token.toAddress, + fromDecimal: token.fromDecimal, + toDecimal: token.toDecimal, + fromScAddress: token.fromScAddress, + toScAddress: token.toScAddress, status: ETokenPairStatus.ENABLE, }); await repository.insert(newToken); diff --git a/src/guards/roles/admin.guard.ts b/src/guards/roles/admin.guard.ts deleted file mode 100644 index c027d3a..0000000 --- a/src/guards/roles/admin.guard.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { CanActivate, ExecutionContext, Inject } from '@nestjs/common'; -import { DataSource } from 'typeorm'; - -import { IJwtPayload } from '@modules/auth/interfaces/auth.interface'; -import { User } from '@modules/users/entities/user.entity'; - -export class AdminGuard implements CanActivate { - constructor(@Inject(DataSource) private readonly dataSource: DataSource) {} - - async canActivate(context: ExecutionContext) { - const { user } = context.switchToHttp().getRequest() as { - user: IJwtPayload; - }; - - return true; - } - - getUser(userId: number) { - return this.dataSource.getRepository(User).findOne({ - where: { id: userId }, - }); - } -} diff --git a/src/guards/roles/super-admin.guard.ts b/src/guards/roles/super-admin.guard.ts deleted file mode 100644 index 9f72626..0000000 --- a/src/guards/roles/super-admin.guard.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { CanActivate, ExecutionContext } from '@nestjs/common'; -import { Observable } from 'rxjs'; - -import { EError } from '@constants/error.constant'; - -import { IJwtPayload } from '@modules/auth/interfaces/auth.interface'; - -import { httpForbidden } from '@shared/exceptions/http-exeption'; - -export class SuperAdminGuard implements CanActivate { - canActivate(context: ExecutionContext): boolean | Promise | Observable { - const { user } = context.switchToHttp().getRequest() as { - user: IJwtPayload; - }; - - return true; - } -} diff --git a/src/guards/roles/user.guard.ts b/src/guards/roles/user.guard.ts index c80a251..07a7def 100644 --- a/src/guards/roles/user.guard.ts +++ b/src/guards/roles/user.guard.ts @@ -1,18 +1,21 @@ import { CanActivate, ExecutionContext, Inject, Injectable } from '@nestjs/common'; +import { isNotEmpty } from 'class-validator'; import { DataSource } from 'typeorm'; import { IJwtPayload } from '@modules/auth/interfaces/auth.interface'; import { User } from '@modules/users/entities/user.entity'; @Injectable() -export class UserGuard implements CanActivate { +export class AdminGuard implements CanActivate { constructor(@Inject(DataSource) private readonly dataSource: DataSource) {} async canActivate(context: ExecutionContext) { - const { user } = context.switchToHttp().getRequest() as { + const { + user, + }: { user: IJwtPayload; - }; - return true; + } = context.switchToHttp().getRequest(); + return isNotEmpty(user); } getUser(userId: number) { diff --git a/src/main.ts b/src/main.ts index 35b9520..d0914b9 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,6 +2,8 @@ import { ValidationPipe } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { NestFactory } from '@nestjs/core'; +import { isDevelopmentEnvironment } from '@shared/utils/util'; + import { AppModule } from './app.module'; import { EEnvKey } from './constants/env.constant'; import './core/paginate-typeorm'; @@ -11,15 +13,21 @@ async function bootstrap() { const app = await NestFactory.create(AppModule); const configService = app.get(ConfigService); + const listenPort = configService.get(EEnvKey.PORT) || 3000; + app.setGlobalPrefix(configService.get(EEnvKey.GLOBAL_PREFIX) || 'api'); app.enableCors(); - app.useGlobalPipes(new ValidationPipe({ transform: true })); - // Swagger - if (configService.get(EEnvKey.SWAGGER_PATH)) { + app.useGlobalPipes(new ValidationPipe({ transform: true, whitelist: true })); + + if (isDevelopmentEnvironment()) { initSwagger(app, configService.get(EEnvKey.SWAGGER_PATH)); } - const listenPort = configService.get(EEnvKey.PORT) || 3000; - await app.listen(listenPort); - console.log(`🚀🚀🚀 Mina Bridge backend is running on port ${listenPort}`); + await app.listen(listenPort, async () => { + const appUrl = await app.getUrl(); + console.log(`🚀🚀🚀 Mina Bridge backend is running at ${appUrl}`); + if (isDevelopmentEnvironment()) { + console.log(`📖📖📖 Documentation is running at ${appUrl}/${configService.get(EEnvKey.SWAGGER_PATH)}`); + } + }); } bootstrap(); diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index 9b8bce0..ecf3524 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -1,14 +1,11 @@ -import { Body, Controller, Get, Inject, Post } from '@nestjs/common'; +import { Body, Controller, Get, Post } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; import { EEnvKey } from '@constants/env.constant'; -import { ETH_BRIDGE_ADDRESS_INJECT, RPC_ETH_SERVICE_INJECT, RPC_SERVICE_INJECT } from '@constants/service.constant'; import { GuardPublic } from '@guards/guard.decorator'; -import { IRpcService } from '@shared/modules/web3/web3.module'; - import { AuthService } from './auth.service'; import { LoginDto, LoginMinaDto, RefreshTokenRequestDto } from './dto/auth-request.dto'; import { LoginResponseDto, MessageResponseDto, RefreshTokenResponseDto } from './dto/auth-response.dto'; @@ -18,17 +15,11 @@ import { LoginResponseDto, MessageResponseDto, RefreshTokenResponseDto } from '. export class AuthController { private readonly ethBridgeStartBlock: number; constructor( - @Inject(ETH_BRIDGE_ADDRESS_INJECT) private ethBridgeContractAddress: string, - @Inject(RPC_SERVICE_INJECT) private rpcService: IRpcService, - @Inject(RPC_ETH_SERVICE_INJECT) private rpcETHService: IRpcService, private authService: AuthService, private readonly configService: ConfigService, ) { this.ethBridgeStartBlock = this.configService.get(EEnvKey.ETH_BRIDGE_START_BLOCK); } - setETHBridgeAddress(newValue: string): void { - this.ethBridgeContractAddress = newValue; - } @Post('/login-admin-evm') @GuardPublic() diff --git a/src/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts index 452387d..bb023d7 100644 --- a/src/modules/auth/auth.module.ts +++ b/src/modules/auth/auth.module.ts @@ -6,12 +6,11 @@ import { PassportModule } from '@nestjs/passport'; import { UserRepository } from 'database/repositories/user.repository'; import { CustomRepositoryModule } from 'nestjs-typeorm-custom-repository'; +import { JWT_TOKEN_EXPIRE_DURATION } from '@constants/api.constant'; import { EEnvKey } from '@constants/env.constant'; import { UsersModule } from '@modules/users/users.module'; -import { Web3Module } from '@shared/modules/web3/web3.module'; - import { AuthController } from './auth.controller'; import { AuthService } from './auth.service'; import { JwtStrategy } from './strategies/jwt.strategy'; @@ -26,14 +25,13 @@ import { JwtStrategy } from './strategies/jwt.strategy'; ({ secret: configService.get(EEnvKey.JWT_SECRET_KEY), signOptions: { - expiresIn: '1d', + expiresIn: JWT_TOKEN_EXPIRE_DURATION, }, }) as JwtModuleOptions, inject: [ConfigService], }), HttpModule.register({ timeout: 3000 }), UsersModule, - Web3Module, ], providers: [AuthService, JwtStrategy], controllers: [AuthController], diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts index 3704783..95f03c5 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -4,11 +4,10 @@ import { JwtService } from '@nestjs/jwt'; import { UserRepository } from 'database/repositories/user.repository'; import { Logger } from 'log4js'; import Client from 'mina-signer'; -import { DataSource } from 'typeorm'; -import Web3 from 'web3'; import { toChecksumAddress } from 'web3-utils'; -import { EEnvKey } from '@constants/env.constant'; +import { EMinaChainEnviroment } from '@constants/blockchain.constant'; +import { EEnvironments, EEnvKey } from '@constants/env.constant'; import { EError } from '@constants/error.constant'; import { User } from '@modules/users/entities/user.entity'; @@ -22,14 +21,13 @@ import { IJwtPayload } from './interfaces/auth.interface'; @Injectable() export class AuthService { - private web3: Web3; private readonly logger: Logger; constructor( private jwtService: JwtService, private configService: ConfigService, private readonly userRepository: UserRepository, - private readonly ethBridgeContract: ETHBridgeContract, private readonly loggerService: LoggerService, + private readonly ethBridgeContract: ETHBridgeContract, ) { this.logger = loggerService.getLogger('AUTH_SERVICE'); } @@ -90,9 +88,9 @@ export class AuthService { } private async validateSignatureMina(address: string, signature) { - let client = new Client({ network: 'mainnet' }); - if (process.env.NODE_ENV !== 'production') { - client = new Client({ network: 'testnet' }); + let client = new Client({ network: EMinaChainEnviroment.MAINNET }); + if (process.env.NODE_ENV !== EEnvironments.PRODUCTION) { + client = new Client({ network: EMinaChainEnviroment.TESTNET }); } const signer = { @@ -120,7 +118,7 @@ export class AuthService { try { jwtData = this.jwtService.verify(refreshToken, { secret, - }) as IJwtPayload; + }); } catch (error) { httpBadRequest(EError.INVALID_TOKEN); } diff --git a/src/modules/auth/dto/auth-request.dto.ts b/src/modules/auth/dto/auth-request.dto.ts index 889a499..6f375b6 100644 --- a/src/modules/auth/dto/auth-request.dto.ts +++ b/src/modules/auth/dto/auth-request.dto.ts @@ -1,60 +1,64 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Exclude, Expose } from 'class-transformer'; -import { IsEmail, IsEthereumAddress, IsNotEmpty, IsString } from 'class-validator'; +import { ObjectField, StringField } from '@shared/decorators/field.decorator'; -@Exclude() export class SignupDto { - @ApiProperty({ example: 'John' }) - @IsEmail() - @IsNotEmpty() - @Expose() + @StringField({ + required: true, + isEmail: true, + example: 'name@mail.com', + }) email: string; - @ApiProperty({ example: 'Abc@123' }) - @IsString() - @IsNotEmpty() - @Expose() + @StringField({ + required: true, + example: 'ABC@123', + }) password: string; } export class LoginDto { - @ApiProperty() - @IsEthereumAddress() - @IsNotEmpty() + @StringField({ + required: true, + }) address: string; - @ApiProperty() - @IsString() - @IsNotEmpty() + @StringField({ + required: true, + }) signature: string; } -class SignatureMina { +export class MinaSignatureDto { + @StringField({ + required: true, + example: '13103062255371554830871806571266501056569826727061194167717383802935285095667', + }) field: string; + + @StringField({ + required: true, + example: '8184099996718391251128744530931690607354984861474783138892757893603123747186', + }) scalar: string; } export class LoginMinaDto { - @ApiProperty({ example: 'B62qph8sAdxKn1JChJRLzCWek7kkdi8QPLWdfhpFEMDNbM4Ficpradb' }) - @IsString() - @IsNotEmpty() + @StringField({ + required: true, + example: 'B62qph8sAdxKn1JChJRLzCWek7kkdi8QPLWdfhpFEMDNbM4Ficpradb', + }) address: string; - @ApiProperty({ - example: { - field: '13103062255371554830871806571266501056569826727061194167717383802935285095667', - scalar: '8184099996718391251128744530931690607354984861474783138892757893603123747186', - }, + @ObjectField({ + required: true, + type: MinaSignatureDto, }) - @IsNotEmpty() - signature: SignatureMina; + signature: MinaSignatureDto; } export class RefreshTokenRequestDto { - @ApiProperty({ + @StringField({ + required: true, example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiIxIiwicm9sZSI6InVzZXIifQ.iFGDbsCMdIiMRVr2g4oaG6_9wDGi9wkjPNEAnLsPmyU', }) - @IsString() - @IsNotEmpty() refreshToken: string; } diff --git a/src/modules/crawler/batch.tokenprice.ts b/src/modules/crawler/batch.tokenprice.ts index 3cef633..85ea192 100644 --- a/src/modules/crawler/batch.tokenprice.ts +++ b/src/modules/crawler/batch.tokenprice.ts @@ -3,6 +3,7 @@ import { ConfigService } from '@nestjs/config'; import axios from 'axios'; import { TokenPriceRepository } from 'database/repositories/token-price.repository'; +import { EAsset } from '@constants/api.constant'; import { EEnvKey } from '@constants/env.constant'; import { TokenPrice } from './entities'; @@ -13,39 +14,35 @@ export class BatchJobGetPriceToken { private readonly configService: ConfigService, private readonly tokenPriceRepository: TokenPriceRepository, ) {} + public async handleGetPriceToken() { - try { - const apiKey = this.configService.get(EEnvKey.COINMARKET_KEY); - const apiUrl = this.configService.get(EEnvKey.COINMARKET_URL); - const headers = { - 'X-CMC_PRO_API_KEY': apiKey, - }; + const apiKey = this.configService.get(EEnvKey.COINMARKET_KEY); + const apiUrl = this.configService.get(EEnvKey.COINMARKET_URL); + const headers = { + 'X-CMC_PRO_API_KEY': apiKey, + }; - const result = await axios.get(apiUrl, { headers }); + const result = await axios.get(apiUrl, { headers }); - result?.data?.data.forEach(async e => { - if (e.symbol == 'MINA') { - const tokenMina = await this.tokenPriceRepository.getTokenPriceBySymbol('MINA'); - if (!tokenMina) { - this.tokenPriceRepository.save(new TokenPrice({ symbol: 'MINA', priceUsd: e.quote.USD.price || 1 })); - } else { - tokenMina.priceUsd = e.quote.USD.price; - tokenMina.save(); - } + result?.data?.data.forEach(async e => { + if (e.symbol == EAsset.MINA) { + const tokenMina = await this.tokenPriceRepository.getTokenPriceBySymbol(EAsset.MINA); + if (!tokenMina) { + this.tokenPriceRepository.save(new TokenPrice({ symbol: EAsset.MINA, priceUsd: e.quote.USD.price || 1 })); + } else { + tokenMina.priceUsd = e.quote.USD.price; + tokenMina.save(); } - if (e.symbol == 'ETH') { - const tokenMina = await this.tokenPriceRepository.getTokenPriceBySymbol('ETH'); - if (!tokenMina) { - this.tokenPriceRepository.save(new TokenPrice({ symbol: 'ETH', priceUsd: e.quote.USD.price || 2300 })); - } else { - tokenMina.priceUsd = e.quote.USD.price; - tokenMina.save(); - } + } + if (e.symbol == EAsset.ETH) { + const tokenMina = await this.tokenPriceRepository.getTokenPriceBySymbol(EAsset.ETH); + if (!tokenMina) { + this.tokenPriceRepository.save(new TokenPrice({ symbol: EAsset.ETH, priceUsd: e.quote.USD.price || 2300 })); + } else { + tokenMina.priceUsd = e.quote.USD.price; + tokenMina.save(); } - }); - } catch (error) { - throw error; - } finally { - } + } + }); } } diff --git a/src/modules/crawler/crawler.evmbridge.ts b/src/modules/crawler/crawler.evmbridge.ts index 0c158a6..ce88148 100644 --- a/src/modules/crawler/crawler.evmbridge.ts +++ b/src/modules/crawler/crawler.evmbridge.ts @@ -6,6 +6,7 @@ import { Logger } from 'log4js'; import { DataSource, QueryRunner } from 'typeorm'; import { EventData } from 'web3-eth-contract'; +import { EAsset } from '@constants/api.constant'; import { EEventName, EEventStatus, ENetworkName } from '@constants/blockchain.constant'; import { EEnvKey } from '@constants/env.constant'; @@ -21,10 +22,10 @@ export class BlockchainEVMCrawler { constructor( private readonly configService: ConfigService, private readonly dataSource: DataSource, - private readonly ethBridgeContract: ETHBridgeContract, private readonly crawlContractRepository: CrawlContractRepository, private readonly tokenPairRepository: TokenPairRepository, private readonly loggerService: LoggerService, + private readonly ethBridgeContract: ETHBridgeContract, ) { this.numberOfBlockPerJob = +this.configService.get(EEnvKey.NUMBER_OF_BLOCK_PER_JOB); this.logger = loggerService.getLogger('BLOCKCHAIN_EVM_CRAWLER'); @@ -37,6 +38,7 @@ export class BlockchainEVMCrawler { try { const { startBlockNumber, toBlock } = await this.getFromToBlock(); const events = await this.ethBridgeContract.getEvent(startBlockNumber, toBlock); + for (const event of events) { switch (event.event) { case 'Lock': @@ -60,7 +62,7 @@ export class BlockchainEVMCrawler { } } - private async handlerLockEvent(event: EventData, queryRunner: QueryRunner) { + public async handlerLockEvent(event: EventData, queryRunner: QueryRunner) { const blockTimeOfBlockNumber = await this.ethBridgeContract.getBlockTimeByBlockNumber(event.blockNumber); const eventUnlock = { senderAddress: event.returnValues.locker, @@ -94,9 +96,13 @@ export class BlockchainEVMCrawler { } await queryRunner.manager.save(EventLog, eventUnlock); + + return { + success: true, + }; } - private async handlerUnLockEvent(event: EventData, queryRunner: QueryRunner) { + public async handlerUnLockEvent(event: EventData, queryRunner: QueryRunner) { const existLockTx = await queryRunner.manager.findOne(EventLog, { where: { txHashLock: event.returnValues.hash }, }); @@ -110,11 +116,15 @@ export class BlockchainEVMCrawler { amountReceived: event.returnValues.amount, tokenReceivedAddress: event.returnValues.token, protocolFee: event.returnValues.fee, - tokenReceivedName: 'ETH', + tokenReceivedName: EAsset.ETH, }); + + return { + success: true, + }; } - private async updateLatestBlockCrawl(blockNumber: number, queryRunner: QueryRunner) { + public async updateLatestBlockCrawl(blockNumber: number, queryRunner: QueryRunner) { await queryRunner.manager.update( CrawlContract, { @@ -131,7 +141,7 @@ export class BlockchainEVMCrawler { let startBlockNumber = this.ethBridgeContract.getStartBlock(); let toBlock = await this.ethBridgeContract.getBlockNumber(); - let currentCrawledBlock = await this.crawlContractRepository.findOne({ + const currentCrawledBlock = await this.crawlContractRepository.findOne({ where: { networkName: ENetworkName.ETH }, }); if (!currentCrawledBlock) { @@ -140,7 +150,7 @@ export class BlockchainEVMCrawler { networkName: ENetworkName.ETH, latestBlock: startBlockNumber, }); - currentCrawledBlock = await this.crawlContractRepository.save(tmpData); + await this.crawlContractRepository.save(tmpData); } else { startBlockNumber = Number(currentCrawledBlock.latestBlock) + 1; } diff --git a/src/modules/crawler/crawler.minabridge.ts b/src/modules/crawler/crawler.minabridge.ts index 4ffae53..3772b75 100644 --- a/src/modules/crawler/crawler.minabridge.ts +++ b/src/modules/crawler/crawler.minabridge.ts @@ -7,7 +7,8 @@ import { Logger } from 'log4js'; import { Field, Mina, PublicKey, UInt32 } from 'o1js'; import { DataSource, QueryRunner } from 'typeorm'; -import { EEventName, EEventStatus, ENetworkName } from '@constants/blockchain.constant'; +import { EAsset } from '@constants/api.constant'; +import { DEFAULT_ADDRESS_PREFIX, EEventName, EEventStatus, ENetworkName } from '@constants/blockchain.constant'; import { EEnvKey } from '@constants/env.constant'; import { CrawlContract, EventLog } from '@modules/crawler/entities'; @@ -41,7 +42,6 @@ export class SCBridgeMinaCrawler { }); Mina.setActiveInstance(Network); const zkappAddress = PublicKey.fromBase58(this.configService.get(EEnvKey.MINA_BRIDGE_CONTRACT_ADDRESS)); - // const zkAppToken = PublicKey.fromBase58(this.configService.get(EEnvKey.MINA_TOKEN_BRIDGE_ADDRESS)); const zkapp = new Bridge(zkappAddress); const events = await zkapp.fetchEvents(UInt32.from(Number(startBlockNumber) + 1)); @@ -61,7 +61,8 @@ export class SCBridgeMinaCrawler { } this.logger.info(`[handleCrawlMinaBridge] Crawled from============================= ${startBlockNumber}`); if (events.length > 0) { - await this.updateLatestBlockCrawl(Number(events.reverse()[0].blockHeight.toString()), queryRunner); + // udpate current latest block + await this.updateLatestBlockCrawl(Number(events.pop().blockHeight.toString()), queryRunner); } return await queryRunner.commitTransaction(); } catch (error) { @@ -72,7 +73,7 @@ export class SCBridgeMinaCrawler { } } - private async handlerUnLockEvent(event, queryRunner: QueryRunner) { + public async handlerUnLockEvent(event, queryRunner: QueryRunner) { const existLockTx = await queryRunner.manager.findOne(EventLog, { where: { id: event.event.data.id.toString() }, }); @@ -85,24 +86,26 @@ export class SCBridgeMinaCrawler { status: EEventStatus.COMPLETED, txHashUnlock: event.event.transactionInfo.transactionHash, amountReceived: event.event.data.amount.toString(), - tokenReceivedAddress: event.event.data.tokenAddress.toBase58(), - tokenReceivedName: 'WETH', + tokenReceivedAddress: event.event.data.tokenAddress, + tokenReceivedName: EAsset.WETH, }); + + return { + success: true, + }; } - private async handlerLockEvent(event, queryRunner: QueryRunner) { + public async handlerLockEvent(event, queryRunner: QueryRunner) { const field = Field.from(event.event.data.receipt.toString()); - const receiveAddress = '0x' + field.toBigInt().toString(16); - - // const timeLock = await this.getDateTimeByBlock(event.blockHeight.toString()); + const receiveAddress = DEFAULT_ADDRESS_PREFIX + field.toBigInt().toString(16); const eventUnlock = { - senderAddress: event.event.data.locker.toBase58(), + senderAddress: event.event.data.locker, amountFrom: event.event.data.amount.toString(), tokenFromAddress: this.configService.get(EEnvKey.MINA_TOKEN_BRIDGE_ADDRESS), networkFrom: ENetworkName.MINA, networkReceived: ENetworkName.ETH, - tokenFromName: 'WETH', + tokenFromName: EAsset.WETH, tokenReceivedAddress: this.configService.get(EEnvKey.ETH_TOKEN_BRIDGE_ADDRESS), txHashLock: event.event.transactionInfo.transactionHash, receiveAddress: receiveAddress, @@ -130,36 +133,10 @@ export class SCBridgeMinaCrawler { this.logger.info({ eventUnlock }); await queryRunner.manager.save(EventLog, eventUnlock); - } - - private async getDateTimeByBlock(blockNumber: number) { - const endpoint = this.configService.get(EEnvKey.MINA_BRIDGE_RPC_OPTIONS); - const query = ` - query { - transaction(query: {blockHeight: ${blockNumber}}) { - dateTime - } - } - `; - const response = await fetch(endpoint, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - query, - }), - }); - - const result = await response.json(); - this.logger.info('=========', result.data.transaction); - - const dateTime = dayjs(result.data.transaction.dateTime); - - // Convert DateTime to Unix timestamp in seconds - const unixTimestampInSeconds = Math.floor(dateTime.valueOf() / 1000); - return unixTimestampInSeconds; + return { + success: true, + }; } private async updateLatestBlockCrawl(blockNumber: number, queryRunner: QueryRunner) { diff --git a/src/modules/crawler/sender.evmbridge.ts b/src/modules/crawler/sender.evmbridge.ts index 819365d..db2413e 100644 --- a/src/modules/crawler/sender.evmbridge.ts +++ b/src/modules/crawler/sender.evmbridge.ts @@ -4,7 +4,7 @@ import { CommonConfigRepository } from 'database/repositories/common-configurati import { EventLogRepository } from 'database/repositories/event-log.repository'; import { TokenPairRepository } from 'database/repositories/token-pair.repository'; -import { EEventStatus, ENetworkName } from '@constants/blockchain.constant'; +import { DECIMAL_BASE, EEventStatus, ENetworkName } from '@constants/blockchain.constant'; import { EError } from '@constants/error.constant'; import { ETHBridgeContract } from '@shared/modules/web3/web3.service'; @@ -14,9 +14,9 @@ import { addDecimal, calculateFee } from '@shared/utils/bignumber'; export class SenderEVMBridge { constructor( private readonly eventLogRepository: EventLogRepository, - private readonly ethBridgeContract: ETHBridgeContract, private readonly commonConfigRepository: CommonConfigRepository, private readonly tokenPairRepository: TokenPairRepository, + private readonly ethBridgeContract: ETHBridgeContract, ) {} async handleUnlockEVM() { @@ -46,8 +46,8 @@ export class SenderEVMBridge { } const amountReceive = BigNumber(amountFrom) - .dividedBy(BigNumber(10).pow(tokenPair.fromDecimal)) - .multipliedBy(BigNumber(10).pow(tokenPair.toDecimal)) + .dividedBy(BigNumber(DECIMAL_BASE).pow(tokenPair.fromDecimal)) + .multipliedBy(BigNumber(DECIMAL_BASE).pow(tokenPair.toDecimal)) .toString(); const isPassDailyQuota = await this.isPassDailyQuota(senderAddress, tokenPair.fromDecimal); @@ -60,7 +60,6 @@ export class SenderEVMBridge { ); return; } - const gasFee = await this.ethBridgeContract.getEstimateGas( tokenReceivedAddress, BigNumber(amountReceive), diff --git a/src/modules/crawler/sender.minabridge.ts b/src/modules/crawler/sender.minabridge.ts index 2658705..5359629 100644 --- a/src/modules/crawler/sender.minabridge.ts +++ b/src/modules/crawler/sender.minabridge.ts @@ -8,7 +8,7 @@ import { TokenPriceRepository } from 'database/repositories/token-price.reposito import { Logger } from 'log4js'; import { AccountUpdate, fetchAccount, Mina, PrivateKey, PublicKey, UInt64 } from 'o1js'; -import { EEventStatus, ENetworkName } from '@constants/blockchain.constant'; +import { DECIMAL_BASE, EEventStatus, ENetworkName } from '@constants/blockchain.constant'; import { EEnvKey } from '@constants/env.constant'; import { EError } from '@constants/error.constant'; @@ -21,6 +21,7 @@ import { Bridge } from './minaSc/minaBridgeSC'; @Injectable() export class SenderMinaBridge { private readonly logger: Logger; + private isContractCompiled = false; constructor( private readonly configService: ConfigService, private readonly eventLogRepository: EventLogRepository, @@ -31,7 +32,13 @@ export class SenderMinaBridge { ) { this.logger = this.loggerService.getLogger('SENDER_MINA_BRIDGE'); } - + private async compileContract() { + if (!this.isContractCompiled) { + await Bridge.compile(); + await FungibleToken.compile(); + this.isContractCompiled = true; + } + } public async handleUnlockMina() { let dataLock, configTip, rateethmina; try { @@ -58,8 +65,8 @@ export class SenderMinaBridge { } const amountReceiveConvert = BigNumber(amountFrom) - .dividedBy(BigNumber(10).pow(tokenPair.fromDecimal)) - .multipliedBy(BigNumber(10).pow(tokenPair.toDecimal)) + .dividedBy(BigNumber(DECIMAL_BASE).pow(tokenPair.fromDecimal)) + .multipliedBy(BigNumber(DECIMAL_BASE).pow(tokenPair.toDecimal)) .toString(); const protocolFeeAmount = calculateFee( amountReceiveConvert, @@ -114,7 +121,7 @@ export class SenderMinaBridge { const feepayerKey = PrivateKey.fromBase58(this.configService.get(EEnvKey.SIGNER_MINA_PRIVATE_KEY)); const zkAppKey = PrivateKey.fromBase58(this.configService.get(EEnvKey.MINA_BRIDGE_SC_PRIVATE_KEY)); const receiverPublicKey = PublicKey.fromBase58(receiveAddress); - // TODO: move these urls to env + const MINAURL = this.configService.get(EEnvKey.MINA_BRIDGE_RPC_OPTIONS); const ARCHIVEURL = this.configService.get(EEnvKey.MINA_BRIDGE_ARCHIVE_RPC_OPTIONS); @@ -126,8 +133,7 @@ export class SenderMinaBridge { this.logger.info('compile the contract...'); - await Bridge.compile(); - await FungibleToken.compile(); + await this.compileContract(); const fee = protocolFeeAmount * rateMINAETH + +this.configService.get(EEnvKey.BASE_MINA_BRIDGE_FEE); // in nanomina (1 billion = 1.0 mina) const feepayerAddress = feepayerKey.toPublicKey(); @@ -144,7 +150,7 @@ export class SenderMinaBridge { // call update() and send transaction this.logger.info('build transaction and create proof...'); const tx = await Mina.transaction({ sender: feepayerAddress, fee }, async () => { - if (!hasAccount) AccountUpdate.fundNewAccount(feepayerAddress, 1); + if (!hasAccount) AccountUpdate.fundNewAccount(feepayerAddress); await zkBridge.unlock(UInt64.from(amount), receiverPublicKey, UInt64.from(txId)); }); await tx.prove(); @@ -180,30 +186,4 @@ export class SenderMinaBridge { } return true; } - - private async fetchNonce(feepayerAddress) { - const url = this.configService.get(EEnvKey.MINA_BRIDGE_RPC_OPTIONS); - const query = ` - query { - account(publicKey: "${feepayerAddress}") { - inferredNonce - } - } - `; - const response = await fetch(url, { - method: 'POST', - body: JSON.stringify({ operationName: null, query, variables: {} }), - headers: { 'Content-Type': 'application/json' }, - }); - const json = await response.json(); - - const inferredNonce = Number(json.data.account.inferredNonce); - return inferredNonce; - } - - private tweakMintPrecondition(token: FungibleToken, mempoolMintAmount: number) { - // here we take `circulating` variable from state slot with index 3 and increase it by `mempoolMintAmount` - const prevPreconditionVal = token.self.body.preconditions.account.state[3]!.value; - token.self.body.preconditions.account.state[3]!.value = prevPreconditionVal.add(mempoolMintAmount); - } } diff --git a/src/modules/crawler/tests/evm-crawler.spec.ts b/src/modules/crawler/tests/evm-crawler.spec.ts index e69de29..926cea1 100644 --- a/src/modules/crawler/tests/evm-crawler.spec.ts +++ b/src/modules/crawler/tests/evm-crawler.spec.ts @@ -0,0 +1,317 @@ +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { Test, TestingModule } from '@nestjs/testing'; +import { CrawlContractRepository } from 'database/repositories/crawl-contract.repository'; +import { TokenPairRepository } from 'database/repositories/token-pair.repository'; +import { DataSource, QueryRunner } from 'typeorm'; +import { EventData } from 'web3-eth-contract'; + +import { initializeEthContract } from '@config/common.config'; +import { ConfigurationModule } from '@config/config.module'; + +import { EEventName, EEventStatus, ENetworkName } from '@constants/blockchain.constant'; +import { EEnvKey } from '@constants/env.constant'; + +import { TokenPair } from '@modules/users/entities/tokenpair.entity'; + +import { LoggerService } from '@shared/modules/logger/logger.service'; +import { DefaultContract, ETHBridgeContract } from '@shared/modules/web3/web3.service'; + +import { BlockchainEVMCrawler } from '../crawler.evmbridge'; +import { CrawlContract, EventLog } from '../entities'; + +describe('BlockchainEVMCraler', () => { + let crawler: BlockchainEVMCrawler; + let dataSource: DataSource; + let newEthBridgeContract: ETHBridgeContract; + let queryRunner: QueryRunner; + let configService: ConfigService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + }), + ConfigurationModule, + ], + providers: [ + BlockchainEVMCrawler, + { + provide: DataSource, + useValue: { + createQueryRunner: jest.fn().mockReturnValue({ + connect: jest.fn(), + startTransaction: jest.fn(), + commitTransaction: jest.fn(), + rollbackTransaction: jest.fn(), + release: jest.fn(), + manager: { + save: jest.fn(), + findOne: jest.fn(), + update: jest.fn(), + }, + }), + }, + }, + { + provide: CrawlContractRepository, + useValue: { + findOne: jest.fn(), + create: jest.fn(), + save: jest.fn(), + }, + }, + { + provide: TokenPairRepository, + useValue: { + getTokenPair: jest.fn(), + }, + }, + { + provide: LoggerService, + useValue: { + getLogger: jest.fn().mockReturnValue({ + info: jest.fn(), + }), + }, + }, + { + provide: ETHBridgeContract, + useValue: { + getEvent: jest.fn(), + getBlockTimeByBlockNumber: jest.fn(), + getStartBlock: jest.fn(), + getBlockNumber: jest.fn(), + getContractAddress: jest.fn(), + }, + }, + { + provide: DefaultContract, + useValue: { + getEvent: jest.fn(), + wrapper: jest.fn(), + initContract: jest.fn(), + }, + }, + ], + }).compile(); + + crawler = module.get(BlockchainEVMCrawler); + dataSource = module.get(DataSource); + newEthBridgeContract = module.get(ETHBridgeContract); + configService = module.get(ConfigService); + queryRunner = dataSource.createQueryRunner(); + }); + + describe('handleEventCrawlBlock', () => { + it('should successfully handle events and commit transaction', async () => { + // Mocking getFromToBlock to return start and end block numbers + jest.spyOn(crawler as any, 'getFromToBlock').mockResolvedValue({ startBlockNumber: 5434541, toBlock: 5434552 }); + const ethBridgeContract = await initializeEthContract(configService); + const getEvent = await ethBridgeContract.getEvent( + await ( + await crawler['getFromToBlock']() + ).startBlockNumber, + await ( + await crawler['getFromToBlock']() + ).toBlock, + ); + + expect(getEvent).toBeDefined(); + + const receivedObject = { + address: '0x83e21AccD43Bb7C23C51e68fFa345fab3983FfeC', + blockHash: '0xbb4323b3443ee6ba9dc12597e5df383b3f497fa7b875e438c33fecdb4efe6278', + blockNumber: 5434542, + event: 'Lock', + logIndex: 16, + raw: { + data: '0x000000000000000000000000c31cbd88f0b0fbf9686200a6a3c41b23e8901be700000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000016345785d8a0000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000374236327171505a576a7252377453676e395470766a455955484e675a6f5a78784d315a764c367053414863634142517a7877694b78544700000000000000000000000000000000000000000000000000000000000000000000000000000000034554480000000000000000000000000000000000000000000000000000000000', + topics: ['0xdb2afed7fa09277d868c7d909e59f48f8fe738cf8b88268672f81075db44e0a3'], + }, + returnValues: { + '0': '0xC31cBD88f0b0fBF9686200a6A3c41b23E8901bE7', + '1': 'B62qqPZWjrR7tSgn9TpvjEYUHNgZoZxxM1ZvL6pSAHccABQzxwiKxTG', + '2': '0x0000000000000000000000000000000000000000', + '3': '100000000000000000', + '4': 'ETH', + amount: '100000000000000000', + locker: '0xC31cBD88f0b0fBF9686200a6A3c41b23E8901bE7', + receipt: 'B62qqPZWjrR7tSgn9TpvjEYUHNgZoZxxM1ZvL6pSAHccABQzxwiKxTG', + token: '0x0000000000000000000000000000000000000000', + tokenName: 'ETH', + }, + signature: '0xdb2afed7fa09277d868c7d909e59f48f8fe738cf8b88268672f81075db44e0a3', + transactionHash: '0x662e9ae1929f82153e99522a755d4db0c36544cbaab390a6cc7d546b1b4b4095', + transactionIndex: 33, + }; + + jest.spyOn(crawler as any, 'handlerLockEvent').mockResolvedValue(receivedObject); + (newEthBridgeContract.getEvent as jest.Mock).mockResolvedValue(getEvent); + await crawler.handleEventCrawlBlock(); + + expect(queryRunner.connect).toHaveBeenCalled(); + expect(queryRunner.startTransaction).toHaveBeenCalled(); + expect(queryRunner.commitTransaction).toHaveBeenCalled(); + expect(queryRunner.release).toHaveBeenCalled(); + }); + + it('should save the correct event log in handlerLockEvent', async () => { + const ethBridgeContract = await initializeEthContract(configService); + const lockReturnObject = { + address: '0x83e21AccD43Bb7C23C51e68fFa345fab3983FfeC', + blockHash: '0xbb4323b3443ee6ba9dc12597e5df383b3f497fa7b875e438c33fecdb4efe6278', + blockNumber: 5434542, + event: 'Lock', + logIndex: 16, + raw: { + data: '0x000000000000000000000000c31cbd88f0b0fbf9686200a6a3c41b23e8901be700000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000016345785d8a0000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000374236327171505a576a7252377453676e395470766a455955484e675a6f5a78784d315a764c367053414863634142517a7877694b78544700000000000000000000000000000000000000000000000000000000000000000000000000000000034554480000000000000000000000000000000000000000000000000000000000', + topics: ['0xdb2afed7fa09277d868c7d909e59f48f8fe738cf8b88268672f81075db44e0a3'], + }, + returnValues: { + '0': '0xC31cBD88f0b0fBF9686200a6A3c41b23E8901bE7', + '1': 'B62qqPZWjrR7tSgn9TpvjEYUHNgZoZxxM1ZvL6pSAHccABQzxwiKxTG', + '2': '0x0000000000000000000000000000000000000000', + '3': '100000000000000000', + '4': 'ETH', + amount: '100000000000000000', + locker: '0xC31cBD88f0b0fBF9686200a6A3c41b23E8901bE7', + receipt: 'B62qqPZWjrR7tSgn9TpvjEYUHNgZoZxxM1ZvL6pSAHccABQzxwiKxTG', + token: '0x0000000000000000000000000000000000000000', + tokenName: 'ETH', + }, + signature: '0xdb2afed7fa09277d868c7d909e59f48f8fe738cf8b88268672f81075db44e0a3', + transactionHash: '0x662e9ae1929f82153e99522a755d4db0c36544cbaab390a6cc7d546b1b4b4095', + transactionIndex: 33, + } as EventData; + + const mockTokenPair = { + fromAddress: '0xFromAddress', + toAddress: '0xToAddress', + fromDecimal: 18, + toDecimal: 18, + } as TokenPair; + const mockBlockTime = await ethBridgeContract.getBlockTimeByBlockNumber(lockReturnObject.blockNumber); + (newEthBridgeContract.getBlockTimeByBlockNumber as jest.Mock).mockResolvedValue(mockBlockTime); + const result = await crawler.handlerLockEvent(lockReturnObject, queryRunner); + jest.spyOn(crawler as any, 'handlerLockEvent').mockResolvedValue({ + blockNumber: lockReturnObject.blockNumber, + senderAddress: lockReturnObject.returnValues.locker, + amountFrom: lockReturnObject.returnValues.amount, + tokenFromAddress: lockReturnObject.returnValues.token, + networkFrom: ENetworkName.ETH, + networkReceived: ENetworkName.MINA, + tokenFromName: lockReturnObject.returnValues.tokenName, + tokenReceivedAddress: '0xMinaTokenBridgeAddress', + txHashLock: lockReturnObject.transactionHash, + receiveAddress: lockReturnObject.returnValues.receipt, + event: EEventName.LOCK, + returnValues: JSON.stringify(lockReturnObject.returnValues), + status: EEventStatus.WAITING, + retry: 0, + fromTokenDecimal: mockTokenPair.fromDecimal, + toTokenDecimal: mockTokenPair.toDecimal, + blockTimeLock: mockBlockTime.timestamp, + }); + + expect(result.success).toBe(true); + }); + + it('should save the correct event log in handlerUnlockEvent', async () => { + const unlockObject = { + address: '0x83e21AccD43Bb7C23C51e68fFa345fab3983FfeC', + blockNumber: 6591652, + transactionHash: '0x8d863f9701b6bf9684f57ff9a1949f54725673349c7b1f2c0a67f87dc9bf7d41', + transactionIndex: 70, + blockHash: '0xb901e9c50cea83fcb94287bc63aa4189ba78c56cbcb2951c8617fbb6de42819e', + logIndex: 148, + removed: false, + id: 'log_8bf6b52d', + returnValues: { + '0': '0xDc450585987fEcC247B0de5ea03522000361e16c', + '1': '0x0000000000000000000000000000000000000000', + '2': '994999999999999', + '3': '5JtqwqhHqA9pifP9eJJ6rVsVp8DLnTDKjPBeMHRWuZ2kvG9fckef', + '4': '5000000000001', + user: '0xDc450585987fEcC247B0de5ea03522000361e16c', + token: '0x0000000000000000000000000000000000000000', + amount: '994999999999999', + hash: '5JtqwqhHqA9pifP9eJJ6rVsVp8DLnTDKjPBeMHRWuZ2kvG9fckef', + fee: '5000000000001', + }, + event: 'Unlock', + signature: '0x7fbd879c56999418e89f051bc4891f2af0bac78cf32eb10b1cf3640ae214358f', + raw: { + data: '0x000000000000000000000000dc450585987fecc247b0de5ea03522000361e16c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000388f27d8d2fff00000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000048c273950010000000000000000000000000000000000000000000000000000000000000034354a7471777168487141397069665039654a4a36725673567038444c6e54444b6a5042654d485257755a326b76473966636b6566000000000000000000000000', + topics: ['0x7fbd879c56999418e89f051bc4891f2af0bac78cf32eb10b1cf3640ae214358f'], + }, + } as EventData; + + const mockExistingLockTx = { + id: 1, + txHashLock: '0x12345', + status: EEventStatus.WAITING, + }; + + jest.spyOn(queryRunner.manager, 'findOne').mockResolvedValue(mockExistingLockTx); + + const result = await crawler.handlerUnLockEvent(unlockObject, queryRunner); + + expect(queryRunner.manager.findOne).toHaveBeenCalledWith(EventLog, { + where: { txHashLock: unlockObject.returnValues.hash }, + }); + + expect(queryRunner.manager.update).toHaveBeenCalledWith( + EventLog, + mockExistingLockTx.id, + expect.objectContaining({ + status: EEventStatus.COMPLETED, + txHashUnlock: unlockObject.transactionHash, + amountReceived: unlockObject.returnValues.amount, + tokenReceivedAddress: unlockObject.returnValues.token, + protocolFee: unlockObject.returnValues.fee, + tokenReceivedName: 'ETH', + }), + ); + + expect(result.success).toBe(true); + }); + + it('should call queryRunner.manager.update with correct parameters', async () => { + const mockBlockNumber = await (await crawler['getFromToBlock']()).toBlock; + const mockContractAddress = configService.get(EEnvKey.ETH_BRIDGE_CONTRACT_ADDRESS); + const mockNetworkName = ENetworkName.ETH; + + await crawler.updateLatestBlockCrawl(mockBlockNumber, queryRunner); + + expect(queryRunner.manager.update).toHaveBeenCalledWith( + CrawlContract, + { + contractAddress: mockContractAddress, + networkName: mockNetworkName, + }, + { + latestBlock: mockBlockNumber, + }, + ); + }); + it('should rollback transaction on error', async () => { + // Mocking getFromToBlock to return start and end block numbers + jest.spyOn(crawler as any, 'getFromToBlock').mockResolvedValue({ startBlockNumber: 1, toBlock: 100 }); + + // Mocking ethBridgeContract.getEvent to throw an error + newEthBridgeContract.getEvent = jest.fn().mockRejectedValue(new Error('Event fetch error')); + + await expect(crawler.handleEventCrawlBlock()).rejects.toThrow('Event fetch error'); + + expect(queryRunner.connect).toHaveBeenCalled(); + expect(queryRunner.startTransaction).toHaveBeenCalled(); + expect(queryRunner.rollbackTransaction).toHaveBeenCalled(); + expect(queryRunner.release).toHaveBeenCalled(); + }); + }); +}); + +afterEach(() => { + jest.clearAllMocks(); +}); diff --git a/src/modules/crawler/tests/mina-crawler.spec.ts b/src/modules/crawler/tests/mina-crawler.spec.ts index a5a3da5..60db63a 100644 --- a/src/modules/crawler/tests/mina-crawler.spec.ts +++ b/src/modules/crawler/tests/mina-crawler.spec.ts @@ -1,51 +1,259 @@ +import { ConfigModule, ConfigService } from '@nestjs/config'; import { JwtService } from '@nestjs/jwt'; import { Test, TestingModule } from '@nestjs/testing'; -import { DataSource } from 'typeorm'; +import { CrawlContractRepository } from 'database/repositories/crawl-contract.repository'; +import { TokenPairRepository } from 'database/repositories/token-pair.repository'; +import dayjs from 'dayjs'; +import { Field, Mina, provablePure, ProvablePure, PublicKey, UInt32 } from 'o1js'; +import { DataSource, QueryRunner } from 'typeorm'; import { ConfigurationModule } from '@config/config.module'; +import { EEventName, EEventStatus, ENetworkName } from '@constants/blockchain.constant'; +import { EEnvKey } from '@constants/env.constant'; + +import { LoggerService } from '@shared/modules/logger/logger.service'; import { Web3Module } from '@shared/modules/web3/web3.module'; import { UserRepository } from '../../../database/repositories/user.repository'; import { SCBridgeMinaCrawler } from '../crawler.minabridge'; +import { EventLog } from '../entities'; +import { Bridge } from '../minaSc/minaBridgeSC'; // Assuming AuthService houses the login function - +let minaCrawlerService: SCBridgeMinaCrawler; +let configService: ConfigService; +let dataSource: DataSource; +let queryRunner: QueryRunner; // Mock objects const mockJwtService = { // Mock methods if needed sign: jest.fn(), }; -const mockDataSource = { - // Mock methods if needed -}; const mockUserRepository = { findOneBy: jest.fn(), }; -describe('AuthService', () => { - let minaCrawlerService: SCBridgeMinaCrawler; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - imports: [Web3Module, ConfigurationModule], - providers: [ - SCBridgeMinaCrawler, // Include the AuthService provider - { provide: JwtService, useValue: mockJwtService }, - { provide: UserRepository, useValue: mockUserRepository }, - { provide: DataSource, useValue: mockDataSource }, - ], - }).compile(); - - minaCrawlerService = module.get(SCBridgeMinaCrawler); + +beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [ + Web3Module, + ConfigurationModule, + ConfigModule.forRoot({ + isGlobal: true, + }), + ], + providers: [ + SCBridgeMinaCrawler, // Include the AuthService provider + { provide: JwtService, useValue: mockJwtService }, + { provide: UserRepository, useValue: mockUserRepository }, + { + provide: DataSource, + useValue: { + createQueryRunner: jest.fn().mockReturnValue({ + connect: jest.fn(), + startTransaction: jest.fn(), + commitTransaction: jest.fn(), + rollbackTransaction: jest.fn(), + release: jest.fn(), + manager: { + save: jest.fn(), + findOne: jest.fn(), + update: jest.fn(), + }, + }), + }, + }, + { + provide: CrawlContractRepository, + useValue: { + findOne: jest.fn(), + create: jest.fn(), + save: jest.fn(), + }, + }, + { + provide: TokenPairRepository, + useValue: { + getTokenPair: jest.fn(), + }, + }, + { + provide: LoggerService, + useValue: { + getLogger: jest.fn().mockReturnValue({ + info: jest.fn(), + }), + }, + }, + ], + }).compile(); + + minaCrawlerService = module.get(SCBridgeMinaCrawler); + dataSource = module.get(DataSource); + configService = module.get(ConfigService); + queryRunner = dataSource.createQueryRunner(); +}); +describe('handleMinaChainCrawler', () => { + const transformedUnlockObject = { + type: 'Unlock', + event: { + data: {} as ProvablePure, + transactionInfo: { + transactionHash: '5Jv2SKAfsLRGaxf2mTBe4tk8SANZQ4RjgBHrvFXAxT9kUagmmEET', + transactionStatus: 'applied', + transactionMemo: 'E4YM2vTHhWEg66xpj52JErHUBU4pZ1yageL4TVDDpTTSsv8mK6YaH', + }, + }, + blockHeight: UInt32.from(344242), + blockHash: '3NLTWrxnTKf37tSoFwK9riA8VatkPWy8GDxfj9tBS9boxbzsKcvK', + parentBlockHash: '3NLwxgzCnLyYXSKiprqjoousFkks3UjKMD3A6YKwbrX9BMQbhJCV', + globalSlot: UInt32.from(517024), + chainStatus: 'pending', + }; + const transformedLockObject = { + type: 'Lock', + event: { + data: {} as ProvablePure, + transactionInfo: { + transactionHash: '5JtqwqhHqA9pifP9eJJ6rVsVp8DLnTDKjPBeMHRWuZ2kvG9fckef', + transactionStatus: 'applied', + transactionMemo: 'E4YM2vTHhWEg66xpj52JErHUBU4pZ1yageL4TVDDpTTSsv8mK6YaH', + }, + }, + blockHeight: UInt32.from(341988), + blockHash: '3NKM2aH6CoWSdnJKLSh6v7DsL2vcrTxkjJP821rpBNSBJBkxWL7u', + parentBlockHash: '3NLHBiwALvsBCdvHkGx7tHgTsJWxCK2cexQma8hh4SGcpK8GFy7g', + globalSlot: UInt32.from(513687), + chainStatus: 'canonical', + }; + + const transformedEventArr = [transformedUnlockObject, transformedLockObject]; + + it('should successfully handle events and commit transaction', async () => { + jest + .spyOn(minaCrawlerService as any, 'getFromToBlock') + .mockResolvedValue({ startBlockNumber: configService.get(EEnvKey.MINA_BRIDGE_START_BLOCK), toBlock: 355555 }); + + const fetchEventsMock = jest.fn().mockResolvedValue([transformedUnlockObject, transformedLockObject]); + const zaAppAddress = jest + .fn() + .mockResolvedValue(PublicKey.fromBase58(configService.get(EEnvKey.MINA_BRIDGE_CONTRACT_ADDRESS))); + + jest.spyOn(new Bridge(zaAppAddress), 'fetchEvents').mockResolvedValue(transformedEventArr); + jest.spyOn(new Bridge(zaAppAddress), 'fetchEvents').mockImplementation(fetchEventsMock); + + await minaCrawlerService.handleEventCrawlBlock(); + + expect(queryRunner.connect).toHaveBeenCalled(); + expect(queryRunner.startTransaction).toHaveBeenCalled(); + expect(queryRunner.commitTransaction).toHaveBeenCalled(); + expect(queryRunner.release).toHaveBeenCalled(); }); - it('should handle lock events', async () => { + it('should save the correct event log in handlerLockEvent', async () => { + const transformedLockObject = { + type: 'Lock', + event: { + data: { + locker: 'B62qjWwgHupW7k7fcTbb2Kszp4RPYBWYdL4KMmoqfkMH3iRN2FN8u5n', + receipt: '1257517202021634646715564873528857334151894917484', + amount: '1000000', + tokenAddress: 'B62qqki2ZnVzaNsGaTDAP6wJYCth5UAcY6tPX2TQYHdwD8D4uBgrDKC', + }, + transactionInfo: { + transactionHash: '5JtqwqhHqA9pifP9eJJ6rVsVp8DLnTDKjPBeMHRWuZ2kvG9fckef', + transactionStatus: 'applied', + transactionMemo: 'E4YM2vTHhWEg66xpj52JErHUBU4pZ1yageL4TVDDpTTSsv8mK6YaH', + }, + }, + blockHeight: UInt32.from(341988), + blockHash: '3NKM2aH6CoWSdnJKLSh6v7DsL2vcrTxkjJP821rpBNSBJBkxWL7u', + parentBlockHash: '3NLHBiwALvsBCdvHkGx7tHgTsJWxCK2cexQma8hh4SGcpK8GFy7g', + globalSlot: UInt32.from(513687), + chainStatus: 'canonical', + }; mockJwtService.sign.mockResolvedValue('true'); - const result = await minaCrawlerService.handleEventCrawlBlock(); + const field = Field.from(transformedLockObject.event.data.receipt.toString()); + const receiveAddress = '0x' + field.toBigInt().toString(16); + const result = await minaCrawlerService.handlerLockEvent(transformedLockObject, queryRunner); + jest.spyOn(minaCrawlerService as any, 'handlerLockEvent').mockResolvedValue({ + senderAddress: transformedLockObject.event.data.locker, + amountFrom: transformedLockObject.event.data.amount.toString(), + tokenFromAddress: configService.get(EEnvKey.MINA_TOKEN_BRIDGE_ADDRESS), + networkFrom: ENetworkName.MINA, + networkReceived: ENetworkName.ETH, + tokenFromName: 'WETH', + tokenReceivedAddress: configService.get(EEnvKey.ETH_TOKEN_BRIDGE_ADDRESS), + txHashLock: transformedLockObject.event.transactionInfo.transactionHash, + receiveAddress: receiveAddress, + blockNumber: transformedLockObject.blockHeight.toString(), + blockTimeLock: Number(Math.floor(dayjs().valueOf() / 1000)), + event: EEventName.LOCK, + returnValues: JSON.stringify(transformedLockObject), + status: EEventStatus.WAITING, + retry: 0, + fromTokenDecimal: null, + toTokenDecimal: null, + }); - expect(result).toBeDefined(); + expect(result.success).toBe(true); }); +}); + +it('should save the correct event log in handlerUnlockEvent', async () => { + const transformedUnlockObject = { + type: 'Unlock', + event: { + data: { + receiver: 'B62qjWwgHupW7k7fcTbb2Kszp4RPYBWYdL4KMmoqfkMH3iRN2FN8u5n', + tokenAddress: 'B62qqki2ZnVzaNsGaTDAP6wJYCth5UAcY6tPX2TQYHdwD8D4uBgrDKC', + amount: '15610555', + id: '333', + }, + transactionInfo: { + transactionHash: '5Jv2SKAfsLRGaxf2mTBe4tk8SANZQ4RjgBHrvFXAxT9kUagmmEET', + transactionStatus: 'applied', + transactionMemo: 'E4YM2vTHhWEg66xpj52JErHUBU4pZ1yageL4TVDDpTTSsv8mK6YaH', + }, + }, + blockHeight: UInt32.from(344242), + blockHash: '3NLTWrxnTKf37tSoFwK9riA8VatkPWy8GDxfj9tBS9boxbzsKcvK', + parentBlockHash: '3NLwxgzCnLyYXSKiprqjoousFkks3UjKMD3A6YKwbrX9BMQbhJCV', + globalSlot: UInt32.from(517024), + chainStatus: 'pending', + }; + + const mockExistingLockTx = { + id: 1, + txHashLock: '0x12345', + status: EEventStatus.WAITING, + }; + + jest.spyOn(queryRunner.manager, 'findOne').mockResolvedValue(mockExistingLockTx); + + const result = await minaCrawlerService.handlerUnLockEvent(transformedUnlockObject, queryRunner); + + expect(queryRunner.manager.findOne).toHaveBeenCalledWith(EventLog, { + where: { id: transformedUnlockObject.event.data.id.toString() }, + }); + + expect(queryRunner.manager.update).toHaveBeenCalledWith( + EventLog, + mockExistingLockTx.id, + expect.objectContaining({ + status: EEventStatus.COMPLETED, + txHashUnlock: transformedUnlockObject.event.transactionInfo.transactionHash, + amountReceived: transformedUnlockObject.event.data.amount.toString(), + tokenReceivedAddress: transformedUnlockObject.event.data.tokenAddress, + tokenReceivedName: 'WETH', + }), + ); + + expect(result.success).toBe(true); +}); - // ... other test cases as before (omitted for brevity) +afterEach(() => { + jest.clearAllMocks(); }); diff --git a/src/modules/users/admin.controller.ts b/src/modules/users/admin.controller.ts index 156cde2..c820ca7 100644 --- a/src/modules/users/admin.controller.ts +++ b/src/modules/users/admin.controller.ts @@ -2,11 +2,11 @@ import { Body, Controller, Get, Param, Put, Query, UseGuards } from '@nestjs/com import { AuthGuard } from '@nestjs/passport'; import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; -import { AuthUserGuard } from '@shared/decorators/http.decorator'; +import { AuthAdminGuard } from '@shared/decorators/http.decorator'; import { UpdateCommonConfigBodyDto } from './dto/common-config-request.dto'; import { GetCommonConfigResponseDto } from './dto/common-config-response.dto'; -import { getHistoryDto, GetHistoryOfUserResponseDto } from './dto/history-response.dto'; +import { GetHistoryOfUserDto, GetHistoryOfUserResponseDto } from './dto/history-response.dto'; import { UsersService } from './users.service'; @ApiTags('Admins') @@ -15,15 +15,15 @@ export class AdminController { constructor(private readonly userService: UsersService) {} @Get('history') - @AuthUserGuard() + @AuthAdminGuard() @UseGuards(AuthGuard('jwt')) @ApiOkResponse({ type: [GetHistoryOfUserResponseDto] }) - getHistoriesOfUser(@Query() query: getHistoryDto) { + getHistoriesOfUser(@Query() query: GetHistoryOfUserDto) { return this.userService.getHistories(query); } @Get('common-config') - @AuthUserGuard() + @AuthAdminGuard() @UseGuards(AuthGuard('jwt')) @ApiOkResponse({ type: GetCommonConfigResponseDto }) getCommonConfig() { @@ -31,7 +31,7 @@ export class AdminController { } @Put('update-common-config/:id') - @AuthUserGuard() + @AuthAdminGuard() @UseGuards(AuthGuard('jwt')) updateCommonConfig(@Param('id') id: number, @Body() updateConfig: UpdateCommonConfigBodyDto) { return this.userService.updateCommonConfig(id, updateConfig); diff --git a/src/modules/users/dto/common-config-request.dto.ts b/src/modules/users/dto/common-config-request.dto.ts index 6da4406..309cb3e 100644 --- a/src/modules/users/dto/common-config-request.dto.ts +++ b/src/modules/users/dto/common-config-request.dto.ts @@ -1,20 +1,17 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Exclude, Expose, Type } from 'class-transformer'; -import { IsNumber, IsOptional, Max } from 'class-validator'; +import { NumberField } from '@shared/decorators/field.decorator'; -@Exclude() export class UpdateCommonConfigBodyDto { - @ApiProperty({ example: 50 }) - @Max(100) - @Type(() => Number) - @IsNumber() - @Expose() - @IsOptional() + @NumberField({ + example: 50, + maximum: 100, + required: false, + }) tip: number; - @ApiProperty({ example: 500 }) - @IsOptional() - @IsNumber() - @Expose() + @NumberField({ + example: 500, + maximum: 100, + required: false, + }) dailyQuota: number; } diff --git a/src/modules/users/dto/history-response.dto.ts b/src/modules/users/dto/history-response.dto.ts index 7c3f863..5d75d54 100644 --- a/src/modules/users/dto/history-response.dto.ts +++ b/src/modules/users/dto/history-response.dto.ts @@ -1,7 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; -import { Expose } from 'class-transformer'; -import { IsOptional, IsString } from 'class-validator'; +import { StringField } from '@shared/decorators/field.decorator'; import { BasePaginationRequestDto } from '@shared/dtos/base-request.dto'; export class GetHistoryOfUserResponseDto { @@ -51,12 +50,11 @@ export class GetHistoryOfUserResponseDto { createdAt: Date; } -export class getHistoryOfUserDto extends BasePaginationRequestDto {} +export class GetHistoryOfUserDto extends BasePaginationRequestDto {} -export class getHistoryDto extends BasePaginationRequestDto { - @ApiProperty({ required: false }) - @IsOptional() - @IsString() - @Expose() +export class GetHistoryDto extends BasePaginationRequestDto { + @StringField({ + required: false, + }) address: string; } diff --git a/src/modules/users/dto/user-request.dto.ts b/src/modules/users/dto/user-request.dto.ts index d88e9be..28e176a 100644 --- a/src/modules/users/dto/user-request.dto.ts +++ b/src/modules/users/dto/user-request.dto.ts @@ -1,37 +1,34 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Exclude, Expose } from 'class-transformer'; -import { IsEmail, IsNumber, IsOptional, IsString } from 'class-validator'; - -import { UseSwaggerDecorator } from '@shared/decorators/swagger.decorator'; +import { NumberField, StringField } from '@shared/decorators/field.decorator'; export class CreateUserDto { - @UseSwaggerDecorator() - @Expose() - @IsEmail() + @StringField({ + required: true, + isEmail: true, + }) email: string; - @UseSwaggerDecorator() - @Expose() - @IsString() + @StringField({ + required: true, + }) password: string; } -@Exclude() export class UpdateProfileBodyDto { - @ApiProperty({ example: 'email' }) - @IsOptional() - @IsEmail() - @Expose() + @StringField({ + required: true, + isEmail: true, + }) email: string; } export class GetProtocolFeeBodyDto { - @ApiProperty({ example: 1 }) - @IsNumber() + @NumberField({ + required: true, + }) pairId: number; - @ApiProperty({ example: 1000 }) - @IsString() - @Expose() + @StringField({ + required: true, + }) amount: string; } diff --git a/src/modules/users/users.controller.ts b/src/modules/users/users.controller.ts index a07ccc6..8072167 100644 --- a/src/modules/users/users.controller.ts +++ b/src/modules/users/users.controller.ts @@ -5,7 +5,7 @@ import { ETableName } from '@constants/entity.constant'; import { GuardPublic } from '@guards/guard.decorator'; -import { getHistoryOfUserDto, GetHistoryOfUserResponseDto } from './dto/history-response.dto'; +import { GetHistoryOfUserDto, GetHistoryOfUserResponseDto } from './dto/history-response.dto'; import { GetProtocolFeeBodyDto } from './dto/user-request.dto'; import { GetListTokenPairResponseDto, GetProtocolFeeResponseDto } from './dto/user-response.dto'; import { UsersService } from './users.service'; @@ -18,7 +18,7 @@ export class UsersController { @Get('history/:address') @GuardPublic() @ApiOkResponse({ type: [GetHistoryOfUserResponseDto] }) - getHistoriesOfUser(@Param('address') address: string, @Query() query: getHistoryOfUserDto) { + getHistoriesOfUser(@Param('address') address: string, @Query() query: GetHistoryOfUserDto) { return this.userService.getHistoriesOfUser(address, query); } diff --git a/src/modules/users/users.service.ts b/src/modules/users/users.service.ts index ba9ac5c..04a0839 100644 --- a/src/modules/users/users.service.ts +++ b/src/modules/users/users.service.ts @@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { CommonConfigRepository } from 'database/repositories/common-configuration.repository'; import { EventLogRepository } from 'database/repositories/event-log.repository'; -import { TokenPriceRepository } from 'database/repositories/token-price.repository'; import { UserRepository } from 'database/repositories/user.repository'; import { Logger } from 'log4js'; import { DataSource } from 'typeorm'; @@ -29,11 +28,10 @@ export class UsersService { private readonly usersRepository: UserRepository, private readonly eventLogRepository: EventLogRepository, private readonly commonConfigRepository: CommonConfigRepository, - private readonly tokenPriceRepository: TokenPriceRepository, private readonly dataSource: DataSource, - private readonly ethBridgeContract: ETHBridgeContract, private readonly configService: ConfigService, private readonly loggerService: LoggerService, + private readonly ethBridgeContract: ETHBridgeContract, ) { this.logger = this.loggerService.getLogger('USER_SERVICE'); } @@ -48,23 +46,17 @@ export class UsersService { } async getHistoriesOfUser(address: string, options) { - try { - const [data, count] = await this.eventLogRepository.getHistoriesOfUser(address, options); - return toPageDto(data, options, count); - } catch (error) {} + const [data, count] = await this.eventLogRepository.getHistoriesOfUser(address, options); + return toPageDto(data, options, count); } async getHistories(options) { - try { - const [data, count] = await this.eventLogRepository.getHistories(options); - return toPageDto(data, options, count); - } catch (error) {} + const [data, count] = await this.eventLogRepository.getHistories(options); + return toPageDto(data, options, count); } async getCommonConfig() { - try { - return this.commonConfigRepository.getCommonConfig(); - } catch (error) {} + return this.commonConfigRepository.getCommonConfig(); } async updateCommonConfig(id: number, updateConfig: UpdateCommonConfigBodyDto) { @@ -95,8 +87,6 @@ export class UsersService { ]); if (tokenPair.toChain == ENetworkName.MINA) { - // const rate = await this.tokenPriceRepository.getRateETHToMina(); - gasFee = addDecimal( this.configService.get(EEnvKey.GASFEEMINA), this.configService.get(EEnvKey.DECIMAL_TOKEN_MINA), @@ -106,7 +96,7 @@ export class UsersService { tokenPair.toAddress, addDecimal(0, tokenPair.toDecimal), 1, - '0xb3Edf83eA590F44f5c400077EBd94CCFE10E4Bb0', + process.env.ADMIN_ADDRESS_EVM, 0, ); } diff --git a/src/ormconfig.ts b/src/ormconfig.ts index dc8808b..d134c3b 100644 --- a/src/ormconfig.ts +++ b/src/ormconfig.ts @@ -4,6 +4,8 @@ import type { DataSourceOptions } from 'typeorm'; import { EEnvKey } from '@constants/env.constant'; +import { isDevelopmentEnvironment } from '@shared/utils/util'; + dotenv.config(); export const migrationDir = join(__dirname, 'database/migrations'); export default { @@ -16,8 +18,7 @@ export default { entities: [join(__dirname, '/modules/**/entities/*.entity{.js,.ts}')], migrationsTableName: 'custom_migration_table', migrations: [join(migrationDir, '*{.js,.ts}')], - logging: process.env[EEnvKey.NODE_ENV] === 'local' ? true : false, - // synchronize: true, + logging: isDevelopmentEnvironment(), cache: true, timezone: 'Z', extra: { decimalNumbers: true }, diff --git a/src/shared/decorators/field.decorator.ts b/src/shared/decorators/field.decorator.ts index 775abd8..dd2943d 100644 --- a/src/shared/decorators/field.decorator.ts +++ b/src/shared/decorators/field.decorator.ts @@ -1,43 +1,60 @@ import { applyDecorators } from '@nestjs/common'; import { ApiProperty, ApiPropertyOptions } from '@nestjs/swagger'; -import { Expose, Transform, Type } from 'class-transformer'; +import { Type } from 'class-transformer'; import { + IsArray, IsBoolean, + IsEmail, IsEnum, IsInt, IsNotEmpty, isNumber, IsNumber, IsNumberString, + IsObject, IsOptional, IsPositive, - isString, IsString, - matches, Max, MaxLength, Min, MinLength, + ValidateNested, } from 'class-validator'; -import { getVariableName } from '@shared/utils/util'; +import { isDevelopmentEnvironment } from '@shared/utils/util'; -import { ToArray, ToBoolean, ToLowerCase, ToUpperCase, Trim } from './transform.decorator'; +import { ToBoolean, ToBooleanArray, ToLowerCase, ToUpperCase, Trim } from './transform.decorator'; -interface IBaseOptions { - swagger?: boolean; - expose?: boolean; -} - -interface IStringFieldOptions extends IBaseOptions { +interface IStringFieldOptions { minLength?: number; maxLength?: number; toLowerCase?: boolean; toUpperCase?: boolean; number?: boolean; + isEmail?: boolean; + isEthereumAddress?: boolean; } +const initSharedDecorator = (options: ApiPropertyOptions, type: any) => { + const sharedDecorators = []; + if (isDevelopmentEnvironment()) { + sharedDecorators.push(ApiProperty({ ...options, type })); + } + if (options.required) { + sharedDecorators.push(IsNotEmpty()); + } else { + sharedDecorators.push(IsOptional()); + } + if (options.enum) { + sharedDecorators.push(IsEnum(options.enum)); + } + if (options.isArray) { + sharedDecorators.push(IsArray()); + } -interface INumberFieldOptions extends IBaseOptions { + return sharedDecorators; +}; +interface INumberFieldOptions { each?: boolean; minimum?: number; maximum?: number; @@ -45,35 +62,10 @@ interface INumberFieldOptions extends IBaseOptions { isPositive?: boolean; } -export function initDecoratorField( - options: ApiPropertyOptions & Partial<{ expose: boolean }>, - decorators: PropertyDecorator[], -) { - if (options?.expose) { - decorators.push(Expose()); - } - - if (options?.required === false) { - decorators.push(IsOptional()); - } else { - decorators.push(IsNotEmpty()); - } - - return applyDecorators(...decorators); -} - export function NumberField(options: Omit & INumberFieldOptions = {}): PropertyDecorator { - const decorators = [Type(() => Number)]; - - const { each, int, minimum, maximum, isPositive, swagger } = options; - - if (swagger !== false) { - decorators.push(ApiProperty({ type: Number, ...options, example: int ? 1 : 1.2 })); - } + const decorators = [Type(() => Number), ...initSharedDecorator(options, Number)]; - if (each) { - decorators.push(ToArray()); - } + const { int, minimum, maximum, isPositive, isArray: each = false } = options; if (int) { decorators.push(IsInt({ each })); @@ -93,22 +85,13 @@ export function NumberField(options: Omit & INumberF decorators.push(IsPositive({ each })); } - return initDecoratorField(options, decorators); -} - -export function NumberFieldOption( - options: Omit & INumberFieldOptions = {}, -): PropertyDecorator { - return NumberField({ ...options, required: false }); + return applyDecorators(...decorators); } export function StringField(options: Omit & IStringFieldOptions = {}): PropertyDecorator { - const decorators = [IsNotEmpty(), Trim()]; - const { swagger, minLength, maxLength, toLowerCase, toUpperCase, number } = options; + const decorators = [Trim(), ...initSharedDecorator(options, String)]; - if (swagger !== false) { - decorators.push(ApiProperty({ type: String, ...options })); - } + const { minLength, maxLength, toLowerCase, toUpperCase, number, isEmail, isArray = false } = options; if (minLength) { decorators.push(MinLength(minLength)); @@ -129,96 +112,36 @@ export function StringField(options: Omit & IStringF if (number) { decorators.push(IsNumberString()); } else { - decorators.push(IsString()); + decorators.push(IsString({ each: isArray })); + } + if (isEmail) { + decorators.push(IsEmail()); } - return initDecoratorField(options, decorators); -} - -export function StringFieldOption( - options: Omit & IStringFieldOptions = {}, -): PropertyDecorator { - return StringField({ ...options, required: false }); + return applyDecorators(...decorators); } export function BooleanField( options: Omit & Partial<{ swagger: boolean }> = {}, ): PropertyDecorator { - const decorators = [IsBoolean(), ToBoolean()]; - - if (options?.swagger !== false) { - decorators.push(ApiProperty({ type: Boolean, ...options })); + const decorators = [...initSharedDecorator(options, Boolean)]; + if (options.isArray) { + decorators.push(IsBoolean({ each: true }), ToBooleanArray()); + } else { + decorators.push(IsBoolean(), ToBoolean()); } - - return initDecoratorField(options, decorators); + return applyDecorators(...decorators); } -export function BooleanFieldOption( - options: Omit & Partial<{ swagger: boolean }> = {}, -): PropertyDecorator { - return BooleanField({ ...options, required: false }); -} +export function ObjectField(options: Omit & { type: CallableFunction }) { + const decorators = initSharedDecorator(options, options.type); -export function EnumField( - getEnum: () => TEnum, - options: Omit & - Partial<{ - each: boolean; - swagger: boolean; - enumNumber: boolean; - }> = {}, -): PropertyDecorator { - const enumValue = getEnum() as any; - const decorators = [IsEnum(enumValue)]; - let description = ''; - - if (options?.enumNumber) { - const enumObject = Object.values(enumValue).filter(x => typeof x === 'string'); - description = Object.keys(enumObject) - .map(key => enumObject[key] + ': ' + enumValue[enumObject[key]]) - .join(', '); - decorators.push(Type(() => Number)); + decorators.push(Type(() => options.type)); + decorators.push(ValidateNested({ each: true })); + if (options.isArray) { + decorators.push(IsArray()); } else { - description = Object.values(enumValue) - .map(key => key) - .join(', '); - } - - if (options?.swagger !== false) { - options = { ...options, description }; - decorators.push( - ApiProperty({ - enumName: getVariableName(getEnum), - ...options, - }), - ); + decorators.push(IsObject()); } - - if (options.each) { - decorators.push(ToArray()); - } - - return initDecoratorField(options, decorators); -} - -export function EnumFieldOptional( - getEnum: () => TEnum, - options: Omit & - Partial<{ each: boolean; swagger: boolean; enumNumber: boolean }> = {}, -): PropertyDecorator { - return EnumField(getEnum, { ...options, required: false }); -} - -export function IsDescriptiveNumber(options = {}) { - const regex = new RegExp(/^~?\d+(\.\d+)?$/); - return applyDecorators( - Transform(({ value }) => { - if (!isString(value)) return value; - if (matches(value, regex) && value.match(/\d/g).length <= 10) return value; // match the pattern (~)number(.number) - return -1; - }), - IsString({ - message: v => `Field ${v.property} must be a number string with less than 10 digit.`, - }), - ); + return applyDecorators(...decorators); } diff --git a/src/shared/decorators/http.decorator.ts b/src/shared/decorators/http.decorator.ts index 3b4df6f..805eb11 100644 --- a/src/shared/decorators/http.decorator.ts +++ b/src/shared/decorators/http.decorator.ts @@ -1,18 +1,8 @@ import { applyDecorators, UseGuards } from '@nestjs/common'; import { ApiBearerAuth } from '@nestjs/swagger'; -import { AdminGuard } from '@guards/roles/admin.guard'; -import { SuperAdminGuard } from '@guards/roles/super-admin.guard'; -import { UserGuard } from '@guards/roles/user.guard'; - -export function AuthUserGuard(): MethodDecorator { - return applyDecorators(UseGuards(UserGuard), ApiBearerAuth('Authorization')); -} +import { AdminGuard } from '@guards/roles/user.guard'; export function AuthAdminGuard(): MethodDecorator { return applyDecorators(UseGuards(AdminGuard), ApiBearerAuth('Authorization')); } - -export function AuthSuperAdminGuard(): MethodDecorator { - return applyDecorators(UseGuards(SuperAdminGuard), ApiBearerAuth('Authorization')); -} diff --git a/src/shared/decorators/swagger.decorator.ts b/src/shared/decorators/swagger.decorator.ts deleted file mode 100644 index 738f724..0000000 --- a/src/shared/decorators/swagger.decorator.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { applyDecorators } from '@nestjs/common'; -import { ApiProperty, ApiPropertyOptions } from '@nestjs/swagger'; -import { IsEnum, IsOptional } from 'class-validator'; - -type CustomSwaggerDecorator = ApiPropertyOptions & { - isRequired?: boolean; - enum?: Record; -}; - -export function UseSwaggerDecorator(options: CustomSwaggerDecorator = {}) { - const { isRequired = true } = options; - const decorators = [] as PropertyDecorator[]; - decorators.push(ApiProperty({ ...options, required: isRequired })); - if (!isRequired) { - decorators.push(IsOptional()); - } - - if (options.enum) decorators.push(IsEnum(options.enum)); - - return applyDecorators(...decorators); -} diff --git a/src/shared/decorators/transform.decorator.ts b/src/shared/decorators/transform.decorator.ts index 2b3a849..ffec8d0 100644 --- a/src/shared/decorators/transform.decorator.ts +++ b/src/shared/decorators/transform.decorator.ts @@ -13,17 +13,23 @@ export function Trim(): PropertyDecorator { return trim(value).replace(/\s\s+/g, ' '); }); } - +function toBoolean(value: string | number | boolean): boolean { + if (typeof value === 'string') return value.toLowerCase() === 'true'; + return Boolean(value).valueOf(); +} export function ToBoolean(): PropertyDecorator { + return Transform( + ({ value }) => { + if (value) return toBoolean(value); + }, + { toClassOnly: true }, + ); +} +export function ToBooleanArray(): PropertyDecorator { return Transform( params => { - switch (params.value) { - case 'true': - return true; - case 'false': - return false; - default: - return params.value; + if (isArray(params.value)) { + return params.value.map(v => toBoolean(v)); } }, { toClassOnly: true }, diff --git a/src/shared/dtos/base-request.dto.ts b/src/shared/dtos/base-request.dto.ts index 2e2d02b..56b9e46 100644 --- a/src/shared/dtos/base-request.dto.ts +++ b/src/shared/dtos/base-request.dto.ts @@ -1,38 +1,35 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { Expose, Type } from 'class-transformer'; -import { IsEnum, IsNumber, IsOptional, IsString, Min } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; import { EDirection } from '@constants/api.constant'; +import { NumberField, StringField } from '@shared/decorators/field.decorator'; + export class BasePaginationRequestDto { - @ApiProperty({ required: false }) - @Min(1) - @IsNumber() - @IsOptional() - @Type(() => Number) - @Expose() + @NumberField({ + required: false, + minimum: 1, + example: 10, + }) limit: number; - @ApiProperty({ required: false }) - @Min(1) - @IsNumber() - @IsOptional() - @Type(() => Number) - @Expose() + @NumberField({ + required: false, + minimum: 1, + example: 1, + }) page: number; } export class BasePaginationWithSortRequestDto extends BasePaginationRequestDto { - @ApiProperty({ required: false }) - @IsString() - @IsOptional() - @Expose() + @StringField({ + required: false, + }) sortBy: string; - @ApiProperty({ enum: EDirection, required: false }) - @IsEnum(EDirection) - @IsOptional() - @Expose() + @StringField({ + required: false, + enum: EDirection, + }) direction: EDirection; } @@ -52,9 +49,8 @@ export class BasePaginationResponseDto { } export class BasePaginationWithSortAndSearchRequestDto extends BasePaginationWithSortRequestDto { - @ApiPropertyOptional() - @IsString() - @IsOptional() - @Expose() + @StringField({ + required: false, + }) search: string; } diff --git a/src/shared/dtos/page-options.dto.ts b/src/shared/dtos/page-options.dto.ts index 32f18c8..f7979c6 100644 --- a/src/shared/dtos/page-options.dto.ts +++ b/src/shared/dtos/page-options.dto.ts @@ -1,32 +1,31 @@ -import { ApiPropertyOptional } from '@nestjs/swagger'; -import { Transform } from 'class-transformer'; -import { IsEnum, IsInt, IsOptional, IsString, Max, Min } from 'class-validator'; - import { EDirection } from '@constants/api.constant'; +import { NumberField, StringField } from '@shared/decorators/field.decorator'; + export class PageOptionsDto { - @ApiPropertyOptional({ default: 1 }) - @Min(1) - @IsInt() - @Transform(({ value }) => +value) - @IsOptional() + @NumberField({ + int: true, + minimum: 1, + required: false, + }) readonly page?: number; - @ApiPropertyOptional({ default: 10 }) - @Max(100) - @Min(1) - @IsInt() - @Transform(({ value }) => +value) - @IsOptional() + @NumberField({ + int: true, + minimum: 1, + maximum: 100, + required: false, + }) readonly limit?: number; - @ApiPropertyOptional() - @IsString() - @IsOptional() + @StringField({ + required: false, + }) readonly orderBy?: string; - @ApiPropertyOptional({ enum: EDirection }) - @IsEnum(EDirection) - @IsOptional() + @StringField({ + required: false, + enum: EDirection, + }) readonly direction?: EDirection = EDirection.ASC; } diff --git a/src/shared/middleware/logger-http-request.middleware.ts b/src/shared/middleware/logger-http-request.middleware.ts index 9881b9c..baa88a5 100644 --- a/src/shared/middleware/logger-http-request.middleware.ts +++ b/src/shared/middleware/logger-http-request.middleware.ts @@ -4,7 +4,7 @@ import { NextFunction, Request, Response } from 'express'; function httpRequestLoggerBuilder(req: Request): string { const param = `\n param: ${JSON.stringify(req.params)}`; const query = `\n query: ${JSON.stringify(req.query)}`; - const hasAuthorizationHeader = req.headers.authorization ? true : false; + const hasAuthorizationHeader = !!req.headers?.authorization; const body = req.method === 'POST' || req.method === 'PUT' ? `\n body: ${JSON.stringify(req.body)}` : ''; const logger = `[${req.method} - ${req.baseUrl}]: ${param} ${query} ${body} hasAuthorizationHeader: ${hasAuthorizationHeader}`; return logger; diff --git a/src/shared/modules/web3/web3.module.ts b/src/shared/modules/web3/web3.module.ts index 46445bf..1f5e80c 100644 --- a/src/shared/modules/web3/web3.module.ts +++ b/src/shared/modules/web3/web3.module.ts @@ -2,14 +2,11 @@ import { Global, Module } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import Web3 from 'web3'; +import { initializeEthContract } from '@config/common.config'; + import { ENetworkName } from '@constants/blockchain.constant'; import { EEnvKey } from '@constants/env.constant'; -import { - ETH_BRIDGE_ADDRESS_INJECT, - ETH_BRIDGE_START_BLOCK_INJECT, - RPC_ETH_SERVICE_INJECT, - RPC_SERVICE_INJECT, -} from '@constants/service.constant'; +import { ASYNC_CONNECTION } from '@constants/service.constant'; import { sleep } from '@shared/utils/promise'; @@ -19,34 +16,23 @@ import { ETHBridgeContract } from './web3.service'; @Module({ providers: [ { - provide: RPC_SERVICE_INJECT, - useFactory: async (configService: ConfigService) => await RpcFactory(configService), - inject: [ConfigService], - }, - { - provide: RPC_ETH_SERVICE_INJECT, - useFactory: async (configService: ConfigService) => await RpcFactory(configService, ENetworkName.ETH), - inject: [ConfigService], - }, - { - provide: ETH_BRIDGE_ADDRESS_INJECT, - useFactory: (configService: ConfigService) => configService.get(EEnvKey.ETH_BRIDGE_CONTRACT_ADDRESS), + provide: ASYNC_CONNECTION, + useFactory: async (configService: ConfigService) => { + const connection = await initializeEthContract(configService); + return connection; + }, + inject: [ConfigService], }, { - provide: ETH_BRIDGE_START_BLOCK_INJECT, - useFactory: (configService: ConfigService) => +configService.get(EEnvKey.ETH_BRIDGE_START_BLOCK), - inject: [ConfigService], + provide: ETHBridgeContract, + useFactory: (connection: ETHBridgeContract) => { + return connection; + }, + inject: [ASYNC_CONNECTION], }, - ETHBridgeContract, - ], - exports: [ - RPC_SERVICE_INJECT, - RPC_ETH_SERVICE_INJECT, - ETH_BRIDGE_ADDRESS_INJECT, - ETH_BRIDGE_START_BLOCK_INJECT, - ETHBridgeContract, ], + exports: [Web3Module, ETHBridgeContract], }) export class Web3Module {} @@ -56,14 +42,12 @@ export interface IRpcService { maxTries: number; privateKeys: string[]; getNonce: (walletAddress: string) => Promise; - handleSignerCallback: () => (callback: CallableFunction, tried?: number) => Promise; } export const RpcFactory = async (configService: ConfigService, network?: ENetworkName): Promise => { let rpcRound = 0; const rpc = configService.get(EEnvKey.ETH_BRIDGE_RPC_OPTIONS); const privateKeys = configService.get(EEnvKey.SIGNER_PRIVATE_KEY); - const keyStatus: Array = privateKeys.map(() => false); // all signers is set to free by default. const getNextRPcRound = (): Web3 => { return new Web3(rpc[rpcRound++ % rpc.length]); @@ -90,25 +74,5 @@ export const RpcFactory = async (configService: ConfigService, network?: ENetwor getNonce: async (walletAddress: string): Promise => { return web3.eth.getTransactionCount(walletAddress); }, - handleSignerCallback: () => { - const handleJob = async (callback: CallableFunction, tried = 0) => { - const freeKeyIndex = keyStatus.findIndex(e => e === false); - if (freeKeyIndex < 0) { - if (tried === 10) throw new Error('cannot find free signer'); - await sleep(1 * tried); - return handleJob(callback, tried + 1); - } - try { - keyStatus[freeKeyIndex] = true; - await callback(freeKeyIndex); - return true; - } catch (error) { - throw error; - } finally { - keyStatus[freeKeyIndex] = false; - } - }; - return handleJob.bind(this); - }, }; }; diff --git a/src/shared/modules/web3/web3.service.ts b/src/shared/modules/web3/web3.service.ts index f77de81..b9cbb2d 100644 --- a/src/shared/modules/web3/web3.service.ts +++ b/src/shared/modules/web3/web3.service.ts @@ -1,15 +1,9 @@ -import { Inject, Injectable, Logger } from '@nestjs/common'; +import { Logger } from '@nestjs/common'; import BigNumber from 'bignumber.js'; import { TransactionReceipt } from 'web3-core'; import { Contract, EventData } from 'web3-eth-contract'; import { toBN, toHex } from 'web3-utils'; -import { - ETH_BRIDGE_ADDRESS_INJECT, - ETH_BRIDGE_START_BLOCK_INJECT, - RPC_ETH_SERVICE_INJECT, -} from '@constants/service.constant'; - import { sleep } from '@shared/utils/promise'; import ETHBridgeAbi from './abis/eth-bridge-contract.json'; @@ -151,10 +145,10 @@ export class DefaultContract { ); const response = []; - for (let index = 0; index < writeData.length; index++) { + for (const element of writeData) { // gas estimation const nonce = await this.rpcService.getNonce(signer.address); - const { method, param } = writeData[index]; + const { method, param } = element; const data = this.contract.methods[method](...param).encodeABI(); const gasPrice = await this.rpcService.web3.eth.getGasPrice(); const rawTx = { @@ -199,15 +193,11 @@ export class DefaultContract { } } -@Injectable() export class ETHBridgeContract extends DefaultContract { - constructor( - @Inject(RPC_ETH_SERVICE_INJECT) rpcETHService: IRpcService, - @Inject(ETH_BRIDGE_ADDRESS_INJECT) address: string, - @Inject(ETH_BRIDGE_START_BLOCK_INJECT) startBlock: number, - ) { - super(rpcETHService, ETHBridgeAbi, address, startBlock); + constructor(rpcETHService: IRpcService, address: string, _startBlock: number) { + super(rpcETHService, ETHBridgeAbi, address, _startBlock); } + public async getBaseURI() { return this.call('getBaseURI', []); } diff --git a/src/shared/utils/check-object.ts b/src/shared/utils/check-object.ts deleted file mode 100644 index a84872b..0000000 --- a/src/shared/utils/check-object.ts +++ /dev/null @@ -1,37 +0,0 @@ -export function isNullOrUndefined(obj: T | null | undefined): obj is null | undefined { - return typeof obj === 'undefined' || obj === null; -} - -export function assignDefined(target, ...sources) { - for (const source of sources) { - for (const key of Object.keys(source)) { - const val = source[key]; - if (val !== undefined) { - target[key] = val; - } - } - } - return target; -} - -export const isLocationDulyFilled = (location: string, longitude: number, latitude: number) => { - //allow all fields empty - if (!location && !longitude && !latitude) return true; - //or all fields must be filled - if (location && longitude && latitude) return true; - return false; -}; - -export const isPhoneNumberValid = (phoneNumber: string, dialCode: string) => { - let isValid = true; - const dialRegex = new RegExp(`^\\${dialCode}`); - const numberOnlyRegex = /^[0-9]+$/; - if (!dialRegex.test(phoneNumber)) { - isValid = false; - } - const phoneWithoutDialCode = phoneNumber.replace(dialRegex, ''); - if (!numberOnlyRegex.test(phoneWithoutDialCode)) { - isValid = false; - } - return isValid; -}; diff --git a/src/shared/utils/hash-string.ts b/src/shared/utils/hash-string.ts deleted file mode 100644 index 408aae8..0000000 --- a/src/shared/utils/hash-string.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { compare, hash } from 'bcrypt'; - -export const generateHash = (password: string) => { - return hash(password, Number(process.env.BCRYPT_SALT_ROUND)); -}; - -export const validateHash = (value: string, hash: string) => compare(value, hash); diff --git a/src/shared/utils/util.ts b/src/shared/utils/util.ts index 45521dc..fd481a2 100644 --- a/src/shared/utils/util.ts +++ b/src/shared/utils/util.ts @@ -1,5 +1,7 @@ import { AES, enc } from 'crypto-js'; +import { EEnvironments, EEnvKey } from '@constants/env.constant'; + export const toLower = (value: string) => value.toLowerCase(); export const compareAddress = (address: string, addressCompare: string) => toLower(address) === toLower(addressCompare); @@ -34,3 +36,5 @@ export const getVariableName = (getVar: () => TResult): string => { }; export const nullToZero = (value: string | number) => (value ? value.toString() : '0'); +export const isDevelopmentEnvironment = () => + [EEnvironments.DEV, EEnvironments.LOCAL].includes(process.env[EEnvKey.NODE_ENV] as EEnvironments); diff --git a/yarn.lock b/yarn.lock index d0091f1..21ce1d9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1437,12 +1437,12 @@ resolved "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz" integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== -"@types/node@*", "@types/node@^20.3.1": - version "20.12.6" - resolved "https://registry.npmjs.org/@types/node/-/node-20.12.6.tgz" - integrity sha512-3KurE8taB8GCvZBPngVbp0lk5CKi8M9f9k1rsADh0Evdz5SzJ+Q+Hx9uHoFGsLnLnd1xmkDQr2hVhlA0Mn0lKQ== +"@types/node@*", "@types/node@^20.16.5": + version "20.16.5" + resolved "https://registry.npmjs.org/@types/node/-/node-20.16.5.tgz" + integrity sha512-VwYCweNo3ERajwy0IUlqqcyZ8/A7Zwa9ZP3MnENWcB11AejO+tLy3pu850goUW2FC/IJMdZUfKpX/yxL1gymCA== dependencies: - undici-types "~5.26.4" + undici-types "~6.19.2" "@types/node@^12.12.6": version "12.20.55" @@ -7536,10 +7536,10 @@ ultron@~1.1.0: resolved "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz" integrity sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og== -undici-types@~5.26.4: - version "5.26.5" - resolved "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz" - integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== +undici-types@~6.19.2: + version "6.19.8" + resolved "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz" + integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== universalify@^0.1.0: version "0.1.2"