diff --git a/design/bridge-eth-mina.puml b/design/bridge-eth-mina.puml index 88f6ba1..a8c577f 100644 --- a/design/bridge-eth-mina.puml +++ b/design/bridge-eth-mina.puml @@ -3,16 +3,18 @@ title Lock From Eth to Mina actor User boundary fe as "Frontend" control be as "Backend" -boundary evm_crawler as "EVM crawler" -boundary mina_validator as "Mina signature validators" -boundary mina_sender as "Mina tx sender" -boundary mina_crawler as "Mina crawler" participant Ethereum -participant Mina +control evm_crawler as "EVM crawler" +control job_provider as "Job provider" database db as "Database" +queue queue1 as "Queue" +control mina_validator as "Mina signature validators" +control mina_sender as "Mina tx sender" +control mina_crawler as "Mina crawler" +participant Mina autonumber -group#LightGreen User lock token from Evm +group#LightGreen (1) User lock token from Evm User -> fe : select network to bridge @@ -35,7 +37,7 @@ group#LightGreen User lock token from Evm end -group#LightGreen Crawler evm fetch lock tx +group#LightGreen (2) Crawler evm fetch lock tx evm_crawler->Ethereum: fetch lock events activate Ethereum Ethereum --> evm_crawler: return locked tx events @@ -46,27 +48,69 @@ group#LightGreen Crawler evm fetch lock tx db --> evm_crawler: saved tx with status waiting to be unlocked deactivate db end -group#LightGreen System unlock token to Mina +group#LightGreen (3) Job Provider create queued job + loop + par + job_provider->db: get pending transaction need signatures + activate db + db --> job_provider: list of pending transaction + deactivate db + job_provider -> queue1: send jobs to queue + activate queue1 + queue1 --> job_provider: success + deactivate queue1 + else + job_provider->db: get validated transaction ready to be sent + activate db + db --> job_provider: list of ready transaction + deactivate db + job_provider -> queue1: send jobs to queue + activate queue1 + queue1 --> job_provider: success + deactivate queue1 + end + end +end +group#LightGreen (4) Job Validator process generating signature for tx - mina_validator -> db: get pending unlock tx and verify theirs signature + queue1 -> mina_validator: trigger job activate mina_validator + mina_validator --> db: get data for needed for queued job activate db - db --> mina_validator: update signature + db --> mina_validator: needed data deactivate db - deactivate mina_validator - mina_sender -> db: get signatured verified tx + mina_validator -->mina_validator: generated the signature + + mina_validator -> db: save the signature activate db + db --> mina_validator: save success + deactivate db + + mina_validator --> queue1: job completed + deactivate mina_validator +end +group#LightGreen (5) Job Sender process sending tx to Mina network + + queue1 -> mina_sender: trigger sender by queued job activate mina_sender - db -> mina_sender: return tx + mina_sender --> db: get data for needed for queued job + activate db + db --> mina_sender: needed data deactivate db - mina_sender -> Mina: prove, send tx + mina_sender --> mina_sender: build transaction + + mina_sender -> Mina: Send the tx to Mina activate Mina Mina --> mina_sender: return tx hash deactivate Mina + mina_sender --> mina_sender: wait for the tx to be applied + mina_sender --> queue1: job completed deactivate mina_sender +end +group#LightGreen (6) Mina crawler confirms sent tx mina_crawler -> Mina: get unlock events activate mina_crawler activate Mina @@ -79,5 +123,6 @@ group#LightGreen System unlock token to Mina deactivate db end + @enduml diff --git a/design/bridge-mina-eth.puml b/design/bridge-mina-eth.puml index f30d367..9237fcc 100644 --- a/design/bridge-mina-eth.puml +++ b/design/bridge-mina-eth.puml @@ -3,16 +3,18 @@ title Lock From Mina to Eth actor User boundary fe as "Frontend" control be as "Backend" -boundary evm_crawler as "Mina crawler" -boundary mina_validator as "Mina signature validators" -boundary mina_sender as "Evm tx sender" -boundary mina_crawler as "Evm crawler" -participant Ethereum as "Mina" -participant Mina as "Evm" +participant Ethereum as "MINA" +control evm_crawler as "MINA crawler" +control job_provider as "Job provider" database db as "Database" +queue queue1 as "Queue" +control mina_validator as "EVM signature validators" +control mina_sender as "EVM tx sender" +control mina_crawler as "EVM crawler" +participant Mina as "EVM" autonumber -group#LightGreen User lock token from Mina +group#LightGreen (1) User lock token from Evm User -> fe : select network to bridge @@ -35,7 +37,7 @@ group#LightGreen User lock token from Mina end -group#LightGreen Crawler Mina fetch lock tx +group#LightGreen (2) Crawler evm fetch lock tx evm_crawler->Ethereum: fetch lock events activate Ethereum Ethereum --> evm_crawler: return locked tx events @@ -46,27 +48,69 @@ group#LightGreen Crawler Mina fetch lock tx db --> evm_crawler: saved tx with status waiting to be unlocked deactivate db end -group#LightGreen System unlock token to Evm +group#LightGreen (3) Job Provider create queued job + loop + par + job_provider->db: get pending transaction need signatures + activate db + db --> job_provider: list of pending transaction + deactivate db + job_provider -> queue1: send jobs to queue + activate queue1 + queue1 --> job_provider: success + deactivate queue1 + else + job_provider->db: get validated transaction ready to be sent + activate db + db --> job_provider: list of ready transaction + deactivate db + job_provider -> queue1: send jobs to queue + activate queue1 + queue1 --> job_provider: success + deactivate queue1 + end + end +end +group#LightGreen (4) Job Validator process generating signature for tx - mina_validator -> db: get pending unlock tx and verify theirs signature + queue1 -> mina_validator: trigger job activate mina_validator + mina_validator --> db: get data for needed for queued job activate db - db --> mina_validator: update signature + db --> mina_validator: needed data deactivate db - deactivate mina_validator - mina_sender -> db: get signatured verified tx + mina_validator -->mina_validator: generated the signature + + mina_validator -> db: save the signature activate db + db --> mina_validator: save success + deactivate db + + mina_validator --> queue1: job completed + deactivate mina_validator +end +group#LightGreen (5) Job Sender process sending tx to Mina network + + queue1 -> mina_sender: trigger sender by queued job activate mina_sender - db -> mina_sender: return tx + mina_sender --> db: get data for needed for queued job + activate db + db --> mina_sender: needed data deactivate db - mina_sender -> Mina: prove, send tx + mina_sender --> mina_sender: build transaction + + mina_sender -> Mina: Send the tx to EVM activate Mina Mina --> mina_sender: return tx hash deactivate Mina + mina_sender --> mina_sender: wait for the tx to be applied + mina_sender --> queue1: job completed deactivate mina_sender +end +group#LightGreen (6) Mina crawler confirms sent tx mina_crawler -> Mina: get unlock events activate mina_crawler activate Mina @@ -79,5 +123,6 @@ group#LightGreen System unlock token to Evm deactivate db end + @enduml diff --git a/package-lock.json b/package-lock.json index 2fe8582..6cbcfbf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,6 +54,7 @@ "@types/jest": "^29.5.2", "@types/node": "^20.16.5", "@types/supertest": "^2.0.12", + "@types/web3": "^1.2.2", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", "eslint": "^8.42.0", @@ -3675,6 +3676,16 @@ "integrity": "sha512-FCTsikRozryfayPuiI46QzH3fnrOoctTjvOYZkho9BTFLCOZ2rgZJHMOVgCOfttjPJcgOx52EpkY0CMfy87MIw==", "license": "MIT" }, + "node_modules/@types/web3": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@types/web3/-/web3-1.2.2.tgz", + "integrity": "sha512-eFiYJKggNrOl0nsD+9cMh2MLk4zVBfXfGnVeRFbpiZzBE20eet4KLA3fXcjSuHaBn0RnQzwLAGdgzgzdet4C0A==", + "deprecated": "This is a stub types definition. web3 provides its own type definitions, so you do not need this installed.", + "dev": true, + "dependencies": { + "web3": "*" + } + }, "node_modules/@types/yargs": { "version": "17.0.32", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", diff --git a/package.json b/package.json index d9b994f..58e85cd 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "@types/jest": "^29.5.2", "@types/node": "^20.16.5", "@types/supertest": "^2.0.12", + "@types/web3": "^1.2.2", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", "eslint": "^8.42.0", @@ -91,5 +92,4 @@ "tsconfig-paths": "^4.2.0", "typescript": "^5.1.3" } - } diff --git a/src/config/common.config.ts b/src/config/common.config.ts index 99d0f0c..044ac29 100644 --- a/src/config/common.config.ts +++ b/src/config/common.config.ts @@ -1,6 +1,5 @@ import { ConfigService } from '@nestjs/config'; -import { ENetworkName } from '../constants/blockchain.constant.js'; import { EEnvKey } from '../constants/env.constant.js'; import { RpcFactory } from '../shared/modules/web3/web3.module.js'; import { ETHBridgeContract } from '../shared/modules/web3/web3.service.js'; @@ -10,14 +9,14 @@ async function createRpcService(configService: ConfigService) { } async function createRpcEthService(configService: ConfigService) { - return await RpcFactory(configService, ENetworkName.ETH); + return await RpcFactory(configService); } function getEthBridgeAddress(configService: ConfigService) { return configService.get(EEnvKey.ETH_BRIDGE_CONTRACT_ADDRESS); } function getEthBridgeStartBlock(configService: ConfigService) { - return +configService.get(EEnvKey.ETH_BRIDGE_START_BLOCK); + return +configService.get(EEnvKey.ETH_BRIDGE_START_BLOCK)!; } async function initializeEthContract(configService: ConfigService) { @@ -28,6 +27,6 @@ async function initializeEthContract(configService: ConfigService) { ]); // Instantiate the ETHBridgeContract with the resolved dependencies - return new ETHBridgeContract(rpcEthService, address, _startBlock); + return new ETHBridgeContract(rpcEthService, address!, _startBlock); } export { createRpcService, createRpcEthService, getEthBridgeAddress, getEthBridgeStartBlock, initializeEthContract }; diff --git a/src/constants/blockchain.constant.ts b/src/constants/blockchain.constant.ts index 89ff519..95e3b77 100644 --- a/src/constants/blockchain.constant.ts +++ b/src/constants/blockchain.constant.ts @@ -17,6 +17,7 @@ export enum EEventStatus { COMPLETED = 'completed', FAILED = 'failed', NOTOKENPAIR = 'noTokenPair', + CANNOT_PROCESS = 'cannotProcess', } export enum ETokenPairStatus { diff --git a/src/core/base-repository.ts b/src/core/base-repository.ts index 69d5f27..4439c93 100644 --- a/src/core/base-repository.ts +++ b/src/core/base-repository.ts @@ -1,10 +1,10 @@ -import { Repository, SelectQueryBuilder } from 'typeorm'; +import { ObjectLiteral, Repository, SelectQueryBuilder } from 'typeorm'; import { EDirection } from '../constants/api.constant.js'; import { ETableName } from '../constants/entity.constant.js'; import { IPagination } from '../shared/interfaces/pagination.interface.js'; -export abstract class BaseRepository extends Repository { +export abstract class BaseRepository extends Repository { protected abstract alias: ETableName; protected createQb() { @@ -25,9 +25,9 @@ export abstract class BaseRepository extends Repository { queryBuilder.take(data.limit); } if (data.page && data.useLimit) { - queryBuilder.offset((data.page - 1) * data.limit); + queryBuilder.offset((data.page - 1) * data.limit!); } else if (data.page) { - queryBuilder.skip((data.page - 1) * data.limit); + queryBuilder.skip((data.page - 1) * data.limit!); } if (data.sortBy) { if (!selections || selections?.includes(`${this.alias}.${data.sortBy}`)) { @@ -51,7 +51,7 @@ export abstract class BaseRepository extends Repository { } if (data.page) { - queryBuilder.offset((data.page - 1) * data.limit); + queryBuilder.offset((data.page - 1) * data.limit!); } if (data.sortBy) { diff --git a/src/database/repositories/common-configuration.repository.ts b/src/database/repositories/common-configuration.repository.ts index 6ea6d79..f6afac1 100644 --- a/src/database/repositories/common-configuration.repository.ts +++ b/src/database/repositories/common-configuration.repository.ts @@ -1,3 +1,4 @@ +import { UpdateCommonConfigBodyDto } from 'modules/users/dto/common-config-request.dto.js'; import { EntityRepository } from 'nestjs-typeorm-custom-repository'; import { ETableName } from '../../constants/entity.constant.js'; @@ -14,7 +15,7 @@ export class CommonConfigRepository extends BaseRepository { .getOne(); } - public updateCommonConfig(id: number, updateConfig) { + public updateCommonConfig(id: number, updateConfig: UpdateCommonConfigBodyDto) { return this.createQueryBuilder(`${this.alias}`) .update(CommonConfig) .set(updateConfig) diff --git a/src/database/repositories/event-log.repository.ts b/src/database/repositories/event-log.repository.ts index 5cc1c2b..8fb63b0 100644 --- a/src/database/repositories/event-log.repository.ts +++ b/src/database/repositories/event-log.repository.ts @@ -7,14 +7,14 @@ import { ETableName } from '../../constants/entity.constant.js'; import { MAX_RETRIES } from '../../constants/service.constant.js'; import { BaseRepository } from '../../core/base-repository.js'; import { EventLog } from '../../modules/crawler/entities/event-logs.entity.js'; -import { GetHistoryDto } from '../../modules/users/dto/history-response.dto.js'; +import { GetHistoryDto, GetHistoryOfUserDto } from '../../modules/users/dto/history-response.dto.js'; import { endOfDayUnix, nowUnix, startOfDayUnix } from '../../shared/utils/time.js'; @EntityRepository(EventLog) export class EventLogRepository extends BaseRepository { protected alias: ETableName = ETableName.EVENT_LOGS; - public async getEventLockWithNetwork(network: ENetworkName, threshold?: number): Promise { + public async getEventLockWithNetwork(network: ENetworkName, threshold?: number): Promise { const qb = this.createQueryBuilder(`${this.alias}`); qb.innerJoinAndSelect(`${this.alias}.validator`, 'signature'); @@ -98,7 +98,7 @@ export class EventLogRepository extends BaseRepository { .execute(); } - public async getHistoriesOfUser(address: string, options) { + public async getHistoriesOfUser(address: string, options: GetHistoryOfUserDto) { const queryBuilder = this.createQb(); queryBuilder .where(`LOWER(${this.alias}.sender_address) = :address OR LOWER(${this.alias}.receive_address) = :address`, { @@ -150,7 +150,7 @@ export class EventLogRepository extends BaseRepository { return queryBuilder.getManyAndCount(); } - public async sumAmountBridgeOfUserInDay(address) { + public async sumAmountBridgeOfUserInDay(address: string) { const qb = this.createQb(); qb.select([`${this.alias}.sender_address`, `SUM(CAST(${this.alias}.amount_from as DECIMAL(100,2))) as totalamount`]) .where(`${this.alias}.sender_address = :address`, { address }) diff --git a/src/database/repositories/multi-signature.repository.ts b/src/database/repositories/multi-signature.repository.ts index adb0a10..d04d613 100644 --- a/src/database/repositories/multi-signature.repository.ts +++ b/src/database/repositories/multi-signature.repository.ts @@ -7,7 +7,7 @@ import { MultiSignature } from '../../modules/crawler/entities/multi-signature.e @EntityRepository(MultiSignature) export class MultiSignatureRepository extends BaseRepository { protected alias: ETableName = ETableName.MULTI_SIGNATURE; - public async upsertErrorAndRetryMultiSignature(validator: string, txId: number, errorCode: unknown) { + public async upsertErrorAndRetryMultiSignature(validator: string, txId: number, errorCode: string) { const validatorSignature = await this.findOne({ where: { txId, validator }, }); diff --git a/src/database/repositories/token-pair.repository.ts b/src/database/repositories/token-pair.repository.ts index 95022d3..b456b0f 100644 --- a/src/database/repositories/token-pair.repository.ts +++ b/src/database/repositories/token-pair.repository.ts @@ -8,7 +8,7 @@ import { TokenPair } from '../../modules/users/entities/tokenpair.entity.js'; export class TokenPairRepository extends BaseRepository { protected alias: ETableName = ETableName.TOKEN_PAIRS; - public async getTokenPair(tokenFromAddress, toAddress) { + public async getTokenPair(tokenFromAddress: string, toAddress: string) { return this.createQueryBuilder(`${this.alias}`) .where(`${this.alias}.fromAddress = :tokenFromAddress`, { tokenFromAddress }) .andWhere(`${this.alias}.toAddress = :toAddress`, { toAddress }) diff --git a/src/database/repositories/token-price.repository.ts b/src/database/repositories/token-price.repository.ts index b78c17a..6b88c4e 100644 --- a/src/database/repositories/token-price.repository.ts +++ b/src/database/repositories/token-price.repository.ts @@ -8,11 +8,11 @@ import { TokenPrice } from '../../modules/crawler/entities/token-prices.entity.j export class TokenPriceRepository extends BaseRepository { protected alias: ETableName = ETableName.TOKEN_PRICES; - public async getTokenPriceBySymbol(symbol) { + public async getTokenPriceBySymbol(symbol: string) { return this.createQueryBuilder(`${this.alias}`).where(`${this.alias}.symbol = :symbol`, { symbol }).getOne(); } - public async getTokenPriceByListSymbol(symbols) { + public async getTokenPriceByListSymbol(symbols: string[]) { return this.createQueryBuilder(`${this.alias}`) .where(`${this.alias}.symbol IN (:...symbols)`, { symbols }) .getMany(); diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index 20d9959..5e6fd1f 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -11,13 +11,10 @@ import { LoginResponseDto, MessageResponseDto, RefreshTokenResponseDto } from '. @ApiTags('Auth') @Controller('auth') export class AuthController { - private readonly ethBridgeStartBlock: number; constructor( private authService: AuthService, private readonly configService: ConfigService, - ) { - this.ethBridgeStartBlock = this.configService.get(EEnvKey.ETH_BRIDGE_START_BLOCK); - } + ) {} @Post('/login-admin-evm') @GuardPublic() diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts index 080497b..8f2facf 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -36,20 +36,21 @@ export class AuthService { const admin = await this.validateAdminAccount(data.address, true); // Generate access and refresh token - return this.getToken(admin); + return this.getToken(admin!); } async loginMina(data: LoginMinaDto) { try { - if (!(await this.validateSignatureMina(data.address, data.signature))) - throw new httpBadRequest(EError.INVALID_SIGNATURE); + if (!(await this.validateSignatureMina(data.address, data.signature))) { + httpBadRequest(EError.INVALID_SIGNATURE); + } const admin = await this.validateAdminAccount(data.address, false); // Generate access and refresh token - return this.getToken(admin); + return this.getToken(admin!); } catch (err) { this.logger.error('[err] auth.service.ts: ---', err); - throw new httpBadRequest(EError.USER_NOT_FOUND); + httpBadRequest(EError.USER_NOT_FOUND); } } @@ -74,7 +75,7 @@ export class AuthService { try { const recover = await this.ethBridgeContract.recover( signature, - this.configService.get(EEnvKey.ADMIN_MESSAGE_FOR_SIGN), + this.configService.get(EEnvKey.ADMIN_MESSAGE_FOR_SIGN)!, ); const checksumRecover = toChecksumAddress(recover); const checksumAddress = toChecksumAddress(address); @@ -85,7 +86,7 @@ export class AuthService { } } - private async validateSignatureMina(address: string, signature) { + private async validateSignatureMina(address: string, signature: any) { let client = new Client({ network: EMinaChainEnviroment.MAINNET }); if (process.env.NODE_ENV !== EEnvironments.PRODUCTION) { client = new Client({ network: EMinaChainEnviroment.TESTNET }); @@ -122,11 +123,11 @@ export class AuthService { } const user = await this.userRepository.findOne({ - where: { id: jwtData.userId }, + where: { id: jwtData!.userId }, }); if (!user) httpNotFound(EError.USER_NOT_FOUND); - return this.getToken(user); + return this.getToken(user!); } } diff --git a/src/modules/auth/strategies/jwt.strategy.ts b/src/modules/auth/strategies/jwt.strategy.ts index 89b78d9..8a83668 100644 --- a/src/modules/auth/strategies/jwt.strategy.ts +++ b/src/modules/auth/strategies/jwt.strategy.ts @@ -1,6 +1,8 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { PassportStrategy } from '@nestjs/passport'; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore import { ExtractJwt, Strategy } from 'passport-jwt'; import { EEnvKey } from '../../../constants/env.constant.js'; diff --git a/src/modules/crawler/crawler.console.ts b/src/modules/crawler/crawler.console.ts index 35acf47..e989f78 100644 --- a/src/modules/crawler/crawler.console.ts +++ b/src/modules/crawler/crawler.console.ts @@ -80,8 +80,11 @@ export class CrawlerConsole { }) async handleSenderETHBridgeUnlock() { this.logger.info('ETH_SENDER_JOB: started'); - await this.queueService.handleQueueJob(EQueueName.EVM_SENDER_QUEUE, (data: IUnlockToken) => { - return this.senderEVMBridge.handleUnlockEVM(data.eventLogId); + await this.queueService.handleQueueJob(EQueueName.EVM_SENDER_QUEUE, async (data: IUnlockToken) => { + const result = await this.senderEVMBridge.handleUnlockEVM(data.eventLogId); + if (result.error) { + throw result.error; + } }); } diff --git a/src/modules/crawler/crawler.evmbridge.ts b/src/modules/crawler/crawler.evmbridge.ts index 6fc8c38..bb8a13f 100644 --- a/src/modules/crawler/crawler.evmbridge.ts +++ b/src/modules/crawler/crawler.evmbridge.ts @@ -8,11 +8,12 @@ import { EventData } from 'web3-eth-contract'; import { EAsset } from '../../constants/api.constant.js'; import { EEventName, EEventStatus, ENetworkName } from '../../constants/blockchain.constant.js'; import { EEnvKey } from '../../constants/env.constant.js'; +import { CommonConfigRepository } from '../../database/repositories/common-configuration.repository.js'; import { CrawlContractRepository } from '../../database/repositories/crawl-contract.repository.js'; -import { TokenPairRepository } from '../../database/repositories/token-pair.repository.js'; import { CrawlContract, EventLog } from '../../modules/crawler/entities/index.js'; import { LoggerService } from '../../shared/modules/logger/logger.service.js'; import { ETHBridgeContract } from '../../shared/modules/web3/web3.service.js'; +import { calculateUnlockFee } from '../../shared/utils/bignumber.js'; @Injectable() export class BlockchainEVMCrawler { @@ -22,11 +23,11 @@ export class BlockchainEVMCrawler { private readonly configService: ConfigService, private readonly dataSource: DataSource, private readonly crawlContractRepository: CrawlContractRepository, - private readonly tokenPairRepository: TokenPairRepository, private readonly loggerService: LoggerService, private readonly ethBridgeContract: ETHBridgeContract, + private readonly commonConfigRepository: CommonConfigRepository, ) { - this.numberOfBlockPerJob = +this.configService.get(EEnvKey.NUMBER_OF_BLOCK_PER_JOB); + this.numberOfBlockPerJob = +this.configService.get(EEnvKey.NUMBER_OF_BLOCK_PER_JOB)!; this.logger = loggerService.getLogger('BLOCKCHAIN_EVM_CRAWLER'); } @@ -68,15 +69,37 @@ export class BlockchainEVMCrawler { } public async handlerLockEvent(event: EventData, queryRunner: QueryRunner) { + const inputAmount = event.returnValues.amount; const isExist = await queryRunner.manager.findOneBy(EventLog, { txHashLock: event.transactionHash }); if (isExist) { this.logger.warn('Duplicated event', event.transactionHash); return; } + const fromTokenDecimal = this.configService.get(EEnvKey.DECIMAL_TOKEN_EVM), + toTokenDecimal = this.configService.get(EEnvKey.DECIMAL_TOKEN_MINA); + const config = await this.commonConfigRepository.getCommonConfig(); + assert(!!config?.tip, 'tip config undefined'); + const { + success, + amountReceiveNoDecimalPlace, + error, + gasFeeWithDecimalPlaces, + protocolFeeNoDecimalPlace, + tipWithDecimalPlaces, + } = calculateUnlockFee({ + fromDecimal: fromTokenDecimal, + toDecimal: toTokenDecimal, + inputAmountNoDecimalPlaces: inputAmount, + gasFeeWithDecimalPlaces: this.configService.get(EEnvKey.GAS_FEE_EVM)!, + tipPercent: +config!.tip, + }); + if (error) { + this.logger.error('Calculate error', error); + } const blockTimeOfBlockNumber = await this.ethBridgeContract.getBlockTimeByBlockNumber(event.blockNumber); - const eventUnlock = { + const eventUnlock: Partial = { senderAddress: event.returnValues.locker, - amountFrom: event.returnValues.amount, + amountFrom: inputAmount, tokenFromAddress: event.returnValues.token, networkFrom: ENetworkName.ETH, networkReceived: ENetworkName.MINA, @@ -88,25 +111,17 @@ export class BlockchainEVMCrawler { blockTimeLock: Number(blockTimeOfBlockNumber.timestamp), event: EEventName.LOCK, returnValues: JSON.stringify(event.returnValues), - status: EEventStatus.WAITING, + status: success ? EEventStatus.WAITING : EEventStatus.PROCESSING, retry: 0, - fromTokenDecimal: null, - toTokenDecimal: null, + fromTokenDecimal, + toTokenDecimal, + gasFee: gasFeeWithDecimalPlaces, + tip: tipWithDecimalPlaces, + amountReceived: amountReceiveNoDecimalPlace, + protocolFee: protocolFeeNoDecimalPlace, }; - const tokenPair = await this.tokenPairRepository.getTokenPair( - this.configService.get(EEnvKey.ETH_TOKEN_BRIDGE_ADDRESS), - this.configService.get(EEnvKey.MINA_TOKEN_BRIDGE_ADDRESS), - ); - if (!tokenPair) { - eventUnlock.status = EEventStatus.NOTOKENPAIR; - } else { - eventUnlock.fromTokenDecimal = tokenPair.fromDecimal; - eventUnlock.toTokenDecimal = tokenPair.toDecimal; - } - - const result = await queryRunner.manager.save(EventLog, eventUnlock); - assert(!!result.id && !!result.networkReceived, 'Cannot add job to signatures queue.'); + await queryRunner.manager.save(EventLog, eventUnlock); return { success: true, }; @@ -147,7 +162,7 @@ export class BlockchainEVMCrawler { ); } - private async getFromToBlock(): Promise<{ startBlockNumber; toBlock }> { + private async getFromToBlock(): Promise<{ startBlockNumber: number; toBlock: number }> { let startBlockNumber = this.ethBridgeContract.getStartBlock(); let toBlock = await this.ethBridgeContract.getBlockNumber(); diff --git a/src/modules/crawler/crawler.minabridge.ts b/src/modules/crawler/crawler.minabridge.ts index 4629fc5..94937e3 100644 --- a/src/modules/crawler/crawler.minabridge.ts +++ b/src/modules/crawler/crawler.minabridge.ts @@ -9,12 +9,34 @@ import { DataSource, QueryRunner } from 'typeorm'; import { EAsset } from '../../constants/api.constant.js'; import { DEFAULT_ADDRESS_PREFIX, EEventName, EEventStatus, ENetworkName } from '../../constants/blockchain.constant.js'; import { EEnvKey } from '../../constants/env.constant.js'; +import { CommonConfigRepository } from '../../database/repositories/common-configuration.repository.js'; import { CrawlContractRepository } from '../../database/repositories/crawl-contract.repository.js'; import { TokenPairRepository } from '../../database/repositories/token-pair.repository.js'; import { CrawlContract, EventLog } from '../../modules/crawler/entities/index.js'; import { LoggerService } from '../../shared/modules/logger/logger.service.js'; +import { calculateUnlockFee } from '../../shared/utils/bignumber.js'; import { Bridge } from './minaSc/Bridge.js'; +interface IMinaLockTokenEventData { + id: UInt32; + tokenAddress: PublicKey; +} +interface IMinaEvent { + type: string; + event: { + data: any; + transactionInfo: { + transactionHash: string; + transactionStatus: string; + transactionMemo: string; + }; + }; + blockHeight: UInt32; + blockHash: string; + parentBlockHash: string; + globalSlot: UInt32; + chainStatus: string; +} @Injectable() export class SCBridgeMinaCrawler { private readonly logger: Logger; @@ -24,6 +46,7 @@ export class SCBridgeMinaCrawler { private readonly crawlContractRepository: CrawlContractRepository, private readonly tokenPairRepository: TokenPairRepository, private readonly loggerService: LoggerService, + private readonly commonConfigRepository: CommonConfigRepository, ) { this.logger = this.loggerService.getLogger('SC_BRIDGE_MINA_CRAWLER'); const Network = Mina.Network({ @@ -38,7 +61,7 @@ export class SCBridgeMinaCrawler { this.logger.warn('Already latest block. Skipped.'); return; } - const zkappAddress = PublicKey.fromBase58(this.configService.get(EEnvKey.MINA_BRIDGE_CONTRACT_ADDRESS)); + const zkappAddress = PublicKey.fromBase58(this.configService.get(EEnvKey.MINA_BRIDGE_CONTRACT_ADDRESS)!); const zkapp = new Bridge(zkappAddress); const events = await zkapp.fetchEvents(startBlockNumber.add(1), toBlock); @@ -72,9 +95,10 @@ export class SCBridgeMinaCrawler { } } - public async handlerUnLockEvent(event, queryRunner: QueryRunner) { + public async handlerUnLockEvent(event: IMinaEvent, queryRunner: QueryRunner) { + const { id, tokenAddress } = event.event.data as IMinaLockTokenEventData; const existLockTx = await queryRunner.manager.findOne(EventLog, { - where: { id: event.event.data.id.toString() }, + where: { id: Number(id.toString()) }, }); if (!existLockTx) { @@ -85,7 +109,7 @@ export class SCBridgeMinaCrawler { status: EEventStatus.COMPLETED, txHashUnlock: event.event.transactionInfo.transactionHash, amountReceived: event.event.data.amount.toString(), - tokenReceivedAddress: (event.event.data.tokenAddress as PublicKey).toBase58(), + tokenReceivedAddress: tokenAddress.toBase58(), tokenReceivedName: EAsset.WETH, }); @@ -94,19 +118,38 @@ export class SCBridgeMinaCrawler { }; } - public async handlerLockEvent(event: any, queryRunner: QueryRunner) { + public async handlerLockEvent(event: IMinaEvent, queryRunner: QueryRunner) { const txHashLock = event.event.transactionInfo.transactionHash; const field = Field.from(event.event.data.receipt.toString()); const receiveAddress = DEFAULT_ADDRESS_PREFIX + field.toBigInt().toString(16); - + const inputAmount = event.event.data.amount.toString(); const isExist = await queryRunner.manager.findOneBy(EventLog, { txHashLock }); if (isExist) { this.logger.warn('Duplicated event', txHashLock); return; } - const eventUnlock = { + const fromTokenDecimal = this.configService.get(EEnvKey.DECIMAL_TOKEN_MINA), + toTokenDecimal = this.configService.get(EEnvKey.DECIMAL_TOKEN_EVM); + const config = await this.commonConfigRepository.getCommonConfig(); + + assert(!!config?.tip, 'tip config undefined'); + + const { + tipWithDecimalPlaces, + gasFeeWithDecimalPlaces, + amountReceiveNoDecimalPlace, + protocolFeeNoDecimalPlace, + success, + } = calculateUnlockFee({ + fromDecimal: fromTokenDecimal, + toDecimal: toTokenDecimal, + inputAmountNoDecimalPlaces: inputAmount, + gasFeeWithDecimalPlaces: this.configService.get(EEnvKey.GAS_FEE_EVM)!, + tipPercent: +config!.tip, + }); + const eventUnlock: Partial = { senderAddress: JSON.parse(JSON.stringify(event.event.data.locker)), - amountFrom: event.event.data.amount.toString(), + amountFrom: inputAmount, tokenFromAddress: this.configService.get(EEnvKey.MINA_TOKEN_BRIDGE_ADDRESS), networkFrom: ENetworkName.MINA, networkReceived: ENetworkName.ETH, @@ -114,27 +157,20 @@ export class SCBridgeMinaCrawler { tokenReceivedAddress: this.configService.get(EEnvKey.ETH_TOKEN_BRIDGE_ADDRESS), txHashLock, receiveAddress: receiveAddress, - blockNumber: event.blockHeight.toString(), + blockNumber: +event.blockHeight.toString(), blockTimeLock: Number(Math.floor(dayjs().valueOf() / 1000)), event: EEventName.LOCK, returnValues: JSON.stringify(event), - status: EEventStatus.WAITING, + status: success ? EEventStatus.WAITING : EEventStatus.CANNOT_PROCESS, retry: 0, - fromTokenDecimal: null, - toTokenDecimal: null, + fromTokenDecimal, + toTokenDecimal, + gasFee: gasFeeWithDecimalPlaces, + tip: tipWithDecimalPlaces, + amountReceived: amountReceiveNoDecimalPlace, + protocolFee: protocolFeeNoDecimalPlace, }; - const tokenPair = await this.tokenPairRepository.getTokenPair( - this.configService.get(EEnvKey.MINA_TOKEN_BRIDGE_ADDRESS), - this.configService.get(EEnvKey.ETH_TOKEN_BRIDGE_ADDRESS), - ); - if (!tokenPair) { - eventUnlock.status = EEventStatus.NOTOKENPAIR; - } else { - eventUnlock.fromTokenDecimal = tokenPair.fromDecimal; - eventUnlock.toTokenDecimal = tokenPair.toDecimal; - } - this.logger.info({ eventUnlock }); const result = await queryRunner.manager.save(EventLog, eventUnlock); @@ -181,7 +217,7 @@ export class SCBridgeMinaCrawler { const toBlock = UInt32.from( latestBlock.blockchainLength .toUInt64() - .sub(this.configService.get(EEnvKey.MINA_CRAWL_SAFE_BLOCK)) + .sub(this.configService.get(EEnvKey.MINA_CRAWL_SAFE_BLOCK) ?? 3) .toString(), ); diff --git a/src/modules/crawler/job-unlock.provider.ts b/src/modules/crawler/job-unlock.provider.ts index c599c09..22bf67f 100644 --- a/src/modules/crawler/job-unlock.provider.ts +++ b/src/modules/crawler/job-unlock.provider.ts @@ -1,13 +1,18 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; +import assert from 'assert'; +import { BigNumber } from 'bignumber.js'; import { In } from 'typeorm'; +import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js'; import { ENetworkName } from '../../constants/blockchain.constant.js'; import { EEnvKey } from '../../constants/env.constant.js'; import { EQueueName, getEvmValidatorQueueName, getMinaValidatorQueueName } from '../../constants/queue.constant.js'; +import { CommonConfigRepository } from '../../database/repositories/common-configuration.repository.js'; import { EventLogRepository } from '../../database/repositories/event-log.repository.js'; import { LoggerService } from '../../shared/modules/logger/logger.service.js'; import { QueueService } from '../../shared/modules/queue/queue.service.js'; +import { addDecimal } from '../../shared/utils/bignumber.js'; import { sleep } from '../../shared/utils/promise.js'; import { getTimeInFutureInMinutes } from '../../shared/utils/time.js'; import { EventLog } from './entities/event-logs.entity.js'; @@ -20,6 +25,7 @@ export class JobUnlockProvider { private readonly configService: ConfigService, private readonly loggerService: LoggerService, private readonly eventLogRepository: EventLogRepository, + private readonly commonConfigRepository: CommonConfigRepository, ) {} private logger = this.loggerService.getLogger('JOB_UNLOCK_PROVIDER'); @@ -72,7 +78,7 @@ export class JobUnlockProvider { } // helpers private updateIntervalStatusForTxs(ids: number[], isSignatureFullFilled: boolean) { - const payload: Partial = {}; + const payload: QueryDeepPartialEntity = {}; const nextTime = getTimeInFutureInMinutes(60 * 5).toString(); if (isSignatureFullFilled) { payload.nextSendTxJobTime = nextTime; @@ -147,4 +153,19 @@ export class JobUnlockProvider { }, ); } + // TODO: fix this + private async isPassDailyQuota(address: string, fromDecimal: number): Promise { + const [dailyQuota, totalamount] = await Promise.all([ + await this.commonConfigRepository.getCommonConfig(), + await this.eventLogRepository.sumAmountBridgeOfUserInDay(address), + ]); + assert(!!dailyQuota, 'daily quota undefined'); + if ( + totalamount && + BigNumber(totalamount.totalamount).isGreaterThanOrEqualTo(addDecimal(dailyQuota.dailyQuota, fromDecimal)) + ) { + return false; + } + return true; + } } diff --git a/src/modules/crawler/sender.evmbridge.ts b/src/modules/crawler/sender.evmbridge.ts index c3efb7d..c9d9cf8 100644 --- a/src/modules/crawler/sender.evmbridge.ts +++ b/src/modules/crawler/sender.evmbridge.ts @@ -2,20 +2,18 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import assert from 'assert'; import { BigNumber } from 'bignumber.js'; +import { isNumberString } from 'class-validator'; import { ethers } from 'ethers'; -import { Not } from 'typeorm'; import { getEthBridgeAddress } from '../../config/common.config.js'; -import { DECIMAL_BASE, EEventStatus, ENetworkName } from '../../constants/blockchain.constant.js'; +import { EEventStatus, ENetworkName } from '../../constants/blockchain.constant.js'; import { EEnvKey } from '../../constants/env.constant.js'; -import { EError } from '../../constants/error.constant.js'; import { CommonConfigRepository } from '../../database/repositories/common-configuration.repository.js'; import { EventLogRepository } from '../../database/repositories/event-log.repository.js'; import { MultiSignatureRepository } from '../../database/repositories/multi-signature.repository.js'; import { TokenPairRepository } from '../../database/repositories/token-pair.repository.js'; import { LoggerService } from '../../shared/modules/logger/logger.service.js'; import { ETHBridgeContract } from '../../shared/modules/web3/web3.service.js'; -import { addDecimal, calculateFee, calculateTip } from '../../shared/utils/bignumber.js'; import { EventLog } from './entities/event-logs.entity.js'; import { MultiSignature } from './entities/multi-signature.entity.js'; @@ -32,120 +30,69 @@ export class SenderEVMBridge { ) {} private logger = this.loggerService.getLogger('SENDER_EVM_CONSOLE'); - async handleUnlockEVM(txId: number) { - const [dataLock, configTip] = await Promise.all([ - this.eventLogRepository.findOne({ - where: { - id: txId, - networkReceived: ENetworkName.ETH, - status: Not(EEventStatus.PROCESSING), - }, - relations: { - validator: true, - }, - }), - this.commonConfigRepository.getCommonConfig(), - ]); + async handleUnlockEVM(txId: number): Promise<{ error: Error | null; success: boolean }> { + const dataLock = await this.eventLogRepository.findOne({ + where: { + id: txId, + networkReceived: ENetworkName.ETH, + }, + relations: { + validator: true, + }, + }); if (!dataLock) { this.logger.warn('data not found with tx', txId); - return; + return { error: null, success: false }; } - const { tokenReceivedAddress, txHashLock, receiveAddress } = dataLock; + assert(isNumberString(dataLock.tip.toString()), 'invalid gasFee'); + assert(isNumberString(dataLock.gasFee.toString()), 'invalid tips'); - const { tokenPair, amountReceived } = await this.getTokenPairAndAmount(dataLock); - if (!tokenPair) { - this.logger.warn('No token pair found!'); - await this.updateLogStatusWithRetry(dataLock, EEventStatus.NOTOKENPAIR); - return; - } + const { tokenReceivedAddress, txHashLock, receiveAddress, amountFrom } = dataLock; - const isPassQuota = await this.isPassDailyQuota(dataLock.senderAddress, tokenPair.fromDecimal); - if (!isPassQuota) { - this.logger.warn('Over daility quota!'); - await this.updateLogStatusWithRetry(dataLock, EEventStatus.FAILED, EError.OVER_DAILY_QUOTA); - return; - } - const gasFeeEvmWithoutDecimals = this.configService.get(EEnvKey.GAS_FEE_EVM); - // fee and received amount. - const gasFeeEth = addDecimal(gasFeeEvmWithoutDecimals, this.configService.get(EEnvKey.DECIMAL_TOKEN_EVM)); - const protocolFee = calculateFee(amountReceived, gasFeeEth, configTip.tip); - // call unlock function const result = await this.ethBridgeContract.unlock( tokenReceivedAddress, - BigNumber(amountReceived), + BigNumber(amountFrom).toString(), txHashLock, receiveAddress, - BigNumber(protocolFee), + BigNumber(dataLock.protocolFee).toString(), dataLock.validator.map(e => e.signature), ); // Update status eventLog when call function unlock if (result.success) { - await this.eventLogRepository.updateStatusAndRetryEvenLog({ - id: dataLock.id, - status: EEventStatus.PROCESSING, - errorDetail: null, - protocolFee, - amountReceived: BigNumber(amountReceived).minus(protocolFee).toFixed(0), - gasFee: gasFeeEvmWithoutDecimals, - tip: calculateTip(amountReceived, gasFeeEth, configTip.tip) - .div(BigNumber(DECIMAL_BASE).pow(tokenPair.toDecimal)) - .toString(), - }); - } else { + await this.updateLogStatusWithRetry(dataLock, EEventStatus.PROCESSING); + } else if (!!result.error) { this.logger.error(result.error); - this.updateLogStatusWithRetry(dataLock, EEventStatus.FAILED); + await this.updateLogStatusWithRetry(dataLock, EEventStatus.FAILED, result.error.message); } - return result; } - async validateUnlockEVMTransaction(txId: number) { + async validateUnlockEVMTransaction(txId: number): Promise<{ error: Error | null; success: boolean }> { const wallet = this.getWallet(); - const [dataLock, configTip] = await Promise.all([ + const [dataLock] = await Promise.all([ this.eventLogRepository.findOneBy({ id: txId, networkReceived: ENetworkName.ETH }), this.commonConfigRepository.getCommonConfig(), ]); if (!dataLock) { this.logger.warn('no data found tx', txId); - return; + return { error: null, success: false }; } const { tokenReceivedAddress, txHashLock, receiveAddress } = dataLock; - const { tokenPair, amountReceived } = await this.getTokenPairAndAmount(dataLock); - if (!tokenPair) { - this.logger.warn('no token pair found tx', dataLock.tokenReceivedAddress, dataLock.tokenFromAddress); - await this.updateLogStatusWithRetry(dataLock, EEventStatus.NOTOKENPAIR); - return; - } - - const protocolFee = calculateFee( - amountReceived, - addDecimal(this.configService.get(EEnvKey.GAS_FEE_EVM), this.configService.get(EEnvKey.DECIMAL_TOKEN_EVM)), - configTip.tip, - ); const signTx = await this.getSignature(wallet, { token: tokenReceivedAddress, - amount: amountReceived, + amount: dataLock.amountReceived, user: receiveAddress, hash: txHashLock, - fee: protocolFee.toString(), + fee: dataLock.protocolFee, }); + assert(signTx.success, `Generate signature failed!`); await this.saveSignature(wallet.address, signTx.signature, dataLock.id); - } - private async isPassDailyQuota(address: string, fromDecimal: number): Promise { - const [dailyQuota, totalamount] = await Promise.all([ - await this.commonConfigRepository.getCommonConfig(), - await this.eventLogRepository.sumAmountBridgeOfUserInDay(address), - ]); - - return totalamount && - BigNumber(totalamount.totalamount).isGreaterThan(addDecimal(dailyQuota.dailyQuota, fromDecimal)) - ? false - : true; + return { error: null, success: true }; } public async getSignature(wallet: ethers.Wallet, value: Record) { @@ -184,20 +131,15 @@ export class SenderEVMBridge { } public async getTokenPairAndAmount(dataLock: EventLog) { - const { tokenReceivedAddress, tokenFromAddress, amountFrom } = dataLock; + const { tokenReceivedAddress, tokenFromAddress } = dataLock; const tokenPair = await this.tokenPairRepository.getTokenPair(tokenFromAddress, tokenReceivedAddress); if (!tokenPair) return { tokenPair: null, amountReceived: null }; - const amountReceived = BigNumber(amountFrom) - .dividedBy(BigNumber(DECIMAL_BASE).pow(tokenPair.fromDecimal)) - .multipliedBy(BigNumber(DECIMAL_BASE).pow(tokenPair.toDecimal)) - .toString(); - - return { tokenPair, amountReceived }; + return { tokenPair }; } - private async updateLogStatusWithRetry(dataLock: EventLog, status: EEventStatus, errorDetail?: EError) { + private async updateLogStatusWithRetry(dataLock: EventLog, status: EEventStatus, errorDetail?: string) { await this.eventLogRepository.updateStatusAndRetryEvenLog({ id: dataLock.id, status, @@ -206,10 +148,10 @@ export class SenderEVMBridge { } public getWallet(): ethers.Wallet { - assert(!!this.configService.get(EEnvKey.EVM_VALIDATOR_PRIVATE_KEY), 'validator private key invalid'); + const privateKey = this.configService.get(EEnvKey.EVM_VALIDATOR_PRIVATE_KEY); + assert(!!privateKey, 'validator private key invalid'); assert(!!this.configService.get(EEnvKey.THIS_VALIDATOR_INDEX), 'invalid validator index'); - const privateKey = this.configService.get(EEnvKey.EVM_VALIDATOR_PRIVATE_KEY); return new ethers.Wallet(privateKey); } } diff --git a/src/modules/crawler/sender.minabridge.ts b/src/modules/crawler/sender.minabridge.ts index 3c3335c..134ec72 100644 --- a/src/modules/crawler/sender.minabridge.ts +++ b/src/modules/crawler/sender.minabridge.ts @@ -2,21 +2,19 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import assert from 'assert'; import { BigNumber } from 'bignumber.js'; +import { isNumberString } from 'class-validator'; import { FungibleToken, FungibleTokenAdmin } from 'mina-fungible-token'; import { AccountUpdate, Bool, fetchAccount, Mina, PrivateKey, PublicKey, Signature, UInt64 } from 'o1js'; -import { Not } from 'typeorm'; import { DECIMAL_BASE, EEventStatus, ENetworkName } from '../../constants/blockchain.constant.js'; import { EEnvKey } from '../../constants/env.constant.js'; -import { EError } from '../../constants/error.constant.js'; import { CommonConfigRepository } from '../../database/repositories/common-configuration.repository.js'; import { EventLogRepository } from '../../database/repositories/event-log.repository.js'; import { MultiSignatureRepository } from '../../database/repositories/multi-signature.repository.js'; import { TokenPairRepository } from '../../database/repositories/token-pair.repository.js'; import { LoggerService } from '../../shared/modules/logger/logger.service.js'; -import { addDecimal, calculateFee, calculateTip } from '../../shared/utils/bignumber.js'; +import { addDecimal, calculateFee } from '../../shared/utils/bignumber.js'; import { TokenPair } from '../users/entities/tokenpair.entity.js'; -import { CommonConfig } from './entities/common-config.entity.js'; import { MultiSignature } from './entities/multi-signature.entity.js'; import { Bridge } from './minaSc/Bridge.js'; import { Manager } from './minaSc/Manager.js'; @@ -36,9 +34,9 @@ export class SenderMinaBridge { private readonly multiSignatureRepository: MultiSignatureRepository, private readonly loggerService: LoggerService, ) { - this.feePayerKey = PrivateKey.fromBase58(this.configService.get(EEnvKey.SIGNER_MINA_PRIVATE_KEY)); - this.bridgeKey = PrivateKey.fromBase58(this.configService.get(EEnvKey.MINA_BRIDGE_SC_PRIVATE_KEY)); - this.tokenPublicKey = PublicKey.fromBase58(this.configService.get(EEnvKey.MINA_TOKEN_BRIDGE_ADDRESS)); + this.feePayerKey = PrivateKey.fromBase58(this.configService.get(EEnvKey.SIGNER_MINA_PRIVATE_KEY)!); + this.bridgeKey = PrivateKey.fromBase58(this.configService.get(EEnvKey.MINA_BRIDGE_SC_PRIVATE_KEY)!); + this.tokenPublicKey = PublicKey.fromBase58(this.configService.get(EEnvKey.MINA_TOKEN_BRIDGE_ADDRESS)!); const network = Mina.Network({ mina: this.configService.get(EEnvKey.MINA_BRIDGE_RPC_OPTIONS), @@ -64,81 +62,48 @@ export class SenderMinaBridge { } private getAmountReceivedAndFee( tokenPair: TokenPair, - config: CommonConfig, + tip: number, + gasFee: number, amountFrom: string, - ): { amountReceived: string; protocolFeeAmount: string; tipAmount: string; gasFeeMina: string } { + ): { amountReceived: string; protocolFeeAmount: string } { // convert decimal from ETH to MINA const amountReceiveConvert = BigNumber(amountFrom) .dividedBy(BigNumber(DECIMAL_BASE).pow(tokenPair.fromDecimal)) .multipliedBy(BigNumber(DECIMAL_BASE).pow(tokenPair.toDecimal)) .toString(); - const gasFeeMina = addDecimal( - this.configService.get(EEnvKey.GASFEEMINA), - this.configService.get(EEnvKey.DECIMAL_TOKEN_MINA), - ); + const gasFeeMina = addDecimal(gasFee, this.configService.get(EEnvKey.DECIMAL_TOKEN_MINA)!); // calc fee follow MINA decimal. - const protocolFeeAmount = BigNumber(calculateFee(amountReceiveConvert, gasFeeMina, config.tip)) + const protocolFeeAmount = BigNumber(calculateFee(amountReceiveConvert, gasFeeMina, tip)) .toFixed(0) .toString(); const amountReceived = BigNumber(amountReceiveConvert).minus(protocolFeeAmount).toFixed(0).toString(); return { amountReceived, protocolFeeAmount, - tipAmount: calculateTip(amountReceiveConvert, gasFeeMina, config.tip) - .div(BigNumber(DECIMAL_BASE).pow(tokenPair.toDecimal)) - .toString(), - gasFeeMina: this.configService.get(EEnvKey.GASFEEMINA), }; } - public async handleUnlockMina(txId: number) { - const [dataLock, configTip] = await Promise.all([ - this.eventLogRepository.findOneBy({ id: txId, networkReceived: ENetworkName.MINA }), - this.commonConfigRepository.getCommonConfig(), - ]); + public async handleUnlockMina(txId: number): Promise<{ error: Error | null; success: boolean }> { + const dataLock = await this.eventLogRepository.findOneBy({ id: txId, networkReceived: ENetworkName.MINA }); + if (!dataLock) { this.logger.warn(`Not found tx with id ${txId}`); - return; + return { error: null, success: false }; } + assert(isNumberString(dataLock.tip.toString()), 'invalid gasFee'); + assert(isNumberString(dataLock.gasFee.toString()), 'invalid tips'); + assert(dataLock.amountReceived, 'invalida amount to unlock'); await this.eventLogRepository.updateLockEvenLog(dataLock.id, EEventStatus.PROCESSING); - const { tokenReceivedAddress, tokenFromAddress, id, receiveAddress, amountFrom, senderAddress } = dataLock; - const tokenPair = await this.tokenPairRepository.getTokenPair(tokenFromAddress, tokenReceivedAddress); - if (!tokenPair) { - this.logger.warn('Token pair not found.'); - await this.eventLogRepository.updateStatusAndRetryEvenLog({ - id: dataLock.id, - status: EEventStatus.NOTOKENPAIR, - }); - return; - } + const { id, receiveAddress, amountReceived } = dataLock; - const isPassDailyQuota = await this.isPassDailyQuota(senderAddress, tokenPair.fromDecimal); - if (!isPassDailyQuota) { - this.logger.warn('Passed daily quota.'); - await this.eventLogRepository.updateStatusAndRetryEvenLog({ - id: dataLock.id, - status: EEventStatus.FAILED, - errorDetail: EError.OVER_DAILY_QUOTA, - }); - return; - } - const { amountReceived, protocolFeeAmount, gasFeeMina, tipAmount } = this.getAmountReceivedAndFee( - tokenPair, - configTip, - amountFrom, - ); const result = await this.callUnlockFunction(amountReceived, id, receiveAddress); // Update status eventLog when call function unlock if (result.success) { await this.eventLogRepository.updateStatusAndRetryEvenLog({ id: dataLock.id, status: EEventStatus.PROCESSING, - errorDetail: result.error, - txHashUnlock: result.data, - amountReceived, - protocolFee: protocolFeeAmount, - gasFee: gasFeeMina, - tip: tipAmount, + errorDetail: result.error?.message, + txHashUnlock: result.data!, }); } else { await this.eventLogRepository.updateStatusAndRetryEvenLog({ @@ -146,11 +111,16 @@ export class SenderMinaBridge { status: EEventStatus.FAILED, errorDetail: JSON.stringify(result.error), }); + throw new Error(`Tx ${txId} cannot be sent due to network error.`); } return result; } - private async callUnlockFunction(amount: string, txId: number, receiveAddress: string) { + private async callUnlockFunction( + amount: string, + txId: number, + receiveAddress: string, + ): Promise<{ success: boolean; error: Error | null; data: string | null }> { try { const generatedSignatures = await this.multiSignatureRepository.findBy({ txId, @@ -203,58 +173,30 @@ export class SenderMinaBridge { return { success: true, error: null, data: sentTx.hash }; } catch (error) { this.logger.error(error); - return { success: false, error, data: null }; + return { success: false, error: error as Error, data: null }; } } - private async isPassDailyQuota(address: string, fromDecimal: number): Promise { - const [dailyQuota, totalamount] = await Promise.all([ - await this.commonConfigRepository.getCommonConfig(), - await this.eventLogRepository.sumAmountBridgeOfUserInDay(address), - ]); - - if ( - totalamount && - BigNumber(totalamount.totalamount).isGreaterThanOrEqualTo(addDecimal(dailyQuota.dailyQuota, fromDecimal)) - ) { - return false; - } - return true; - } - - async handleValidateUnlockTxMina(txId: number) { + async handleValidateUnlockTxMina(txId: number): Promise<{ error: Error | null; success: boolean }> { assert(!!this.configService.get(EEnvKey.MINA_VALIDATOR_PRIVATE_KEY), 'invalid validator private key'); assert(!!this.configService.get(EEnvKey.THIS_VALIDATOR_INDEX), 'invalid validator index'); - const signerPrivateKey = PrivateKey.fromBase58(this.configService.get(EEnvKey.MINA_VALIDATOR_PRIVATE_KEY)); + const signerPrivateKey = PrivateKey.fromBase58(this.configService.get(EEnvKey.MINA_VALIDATOR_PRIVATE_KEY)!); const signerPublicKey = PublicKey.fromPrivateKey(signerPrivateKey).toBase58(); - const [dataLock, config] = await Promise.all([ - this.eventLogRepository.findOneBy({ - id: txId, - networkReceived: ENetworkName.MINA, - status: Not(EEventStatus.PROCESSING), - }), - this.commonConfigRepository.getCommonConfig(), - ]); + const dataLock = await this.eventLogRepository.findOneBy({ + id: txId, + networkReceived: ENetworkName.MINA, + }); if (!dataLock) { this.logger.warn(`Data not found with id ${txId}`); - return; + return { error: null, success: false }; } this.logger.info('Start generating mina signatures for tx', txId); - const { tokenReceivedAddress, tokenFromAddress, receiveAddress, amountFrom } = dataLock; + assert(!!dataLock.amountReceived, 'invalid amount received'); - const tokenPair = await this.tokenPairRepository.getTokenPair(tokenFromAddress, tokenReceivedAddress); - - if (!tokenPair) { - this.logger.warn('Unknown token pair', tokenFromAddress, tokenReceivedAddress); - await this.eventLogRepository.updateStatusAndRetryEvenLog({ - id: dataLock.id, - status: EEventStatus.NOTOKENPAIR, - }); - return; - } + const { receiveAddress, amountReceived } = dataLock; // check if this signature has been tried before. let multiSignature = await this.multiSignatureRepository.findOneBy({ @@ -263,11 +205,10 @@ export class SenderMinaBridge { }); if (multiSignature) { this.logger.warn('signature existed'); - return; + return { error: null, success: false }; } const receiverPublicKey = PublicKey.fromBase58(receiveAddress); - const { amountReceived } = this.getAmountReceivedAndFee(tokenPair, config, amountFrom); const msg = [ ...receiverPublicKey.toFields(), @@ -284,5 +225,6 @@ export class SenderMinaBridge { }); await this.multiSignatureRepository.save(multiSignature); // notice the job unlock provider here + return { error: null, success: true }; } } diff --git a/src/modules/crawler/tests/calculation.spec.ts b/src/modules/crawler/tests/calculation.spec.ts new file mode 100644 index 0000000..c9818b6 --- /dev/null +++ b/src/modules/crawler/tests/calculation.spec.ts @@ -0,0 +1,21 @@ +import { calculateUnlockFee } from '../../../shared/utils/bignumber.js'; + +describe('test amount fee calculation', () => { + it('correct amount', () => { + const fromDecimal = 18, + toDecimal = 9, + gasFeeWithDecimalPlaces = '0.000001', + inputAmountNoDecimalPlaces = '159719371259000000', + tipPercent = 5; + const result = calculateUnlockFee({ + fromDecimal, + gasFeeWithDecimalPlaces, + inputAmountNoDecimalPlaces, + tipPercent, + toDecimal, + }); + console.log(result); + + expect(result.success).toBeTruthy(); + }); +}); diff --git a/src/modules/crawler/tests/evm-sender.spec.ts b/src/modules/crawler/tests/evm-sender.spec.ts index 8d10904..865491e 100644 --- a/src/modules/crawler/tests/evm-sender.spec.ts +++ b/src/modules/crawler/tests/evm-sender.spec.ts @@ -129,9 +129,9 @@ describe('handleValidateUnlockTxEVM', () => { status: ETokenPairStatus.ENABLE, } as TokenPair; - const data = { + const data: Partial = { id: 18, - deletedAt: null, + deletedAt: undefined, senderAddress: 'B62qjWwgHupW7k7fcTbb2Kszp4RPYBWYdL4KMmoqfkMH3iRN2FN8u5n', amountFrom: '2', tokenFromAddress: 'B62qqki2ZnVzaNsGaTDAP6wJYCth5UAcY6tPX2TQYHdwD8D4uBgrDKC', @@ -152,24 +152,24 @@ describe('handleValidateUnlockTxEVM', () => { status: EEventStatus.WAITING, retry: 0, validator: [] as MultiSignature[], - } as EventLog; + }; it('should handle validator signature generation', async () => { const wallet = senderEVMBridge.getWallet(); - data.validator.push({ + data.validator!.push({ validator: wallet.address, txId: 18, retry: 2, signature: '0xc096d8abb2af534fa09b62ca3825a202172239ee0ab3d8438680faca0f0e59153fef0bdc0681162d94cad9fe77b05d4c1945be9c46cb89f9b2821d8576fb28d31b', } as MultiSignature); - jest.spyOn(eventLogRepository, 'getValidatorPendingSignature').mockResolvedValue(data); + jest.spyOn(eventLogRepository, 'getValidatorPendingSignature').mockResolvedValue(data as EventLog); jest.spyOn(commonConfigRepository, 'getCommonConfig').mockResolvedValue(commonConfig); jest.spyOn(tokenPairRepository, 'getTokenPair').mockResolvedValue(tokenPair); - jest.spyOn(multiSignatureRepository, 'findOne').mockResolvedValue(data.validator[0]); - jest.spyOn(multiSignatureRepository, 'update').mockResolvedValue(undefined); + jest.spyOn(multiSignatureRepository, 'findOne').mockResolvedValue(data.validator![0]!); + jest.spyOn(multiSignatureRepository, 'update').mockResolvedValue(true as any); - await senderEVMBridge.unlockEVMTransaction(); + await senderEVMBridge.validateUnlockEVMTransaction(data.id!); expect(eventLogRepository.getValidatorPendingSignature).toHaveBeenCalled(); expect(commonConfigRepository.getCommonConfig).toHaveBeenCalled(); @@ -180,11 +180,11 @@ describe('handleValidateUnlockTxEVM', () => { const threshold = await newEthBridgeContract.getValidatorThreshold(); expect(threshold).toBe('1'); jest.spyOn(commonConfigRepository, 'getCommonConfig').mockResolvedValue(commonConfig); - jest.spyOn(eventLogRepository, 'getEventLockWithNetwork').mockResolvedValue(data); - jest.spyOn(eventLogRepository, 'updateLockEvenLog').mockResolvedValue(undefined); + jest.spyOn(eventLogRepository, 'getEventLockWithNetwork').mockResolvedValue(data as EventLog); + jest.spyOn(eventLogRepository, 'updateLockEvenLog').mockResolvedValue(true as any); jest.spyOn(tokenPairRepository, 'getTokenPair').mockResolvedValue(tokenPair); - jest.spyOn(eventLogRepository, 'updateStatusAndRetryEvenLog').mockResolvedValue(undefined); + jest.spyOn(eventLogRepository, 'updateStatusAndRetryEvenLog').mockResolvedValue(true as any); - await senderEVMBridge.handleUnlockEVM(); + await senderEVMBridge.handleUnlockEVM(data.id!); }); }); diff --git a/src/modules/crawler/tests/mina-crawler.spec.ts b/src/modules/crawler/tests/mina-crawler.spec.ts index 488d9c9..14288bf 100644 --- a/src/modules/crawler/tests/mina-crawler.spec.ts +++ b/src/modules/crawler/tests/mina-crawler.spec.ts @@ -137,7 +137,7 @@ describe('handleMinaChainCrawler', () => { const fetchEventsMock = jest.fn().mockResolvedValue([transformedUnlockObject, transformedLockObject]); const zaAppAddress = jest .fn() - .mockResolvedValue(PublicKey.fromBase58(configService.get(EEnvKey.MINA_BRIDGE_CONTRACT_ADDRESS))); + .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); @@ -196,7 +196,7 @@ describe('handleMinaChainCrawler', () => { toTokenDecimal: null, }); - expect(result.success).toBe(true); + expect(result!.success).toBe(true); }); }); @@ -236,7 +236,7 @@ it('should save the correct event log in handlerUnlockEvent', async () => { expect(queryRunner.manager.findOne).toHaveBeenCalledWith(EventLog, { where: { id: transformedUnlockObject.event.data.id.toString() }, }); - expect(result.success).toBe(true); + expect(result!.success).toBe(true); }); afterEach(() => { diff --git a/src/modules/crawler/tests/mina-sender.spec.ts b/src/modules/crawler/tests/mina-sender.spec.ts index ac707df..05922f4 100644 --- a/src/modules/crawler/tests/mina-sender.spec.ts +++ b/src/modules/crawler/tests/mina-sender.spec.ts @@ -8,6 +8,7 @@ import { MultiSignatureRepository } from '../../../database/repositories/multi-s import { TokenPairRepository } from '../../../database/repositories/token-pair.repository.js'; import { LoggingModule } from '../../../shared/modules/logger/logger.module.js'; import { Web3Module } from '../../../shared/modules/web3/web3.module.js'; +import { EventLog } from '../entities/event-logs.entity.js'; import { SenderMinaBridge } from '../sender.minabridge.js'; // Mock objects @@ -22,6 +23,7 @@ const mockEventLogRepository = { updateLockEvenLog: jest.fn(), sumAmountBridgeOfUserInDay: jest.fn(), update: jest.fn(), + findOneBy: jest.fn(), }; const mockCommonConfigRepository = { getCommonConfig: jest.fn(), @@ -72,37 +74,17 @@ describe('MinaSenderService', () => { signature: `{"r":"5570236603572533401994258050414813854840150826583453247540834111404926928692","s":"21736293135419403848339637561979298566104722930867359295319005331842934575203"}`, }, ]); - mockEventLogRepository.getEventLockWithNetwork.mockResolvedValue({ - id: 333, + mockEventLogRepository.findOneBy.mockResolvedValue({ + id: 1, tokenReceivedAddress: 'B62qqKNnNRpCtgcBexw5khZSpk9K2d9Z7Wzcyir3WZcVd15Bz8eShVi', tokenFromAddress: '0x0000000000000000000000000000000000000000', receiveAddress: 'B62qkkjqtrVmRLQhmkCQPw2dwhCZfUsmxCRTSfgdeUPhyTdoMv7h6b9', amountFrom: '159719371259000000', - senderAddress: '0xb3Edf83eA590F44f5c400077EBd94CCFE10E4Bb0', - }); - mockEventLogRepository.sumAmountBridgeOfUserInDay.mockResolvedValue(0); - mockCommonConfigRepository.getCommonConfig.mockResolvedValue({ - asset: 'ETH', - tip: '0.5', - dailyQuota: '500', - id: 1, - }); - mockTokenPairRepository.getTokenPair.mockResolvedValue({ - id: 5, - deletedAt: null, - fromChain: 'eth', - toChain: 'mina', - fromSymbol: 'ETH', - toSymbol: 'WETH', - fromAddress: '0x0000000000000000000000000000000000000000', - toAddress: 'B62qqKNnNRpCtgcBexw5khZSpk9K2d9Z7Wzcyir3WZcVd15Bz8eShVi', - fromDecimal: 18, - toDecimal: 9, - fromScAddress: '0xa4045da3c53138F84C97668f6deFDf8f4C9348f6', - toScAddress: 'B62qpeJGDMHp36rqyvfxHmjmHEZk7a7ryq2nCFFHkGHRMqXqkq5F2VS', - status: 'enable', - }); - const result = await minaCrawlerService.handleUnlockMina(); + amountReceived: '151732453', + tip: '0.00798591856295', + gasFee: '0.000001', + } as Partial); + const result = await minaCrawlerService.handleUnlockMina(1); expect(result.success).toBe(true); }); diff --git a/src/modules/users/users.service.ts b/src/modules/users/users.service.ts index f1b4ea6..cd8269b 100644 --- a/src/modules/users/users.service.ts +++ b/src/modules/users/users.service.ts @@ -1,6 +1,5 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { Logger } from 'log4js'; import { DataSource } from 'typeorm'; import { ENetworkName } from '../../constants/blockchain.constant.js'; @@ -15,11 +14,11 @@ import { httpBadRequest, httpNotFound } from '../../shared/exceptions/http-exept import { LoggerService } from '../../shared/modules/logger/logger.service.js'; import { addDecimal } from '../../shared/utils/bignumber.js'; import { UpdateCommonConfigBodyDto } from './dto/common-config-request.dto.js'; +import { GetHistoryDto, GetHistoryOfUserDto } from './dto/history-response.dto.js'; import { GetProtocolFeeBodyDto } from './dto/user-request.dto.js'; @Injectable() export class UsersService { - private readonly logger: Logger; constructor( private readonly usersRepository: UserRepository, private readonly eventLogRepository: EventLogRepository, @@ -27,9 +26,8 @@ export class UsersService { private readonly dataSource: DataSource, private readonly configService: ConfigService, private readonly loggerService: LoggerService, - ) { - this.logger = this.loggerService.getLogger('USER_SERVICE'); - } + ) {} + private readonly logger = this.loggerService.getLogger('USER_SERVICE'); async getProfile(userId: number) { const user = await this.usersRepository.findOne({ where: { id: userId }, @@ -40,12 +38,12 @@ export class UsersService { return user; } - async getHistoriesOfUser(address: string, options) { + async getHistoriesOfUser(address: string, options: GetHistoryOfUserDto) { const [data, count] = await this.eventLogRepository.getHistoriesOfUser(address, options); return toPageDto(data, options, count); } - async getHistories(options) { + async getHistories(options: GetHistoryDto) { const [data, count] = await this.eventLogRepository.getHistories(options); return toPageDto(data, options, count); } @@ -82,14 +80,14 @@ export class UsersService { if (!tokenPair) { httpNotFound(EError.RESOURCE_NOT_FOUND); } - if (tokenPair.toChain == ENetworkName.MINA) { + if (tokenPair!.toChain == ENetworkName.MINA) { decimal = this.configService.get(EEnvKey.DECIMAL_TOKEN_EVM); - gasFee = addDecimal(this.configService.get(EEnvKey.GAS_FEE_EVM), decimal); + gasFee = addDecimal(this.configService.get(EEnvKey.GAS_FEE_EVM)!, decimal); } else { decimal = this.configService.get(EEnvKey.DECIMAL_TOKEN_MINA); - gasFee = addDecimal(this.configService.get(EEnvKey.GASFEEMINA), decimal); + gasFee = addDecimal(this.configService.get(EEnvKey.GASFEEMINA)!, decimal); } - return { gasFee, tipRate: configTip.tip, decimal }; + return { gasFee, tipRate: configTip!.tip, decimal }; } } diff --git a/src/ormconfig.ts b/src/ormconfig.ts index b35896d..9add459 100644 --- a/src/ormconfig.ts +++ b/src/ormconfig.ts @@ -13,7 +13,7 @@ export const migrationDir = join(__dirname, 'database/migrations'); export default { type: 'postgres', host: process.env[EEnvKey.DB_HOST], - port: +process.env[EEnvKey.DB_PORT], + port: +process.env[EEnvKey.DB_PORT]!, username: process.env[EEnvKey.DB_USERNAME], password: process.env[EEnvKey.DB_PASSWORD], database: process.env[EEnvKey.DB_DATABASE], diff --git a/src/shared/decorators/transform.decorator.ts b/src/shared/decorators/transform.decorator.ts index 3bae603..0c5c7b8 100644 --- a/src/shared/decorators/transform.decorator.ts +++ b/src/shared/decorators/transform.decorator.ts @@ -1,5 +1,7 @@ import { createParamDecorator, ExecutionContext } from '@nestjs/common'; import { Transform } from 'class-transformer'; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore import pkg from 'lodash'; const { castArray, isArray, isNil, map, trim } = pkg; @@ -8,7 +10,7 @@ export function Trim(): PropertyDecorator { const value = params.value as string[] | string; if (isArray(value)) { - return map(value, v => trim(v).replace(/\s\s+/g, ' ')); + return map(value, (v: any) => trim(v).replace(/\s\s+/g, ' ')); } return trim(value).replace(/\s\s+/g, ' '); @@ -30,7 +32,7 @@ export function ToBooleanArray(): PropertyDecorator { return Transform( params => { if (isArray(params.value)) { - return params.value.map(v => toBoolean(v)); + return params.value.map((v: string | number | boolean) => toBoolean(v)); } }, { toClassOnly: true }, diff --git a/src/shared/dtos/page-meta.dto.ts b/src/shared/dtos/page-meta.dto.ts index 09a06ed..82ddb61 100644 --- a/src/shared/dtos/page-meta.dto.ts +++ b/src/shared/dtos/page-meta.dto.ts @@ -23,10 +23,10 @@ export class PageMetaDto { constructor(pageOptionsDto: PageOptionsDto, itemCount: number) { this.total = itemCount; - this.totalOfPages = Math.ceil(this.total / pageOptionsDto.limit); + this.totalOfPages = Math.ceil(this.total / pageOptionsDto.limit!); this.perPage = pageOptionsDto.limit; this.currentPage = pageOptionsDto.page; - this.hasPreviousPage = pageOptionsDto.page > 1; - this.hasNextPage = pageOptionsDto.page < this.totalOfPages; + this.hasPreviousPage = pageOptionsDto.page! > 1; + this.hasNextPage = pageOptionsDto.page! < this.totalOfPages; } } diff --git a/src/shared/modules/logger/logger.service.ts b/src/shared/modules/logger/logger.service.ts index 8b9092b..aff0a09 100644 --- a/src/shared/modules/logger/logger.service.ts +++ b/src/shared/modules/logger/logger.service.ts @@ -22,7 +22,7 @@ const layouts: Record = { tokens: { remoteAddr: function (logEvent) { let remoteAddr = logEvent.data.toString().split(' ', 1).pop(); - remoteAddr = remoteAddr.replace(/^.*:/, ''); + remoteAddr = remoteAddr?.replace(/^.*:/, ''); remoteAddr = remoteAddr === '1' ? '127.0.0.1' : remoteAddr; return remoteAddr; }, diff --git a/src/shared/modules/queue/queue.service.ts b/src/shared/modules/queue/queue.service.ts index 2839f70..0dac975 100644 --- a/src/shared/modules/queue/queue.service.ts +++ b/src/shared/modules/queue/queue.service.ts @@ -14,7 +14,7 @@ export class QueueService { private readonly loggerService: LoggerService, ) {} private logger = this.loggerService.getLogger('IN_QUEUE'); - private initQueueOnDemand(queueName: string) { + private initQueueOnDemand(queueName: string): Queue { // reduce connection to redis const redisConfig = { host: this.configService.get(EEnvKey.REDIS_HOST), @@ -24,22 +24,23 @@ export class QueueService { this.logger.info('setup Queue', queueName); this.queues.set(queueName, BullLib.createNewQueue(queueName, redisConfig)); } + return this.queues.get(queueName)!; } public async handleQueueJob(queueName: string, handleFunction: CallableFunction, numOfJobs = 1) { - this.initQueueOnDemand(queueName); - await this.queues.get(queueName).process(numOfJobs, async (job: Job, done: DoneCallback) => { + const queue = this.initQueueOnDemand(queueName); + await queue.process(numOfJobs, async (job: Job, done: DoneCallback) => { try { this.logger.info('Handling job', job.data); await handleFunction(job.data); done(); } catch (error) { this.logger.warn('Job failed', job.data, error); - done(error); + done(error as Error); } }); } public addJobToQueue(queueName: string, job: T, options: JobOptions = { attempts: 3, backoff: 5000 }) { - this.initQueueOnDemand(queueName); - return this.queues.get(queueName).add(job, options); + const queue = this.initQueueOnDemand(queueName); + return queue.add(job, options); } } diff --git a/src/shared/modules/web3/web3.module.ts b/src/shared/modules/web3/web3.module.ts index 440a1b0..0688487 100644 --- a/src/shared/modules/web3/web3.module.ts +++ b/src/shared/modules/web3/web3.module.ts @@ -1,9 +1,10 @@ import { Global, Module } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore import Web3 from 'web3/lib/index.js'; import { initializeEthContract } from '../../../config/common.config.js'; -import { ENetworkName } from '../../../constants/blockchain.constant.js'; import { EEnvKey } from '../../../constants/env.constant.js'; import { ASYNC_CONNECTION } from '../../../constants/service.constant.js'; import { sleep } from '../../utils/promise.js'; @@ -41,10 +42,10 @@ export interface IRpcService { getNonce: (walletAddress: string) => Promise; } -export const RpcFactory = async (configService: ConfigService, network?: ENetworkName): Promise => { +export const RpcFactory = async (configService: ConfigService): Promise => { let rpcRound = 0; - const rpc = configService.get(EEnvKey.ETH_BRIDGE_RPC_OPTIONS); - const privateKeys = configService.get(EEnvKey.SIGNER_PRIVATE_KEY); + const rpc = configService.get(EEnvKey.ETH_BRIDGE_RPC_OPTIONS)!; + const privateKeys = configService.get(EEnvKey.SIGNER_PRIVATE_KEY)!; const getNextRPcRound = (): Web3 => { return new Web3(rpc[rpcRound++ % rpc.length]); diff --git a/src/shared/modules/web3/web3.service.ts b/src/shared/modules/web3/web3.service.ts index 3610a34..0050666 100644 --- a/src/shared/modules/web3/web3.service.ts +++ b/src/shared/modules/web3/web3.service.ts @@ -1,5 +1,5 @@ import { Logger } from '@nestjs/common'; -import BigNumber from 'bignumber.js/bignumber.mjs'; +import { BigNumber } from 'bignumber.js'; import { TransactionReceipt } from 'web3-core'; import { Contract, EventData } from 'web3-eth-contract'; import pkg from 'web3-utils'; @@ -37,13 +37,13 @@ export class DefaultContract { return this.startBlock; } public async getBlockNumber() { - const safeBlock = parseInt(process.env.SAFE_BLOCK) || 0; + const safeBlock = parseInt(process.env?.SAFE_BLOCK ?? '0'); const blockNumber: number = await this.wrapper(() => this.rpcService.web3.eth.getBlockNumber()); return blockNumber - safeBlock; } - public async recover(signature, message) { + public async recover(signature: string, message: string) { const recover = this.rpcService.web3.eth.accounts.recover(message, signature); return recover; } @@ -74,7 +74,7 @@ export class DefaultContract { return this.wrapper(() => this.contract.methods[method](...param).call(), true); } - public async estimateGas(method: string, param: Array, specifySignerIndex?: number): Promise { + public async estimateGas(method: string, param: Array): Promise { const signer = this.rpcService.web3.eth.accounts.privateKeyToAccount(this.rpcService.privateKeys); const data = this.contract.methods[method](...param).encodeABI(); const gasPrice = await this.rpcService.web3.eth.getGasPrice(); @@ -96,8 +96,7 @@ export class DefaultContract { public async write( method: string, param: Array, - specifySignerIndex?: number, - ): Promise<{ success: boolean; error: Error; data: TransactionReceipt }> { + ): Promise<{ success: boolean; error: Error | null; data: TransactionReceipt | null }> { try { const signer = this.rpcService.web3.eth.accounts.privateKeyToAccount(this.rpcService.privateKeys); @@ -133,7 +132,7 @@ export class DefaultContract { public async multiWrite( writeData: any[], specifySignerIndex?: number, - ): Promise<{ success: boolean; error: Error; data: TransactionReceipt[] }> { + ): Promise<{ success: boolean; error: Error | null; data: TransactionReceipt[] | null }> { try { const signer = this.rpcService.web3.eth.accounts.privateKeyToAccount( this.rpcService.privateKeys[specifySignerIndex ?? 0], @@ -209,29 +208,24 @@ export class ETHBridgeContract extends DefaultContract { public async mintNFT(toAddress: string) { return this.write('mint', [toAddress]); } - public async unlock(tokenFromAddress, amount, txHashLock, receiveAddress, fee?, _signatures?) { + public async unlock( + tokenReceivedAddress: string, + amount: string, + txHashLock: string, + receiveAddress: string, + fee?: string, + _signatures?: string[], + ) { console.log( - '🚀 ~ ETHBridgeContract ~ unlock ~ tokenFromAddress, amount, txHashLock, receiveAddress, fee?, _signatures?:', - tokenFromAddress, + '🚀 ~ ETHBridgeContract ~ unlock ~ tokenReceivedAddress, amount, txHashLock, receiveAddress, fee?, _signatures?:', + tokenReceivedAddress, amount, txHashLock, receiveAddress, fee, _signatures, ); - return this.write('unlock', [tokenFromAddress, amount, receiveAddress, txHashLock, fee, _signatures]); - } - public async getEstimateGas(tokenFromAddress, amount, txHashLock, receiveAddress, fee?) { - const estimateGas = await this.estimateGas('unlock', [ - tokenFromAddress, - amount, - receiveAddress, - txHashLock, - Number(fee), - [], - ]); - const getGasPrice = await this.convertGasPriceToEther(estimateGas); - return getGasPrice; + return this.write('unlock', [tokenReceivedAddress, amount, receiveAddress, txHashLock, fee, _signatures]); } public async getTokenURI(tokenId: number) { return this.call('tokenURI', [tokenId]); diff --git a/src/shared/utils/bignumber.ts b/src/shared/utils/bignumber.ts index e8dc0cb..b0eaa75 100644 --- a/src/shared/utils/bignumber.ts +++ b/src/shared/utils/bignumber.ts @@ -2,17 +2,80 @@ import { BigNumber } from 'bignumber.js'; import { isNumber, isNumberString } from 'class-validator'; +import { DECIMAL_BASE } from '../../constants/blockchain.constant.js'; + +BigNumber.set({ + EXPONENTIAL_AT: 1e9, +}); + export const addDecimal = (value: string | number, decimal: number) => { if (!isNumber(value) && !isNumberString(value)) return '0'; - return BigNumber(value).multipliedBy(BigNumber(10).pow(decimal)).toFixed().toString(); + return BigNumber(value).multipliedBy(BigNumber(10).pow(decimal)).toFixed(0); +}; +export const removeSuffixDecimal = (value: string | number, decimal: number) => { + if (!isNumber(value) && !isNumberString(value)) return '0'; + return BigNumber(value).div(BigNumber(10).pow(decimal)).toFixed(); }; export const calculateTip = (amount: string, gasFee: string | number, tipPercent: number): BigNumber => { - return BigNumber(amount) - .minus(BigNumber(gasFee)) - .multipliedBy(tipPercent * 10) - .dividedBy(1000); + return BigNumber(amount).minus(BigNumber(gasFee)).multipliedBy(tipPercent).dividedBy(100); }; export const calculateFee = (amount: string, gasFee: string | number, tipPercent: number) => { const tip = calculateTip(amount, gasFee, tipPercent); return BigNumber(gasFee).plus(tip).toFixed(0, BigNumber.ROUND_UP); }; + +interface ICalculateFeeResult { + tipWithDecimalPlaces?: string; + gasFeeWithDecimalPlaces?: string; + amountReceiveNoDecimalPlace?: string; + protocolFeeNoDecimalPlace?: string; + success: boolean; + error: Error | null; +} +export const calculateUnlockFee = ({ + fromDecimal, + gasFeeWithDecimalPlaces, + inputAmountNoDecimalPlaces, + tipPercent, + toDecimal, +}: { + tipPercent: number; + gasFeeWithDecimalPlaces: string; + fromDecimal: number; + toDecimal: number; + inputAmountNoDecimalPlaces: string; +}): ICalculateFeeResult => { + try { + const amountReceiveConvert = BigNumber(inputAmountNoDecimalPlaces) + .dividedBy(BigNumber(DECIMAL_BASE).pow(fromDecimal)) + .multipliedBy(BigNumber(DECIMAL_BASE).pow(toDecimal)); + + const gasFeeMina = addDecimal(gasFeeWithDecimalPlaces, toDecimal); + + const tip = calculateTip(amountReceiveConvert.toString(), gasFeeMina, tipPercent); + + // protocol fee = tip + gas_fee + const protocolFeeNoDecimalPlace = tip.plus(gasFeeMina); + + // amount received= total amount - protocol fee + const amountReceived = BigNumber(amountReceiveConvert).minus(protocolFeeNoDecimalPlace); + + return { + tipWithDecimalPlaces: removeSuffixDecimal(tip.toString(), toDecimal), + amountReceiveNoDecimalPlace: amountReceived.toFixed(0), + gasFeeWithDecimalPlaces: gasFeeWithDecimalPlaces, + protocolFeeNoDecimalPlace: protocolFeeNoDecimalPlace.toFixed(0), + error: null, + success: true, + }; + } catch (error) { + return { + tipWithDecimalPlaces: '0', + amountReceiveNoDecimalPlace: '0', + gasFeeWithDecimalPlaces: gasFeeWithDecimalPlaces, + protocolFeeNoDecimalPlace: '0', + error, + success: false, + }; + } +}; diff --git a/src/shared/utils/queue.ts b/src/shared/utils/queue.ts index 575a812..59252c2 100644 --- a/src/shared/utils/queue.ts +++ b/src/shared/utils/queue.ts @@ -10,7 +10,6 @@ export class BullLib { redis: redisConfig, settings: { lockDuration: defaultLockTime, // lock the job for one hours. - lockRenewTime: Math.floor(defaultLockTime / 2), maxStalledCount: 0, }, }); diff --git a/src/shared/utils/upload.ts b/src/shared/utils/upload.ts index 8620130..983b6bf 100644 --- a/src/shared/utils/upload.ts +++ b/src/shared/utils/upload.ts @@ -1,6 +1,6 @@ import { HttpException, HttpStatus } from '@nestjs/common'; -export const fileFilter = (_: Request, file: any, callback) => { +export const fileFilter = (_: Request, file: any, callback: CallableFunction) => { if (!file) { throw new HttpException({ key: 'FILE_NOT_EMPTY', message: 'File Not Empty!' }, HttpStatus.BAD_REQUEST); } diff --git a/src/shared/utils/util.ts b/src/shared/utils/util.ts index da3ac6f..4d48b0d 100644 --- a/src/shared/utils/util.ts +++ b/src/shared/utils/util.ts @@ -1,42 +1,14 @@ -import * as cryptoJs from 'crypto-js'; - import { EEnvironments, EEnvKey } from '../../constants/env.constant.js'; -const { AES, enc } = cryptoJs; - export const toLower = (value: string) => value.toLowerCase(); export const compareAddress = (address: string, addressCompare: string) => toLower(address) === toLower(addressCompare); -export function isEmail(mail) { +export function isEmail(mail: string) { const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/; return emailRegex.test(mail); } -export function encode(data: string) { - const encryptedData = AES.encrypt(data, process.env.ENCODE_SECRET_KEY).toString(); - return encryptedData; -} - -export function decode(encodedData: string) { - const decryptedData = AES.decrypt(encodedData, process.env.ENCODE_SECRET_KEY).toString(enc.Utf8); - return decryptedData; -} - -export const getVariableName = (getVar: () => TResult): string => { - const m = /\(\)=>(.*)/.exec(getVar.toString().replace(/(\r\n|\n|\r|\s)/gm, '')); - - if (!m) { - throw new Error("The function does not contain a statement matching 'return variableName;'"); - } - - const fullMemberName = m[1]; - - const memberParts = fullMemberName.split('.'); - - return memberParts[memberParts.length - 1]; -}; - export const nullToZero = (value: string | number) => (value ? value.toString() : '0'); export const isDevelopmentEnvironment = () => [EEnvironments.LOCAL, EEnvironments.DEV].includes(process.env[EEnvKey.NODE_ENV] as EEnvironments); diff --git a/tsconfig.json b/tsconfig.json index 946dacf..30f749c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,8 +16,8 @@ "baseUrl": "./src", "incremental": true, "skipLibCheck": true, - "strictNullChecks": false, - "noImplicitAny": false, + "strictNullChecks": true, + "noImplicitAny": true, "strictBindCallApply": false, "forceConsistentCasingInFileNames": false, "noFallthroughCasesInSwitch": false,