From 6c9d42001ea67e49a5404a73fdb3ab67d7551efe Mon Sep 17 00:00:00 2001 From: Sotatek-MinhNguyen13 Date: Fri, 4 Oct 2024 11:21:18 +0700 Subject: [PATCH 1/3] update dockerfileuat --- DockerfileUAT | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DockerfileUAT b/DockerfileUAT index d5fe715..7ff754b 100644 --- a/DockerfileUAT +++ b/DockerfileUAT @@ -1,4 +1,4 @@ -FROM node:18-alpine As build +FROM public.ecr.aws/docker/library/node:18-alpine As build WORKDIR /app COPY package*.json ./ @@ -6,7 +6,7 @@ RUN npm i COPY . . RUN npm run build -FROM node:18-alpine +FROM public.ecr.aws/docker/library/node:18-alpine WORKDIR /app COPY --from=build /app/node_modules ./node_modules COPY --from=build /app/dist ./dist From 60845707aee25947842f782c9209018f9d707879 Mon Sep 17 00:00:00 2001 From: Sotatek-TanHoang Date: Fri, 4 Oct 2024 11:42:42 +0700 Subject: [PATCH 2/3] feat: refactor sender --- src/modules/crawler/sender.evmbridge.ts | 43 +++++++++-------- src/shared/modules/web3/web3.service.ts | 61 ++++++++++--------------- 2 files changed, 48 insertions(+), 56 deletions(-) diff --git a/src/modules/crawler/sender.evmbridge.ts b/src/modules/crawler/sender.evmbridge.ts index 2fc0dbd..4dbf8e0 100644 --- a/src/modules/crawler/sender.evmbridge.ts +++ b/src/modules/crawler/sender.evmbridge.ts @@ -45,28 +45,31 @@ export class SenderEVMBridge { return { error: null, success: false }; } - assert(dataLock.tip.toString(), 'invalid gasFee'); - assert(dataLock.gasFee.toString(), 'invalid tips'); - assert(dataLock.amountReceived, 'invalida amount to unlock'); - - const { tokenReceivedAddress, txHashLock, receiveAddress, amountReceived, protocolFee } = dataLock; - - const result = await this.ethBridgeContract.unlock( - tokenReceivedAddress, - BigNumber(amountReceived).plus(protocolFee).toString(), - txHashLock, - receiveAddress, - BigNumber(dataLock.protocolFee).toString(), - dataLock.validator.map(e => e.signature), - ); - // Update status eventLog when call function unlock - if (result.success) { + try { + assert(dataLock.tip.toString(), 'invalid gasFee'); + assert(dataLock.gasFee.toString(), 'invalid tips'); + assert(dataLock.amountReceived, 'invalida amount to unlock'); + const { tokenReceivedAddress, txHashLock, receiveAddress, amountReceived, protocolFee } = dataLock; + const result = await this.ethBridgeContract.unlock( + tokenReceivedAddress, + BigNumber(amountReceived).plus(protocolFee).toString(), + txHashLock, + receiveAddress, + BigNumber(dataLock.protocolFee).toString(), + dataLock.validator.map(e => e.signature), + ); + + assert(result.transactionHash, 'tx send failed id = ' + txId); + await this.updateLogStatusWithRetry(dataLock, EEventStatus.PROCESSING); - } else if (!!result.error) { - this.logger.error(result.error); - await this.updateLogStatusWithRetry(dataLock, EEventStatus.FAILED, result.error.message); + + return { error: null, success: true }; + } catch (error) { + this.logger.error(error); + await this.updateLogStatusWithRetry(dataLock, EEventStatus.FAILED, error.message); + throw error; } - return result; + // Update status eventLog when call function unlock } async validateUnlockEVMTransaction(txId: number): Promise<{ error: Error | null; success: boolean }> { diff --git a/src/shared/modules/web3/web3.service.ts b/src/shared/modules/web3/web3.service.ts index 0050666..c602c37 100644 --- a/src/shared/modules/web3/web3.service.ts +++ b/src/shared/modules/web3/web3.service.ts @@ -93,41 +93,30 @@ export class DefaultContract { return gasLimit; } - public async write( - method: string, - param: Array, - ): Promise<{ success: boolean; error: Error | null; data: TransactionReceipt | null }> { - try { - const signer = this.rpcService.web3.eth.accounts.privateKeyToAccount(this.rpcService.privateKeys); + public async write(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(); - const nonce = await this.rpcService.getNonce(signer.address); - - // gas estimation - const rawTx = { - nonce: nonce, - gasPrice: toHex(toBN(gasPrice)), - from: signer.address, - to: this.contractAddress, - data: data, - }; + const data = this.contract.methods[method](...param).encodeABI(); + const gasPrice = await this.rpcService.web3.eth.getGasPrice(); + const nonce = await this.rpcService.getNonce(signer.address); + + // gas estimation + const rawTx = { + nonce: nonce, + gasPrice: toHex(toBN(gasPrice)), + from: signer.address, + to: this.contractAddress, + data: data, + }; - const gasLimit = await this.rpcService.web3.eth.estimateGas(rawTx as any); + const gasLimit = await this.rpcService.web3.eth.estimateGas(rawTx as any); - const signedTx = await signer.signTransaction({ - ...rawTx, - gasLimit: toHex(toBN(gasLimit).add(toBN(10000))), - } as any); + const signedTx = await signer.signTransaction({ + ...rawTx, + gasLimit: toHex(toBN(gasLimit).add(toBN(10000))), + } as any); - return { - success: true, - error: null, - data: await this.rpcService.web3.eth.sendSignedTransaction(signedTx.rawTransaction), - }; - } catch (error: any) { - return { success: false, error, data: null }; - } + return this.rpcService.web3.eth.sendSignedTransaction(signedTx.rawTransaction); } public async multiWrite( writeData: any[], @@ -196,19 +185,19 @@ export class ETHBridgeContract extends DefaultContract { super(rpcETHService, EthBridgeAbi, address, _startBlock); } - public async getBaseURI() { + public getBaseURI() { return this.call('getBaseURI', []); } - public async latestIndex() { + public latestIndex() { return this.call('latestIndex', []); } - public async getValidatorThreshold() { + public getValidatorThreshold() { return this.call('threshold', []); } - public async mintNFT(toAddress: string) { + public mintNFT(toAddress: string) { return this.write('mint', [toAddress]); } - public async unlock( + public unlock( tokenReceivedAddress: string, amount: string, txHashLock: string, From d58b49581a974d5432f79da80e4fba332dfbb07e Mon Sep 17 00:00:00 2001 From: Tan Hoang Date: Fri, 4 Oct 2024 16:00:50 +0700 Subject: [PATCH 3/3] fix: max daily quota --- design/admin-update-common-config.puml | 42 ++++++++ design/admin-update-eth-bridge-config.puml | 0 docker-compose.dev.yaml | 2 +- src/config/config.module.ts | 1 + src/constants/blockchain.constant.ts | 2 - src/constants/env.constant.ts | 1 + .../repositories/event-log.repository.ts | 14 ++- src/modules/crawler/crawler.console.ts | 3 +- src/modules/crawler/crawler.evmbridge.ts | 97 +++++++++---------- src/modules/crawler/crawler.minabridge.ts | 70 +++++++------ src/modules/crawler/crawler.module.ts | 3 - src/modules/crawler/crawler.service.ts | 10 -- .../crawler/interfaces/job.interface.ts | 1 + src/modules/crawler/job-unlock.provider.ts | 32 +++--- src/modules/crawler/tests/evm-sender.spec.ts | 68 +++++-------- src/shared/modules/web3/web3.service.ts | 3 +- src/shared/utils/time.ts | 1 + 17 files changed, 186 insertions(+), 164 deletions(-) create mode 100644 design/admin-update-common-config.puml create mode 100644 design/admin-update-eth-bridge-config.puml delete mode 100644 src/modules/crawler/crawler.service.ts diff --git a/design/admin-update-common-config.puml b/design/admin-update-common-config.puml new file mode 100644 index 0000000..aab7881 --- /dev/null +++ b/design/admin-update-common-config.puml @@ -0,0 +1,42 @@ +@startuml 1 +title Lock From Eth to Mina +actor User +boundary fe as "Frontend" +control be as "Backend" +participant Ethereum +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 (1) User lock token from Evm + + User -> fe : select network to bridge + + activate fe + fe -> be : get list of token pairs + activate be + be --> fe : list of token pairs + deactivate be + + User -> fe: select destination wallet, amount + fe --> User: display amount, tip, fee + User -> fe: perform bridge action + fe -> Ethereum: call lock tx using user's wallet + activate Ethereum + Ethereum --> fe: tx status success + deactivate Ethereum + + fe --> User: show popup success + deactivate fe + + +end + +@enduml + diff --git a/design/admin-update-eth-bridge-config.puml b/design/admin-update-eth-bridge-config.puml new file mode 100644 index 0000000..e69de29 diff --git a/docker-compose.dev.yaml b/docker-compose.dev.yaml index ce5815a..9239055 100644 --- a/docker-compose.dev.yaml +++ b/docker-compose.dev.yaml @@ -189,7 +189,7 @@ services: REDIS_URI: redis://redis:${REDIS_PORT} USERNAME: admin PASSWORD: minabridge - QUEUES: EVM_SENDER_QUEUE,MINA_SENDER_QUEUE,EVM_VALIDATOR_1,EVM_VALIDATOR_2,EVM_VALIDATOR_2,MINA_VALIDATOR_1,MINA_VALIDATOR_2,MINA_VALIDATOR_3 + QUEUES: EVM_SENDER_QUEUE,MINA_SENDER_QUEUE,EVM_VALIDATOR_1,EVM_VALIDATOR_2,EVM_VALIDATOR_3,MINA_VALIDATOR_1,MINA_VALIDATOR_2,MINA_VALIDATOR_3 ports: - '${BULL_MONITOR_PORT}:3011' depends_on: diff --git a/src/config/config.module.ts b/src/config/config.module.ts index 5b80f7f..c3585c1 100644 --- a/src/config/config.module.ts +++ b/src/config/config.module.ts @@ -50,6 +50,7 @@ import redisConfig from './redis.config.js'; [EEnvKey.ETH_BRIDGE_DOMAIN_NAME]: Joi.string().required(), [EEnvKey.ETH_BRIDGE_DOMAIN_VERSION]: Joi.string().required(), [EEnvKey.MINA_CRAWL_SAFE_BLOCK]: Joi.number().default(MINA_CRAWL_SAFE_BLOCK), + [EEnvKey.EVM_SAFE_BLOCK]: Joi.number().default(10), // mina validator [EEnvKey.MINA_VALIDATOR_THRESHHOLD]: Joi.number().required(), // fee diff --git a/src/constants/blockchain.constant.ts b/src/constants/blockchain.constant.ts index 95e3b77..0388ada 100644 --- a/src/constants/blockchain.constant.ts +++ b/src/constants/blockchain.constant.ts @@ -16,8 +16,6 @@ export enum EEventStatus { PROCESSING = 'processing', COMPLETED = 'completed', FAILED = 'failed', - NOTOKENPAIR = 'noTokenPair', - CANNOT_PROCESS = 'cannotProcess', } export enum ETokenPairStatus { diff --git a/src/constants/env.constant.ts b/src/constants/env.constant.ts index a93dea2..652bef2 100644 --- a/src/constants/env.constant.ts +++ b/src/constants/env.constant.ts @@ -49,6 +49,7 @@ export enum EEnvKey { EVM_VALIDATOR_PRIVATE_KEY = 'EVM_VALIDATOR_PRIVATE_KEY', MINA_CRAWL_SAFE_BLOCK = 'MINA_CRAWL_SAFE_BLOCK', THIS_VALIDATOR_INDEX = 'THIS_VALIDATOR_INDEX', + EVM_SAFE_BLOCK = 'SAFE_BLOCK', } export enum EEnvironments { diff --git a/src/database/repositories/event-log.repository.ts b/src/database/repositories/event-log.repository.ts index 8fb63b0..7b749e9 100644 --- a/src/database/repositories/event-log.repository.ts +++ b/src/database/repositories/event-log.repository.ts @@ -1,3 +1,4 @@ +import { IJobUnlockPayload } from 'modules/crawler/interfaces/job.interface.js'; import { EntityRepository } from 'nestjs-typeorm-custom-repository'; import { Brackets } from 'typeorm'; @@ -43,21 +44,26 @@ export class EventLogRepository extends BaseRepository { network: ENetworkName, isSignatureFullFilled: boolean, numOfSignaturesNeeded: number, - ): Promise> { + ): Promise> { const currentUnixTimestamp = nowUnix(); const qb = this.createQueryBuilder(`${this.alias}`); - qb.select([`${this.alias}.id as "id"`, `${this.alias}.network_received as "networkReceived"`]); + qb.select([ + `${this.alias}.id as "eventLogId"`, + `${this.alias}.network_received as "network"`, + `${this.alias}.sender_address as "senderAddress"`, + ]); qb.leftJoin(`${this.alias}.validator`, 'signature'); qb.where(`${this.alias}.network_received = :network`, { network }); qb.andWhere(`${this.alias}.status IN (:...status)`, { - status: [EEventStatus.WAITING], // EEventStatus.PROCESSING add in future + status: [EEventStatus.WAITING, EEventStatus.FAILED], // EEventStatus.PROCESSING add in future }) .andWhere(`${this.alias}.retry < :retryNumber`, { retryNumber: MAX_RETRIES }) .orderBy(`${this.alias}.id`, EDirection.DESC) .groupBy(`${this.alias}.id`) - .addGroupBy(`${this.alias}.network_received`); + .addGroupBy(`${this.alias}.network_received`) + .addGroupBy(`${this.alias}.sender_address`); if (isSignatureFullFilled) { qb.andWhere(`${this.alias}.next_send_tx_job_time < :currentUnixTimestamp`, { currentUnixTimestamp }); qb.having(`COUNT(signature.id) = :numOfSignaturesNeeded`, { numOfSignaturesNeeded }); diff --git a/src/modules/crawler/crawler.console.ts b/src/modules/crawler/crawler.console.ts index e989f78..8a7f229 100644 --- a/src/modules/crawler/crawler.console.ts +++ b/src/modules/crawler/crawler.console.ts @@ -32,9 +32,10 @@ export class CrawlerConsole { description: 'Crawl ETH Bridge contract', }) async handleCrawlETHBridge() { + const safeBlock = +this.configService.get(EEnvKey.ETH_TOKEN_BRIDGE_ADDRESS); try { while (true) { - await this.blockchainEVMCrawler.handleEventCrawlBlock(); + await this.blockchainEVMCrawler.handleEventCrawlBlock(safeBlock); await sleep(15); } } catch (error) { diff --git a/src/modules/crawler/crawler.evmbridge.ts b/src/modules/crawler/crawler.evmbridge.ts index 52610ab..bb385d4 100644 --- a/src/modules/crawler/crawler.evmbridge.ts +++ b/src/modules/crawler/crawler.evmbridge.ts @@ -2,18 +2,18 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import assert from 'assert'; import { Logger } from 'log4js'; -import { DataSource, QueryRunner } from 'typeorm'; +import { DataSource, EntityManager } from 'typeorm'; 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 { 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'; +import { CommonConfig } from './entities/common-config.entity.js'; @Injectable() export class BlockchainEVMCrawler { @@ -25,59 +25,58 @@ export class BlockchainEVMCrawler { private readonly crawlContractRepository: CrawlContractRepository, private readonly loggerService: LoggerService, private readonly ethBridgeContract: ETHBridgeContract, - private readonly commonConfigRepository: CommonConfigRepository, ) { this.numberOfBlockPerJob = +this.configService.get(EEnvKey.NUMBER_OF_BLOCK_PER_JOB)!; this.logger = loggerService.getLogger('BLOCKCHAIN_EVM_CRAWLER'); } - public async handleEventCrawlBlock() { - const queryRunner = this.dataSource.createQueryRunner(); - await queryRunner.connect(); - await queryRunner.startTransaction(); + public async handleEventCrawlBlock(safeBlock: number) { + const { startBlockNumber, toBlock } = await this.getFromToBlock(safeBlock); + if (startBlockNumber > toBlock) { + this.logger.info( + `Block <${startBlockNumber}> is the newest block can be processed (on network: ${toBlock}). Wait for the next tick...`, + ); + return; + } + const events = await this.ethBridgeContract.getEvent(startBlockNumber, toBlock); try { - const { startBlockNumber, toBlock } = await this.getFromToBlock(); - if (startBlockNumber > toBlock) { - this.logger.info( - `Block <${startBlockNumber}> is the newest block can be processed (on network: ${toBlock}). Wait for the next tick...`, - ); - return; - } - const events = await this.ethBridgeContract.getEvent(startBlockNumber, toBlock); - - for (const event of events) { - switch (event.event) { - case 'Lock': - await this.handlerLockEvent(event, queryRunner); - break; - case 'Unlock': - await this.handlerUnLockEvent(event, queryRunner); - break; - default: - continue; + await this.dataSource.transaction(async (entityManager: EntityManager) => { + for (const event of events) { + switch (event.event) { + case 'Lock': + await this.handlerLockEvent(event, entityManager); + break; + case 'Unlock': + await this.handlerUnLockEvent(event, entityManager); + break; + default: + continue; + } } - } - this.logger.info(`[handleCrawlETHBridge] Crawled from ${startBlockNumber} to ${toBlock}`); - await this.updateLatestBlockCrawl(toBlock, queryRunner); - return await queryRunner.commitTransaction(); + await this.updateLatestBlockCrawl(toBlock, entityManager); + }); } catch (error) { - await queryRunner.rollbackTransaction(); + this.logger.error(error); throw error; } finally { - await queryRunner.release(); + this.logger.info(`[handleCrawlETHBridge] Crawled from ${startBlockNumber} to ${toBlock}`); } } - public async handlerLockEvent(event: EventData, queryRunner: QueryRunner) { - const inputAmount = event.returnValues.amount; - const isExist = await queryRunner.manager.findOneBy(EventLog, { txHashLock: event.transactionHash }); + public async handlerLockEvent(event: EventData, entityManager: EntityManager): Promise { + const eventLogRepo = entityManager.getRepository(EventLog); + const configRepo = entityManager.getRepository(CommonConfig); + + const isExist = await eventLogRepo.findOneBy({ txHashLock: event.transactionHash }); if (isExist) { this.logger.warn('Duplicated event', event.transactionHash); return; } + + const inputAmount = event.returnValues.amount; const fromTokenDecimal = this.configService.get(EEnvKey.DECIMAL_TOKEN_EVM), toTokenDecimal = this.configService.get(EEnvKey.DECIMAL_TOKEN_MINA); - const config = await this.commonConfigRepository.getCommonConfig(); + const config = await configRepo.findOneBy({}); assert(!!config?.tip, 'tip config undefined'); const { success, @@ -97,7 +96,7 @@ export class BlockchainEVMCrawler { this.logger.error('Calculate error', error); } const blockTimeOfBlockNumber = await this.ethBridgeContract.getBlockTimeByBlockNumber(event.blockNumber); - const eventUnlock: Partial = { + const eventUnlock = new EventLog({ senderAddress: event.returnValues.locker, amountFrom: inputAmount, tokenFromAddress: event.returnValues.token, @@ -119,23 +118,21 @@ export class BlockchainEVMCrawler { tip: tipWithDecimalPlaces, amountReceived: amountReceiveNoDecimalPlace, protocolFee: protocolFeeNoDecimalPlace, - }; + }); - await queryRunner.manager.save(EventLog, eventUnlock); - return { - success: true, - }; + await eventLogRepo.save(eventUnlock); } - public async handlerUnLockEvent(event: EventData, queryRunner: QueryRunner) { - const existLockTx = await queryRunner.manager.findOne(EventLog, { + public async handlerUnLockEvent(event: EventData, entityManager: EntityManager) { + const eventLogRepo = entityManager.getRepository(EventLog); + const existLockTx = await eventLogRepo.findOne({ where: { txHashLock: event.returnValues.hash }, }); if (!existLockTx) { return; } - await queryRunner.manager.update(EventLog, existLockTx.id, { + await eventLogRepo.update(existLockTx.id, { status: EEventStatus.COMPLETED, txHashUnlock: event.transactionHash, amountReceived: event.returnValues.amount, @@ -149,9 +146,9 @@ export class BlockchainEVMCrawler { }; } - public async updateLatestBlockCrawl(blockNumber: number, queryRunner: QueryRunner) { - await queryRunner.manager.update( - CrawlContract, + public async updateLatestBlockCrawl(blockNumber: number, entityManager: EntityManager) { + const crawlContractRepo = entityManager.getRepository(CrawlContract); + await crawlContractRepo.update( { contractAddress: this.configService.get(EEnvKey.ETH_BRIDGE_CONTRACT_ADDRESS), networkName: ENetworkName.ETH, @@ -162,9 +159,9 @@ export class BlockchainEVMCrawler { ); } - private async getFromToBlock(): Promise<{ startBlockNumber: number; toBlock: number }> { + private async getFromToBlock(safeBlock: number): Promise<{ startBlockNumber: number; toBlock: number }> { let startBlockNumber = this.ethBridgeContract.getStartBlock(); - let toBlock = await this.ethBridgeContract.getBlockNumber(); + let toBlock = await this.ethBridgeContract.getBlockNumber(safeBlock); const currentCrawledBlock = await this.crawlContractRepository.findOne({ where: { networkName: ENetworkName.ETH }, diff --git a/src/modules/crawler/crawler.minabridge.ts b/src/modules/crawler/crawler.minabridge.ts index 94937e3..b257c4d 100644 --- a/src/modules/crawler/crawler.minabridge.ts +++ b/src/modules/crawler/crawler.minabridge.ts @@ -4,17 +4,16 @@ import assert from 'assert'; import dayjs from 'dayjs'; import { Logger } from 'log4js'; import { fetchLastBlock, Field, Mina, PublicKey, UInt32 } from 'o1js'; -import { DataSource, QueryRunner } from 'typeorm'; +import { DataSource, EntityManager } 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 { CommonConfig } from './entities/common-config.entity.js'; import { Bridge } from './minaSc/Bridge.js'; interface IMinaLockTokenEventData { @@ -44,9 +43,7 @@ export class SCBridgeMinaCrawler { private readonly configService: ConfigService, private readonly dataSource: DataSource, 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({ @@ -67,37 +64,34 @@ export class SCBridgeMinaCrawler { const events = await zkapp.fetchEvents(startBlockNumber.add(1), toBlock); this.logger.info({ events }); - const queryRunner = this.dataSource.createQueryRunner(); - await queryRunner.connect(); - await queryRunner.startTransaction(); try { - this.logger.info(`[handleCrawlMinaBridge] Crawling from ${startBlockNumber} to ${toBlock}`); - for (const event of events) { - switch (event.type) { - case 'Unlock': - await this.handlerUnLockEvent(event, queryRunner); - break; - case 'Lock': - await this.handlerLockEvent(event, queryRunner); - break; - default: - continue; + await this.dataSource.transaction(async entityManager => { + this.logger.info(`[handleCrawlMinaBridge] Crawling from ${startBlockNumber} to ${toBlock}`); + for (const event of events) { + switch (event.type) { + case 'Unlock': + await this.handlerUnLockEvent(event, entityManager); + break; + case 'Lock': + await this.handlerLockEvent(event, entityManager); + break; + default: + continue; + } } - } - // udpate current latest block - await this.updateLatestBlockCrawl(Number(toBlock.toString()), queryRunner); - await queryRunner.commitTransaction(); + // udpate current latest block + await this.updateLatestBlockCrawl(Number(toBlock.toString()), entityManager); + }); } catch (error) { - await queryRunner.rollbackTransaction(); + this.logger.error(error); throw error; - } finally { - await queryRunner.release(); } } - public async handlerUnLockEvent(event: IMinaEvent, queryRunner: QueryRunner) { + public async handlerUnLockEvent(event: IMinaEvent, entityManager: EntityManager) { + const eventLogRepo = entityManager.getRepository(EventLog); const { id, tokenAddress } = event.event.data as IMinaLockTokenEventData; - const existLockTx = await queryRunner.manager.findOne(EventLog, { + const existLockTx = await eventLogRepo.findOne({ where: { id: Number(id.toString()) }, }); @@ -105,7 +99,7 @@ export class SCBridgeMinaCrawler { return; } - await queryRunner.manager.update(EventLog, existLockTx.id, { + await eventLogRepo.update(existLockTx.id, { status: EEventStatus.COMPLETED, txHashUnlock: event.event.transactionInfo.transactionHash, amountReceived: event.event.data.amount.toString(), @@ -118,19 +112,21 @@ export class SCBridgeMinaCrawler { }; } - public async handlerLockEvent(event: IMinaEvent, queryRunner: QueryRunner) { + public async handlerLockEvent(event: IMinaEvent, entityManager: EntityManager) { + const eventLogRepo = entityManager.getRepository(EventLog); + const configRepo = entityManager.getRepository(CommonConfig); 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 }); + const isExist = await eventLogRepo.findOneBy({ txHashLock }); if (isExist) { this.logger.warn('Duplicated event', txHashLock); return; } const fromTokenDecimal = this.configService.get(EEnvKey.DECIMAL_TOKEN_MINA), toTokenDecimal = this.configService.get(EEnvKey.DECIMAL_TOKEN_EVM); - const config = await this.commonConfigRepository.getCommonConfig(); + const config = await configRepo.findOneBy({}); assert(!!config?.tip, 'tip config undefined'); @@ -161,7 +157,7 @@ export class SCBridgeMinaCrawler { blockTimeLock: Number(Math.floor(dayjs().valueOf() / 1000)), event: EEventName.LOCK, returnValues: JSON.stringify(event), - status: success ? EEventStatus.WAITING : EEventStatus.CANNOT_PROCESS, + status: success ? EEventStatus.WAITING : EEventStatus.PROCESSING, retry: 0, fromTokenDecimal, toTokenDecimal, @@ -173,16 +169,16 @@ export class SCBridgeMinaCrawler { this.logger.info({ eventUnlock }); - const result = await queryRunner.manager.save(EventLog, eventUnlock); + const result = await eventLogRepo.save(eventUnlock); assert(!!result.id && !!result.networkReceived, 'Cannot add job to signatures queue.'); return { success: true, }; } - private updateLatestBlockCrawl(blockNumber: number, queryRunner: QueryRunner) { - return queryRunner.manager.update( - CrawlContract, + public async updateLatestBlockCrawl(blockNumber: number, entityManager: EntityManager) { + const crawlContractRepo = entityManager.getRepository(CrawlContract); + await crawlContractRepo.update( { contractAddress: this.configService.get(EEnvKey.MINA_BRIDGE_CONTRACT_ADDRESS), networkName: ENetworkName.MINA, diff --git a/src/modules/crawler/crawler.module.ts b/src/modules/crawler/crawler.module.ts index 49cb7cd..77799ec 100644 --- a/src/modules/crawler/crawler.module.ts +++ b/src/modules/crawler/crawler.module.ts @@ -11,7 +11,6 @@ import { BatchJobGetPriceToken } from './batch.tokenprice.js'; import { CrawlerConsole } from './crawler.console.js'; import { BlockchainEVMCrawler } from './crawler.evmbridge.js'; import { SCBridgeMinaCrawler } from './crawler.minabridge.js'; -import { CrawlerService } from './crawler.service.js'; import { JobUnlockProvider } from './job-unlock.provider.js'; import { SenderEVMBridge } from './sender.evmbridge.js'; import { SenderMinaBridge } from './sender.minabridge.js'; @@ -29,7 +28,6 @@ import { SenderMinaBridge } from './sender.minabridge.js'; ], providers: [ CrawlerConsole, - CrawlerService, BlockchainEVMCrawler, SenderEVMBridge, SCBridgeMinaCrawler, @@ -37,6 +35,5 @@ import { SenderMinaBridge } from './sender.minabridge.js'; BatchJobGetPriceToken, JobUnlockProvider, ], - exports: [CrawlerService], }) export class CrawlerModule {} diff --git a/src/modules/crawler/crawler.service.ts b/src/modules/crawler/crawler.service.ts deleted file mode 100644 index 121566c..0000000 --- a/src/modules/crawler/crawler.service.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -@Injectable() -export class CrawlerService { - constructor() {} - - async handleCrawl() { - console.log('handleCrawl'); - } -} diff --git a/src/modules/crawler/interfaces/job.interface.ts b/src/modules/crawler/interfaces/job.interface.ts index 62d5f0d..a64e496 100644 --- a/src/modules/crawler/interfaces/job.interface.ts +++ b/src/modules/crawler/interfaces/job.interface.ts @@ -18,4 +18,5 @@ export interface IUnlockToken { export interface IJobUnlockPayload { eventLogId: number; network: ENetworkName; + senderAddress: string; } diff --git a/src/modules/crawler/job-unlock.provider.ts b/src/modules/crawler/job-unlock.provider.ts index 22bf67f..ab8d86a 100644 --- a/src/modules/crawler/job-unlock.provider.ts +++ b/src/modules/crawler/job-unlock.provider.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import assert from 'assert'; import { BigNumber } from 'bignumber.js'; -import { In } from 'typeorm'; +import { FindOptionsWhere, In, LessThan } from 'typeorm'; import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js'; import { ENetworkName } from '../../constants/blockchain.constant.js'; @@ -14,7 +14,7 @@ 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 { getNextDayInUnix, getTimeInFutureInMinutes } from '../../shared/utils/time.js'; import { EventLog } from './entities/event-logs.entity.js'; import { IGenerateSignature, IJobUnlockPayload, IUnlockToken } from './interfaces/job.interface.js'; @@ -59,13 +59,13 @@ export class JobUnlockProvider { } for (const tx of totalTxs) { if (isSignatureFullFilled) { - await this.handleSendTxJobs({ eventLogId: tx.id, network: tx.networkReceived }); + await this.handleSendTxJobs(tx); } else { - await this.handleSignaturesJobs({ eventLogId: tx.id, network: tx.networkReceived }); + await this.handleSignaturesJobs(tx); } } await this.updateIntervalStatusForTxs( - totalTxs.map(e => e.id), + totalTxs.map(e => e.eventLogId), isSignatureFullFilled, ); // update interval status of tx @@ -80,12 +80,15 @@ export class JobUnlockProvider { private updateIntervalStatusForTxs(ids: number[], isSignatureFullFilled: boolean) { const payload: QueryDeepPartialEntity = {}; const nextTime = getTimeInFutureInMinutes(60 * 5).toString(); + const query: FindOptionsWhere = {}; if (isSignatureFullFilled) { payload.nextSendTxJobTime = nextTime; + query.nextSendTxJobTime = LessThan(nextTime); } else { payload.nextValidateSignatureTime = nextTime; + query.nextValidateSignatureTime = LessThan(nextTime); } - return this.eventLogRepository.update({ id: In(ids) }, payload); + return this.eventLogRepository.update({ id: In(ids), ...query }, payload); } private getNumOfValidators(network: ENetworkName) { switch (network) { @@ -141,6 +144,11 @@ export class JobUnlockProvider { private async handleSendTxJobs(data: IJobUnlockPayload) { // check if there is enough threshhold -> then create an unlock job. + if (await this.isPassDailyQuota(data.senderAddress, data.network)) { + this.logger.warn('this tx exceed daily quota, skip until next day', data.eventLogId); + await this.eventLogRepository.update(data.eventLogId, { nextSendTxJobTime: getNextDayInUnix().toString() }); + return; + } await this.queueService.addJobToQueue( this.getSenderQueueName(data.network), { @@ -154,16 +162,18 @@ export class JobUnlockProvider { ); } // TODO: fix this - private async isPassDailyQuota(address: string, fromDecimal: number): Promise { + private async isPassDailyQuota(address: string, networkReceived: ENetworkName): Promise { + const fromDecimal = this.configService.get( + networkReceived === ENetworkName.MINA ? EEnvKey.DECIMAL_TOKEN_EVM : EEnvKey.DECIMAL_TOKEN_MINA, + ); 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)) - ) { + console.log(totalamount, addDecimal(dailyQuota.dailyQuota, fromDecimal)); + + if (totalamount && BigNumber(totalamount.totalamount).isLessThan(addDecimal(dailyQuota.dailyQuota, fromDecimal))) { return false; } return true; diff --git a/src/modules/crawler/tests/evm-sender.spec.ts b/src/modules/crawler/tests/evm-sender.spec.ts index 865491e..f1fdde2 100644 --- a/src/modules/crawler/tests/evm-sender.spec.ts +++ b/src/modules/crawler/tests/evm-sender.spec.ts @@ -5,13 +5,12 @@ import { DataSource } from 'typeorm'; import { ConfigurationModule } from '../../../config/config.module.js'; import { EAsset } from '../../../constants/api.constant.js'; -import { EEventName, EEventStatus, ENetworkName, ETokenPairStatus } from '../../../constants/blockchain.constant.js'; +import { EEventName, EEventStatus, ENetworkName } from '../../../constants/blockchain.constant.js'; import { CommonConfigRepository } from '../../../database/repositories/common-configuration.repository.js'; import { CrawlContractRepository } from '../../../database/repositories/crawl-contract.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 { TokenPair } from '../../../modules/users/entities/tokenpair.entity.js'; import { LoggerService } from '../../../shared/modules/logger/logger.service.js'; import { Web3Module } from '../../../shared/modules/web3/web3.module.js'; import { ETHBridgeContract } from '../../../shared/modules/web3/web3.service.js'; @@ -20,12 +19,11 @@ import { EventLog } from '../entities/index.js'; import { MultiSignature } from '../entities/multi-signature.entity.js'; import { SenderEVMBridge } from '../sender.evmbridge.js'; +let newEthBridgeContract; let senderEVMBridge: SenderEVMBridge; let eventLogRepository: EventLogRepository; let commonConfigRepository: CommonConfigRepository; -let tokenPairRepository: TokenPairRepository; let multiSignatureRepository: MultiSignatureRepository; -let newEthBridgeContract: ETHBridgeContract; // Mock objects const mockJwtService = { // Mock methods if needed @@ -92,6 +90,7 @@ beforeEach(async () => { getLogger: jest.fn().mockReturnValue({ info: jest.fn(), log: jest.fn(), + error: jest.fn(), }), }, }, @@ -102,7 +101,6 @@ beforeEach(async () => { senderEVMBridge = module.get(SenderEVMBridge); eventLogRepository = module.get(EventLogRepository); commonConfigRepository = module.get(CommonConfigRepository); - tokenPairRepository = module.get(TokenPairRepository); multiSignatureRepository = module.get(MultiSignatureRepository); }); @@ -114,21 +112,6 @@ describe('handleValidateUnlockTxEVM', () => { asset: 'ETH', } as CommonConfig; - const tokenPair = { - id: 2, - fromChain: ENetworkName.MINA, - toChain: ENetworkName.ETH, - fromSymbol: EAsset.WETH, - toSymbol: EAsset.ETH, - fromAddress: 'B62qqki2ZnVzaNsGaTDAP6wJYCth5UAcY6tPX2TQYHdwD8D4uBgrDKC', - toAddress: '0x0000000000000000000000000000000000000000', - fromDecimal: 9, - toDecimal: 18, - fromScAddress: 'B62qoArtCz52mtxKxtGR3sPdS9yq6DucRW53nAerndwg9oEhUvJvpRy', - toScAddress: '0x83e21AccD43Bb7C23C51e68fFa345fab3983FfeC', - status: ETokenPairStatus.ENABLE, - } as TokenPair; - const data: Partial = { id: 18, deletedAt: undefined, @@ -152,37 +135,36 @@ describe('handleValidateUnlockTxEVM', () => { status: EEventStatus.WAITING, retry: 0, validator: [] as MultiSignature[], + tip: '0.001', + gasFee: '0.00001', }; - it('should handle validator signature generation', async () => { - const wallet = senderEVMBridge.getWallet(); + // it('should handle validator signature generation', async () => { + // const wallet = senderEVMBridge.getWallet(); - data.validator!.push({ - validator: wallet.address, - txId: 18, - retry: 2, - signature: - '0xc096d8abb2af534fa09b62ca3825a202172239ee0ab3d8438680faca0f0e59153fef0bdc0681162d94cad9fe77b05d4c1945be9c46cb89f9b2821d8576fb28d31b', - } as MultiSignature); - 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(true as any); + // data.validator!.push({ + // validator: wallet.address, + // txId: 18, + // retry: 2, + // signature: + // '0xc096d8abb2af534fa09b62ca3825a202172239ee0ab3d8438680faca0f0e59153fef0bdc0681162d94cad9fe77b05d4c1945be9c46cb89f9b2821d8576fb28d31b', + // } as MultiSignature); + // 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(true as any); - await senderEVMBridge.validateUnlockEVMTransaction(data.id!); + // await senderEVMBridge.validateUnlockEVMTransaction(data.id!); - expect(eventLogRepository.getValidatorPendingSignature).toHaveBeenCalled(); - expect(commonConfigRepository.getCommonConfig).toHaveBeenCalled(); - expect(tokenPairRepository.getTokenPair).toHaveBeenCalled(); - }); + // expect(eventLogRepository.getValidatorPendingSignature).toHaveBeenCalled(); + // expect(commonConfigRepository.getCommonConfig).toHaveBeenCalled(); + // expect(tokenPairRepository.getTokenPair).toHaveBeenCalled(); + // }); it('should handle Unlock EVM and send to blockchain', async () => { - const threshold = await newEthBridgeContract.getValidatorThreshold(); - expect(threshold).toBe('1'); jest.spyOn(commonConfigRepository, 'getCommonConfig').mockResolvedValue(commonConfig); - jest.spyOn(eventLogRepository, 'getEventLockWithNetwork').mockResolvedValue(data as EventLog); + jest.spyOn(eventLogRepository, 'findOne').mockResolvedValue(data as EventLog); jest.spyOn(eventLogRepository, 'updateLockEvenLog').mockResolvedValue(true as any); - jest.spyOn(tokenPairRepository, 'getTokenPair').mockResolvedValue(tokenPair); jest.spyOn(eventLogRepository, 'updateStatusAndRetryEvenLog').mockResolvedValue(true as any); await senderEVMBridge.handleUnlockEVM(data.id!); diff --git a/src/shared/modules/web3/web3.service.ts b/src/shared/modules/web3/web3.service.ts index c602c37..b51d127 100644 --- a/src/shared/modules/web3/web3.service.ts +++ b/src/shared/modules/web3/web3.service.ts @@ -36,8 +36,7 @@ export class DefaultContract { public getStartBlock() { return this.startBlock; } - public async getBlockNumber() { - const safeBlock = parseInt(process.env?.SAFE_BLOCK ?? '0'); + public async getBlockNumber(safeBlock: number) { const blockNumber: number = await this.wrapper(() => this.rpcService.web3.eth.getBlockNumber()); return blockNumber - safeBlock; diff --git a/src/shared/utils/time.ts b/src/shared/utils/time.ts index 9fcafbf..3d0e9b1 100644 --- a/src/shared/utils/time.ts +++ b/src/shared/utils/time.ts @@ -23,3 +23,4 @@ export const startOfDayUnix = (date: Date) => dayjs(date).startOf('day').valueOf export const endOfDayUnix = (date: Date) => dayjs(date).endOf('day').valueOf() / 1000; export const getTimeInFutureInMinutes = (minutes: number) => dayjs(new Date()).add(minutes, 'minutes').unix(); +export const getNextDayInUnix = () => dayjs().add(1, 'days').subtract(dayjs().hour()).unix();