From 121891e8349932ee20023c8fdb3e889976878137 Mon Sep 17 00:00:00 2001 From: Kenneth Hoang Date: Fri, 3 Jan 2025 14:42:36 +0700 Subject: [PATCH 01/26] feat: add new token --- design/add-new-token.puml | 41 +++++++++ src/constants/blockchain.constant.ts | 2 + ...15966984-add-token-fields-common-config.ts | 88 +++++++++++++++++++ src/database/seeds/common_config.seed.ts | 16 +++- src/modules/crawler/crawler.evmbridge.ts | 10 +-- src/modules/crawler/crawler.minabridge.ts | 8 +- src/modules/crawler/deploy-token.ts | 66 ++++++++++++++ .../crawler/entities/common-config.entity.ts | 47 +++++++++- src/modules/crawler/sender.evmbridge.ts | 2 - src/modules/crawler/sender.minabridge.ts | 4 - .../services/token-poa-sync.service.ts | 20 ++--- src/modules/users/admin.controller.ts | 17 +++- src/modules/users/admin.service.ts | 38 ++++++++ src/modules/users/dto/admin-request.dto.ts | 47 ++++++++++ src/modules/users/users.module.ts | 3 +- src/modules/users/users.service.ts | 54 ++++++------ src/shared/decorators/field.decorator.ts | 16 +++- src/shared/modules/web3/web3.service.ts | 28 +++++- 18 files changed, 440 insertions(+), 67 deletions(-) create mode 100644 design/add-new-token.puml create mode 100644 src/database/migrations/1735615966984-add-token-fields-common-config.ts create mode 100644 src/modules/crawler/deploy-token.ts create mode 100644 src/modules/users/admin.service.ts create mode 100644 src/modules/users/dto/admin-request.dto.ts diff --git a/design/add-new-token.puml b/design/add-new-token.puml new file mode 100644 index 0000000..a279d3e --- /dev/null +++ b/design/add-new-token.puml @@ -0,0 +1,41 @@ +@startuml +actor admin as "Admin" +boundary admin_site as "Admin Site" +queue bull_queue +boundary metamask +boundary aurowallet +control api +database db +actor user as "User" +boundary user_site as "User Site" + +group Admin create new pair on web ui + + admin -> admin_site: enter create pair page + admin -> admin_site: fill in form + admin -> admin_site: click create button + admin_site -> api: create pair in database + + api -> bull_queue: create job new pair + api --> admin_site: pair created ok + + +end +group Job deploy token + bull_queue -> job: trigger job + job -> mina: create token + job -> eth: whitelist token +end +group Crawler confirm new pair + mina -> crawler: event from mina bridge + eth -> crawler: event from eth bridge + crawler --> crawler: process event + crawler -> db: save pair status +end +group User see new pair + user -> user_site: view pair list page + user_site -> api: list of pairs + api-> user_site: pairs include new pair + user_site --> user: display pairs +end +@enduml \ No newline at end of file diff --git a/src/constants/blockchain.constant.ts b/src/constants/blockchain.constant.ts index 0388ada..200f6bf 100644 --- a/src/constants/blockchain.constant.ts +++ b/src/constants/blockchain.constant.ts @@ -21,6 +21,8 @@ export enum EEventStatus { export enum ETokenPairStatus { ENABLE = 'enable', DISABLE = 'disable', + CREATED = 'created', + DEPLOYING = 'deploying', } export enum EMinaChainEnviroment { diff --git a/src/database/migrations/1735615966984-add-token-fields-common-config.ts b/src/database/migrations/1735615966984-add-token-fields-common-config.ts new file mode 100644 index 0000000..80b22df --- /dev/null +++ b/src/database/migrations/1735615966984-add-token-fields-common-config.ts @@ -0,0 +1,88 @@ +import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm'; + +export class AddTokenFieldsCommonConfig1735615966984 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + return queryRunner.addColumns('common_configuration', [ + new TableColumn({ + name: 'from_chain', + type: 'varchar', + length: '255', + isNullable: true, + }), + new TableColumn({ + name: 'to_chain', + type: 'varchar', + length: '255', + isNullable: true, + }), + new TableColumn({ + name: 'from_symbol', + type: 'varchar', + length: '255', + isNullable: true, + }), + new TableColumn({ + name: 'to_symbol', + type: 'varchar', + length: '255', + isNullable: true, + }), + new TableColumn({ + name: 'from_address', + type: 'varchar', + length: '255', + isNullable: true, + }), + new TableColumn({ + name: 'to_address', + type: 'varchar', + length: '255', + isNullable: true, + }), + new TableColumn({ + name: 'from_decimal', + type: 'int', + isNullable: true, + }), + new TableColumn({ + name: 'to_decimal', + type: 'int', + isNullable: true, + }), + new TableColumn({ + name: 'from_sc_address', + type: 'varchar', + length: '255', + isNullable: true, + }), + new TableColumn({ + name: 'to_sc_address', + type: 'varchar', + length: '255', + isNullable: true, + }), + new TableColumn({ + name: 'status', + type: 'varchar', + length: '255', + isNullable: true, + }), + ]); + } + + public async down(queryRunner: QueryRunner): Promise { + return queryRunner.dropColumns('common_configuration', [ + 'from_chain', + 'to_chain', + 'from_symbol', + 'to_symbol', + 'from_address', + 'to_address', + 'from_decimal', + 'to_decimal', + 'from_sc_address', + 'to_sc_address', + 'status', + ]); + } +} diff --git a/src/database/seeds/common_config.seed.ts b/src/database/seeds/common_config.seed.ts index 682a888..63d15a4 100644 --- a/src/database/seeds/common_config.seed.ts +++ b/src/database/seeds/common_config.seed.ts @@ -1,3 +1,5 @@ +import { ENetworkName } from 'constants/blockchain.constant.js'; +import { EEnvKey } from 'constants/env.constant.js'; import * as dotenv from 'dotenv'; import { DataSource } from 'typeorm'; import { Seeder } from 'typeorm-extension'; @@ -12,11 +14,19 @@ export default class CommonConfigSeeder implements Seeder { await repository.delete({}); await repository.insert( new CommonConfig({ - tip: COMMOM_CONFIG_TIP, + bridgeFee: COMMOM_CONFIG_TIP, dailyQuota: COMMON__CONFIG_DAILY_QUOTA, - feeUnlockEth: '0.0001', - feeUnlockMina: '0.00001', + unlockingFee: '0.0001', + mintingFee: '0.00001', asset: EAsset.ETH, + fromAddress: '0x0000000000000000000000000000000000000000', + toAddress: process.env[EEnvKey.MINA_TOKEN_BRIDGE_ADDRESS], + fromDecimal: 18, + toDecimal: 9, + fromSymbol: 'ETH', + toSymbol: 'WETH', + fromChain: ENetworkName.ETH, + toChain: ENetworkName.MINA, }), ); } diff --git a/src/modules/crawler/crawler.evmbridge.ts b/src/modules/crawler/crawler.evmbridge.ts index 7dde7ee..7172693 100644 --- a/src/modules/crawler/crawler.evmbridge.ts +++ b/src/modules/crawler/crawler.evmbridge.ts @@ -82,8 +82,8 @@ export class BlockchainEVMCrawler { 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 configRepo.findOneBy({}); - assert(!!config?.tip, 'tip config undefined'); + const config = await configRepo.findOneBy({ fromAddress: event.returnValues.tokenAddress }); + assert(!!config?.bridgeFee, 'tip config undefined'); const { success, amountReceiveNoDecimalPlace, @@ -95,8 +95,8 @@ export class BlockchainEVMCrawler { fromDecimal: fromTokenDecimal, toDecimal: toTokenDecimal, inputAmountNoDecimalPlaces: inputAmount, - gasFeeWithDecimalPlaces: config.feeUnlockMina, - tipPercent: +config!.tip, + gasFeeWithDecimalPlaces: config.mintingFee, + tipPercent: +config!.bridgeFee, }); if (error) { this.logger.error('Calculate error', error); @@ -151,7 +151,7 @@ export class BlockchainEVMCrawler { tokenReceivedName: EAsset.ETH, }); // update total weth burned. - const currentConfig = await configRepo.findOneBy({}); + const currentConfig = await configRepo.findOneBy({ fromAddress: event.returnValues.tokenAddress }); assert(currentConfig, 'comomn config not exist'); const newTotalEthBurnt = new BigNumber(currentConfig.totalWethBurnt).plus(existLockTx.amountFrom).toString(); diff --git a/src/modules/crawler/crawler.minabridge.ts b/src/modules/crawler/crawler.minabridge.ts index 0442ca8..b56a0fc 100644 --- a/src/modules/crawler/crawler.minabridge.ts +++ b/src/modules/crawler/crawler.minabridge.ts @@ -142,9 +142,9 @@ export class SCBridgeMinaCrawler { } const fromTokenDecimal = this.configService.get(EEnvKey.DECIMAL_TOKEN_MINA), toTokenDecimal = this.configService.get(EEnvKey.DECIMAL_TOKEN_EVM); - const config = await configRepo.findOneBy({}); + const config = await configRepo.findOneBy({ fromAddress: event.event.data.tokenAddress.toBase58() }); - assert(!!config?.tip, 'tip config undefined'); + assert(!!config?.bridgeFee, 'tip config undefined'); const { tipWithDecimalPlaces, @@ -156,8 +156,8 @@ export class SCBridgeMinaCrawler { fromDecimal: fromTokenDecimal, toDecimal: toTokenDecimal, inputAmountNoDecimalPlaces: inputAmount, - gasFeeWithDecimalPlaces: config.feeUnlockEth, - tipPercent: Number(config.tip).valueOf(), + gasFeeWithDecimalPlaces: config.unlockingFee, + tipPercent: Number(config.bridgeFee).valueOf(), }); const eventUnlock: Partial = { senderAddress: JSON.parse(JSON.stringify(event.event.data.locker)), diff --git a/src/modules/crawler/deploy-token.ts b/src/modules/crawler/deploy-token.ts new file mode 100644 index 0000000..2994eed --- /dev/null +++ b/src/modules/crawler/deploy-token.ts @@ -0,0 +1,66 @@ +import { Injectable } from '@nestjs/common'; +import { FungibleToken, FungibleTokenAdmin } from 'mina-fungible-token'; +import { AccountUpdate, Bool, Mina, UInt8 } from 'o1js'; + +import { ETHBridgeContract } from '../../shared/modules/web3/web3.service.js'; +import { Bridge } from './minaSc/Bridge.js'; +import { Manager } from './minaSc/Manager.js'; +import { ValidatorManager } from './minaSc/ValidatorManager.js'; + +@Injectable() +export class TokenDeployer { + constructor(private readonly ethBridgeContract: ETHBridgeContract) {} + private async deployTokenEth(tokenAddress: string): Promise { + const res = await this.ethBridgeContract.whitelistToken(tokenAddress); + + return res.tokenAddress; + } + private async deployTokenMina() { + // const src = 'https://github.com/MinaFoundation/mina-fungible-token/blob/main/FungibleToken.ts'; + // const MINAURL = 'https://proxy.devnet.minaexplorer.com/graphql'; + // const ARCHIVEURL = 'https://api.minascan.io/archive/devnet/v1/graphql/'; + + // const network = Mina.Network({ + // mina: MINAURL, + // archive: ARCHIVEURL, + // }); + // Mina.setActiveInstance(network); + // const token = new FungibleToken(tokenAddress); + // const adminContract = new FungibleTokenAdmin(adminContractAddress); + // const bridgeContract = new Bridge(bridgeAddress); + // const managerContract = new Manager(managerAddress); + // const validatorManagerContract = new ValidatorManager(validatorManagerAddress); + + // let sentTx; + // // compile the contract to create prover keys + // // await fetchAccount({publicKey: feepayerAddress}); + // try { + // // call update() and send transaction + // console.log('Deploying...'); + // let tx = await Mina.transaction({ sender: feepayerAddress, fee }, async () => { + // AccountUpdate.fundNewAccount(feepayerAddress, 3); + // await adminContract.deploy({ adminPublicKey: feepayerAddress }); + // await token.deploy({ + // symbol: symbol, + // src: src, + // }); + // await token.initialize(adminContractAddress, UInt8.from(9), Bool(false)); + // }); + // console.log('prove transaction...'); + // await tx.prove(); + // console.log('send transaction...'); + // sentTx = await tx.sign([feepayerKey, adminContractKey, tokenKey]).send(); + // } catch (err) { + // console.log(err); + // } + // console.log('=====================txhash: ', sentTx?.hash); + // await sentTx?.wait(); + // // Save all private and public keys to a single JSON file + // const keysToSave = [ + // { name: 'token', privateKey: tokenKey, publicKey: tokenAddress }, + // { name: 'adminContract', privateKey: adminContractKey, publicKey: adminContractAddress }, + // ]; + } + + public async deployNewToken() {} +} diff --git a/src/modules/crawler/entities/common-config.entity.ts b/src/modules/crawler/entities/common-config.entity.ts index fddf9db..6db57b5 100644 --- a/src/modules/crawler/entities/common-config.entity.ts +++ b/src/modules/crawler/entities/common-config.entity.ts @@ -1,5 +1,6 @@ import { Column, Entity } from 'typeorm'; +import { ENetworkName, ETokenPairStatus } from '../../../constants/blockchain.constant.js'; import { ETableName } from '../../../constants/entity.constant.js'; import { BaseEntityIncludeTime } from '../../../core/base.entity.js'; @@ -23,19 +24,19 @@ export class CommonConfig extends BaseEntityIncludeTime { default: 0, nullable: false, }) - tip: number; + bridgeFee: number; @Column({ name: 'fee_unlock_mina', type: 'varchar', }) - feeUnlockMina: string; + mintingFee: string; @Column({ name: 'fee_unlock_eth', type: 'varchar', }) - feeUnlockEth: string; + unlockingFee: string; @Column({ name: 'asset', type: 'varchar', nullable: true }) asset: string; @@ -46,6 +47,46 @@ export class CommonConfig extends BaseEntityIncludeTime { @Column({ name: 'total_weth_burnt', type: 'varchar', default: '0' }) totalWethBurnt: string; + // pairs detail + @Column({ name: 'from_chain', type: 'varchar', enum: ENetworkName, nullable: false }) + fromChain: ENetworkName; + + @Column({ name: 'to_chain', type: 'varchar', enum: ENetworkName, nullable: false }) + toChain: ENetworkName; + + @Column({ name: 'from_symbol', type: 'varchar', nullable: false }) + fromSymbol: string; + + @Column({ name: 'to_symbol', type: 'varchar', nullable: false }) + toSymbol: string; + + @Column({ name: 'from_address', type: 'varchar', nullable: false }) + fromAddress: string; + + @Column({ name: 'to_address', type: 'varchar', nullable: false }) + toAddress: string; + + @Column({ name: 'from_decimal', type: 'int', nullable: true }) + fromDecimal: number; + + @Column({ name: 'to_decimal', type: 'int', nullable: true }) + toDecimal: number; + + @Column({ name: 'from_sc_address', type: 'varchar', nullable: true }) + fromScAddress: string; + + @Column({ name: 'to_sc_address', type: 'varchar', nullable: true }) + toScAddress: string; + + @Column({ + name: 'status', + type: 'varchar', + enum: ETokenPairStatus, + default: ETokenPairStatus.ENABLE, + nullable: false, + }) + status: ETokenPairStatus; + // end pair detail constructor(value: Partial) { super(); Object.assign(this, value); diff --git a/src/modules/crawler/sender.evmbridge.ts b/src/modules/crawler/sender.evmbridge.ts index 0912b88..2536d19 100644 --- a/src/modules/crawler/sender.evmbridge.ts +++ b/src/modules/crawler/sender.evmbridge.ts @@ -8,7 +8,6 @@ import { getEthBridgeAddress } from '../../config/common.config.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'; @@ -22,7 +21,6 @@ import { MultiSignature } from './entities/multi-signature.entity.js'; export class SenderEVMBridge { constructor( private readonly eventLogRepository: EventLogRepository, - private readonly commonConfigRepository: CommonConfigRepository, private readonly tokenPairRepository: TokenPairRepository, private readonly multiSignatureRepository: MultiSignatureRepository, private readonly ethBridgeContract: ETHBridgeContract, diff --git a/src/modules/crawler/sender.minabridge.ts b/src/modules/crawler/sender.minabridge.ts index 0eacc7f..b5fba41 100644 --- a/src/modules/crawler/sender.minabridge.ts +++ b/src/modules/crawler/sender.minabridge.ts @@ -7,10 +7,8 @@ import { AccountUpdate, Bool, fetchAccount, Mina, PrivateKey, PublicKey, Signatu 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 { canTxRetry } from '../../shared/utils/unlock.js'; import { MultiSignature } from './entities/multi-signature.entity.js'; @@ -27,8 +25,6 @@ export class SenderMinaBridge { constructor( private readonly configService: ConfigService, private readonly eventLogRepository: EventLogRepository, - private readonly commonConfigRepository: CommonConfigRepository, - private readonly tokenPairRepository: TokenPairRepository, private readonly multiSignatureRepository: MultiSignatureRepository, private readonly loggerService: LoggerService, ) { diff --git a/src/modules/crawler/services/token-poa-sync.service.ts b/src/modules/crawler/services/token-poa-sync.service.ts index 5b29069..ce4ab50 100644 --- a/src/modules/crawler/services/token-poa-sync.service.ts +++ b/src/modules/crawler/services/token-poa-sync.service.ts @@ -1,7 +1,5 @@ import { Injectable } from '@nestjs/common'; -import assert from 'assert'; -import { ENetworkName } from '../../../constants/blockchain.constant.js'; import { CommonConfigRepository } from '../../../database/repositories/common-configuration.repository.js'; import { EventLogRepository } from '../../../database/repositories/event-log.repository.js'; @@ -12,14 +10,14 @@ export class POASync { private readonly eventLogRepo: EventLogRepository, ) {} public async handleSyncPOA() { - const currentConfig = await this.commonConfigRepo.findOneBy({}); - assert(currentConfig, 'please seed common config'); - - const mina = await this.eventLogRepo.getTotalAmoutFromNetworkReceived(ENetworkName.MINA); - const eth = await this.eventLogRepo.getTotalAmoutFromNetworkReceived(ENetworkName.ETH); - await this.commonConfigRepo.update(currentConfig.id, { - totalWethBurnt: eth.amountFromTotal, - totalWethMinted: mina.amountReceivedTotal, - }); + // TODO: update this to adapt adding new Token. + // const currentConfig = await this.commonConfigRepo.findOneBy({}); + // assert(currentConfig, 'please seed common config'); + // const mina = await this.eventLogRepo.getTotalAmoutFromNetworkReceived(ENetworkName.MINA); + // const eth = await this.eventLogRepo.getTotalAmoutFromNetworkReceived(ENetworkName.ETH); + // await this.commonConfigRepo.update(currentConfig.id, { + // totalWethBurnt: eth.amountFromTotal, + // totalWethMinted: mina.amountReceivedTotal, + // }); } } diff --git a/src/modules/users/admin.controller.ts b/src/modules/users/admin.controller.ts index b1bb1ac..12cafab 100644 --- a/src/modules/users/admin.controller.ts +++ b/src/modules/users/admin.controller.ts @@ -1,8 +1,11 @@ -import { Body, Controller, Get, Param, Put, Query, UseGuards } from '@nestjs/common'; +import { Body, Controller, Get, Param, Post, Put, Query, UseGuards } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; +import { GuardPublic } from '../../guards/guard.decorator.js'; import { AuthAdminGuard } from '../../shared/decorators/http.decorator.js'; +import { AdminService } from './admin.service.js'; +import { CreateTokenReqDto } from './dto/admin-request.dto.js'; import { UpdateCommonConfigBodyDto } from './dto/common-config-request.dto.js'; import { GetCommonConfigResponseDto } from './dto/common-config-response.dto.js'; import { GetHistoryDto, GetHistoryOfUserResponseDto } from './dto/history-response.dto.js'; @@ -11,7 +14,10 @@ import { UsersService } from './users.service.js'; @ApiTags('Admins') @Controller('admin') export class AdminController { - constructor(private readonly userService: UsersService) {} + constructor( + private readonly userService: UsersService, + private readonly adminService: AdminService, + ) {} @Get('history') @AuthAdminGuard() @@ -35,4 +41,11 @@ export class AdminController { updateCommonConfig(@Param('id') id: number, @Body() updateConfig: UpdateCommonConfigBodyDto) { return this.userService.updateCommonConfig(id, updateConfig); } + @Post('new-token') + @GuardPublic() + // @AuthAdminGuard() + // @UseGuards(AuthGuard('jwt')) + addNewToken(@Body() payload: CreateTokenReqDto) { + return this.adminService.createNewToken(payload); + } } diff --git a/src/modules/users/admin.service.ts b/src/modules/users/admin.service.ts new file mode 100644 index 0000000..b16b4e5 --- /dev/null +++ b/src/modules/users/admin.service.ts @@ -0,0 +1,38 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { DataSource, EntityManager } from 'typeorm'; + +import { ENetworkName, ETokenPairStatus } from '../../constants/blockchain.constant.js'; +import { EEnvKey } from '../../constants/env.constant.js'; +import { CommonConfig } from '../crawler/entities/common-config.entity.js'; +import { CreateTokenReqDto } from './dto/admin-request.dto.js'; + +@Injectable() +export class AdminService { + constructor( + private readonly dataSource: DataSource, + private readonly configService: ConfigService, + ) {} + createNewToken(payload: CreateTokenReqDto) { + // get erc20 metadata: decimal... + return this.dataSource.transaction(async (e: EntityManager) => { + const commonConfigRepo = e.getRepository(CommonConfig); + // move create pair logic to helpers + const newCommonConfig = commonConfigRepo.create(); + newCommonConfig.asset = payload.assetName; + newCommonConfig.fromAddress = payload.assetAddress; + newCommonConfig.dailyQuota = +payload.dailyQuota; + newCommonConfig.fromChain = ENetworkName.ETH; + newCommonConfig.toChain = ENetworkName.MINA; + newCommonConfig.fromDecimal = 18; // get from network + newCommonConfig.toDecimal = 9; + newCommonConfig.fromScAddress = this.configService.get(EEnvKey.ETH_BRIDGE_CONTRACT_ADDRESS)!; + newCommonConfig.toScAddress = this.configService.get(EEnvKey.MINA_BRIDGE_CONTRACT_ADDRESS)!; + newCommonConfig.bridgeFee = payload.bridgeFee; + newCommonConfig.mintingFee = payload.mintingFee; + newCommonConfig.unlockingFee = payload.unlockingFee; + newCommonConfig.status = ETokenPairStatus.CREATED; + return newCommonConfig.save(); + }); + } +} diff --git a/src/modules/users/dto/admin-request.dto.ts b/src/modules/users/dto/admin-request.dto.ts new file mode 100644 index 0000000..0f5a6d8 --- /dev/null +++ b/src/modules/users/dto/admin-request.dto.ts @@ -0,0 +1,47 @@ +import { NumberField, StringField } from '../../../shared/decorators/field.decorator.js'; + +export class CreateTokenReqDto { + @StringField({ + isEthereumAddress: true, // may support other chains than ETH in future. + }) + assetAddress: string; + + @StringField({ + default: 'ETH', + }) + assetName: string; + + @StringField({ + minimum: 0, + number: true, + }) + minAmountToBridge: string; + + @StringField({ + minimum: 0, + number: true, + }) + maxAmountToBridge: string; + + @NumberField({ + minimum: 0, + }) + dailyQuota: number; + + @NumberField({ + minimum: 0, + }) + bridgeFee: number; + + @StringField({ + minimum: 0, + number: true, + }) + unlockingFee: string; + + @StringField({ + minimum: 0, + number: true, + }) + mintingFee: string; +} diff --git a/src/modules/users/users.module.ts b/src/modules/users/users.module.ts index 45b521b..3af3c02 100644 --- a/src/modules/users/users.module.ts +++ b/src/modules/users/users.module.ts @@ -7,6 +7,7 @@ import { TokenPairRepository } from '../../database/repositories/token-pair.repo import { TokenPriceRepository } from '../../database/repositories/token-price.repository.js'; import { UserRepository } from '../../database/repositories/user.repository.js'; import { AdminController } from './admin.controller.js'; +import { AdminService } from './admin.service.js'; import { UsersController } from './users.controller.js'; import { UsersService } from './users.service.js'; @@ -21,7 +22,7 @@ import { UsersService } from './users.service.js'; ]), ], controllers: [UsersController, AdminController], - providers: [UsersService], + providers: [UsersService, AdminService], exports: [UsersService], }) export class UsersModule {} diff --git a/src/modules/users/users.service.ts b/src/modules/users/users.service.ts index f1d4fb0..ef10b70 100644 --- a/src/modules/users/users.service.ts +++ b/src/modules/users/users.service.ts @@ -10,13 +10,11 @@ import { EError } from '../../constants/error.constant.js'; import { toPageDto } from '../../core/paginate-typeorm.js'; import { CommonConfigRepository } from '../../database/repositories/common-configuration.repository.js'; import { EventLogRepository } from '../../database/repositories/event-log.repository.js'; -import { TokenPairRepository } from '../../database/repositories/token-pair.repository.js'; import { TokenPriceRepository } from '../../database/repositories/token-price.repository.js'; import { UserRepository } from '../../database/repositories/user.repository.js'; -import { httpBadRequest, httpNotFound } from '../../shared/exceptions/http-exeption.js'; +import { httpBadRequest } from '../../shared/exceptions/http-exeption.js'; import { LoggerService } from '../../shared/modules/logger/logger.service.js'; import { RedisClientService } from '../../shared/modules/redis/redis-client.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'; @@ -31,7 +29,6 @@ export class UsersService { private readonly configService: ConfigService, private readonly loggerService: LoggerService, private readonly tokenPriceRepository: TokenPriceRepository, - private readonly tokenPairRepostitory: TokenPairRepository, private readonly redisClientService: RedisClientService, ) {} private readonly logger = this.loggerService.getLogger('USER_SERVICE'); @@ -74,30 +71,31 @@ export class UsersService { } async getListTokenPair() { - return this.tokenPairRepostitory.find(); + return this.commonConfigRepository.find(); } - async getProtocolFee({ pairId }: GetProtocolFeeBodyDto) { - let gasFee, decimal; - const [tokenPair, config] = await Promise.all([ - this.tokenPairRepostitory.findOneBy({ - id: pairId, - }), - this.commonConfigRepository.getCommonConfig(), - ]); - if (!tokenPair) { - httpNotFound(EError.RESOURCE_NOT_FOUND); - } - assert(config, 'system common config not found!'); - if (tokenPair!.toChain == ENetworkName.MINA) { - decimal = this.configService.get(EEnvKey.DECIMAL_TOKEN_MINA); - gasFee = addDecimal(config.feeUnlockMina, decimal); - } else { - decimal = this.configService.get(EEnvKey.DECIMAL_TOKEN_EVM); - gasFee = addDecimal(config.feeUnlockEth, decimal); - } - - return { gasFee, tipRate: config.tip, decimal }; + async getProtocolFee(dto: GetProtocolFeeBodyDto) { + // let gasFee, decimal; + // const [tokenPair, config] = await Promise.all([ + // this.commonConfigRepository.findOneBy({ + // id: pairId, + // }), + // this.commonConfigRepository.getCommonConfig(), + // ]); + // if (!tokenPair) { + // httpNotFound(EError.RESOURCE_NOT_FOUND); + // } + // assert(config, 'system common config not found!'); + // if (tokenPair!.toChain == ENetworkName.MINA) { + // decimal = this.configService.get(EEnvKey.DECIMAL_TOKEN_MINA); + // gasFee = addDecimal(config.mintingFee, decimal); + // } else { + // decimal = this.configService.get(EEnvKey.DECIMAL_TOKEN_EVM); + // gasFee = addDecimal(config.unlockingFee, decimal); + // } + + // return { gasFee, tipRate: config.tip, decimal }; + return 'use fees returned from pair detail'; } async getTokensPrices(): Promise { const result = { @@ -133,9 +131,9 @@ export class UsersService { totalWethInCirculation, }; } - calcWaitingTime(receivedNetwork: ENetworkName, currentPendingTx: number): number { + private calcWaitingTime(receivedNetwork: ENetworkName, currentPendingTx: number): number { const receivedNetworkEstWaiting = { - [ENetworkName.MINA]: 10 * 60 * (1 + currentPendingTx), + [ENetworkName.MINA]: 10 * 60 * (1 + +currentPendingTx), [ENetworkName.ETH]: 10 * (1 + currentPendingTx), }; // total waiting tx * time_process_each + crawler delays from both lock and unlock diff --git a/src/shared/decorators/field.decorator.ts b/src/shared/decorators/field.decorator.ts index 98a4271..cb5b05b 100644 --- a/src/shared/decorators/field.decorator.ts +++ b/src/shared/decorators/field.decorator.ts @@ -7,6 +7,7 @@ import { IsDecimal, IsEmail, IsEnum, + IsEthereumAddress, IsInt, IsNotEmpty, isNumber, @@ -95,7 +96,16 @@ export function NumberField(options: Omit & INumberF export function StringField(options: Omit & IStringFieldOptions = {}): PropertyDecorator { const decorators = [Trim(), ...initSharedDecorator(options, String)]; - const { minLength, maxLength, toLowerCase, toUpperCase, number, isEmail, isArray = false } = options; + const { + minLength, + maxLength, + toLowerCase, + toUpperCase, + number, + isEmail, + isEthereumAddress, + isArray = false, + } = options; if (minLength) { decorators.push(MinLength(minLength)); @@ -108,7 +118,9 @@ export function StringField(options: Omit & IStringF if (toLowerCase) { decorators.push(ToLowerCase()); } - + if (isEthereumAddress) { + decorators.push(IsEthereumAddress()); + } if (toUpperCase) { decorators.push(ToUpperCase()); } diff --git a/src/shared/modules/web3/web3.service.ts b/src/shared/modules/web3/web3.service.ts index b51d127..97f936e 100644 --- a/src/shared/modules/web3/web3.service.ts +++ b/src/shared/modules/web3/web3.service.ts @@ -11,12 +11,12 @@ import { IRpcService } from './web3.module.js'; const { toBN, toHex } = pkg; export class DefaultContract { private readonly logger = new Logger('CONTRACT'); - private contract: Contract; + protected contract: Contract; private readonly contractAddress: string; private readonly abi: any; private readonly startBlock: number; constructor( - private rpcService: IRpcService, + protected rpcService: IRpcService, _abi: any, _contractAddress: any, _startBlock: number, @@ -177,6 +177,17 @@ export class DefaultContract { this.logger.error('Error getting gas price:', error); } } + public async pollingTxStatus(txHash: string) { + const maxTries = 20; + for (let i = 0; i < maxTries; i++) { + const hash = await this.rpcService.web3.eth.getTransactionReceipt(txHash); + if (hash) { + return; + } + await sleep(5); + } + throw new Error(`polling for tx ${txHash} exceed max tries.`); + } } export class ETHBridgeContract extends DefaultContract { @@ -193,6 +204,17 @@ export class ETHBridgeContract extends DefaultContract { public getValidatorThreshold() { return this.call('threshold', []); } + public async whitelistToken(tokenAddress: string): Promise<{ isWhitelisted: boolean; tokenAddress: string }> { + const isWhitelisted = await this.call('check', [tokenAddress]); + if (isWhitelisted) { + return { isWhitelisted: true, tokenAddress }; + } + const res = await this.write('whitelistToken', [tokenAddress]); + // pooling tx hash + await this.pollingTxStatus(res.transactionHash); + + return { isWhitelisted: true, tokenAddress }; + } public mintNFT(toAddress: string) { return this.write('mint', [toAddress]); } @@ -219,3 +241,5 @@ export class ETHBridgeContract extends DefaultContract { return this.call('tokenURI', [tokenId]); } } + +export class Erc20Contract extends DefaultContract {} From 22e1bc68317e2e5e23b6e91ce2a3c8b3862a21fc Mon Sep 17 00:00:00 2001 From: Kenneth Hoang Date: Fri, 3 Jan 2025 14:48:49 +0700 Subject: [PATCH 02/26] feat: update git workflows secrets --- .github/workflows/auto-deploy-develop.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/auto-deploy-develop.yml b/.github/workflows/auto-deploy-develop.yml index 34325dd..6d6a4e1 100644 --- a/.github/workflows/auto-deploy-develop.yml +++ b/.github/workflows/auto-deploy-develop.yml @@ -13,6 +13,6 @@ jobs: with: node-version: v18.20.4 - run: | - echo "${{ vars.MINA_BRIDGE_BE_DEV }}" >> .env + echo "${{ secrets.MINA_BRIDGE_BE_DEV }}" >> .env docker build . -t mina-bridge:1.0.0 - docker compose -f docker-compose.dev.yaml up -d --remove-orphans + docker-compose -f docker-compose.dev.yaml up -d --remove-orphans From 2c3a5ef3dba2dbc1ce0390a984a2cda81aafebf7 Mon Sep 17 00:00:00 2001 From: Kenneth Hoang Date: Fri, 3 Jan 2025 15:08:01 +0700 Subject: [PATCH 03/26] feat: update apis for tokens --- src/modules/users/admin.controller.ts | 8 ++++---- src/modules/users/admin.service.ts | 5 +++++ src/modules/users/dto/common-config-request.dto.ts | 6 +++--- src/modules/users/users.service.ts | 14 +++++++------- 4 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/modules/users/admin.controller.ts b/src/modules/users/admin.controller.ts index 12cafab..d853fc5 100644 --- a/src/modules/users/admin.controller.ts +++ b/src/modules/users/admin.controller.ts @@ -27,19 +27,19 @@ export class AdminController { return this.userService.getHistories(query); } - @Get('common-config') + @Get('tokens') @AuthAdminGuard() @UseGuards(AuthGuard('jwt')) @ApiOkResponse({ type: GetCommonConfigResponseDto }) getCommonConfig() { - return this.userService.getCommonConfig(); + return this.adminService.getListToken(); } - @Put('update-common-config/:id') + @Put('token/:id') @AuthAdminGuard() @UseGuards(AuthGuard('jwt')) updateCommonConfig(@Param('id') id: number, @Body() updateConfig: UpdateCommonConfigBodyDto) { - return this.userService.updateCommonConfig(id, updateConfig); + return this.userService.updateTokenConfig(id, updateConfig); } @Post('new-token') @GuardPublic() diff --git a/src/modules/users/admin.service.ts b/src/modules/users/admin.service.ts index b16b4e5..cbbe495 100644 --- a/src/modules/users/admin.service.ts +++ b/src/modules/users/admin.service.ts @@ -4,6 +4,7 @@ import { DataSource, EntityManager } from 'typeorm'; import { ENetworkName, ETokenPairStatus } from '../../constants/blockchain.constant.js'; import { EEnvKey } from '../../constants/env.constant.js'; +import { CommonConfigRepository } from '../../database/repositories/common-configuration.repository.js'; import { CommonConfig } from '../crawler/entities/common-config.entity.js'; import { CreateTokenReqDto } from './dto/admin-request.dto.js'; @@ -12,6 +13,7 @@ export class AdminService { constructor( private readonly dataSource: DataSource, private readonly configService: ConfigService, + private readonly commonConfigRepo: CommonConfigRepository, ) {} createNewToken(payload: CreateTokenReqDto) { // get erc20 metadata: decimal... @@ -35,4 +37,7 @@ export class AdminService { return newCommonConfig.save(); }); } + getListToken() { + return this.commonConfigRepo.find(); + } } diff --git a/src/modules/users/dto/common-config-request.dto.ts b/src/modules/users/dto/common-config-request.dto.ts index 3580427..cf19c8c 100644 --- a/src/modules/users/dto/common-config-request.dto.ts +++ b/src/modules/users/dto/common-config-request.dto.ts @@ -6,7 +6,7 @@ export class UpdateCommonConfigBodyDto { example: 50, required: false, }) - tip: number; + bridgeFee: number; @NumberField({ example: 500, @@ -21,7 +21,7 @@ export class UpdateCommonConfigBodyDto { }, required: false, }) - feeUnlockMina: string; + mintingFee: string; @StringField({ example: 500, @@ -30,5 +30,5 @@ export class UpdateCommonConfigBodyDto { }, required: false, }) - feeUnlockEth: string; + unlockingFee: string; } diff --git a/src/modules/users/users.service.ts b/src/modules/users/users.service.ts index ef10b70..c8c4478 100644 --- a/src/modules/users/users.service.ts +++ b/src/modules/users/users.service.ts @@ -4,7 +4,7 @@ import assert from 'assert'; import { BigNumber } from 'bignumber.js'; import { EAsset } from '../../constants/api.constant.js'; -import { DECIMAL_BASE, ENetworkName } from '../../constants/blockchain.constant.js'; +import { DECIMAL_BASE, ENetworkName, ETokenPairStatus } from '../../constants/blockchain.constant.js'; import { EEnvKey } from '../../constants/env.constant.js'; import { EError } from '../../constants/error.constant.js'; import { toPageDto } from '../../core/paginate-typeorm.js'; @@ -52,11 +52,7 @@ export class UsersService { return toPageDto(data, options, count); } - async getCommonConfig() { - return this.commonConfigRepository.getCommonConfig(); - } - - async updateCommonConfig(id: number, updateConfig: UpdateCommonConfigBodyDto) { + async updateTokenConfig(id: number, updateConfig: UpdateCommonConfigBodyDto) { await this.commonConfigRepository.updateCommonConfig(id, updateConfig); return updateConfig; } @@ -71,7 +67,11 @@ export class UsersService { } async getListTokenPair() { - return this.commonConfigRepository.find(); + return this.commonConfigRepository.find({ + where: { + status: ETokenPairStatus.ENABLE, + }, + }); } async getProtocolFee(dto: GetProtocolFeeBodyDto) { From 79befb4298536375e397e381e12b0637af8f0363 Mon Sep 17 00:00:00 2001 From: Kenneth Hoang Date: Mon, 6 Jan 2025 16:50:28 +0700 Subject: [PATCH 04/26] feat: get tokens pagination --- src/config/config.module.ts | 2 +- .../common-configuration.repository.ts | 12 ++ src/database/seeds/common_config.seed.ts | 4 +- src/database/seeds/token_pairs.seed.ts | 48 ------- src/modules/crawler/crawler.console.ts | 12 +- src/modules/crawler/crawler.module.ts | 2 + src/modules/crawler/deploy-token.ts | 117 +++++++++++------- src/modules/crawler/sender.minabridge.ts | 26 ++-- src/modules/users/admin.controller.ts | 15 +-- src/modules/users/admin.service.ts | 7 +- src/modules/users/dto/user-request.dto.ts | 6 + src/shared/decorators/field.decorator.ts | 9 +- 12 files changed, 140 insertions(+), 120 deletions(-) diff --git a/src/config/config.module.ts b/src/config/config.module.ts index f891936..d0180d6 100644 --- a/src/config/config.module.ts +++ b/src/config/config.module.ts @@ -52,7 +52,7 @@ const getEnvFile = () => { [EEnvKey.MINA_BRIDGE_CONTRACT_ADDRESS]: Joi.string().required(), [EEnvKey.ETH_BRIDGE_CONTRACT_ADDRESS]: Joi.string().required(), [EEnvKey.ETH_TOKEN_BRIDGE_ADDRESS]: Joi.string().required(), - [EEnvKey.MINA_TOKEN_BRIDGE_ADDRESS]: Joi.string().required(), + // [EEnvKey.MINA_TOKEN_BRIDGE_ADDRESS]: Joi.string().required(), [EEnvKey.ADMIN_MESSAGE_FOR_SIGN]: Joi.string().required(), [EEnvKey.MINA_BRIDGE_SC_PRIVATE_KEY]: Joi.string().required(), [EEnvKey.ETH_BRIDGE_DOMAIN_NAME]: Joi.string().required(), diff --git a/src/database/repositories/common-configuration.repository.ts b/src/database/repositories/common-configuration.repository.ts index ee6e5bb..3cec428 100644 --- a/src/database/repositories/common-configuration.repository.ts +++ b/src/database/repositories/common-configuration.repository.ts @@ -1,9 +1,12 @@ +import { isArray } from 'class-validator'; import { UpdateCommonConfigBodyDto } from 'modules/users/dto/common-config-request.dto.js'; import { EntityRepository } from 'nestjs-typeorm-custom-repository'; +import { In } from 'typeorm'; import { ETableName } from '../../constants/entity.constant.js'; import { BaseRepository } from '../../core/base-repository.js'; import { CommonConfig } from '../../modules/crawler/entities/common-config.entity.js'; +import { GetTokensReqDto } from '../../modules/users/dto/user-request.dto.js'; @EntityRepository(CommonConfig) export class CommonConfigRepository extends BaseRepository { @@ -13,6 +16,15 @@ export class CommonConfigRepository extends BaseRepository { return this.createQueryBuilder(`${this.alias}`).select().getOne(); } + public getManyAndPagination(dto: GetTokensReqDto) { + const qb = this.createQb(); + qb.select(); + if (isArray(dto.statuses)) { + qb.andWhere({ status: In(dto.statuses) }); + } + this.queryBuilderAddPagination(qb, dto); + return qb.getManyAndCount(); + } public updateCommonConfig(id: number, updateConfig: UpdateCommonConfigBodyDto) { return this.createQueryBuilder(`${this.alias}`) .update(CommonConfig) diff --git a/src/database/seeds/common_config.seed.ts b/src/database/seeds/common_config.seed.ts index 63d15a4..205e1c5 100644 --- a/src/database/seeds/common_config.seed.ts +++ b/src/database/seeds/common_config.seed.ts @@ -1,10 +1,10 @@ -import { ENetworkName } from 'constants/blockchain.constant.js'; -import { EEnvKey } from 'constants/env.constant.js'; import * as dotenv from 'dotenv'; import { DataSource } from 'typeorm'; import { Seeder } from 'typeorm-extension'; import { COMMOM_CONFIG_TIP, COMMON__CONFIG_DAILY_QUOTA, EAsset } from '../../constants/api.constant.js'; +import { ENetworkName } from '../../constants/blockchain.constant.js'; +import { EEnvKey } from '../../constants/env.constant.js'; import { CommonConfig } from '../../modules/crawler/entities/common-config.entity.js'; export default class CommonConfigSeeder implements Seeder { diff --git a/src/database/seeds/token_pairs.seed.ts b/src/database/seeds/token_pairs.seed.ts index f778a72..8a05688 100644 --- a/src/database/seeds/token_pairs.seed.ts +++ b/src/database/seeds/token_pairs.seed.ts @@ -2,56 +2,8 @@ import * as dotenv from 'dotenv'; import { DataSource } from 'typeorm'; import { Seeder } from 'typeorm-extension'; -import { EAsset } from '../../constants/api.constant.js'; -import { ENetworkName, ETokenPairStatus } from '../../constants/blockchain.constant.js'; -import { TokenPair } from '../../modules/users/entities/tokenpair.entity.js'; - export default class TokenPairsSeeder implements Seeder { public async run(dataSource: DataSource): Promise { dotenv.config(); - const repository = dataSource.getRepository(TokenPair); - const listToken = [ - { - fromChain: ENetworkName.ETH, - toChain: ENetworkName.MINA, - fromSymbol: EAsset.ETH, - toSymbol: EAsset.WETH, - fromAddress: process.env.ETH_TOKEN_BRIDGE_ADDRESS, - toAddress: process.env.MINA_TOKEN_BRIDGE_ADDRESS, - fromDecimal: 18, - toDecimal: 9, - fromScAddress: process.env.ETH_BRIDGE_CONTRACT_ADDRESS, - toScAddress: process.env.MINA_BRIDGE_CONTRACT_ADDRESS, - }, - { - fromChain: ENetworkName.MINA, - toChain: ENetworkName.ETH, - fromSymbol: EAsset.WETH, - toSymbol: EAsset.ETH, - fromAddress: process.env.MINA_TOKEN_BRIDGE_ADDRESS, - toAddress: process.env.ETH_TOKEN_BRIDGE_ADDRESS, - fromDecimal: 9, - toDecimal: 18, - fromScAddress: process.env.MINA_BRIDGE_CONTRACT_ADDRESS, - toScAddress: process.env.ETH_BRIDGE_CONTRACT_ADDRESS, - }, - ]; - await repository.delete({}); - for (const token of listToken) { - const newToken = new TokenPair({ - fromChain: token.fromChain, - toChain: token.toChain, - fromSymbol: token.fromSymbol, - toSymbol: token.toSymbol, - fromAddress: token.fromAddress, - toAddress: token.toAddress, - fromDecimal: token.fromDecimal, - toDecimal: token.toDecimal, - fromScAddress: token.fromScAddress, - toScAddress: token.toScAddress, - status: ETokenPairStatus.ENABLE, - }); - await repository.insert(newToken); - } } } diff --git a/src/modules/crawler/crawler.console.ts b/src/modules/crawler/crawler.console.ts index 952c355..17b9125 100644 --- a/src/modules/crawler/crawler.console.ts +++ b/src/modules/crawler/crawler.console.ts @@ -8,6 +8,7 @@ import { QueueService } from '../../shared/modules/queue/queue.service.js'; import { sleep } from '../../shared/utils/promise.js'; import { BlockchainEVMCrawler } from './crawler.evmbridge.js'; import { SCBridgeMinaCrawler } from './crawler.minabridge.js'; +import { TokenDeployer } from './deploy-token.js'; import { IGenerateSignature, IUnlockToken } from './interfaces/job.interface.js'; import { JobUnlockProvider } from './job-unlock.provider.js'; import { SenderEVMBridge } from './sender.evmbridge.js'; @@ -26,6 +27,7 @@ export class CrawlerConsole { private readonly queueService: QueueService, private readonly unlockProviderService: JobUnlockProvider, private readonly poaSyncer: POASync, + private readonly tokenDeployerService: TokenDeployer, ) {} private readonly logger = this.loggerService.getLogger('CRAWLER_CONSOLE'); @@ -34,7 +36,7 @@ export class CrawlerConsole { description: 'Crawl ETH Bridge contract', }) async handleCrawlETHBridge() { - const safeBlock = +this.configService.get(EEnvKey.ETH_TOKEN_BRIDGE_ADDRESS); + const safeBlock = +this.configService.get(EEnvKey.EVM_SAFE_BLOCK); while (true) { try { await this.blockchainEVMCrawler.handleEventCrawlBlock(safeBlock); @@ -136,4 +138,12 @@ export class CrawlerConsole { this.logger.info('SYNC_POA: started'); await this.poaSyncer.handleSyncPOA(); } + @Command({ + command: 'deploy-token-job', + description: 'handle deploy new token.', + }) + async handleJobDeployToken() { + this.logger.info('DEPLOY_TOKEN: started'); + await this.tokenDeployerService.deployNewToken(); + } } diff --git a/src/modules/crawler/crawler.module.ts b/src/modules/crawler/crawler.module.ts index 7fcb727..ad7654a 100644 --- a/src/modules/crawler/crawler.module.ts +++ b/src/modules/crawler/crawler.module.ts @@ -11,6 +11,7 @@ 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 { TokenDeployer } from './deploy-token.js'; import { JobUnlockProvider } from './job-unlock.provider.js'; import { SenderEVMBridge } from './sender.evmbridge.js'; import { SenderMinaBridge } from './sender.minabridge.js'; @@ -36,6 +37,7 @@ import { POASync } from './services/token-poa-sync.service.js'; BatchJobGetPriceToken, JobUnlockProvider, POASync, + TokenDeployer, ], }) export class CrawlerModule {} diff --git a/src/modules/crawler/deploy-token.ts b/src/modules/crawler/deploy-token.ts index 2994eed..3be5fd3 100644 --- a/src/modules/crawler/deploy-token.ts +++ b/src/modules/crawler/deploy-token.ts @@ -1,66 +1,87 @@ import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { FungibleToken, FungibleTokenAdmin } from 'mina-fungible-token'; -import { AccountUpdate, Bool, Mina, UInt8 } from 'o1js'; +import { AccountUpdate, Bool, fetchAccount, Mina, PrivateKey, PublicKey, UInt8 } from 'o1js'; +import { EEnvKey } from '../../constants/env.constant.js'; import { ETHBridgeContract } from '../../shared/modules/web3/web3.service.js'; -import { Bridge } from './minaSc/Bridge.js'; -import { Manager } from './minaSc/Manager.js'; -import { ValidatorManager } from './minaSc/ValidatorManager.js'; @Injectable() export class TokenDeployer { - constructor(private readonly ethBridgeContract: ETHBridgeContract) {} + constructor( + private readonly ethBridgeContract: ETHBridgeContract, + private readonly configService: ConfigService, + ) {} private async deployTokenEth(tokenAddress: string): Promise { const res = await this.ethBridgeContract.whitelistToken(tokenAddress); return res.tokenAddress; } private async deployTokenMina() { - // const src = 'https://github.com/MinaFoundation/mina-fungible-token/blob/main/FungibleToken.ts'; - // const MINAURL = 'https://proxy.devnet.minaexplorer.com/graphql'; - // const ARCHIVEURL = 'https://api.minascan.io/archive/devnet/v1/graphql/'; + const src = 'https://github.com/MinaFoundation/mina-fungible-token/blob/main/FungibleToken.ts'; + const MINAURL = 'https://proxy.devnet.minaexplorer.com/graphql'; + const ARCHIVEURL = 'https://api.minascan.io/archive/devnet/v1/graphql/'; - // const network = Mina.Network({ - // mina: MINAURL, - // archive: ARCHIVEURL, - // }); - // Mina.setActiveInstance(network); - // const token = new FungibleToken(tokenAddress); - // const adminContract = new FungibleTokenAdmin(adminContractAddress); - // const bridgeContract = new Bridge(bridgeAddress); - // const managerContract = new Manager(managerAddress); - // const validatorManagerContract = new ValidatorManager(validatorManagerAddress); + const network = Mina.Network({ + mina: MINAURL, + archive: ARCHIVEURL, + }); + Mina.setActiveInstance(network); - // let sentTx; - // // compile the contract to create prover keys - // // await fetchAccount({publicKey: feepayerAddress}); - // try { - // // call update() and send transaction - // console.log('Deploying...'); - // let tx = await Mina.transaction({ sender: feepayerAddress, fee }, async () => { - // AccountUpdate.fundNewAccount(feepayerAddress, 3); - // await adminContract.deploy({ adminPublicKey: feepayerAddress }); - // await token.deploy({ - // symbol: symbol, - // src: src, - // }); - // await token.initialize(adminContractAddress, UInt8.from(9), Bool(false)); - // }); - // console.log('prove transaction...'); - // await tx.prove(); - // console.log('send transaction...'); - // sentTx = await tx.sign([feepayerKey, adminContractKey, tokenKey]).send(); - // } catch (err) { - // console.log(err); - // } - // console.log('=====================txhash: ', sentTx?.hash); - // await sentTx?.wait(); - // // Save all private and public keys to a single JSON file - // const keysToSave = [ - // { name: 'token', privateKey: tokenKey, publicKey: tokenAddress }, - // { name: 'adminContract', privateKey: adminContractKey, publicKey: adminContractAddress }, - // ]; + // compile contract + await FungibleToken.compile(); + await FungibleTokenAdmin.compile(); + + const feePayerPrivateKey = PrivateKey.fromBase58('EKFQWW89p2oVCd8yfM5SYVUGCiSMAByQ3yWzegLVz2cvcqMPJXgQ'); // a minter + const tokenPrivateKey = PrivateKey.random(); + const tokenAdminContractPrivateKey = PrivateKey.random(); + + const fee = +this.configService.get(EEnvKey.BASE_MINA_BRIDGE_FEE); // in nanomina (1 billion = 1.0 mina) + + await Promise.all([fetchAccount({ publicKey: feePayerPrivateKey.toPublicKey() })]); + + const token = new FungibleToken(tokenPrivateKey.toPublicKey()); + const tokenAdminContract = new FungibleTokenAdmin(tokenAdminContractPrivateKey.toPublicKey()); + + console.log('feePayerPrivateKey', feePayerPrivateKey.toPublicKey().toBase58()); + + let sentTx; + try { + // call update() and send transaction + console.log('Deploying...'); + const tx = await Mina.transaction({ sender: feePayerPrivateKey.toPublicKey(), fee }, async () => { + AccountUpdate.fundNewAccount(feePayerPrivateKey.toPublicKey(), 3); + await tokenAdminContract.deploy({ + adminPublicKey: PublicKey.fromBase58(this.configService.get(EEnvKey.MINA_BRIDGE_CONTRACT_ADDRESS)!), + }); + await token.deploy({ + symbol: 'symbol', + src: src, + }); + await token.initialize(tokenAdminContractPrivateKey.toPublicKey(), UInt8.from(9), Bool(false)); + }); + console.log('prove transaction...'); + await tx.prove(); + console.log('send transaction...'); + sentTx = await tx.sign([feePayerPrivateKey, tokenAdminContractPrivateKey, tokenPrivateKey]).send(); + } catch (err) { + console.log(err); + } + console.log('=====================txhash: ', sentTx?.hash); + await sentTx?.wait(); + // Save all private and public keys to a single JSON file + const keysToSave = [ + { name: 'token', privateKey: tokenPrivateKey.toBase58(), publicKey: tokenPrivateKey.toPublicKey().toBase58() }, + { + name: 'tokenAdminContract', + privateKey: tokenAdminContractPrivateKey.toBase58(), + publicKey: tokenAdminContractPrivateKey.toPublicKey().toBase58(), + }, + ]; + console.log(keysToSave); } - public async deployNewToken() {} + public async deployNewToken() { + await this.deployTokenMina(); + } } diff --git a/src/modules/crawler/sender.minabridge.ts b/src/modules/crawler/sender.minabridge.ts index b5fba41..f2eaada 100644 --- a/src/modules/crawler/sender.minabridge.ts +++ b/src/modules/crawler/sender.minabridge.ts @@ -21,7 +21,6 @@ export class SenderMinaBridge { private isContractCompiled = false; private readonly feePayerKey: PrivateKey; private readonly bridgeKey: PrivateKey; - private readonly tokenPublicKey: PublicKey; constructor( private readonly configService: ConfigService, private readonly eventLogRepository: EventLogRepository, @@ -30,7 +29,6 @@ export class SenderMinaBridge { ) { 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), @@ -43,7 +41,6 @@ export class SenderMinaBridge { private getContractsInfo() { this.logger.log('Bridge: ' + this.bridgeKey.toPublicKey().toBase58()); this.logger.log('FeePayer: ' + this.feePayerKey.toPublicKey().toBase58()); - this.logger.log('Token: ' + this.tokenPublicKey.toBase58()); } private async compileContract() { if (!this.isContractCompiled) { @@ -72,7 +69,7 @@ export class SenderMinaBridge { assert(dataLock?.amountReceived, 'invalida amount to unlock'); const { id, receiveAddress, amountReceived } = dataLock; - const result = await this.callUnlockFunction(amountReceived, id, receiveAddress); + const result = await this.callUnlockFunction(amountReceived, id, receiveAddress, dataLock.tokenReceivedAddress); // Update status eventLog when call function unlock if (result.success) { await this.eventLogRepository.updateStatusAndRetryEvenLog({ @@ -102,14 +99,15 @@ export class SenderMinaBridge { amount: string, txId: number, receiveAddress: string, + receiveTokenAddress: string, ): Promise<{ success: boolean; error: Error | null; data: string | null }> { try { - this.logger.info(`Bridge: ${this.bridgeKey.toPublicKey().toBase58()}\nToken: ${this.tokenPublicKey.toBase58()}`); + this.logger.info(`Bridge: ${this.bridgeKey.toPublicKey().toBase58()}\nToken: ${receiveTokenAddress}`); const generatedSignatures = await this.multiSignatureRepository.findBy({ txId, }); const signatureData = generatedSignatures - .map(e => [Bool(true), PublicKey.fromBase58(e.validator), Signature.fromJSON(JSON.parse(e.signature))]) + .map(e => [Bool(false), PublicKey.fromBase58(e.validator), Signature.fromJSON(JSON.parse(e.signature))]) .flat(1); this.logger.info(`Found ${generatedSignatures.length} signatures for txId= ${txId}`); this.logger.info('compile the contract...'); @@ -119,16 +117,17 @@ export class SenderMinaBridge { const feePayerPublicKey = this.feePayerKey.toPublicKey(); const bridgePublicKey = this.bridgeKey.toPublicKey(); const receiverPublicKey = PublicKey.fromBase58(receiveAddress); + const receiveTokenPublicKey = PublicKey.fromBase58(receiveTokenAddress); const zkBridge = new Bridge(bridgePublicKey); - const token = new FungibleToken(this.tokenPublicKey); + const token = new FungibleToken(receiveTokenPublicKey); const tokenId = token.deriveTokenId(); await Promise.all([ fetchAccount({ publicKey: bridgePublicKey }), fetchAccount({ publicKey: feePayerPublicKey }), fetchAccount({ publicKey: receiverPublicKey, tokenId }), fetchAccount({ - publicKey: this.tokenPublicKey, + publicKey: receiveTokenPublicKey, tokenId, }), ]); @@ -142,7 +141,13 @@ export class SenderMinaBridge { this.logger.info('build transaction and create proof...'); const tx = await Mina.transaction({ sender: feePayerPublicKey, fee }, async () => { if (!hasAccount) AccountUpdate.fundNewAccount(feePayerPublicKey); - await zkBridge.unlock(typedAmount, receiverPublicKey, UInt64.from(txId), this.tokenPublicKey, ...signatureData); + await zkBridge.unlock( + typedAmount, + receiverPublicKey, + UInt64.from(txId), + receiveTokenPublicKey, + ...signatureData, + ); }); const sentTx = await this.handleSendTxMina(txId, tx); @@ -190,7 +195,7 @@ export class SenderMinaBridge { const msg = [ ...receiverPublicKey.toFields(), ...UInt64.from(amountReceived).toFields(), - ...this.tokenPublicKey.toFields(), + ...PublicKey.fromBase58(dataLock.tokenReceivedAddress).toFields(), ]; const signature = Signature.create(signerPrivateKey, msg).toJSON(); @@ -223,6 +228,7 @@ export class SenderMinaBridge { await sentTx?.wait({ maxAttempts: 300 }); assert(sentTx?.hash, 'transaction failed'); + this.logger.info(`Tx ${sentTx.hash} is sent and waiting for crawler confirmation.`); return sentTx; } } diff --git a/src/modules/users/admin.controller.ts b/src/modules/users/admin.controller.ts index d853fc5..3caa8cd 100644 --- a/src/modules/users/admin.controller.ts +++ b/src/modules/users/admin.controller.ts @@ -9,6 +9,7 @@ import { CreateTokenReqDto } from './dto/admin-request.dto.js'; import { UpdateCommonConfigBodyDto } from './dto/common-config-request.dto.js'; import { GetCommonConfigResponseDto } from './dto/common-config-response.dto.js'; import { GetHistoryDto, GetHistoryOfUserResponseDto } from './dto/history-response.dto.js'; +import { GetTokensReqDto } from './dto/user-request.dto.js'; import { UsersService } from './users.service.js'; @ApiTags('Admins') @@ -28,11 +29,12 @@ export class AdminController { } @Get('tokens') - @AuthAdminGuard() - @UseGuards(AuthGuard('jwt')) + @GuardPublic() + // @AuthAdminGuard() + // @UseGuards(AuthGuard('jwt')) @ApiOkResponse({ type: GetCommonConfigResponseDto }) - getCommonConfig() { - return this.adminService.getListToken(); + getCommonConfig(@Query() query: GetTokensReqDto) { + return this.adminService.getListToken(query); } @Put('token/:id') @@ -42,9 +44,8 @@ export class AdminController { return this.userService.updateTokenConfig(id, updateConfig); } @Post('new-token') - @GuardPublic() - // @AuthAdminGuard() - // @UseGuards(AuthGuard('jwt')) + @AuthAdminGuard() + @UseGuards(AuthGuard('jwt')) addNewToken(@Body() payload: CreateTokenReqDto) { return this.adminService.createNewToken(payload); } diff --git a/src/modules/users/admin.service.ts b/src/modules/users/admin.service.ts index cbbe495..0d18703 100644 --- a/src/modules/users/admin.service.ts +++ b/src/modules/users/admin.service.ts @@ -4,9 +4,11 @@ import { DataSource, EntityManager } from 'typeorm'; import { ENetworkName, ETokenPairStatus } from '../../constants/blockchain.constant.js'; import { EEnvKey } from '../../constants/env.constant.js'; +import { toPageDto } from '../../core/paginate-typeorm.js'; import { CommonConfigRepository } from '../../database/repositories/common-configuration.repository.js'; import { CommonConfig } from '../crawler/entities/common-config.entity.js'; import { CreateTokenReqDto } from './dto/admin-request.dto.js'; +import { GetTokensReqDto } from './dto/user-request.dto.js'; @Injectable() export class AdminService { @@ -37,7 +39,8 @@ export class AdminService { return newCommonConfig.save(); }); } - getListToken() { - return this.commonConfigRepo.find(); + async getListToken(payload: GetTokensReqDto) { + const [tokens, count] = await this.commonConfigRepo.getManyAndPagination(payload); + return toPageDto(tokens, payload, count); } } diff --git a/src/modules/users/dto/user-request.dto.ts b/src/modules/users/dto/user-request.dto.ts index db4050c..855057a 100644 --- a/src/modules/users/dto/user-request.dto.ts +++ b/src/modules/users/dto/user-request.dto.ts @@ -1,4 +1,6 @@ +import { ETokenPairStatus } from '../../../constants/blockchain.constant.js'; import { NumberField, StringField } from '../../../shared/decorators/field.decorator.js'; +import { BasePaginationRequestDto } from '../../../shared/dtos/base-request.dto.js'; export class CreateUserDto { @StringField({ @@ -27,3 +29,7 @@ export class GetProtocolFeeBodyDto { }) pairId: number; } +export class GetTokensReqDto extends BasePaginationRequestDto { + @StringField({ isArray: true, example: ETokenPairStatus.CREATED, required: false }) + statuses: ETokenPairStatus[]; +} diff --git a/src/shared/decorators/field.decorator.ts b/src/shared/decorators/field.decorator.ts index cb5b05b..6708b6b 100644 --- a/src/shared/decorators/field.decorator.ts +++ b/src/shared/decorators/field.decorator.ts @@ -1,6 +1,6 @@ import { applyDecorators } from '@nestjs/common'; import { ApiProperty, ApiPropertyOptions } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; +import { Transform, Type } from 'class-transformer'; import { IsArray, IsBoolean, @@ -124,6 +124,13 @@ export function StringField(options: Omit & IStringF if (toUpperCase) { decorators.push(ToUpperCase()); } + if (isArray) { + decorators.push( + Transform(({ value }) => { + if (typeof value === 'string') return [value]; + }), + ); + } // strings, number string validation if (typeof number == 'object') { decorators.push( From 826800f674b94d27d1c665c2d9e421e6b408218d Mon Sep 17 00:00:00 2001 From: Kenneth Hoang Date: Mon, 6 Jan 2025 18:10:30 +0700 Subject: [PATCH 05/26] feat: add filters for get tokens api --- .env.example | 4 ++-- .../common-configuration.repository.ts | 15 +++++++++++++-- src/modules/users/dto/user-request.dto.ts | 6 ++++++ 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/.env.example b/.env.example index cc883d3..653fc18 100644 --- a/.env.example +++ b/.env.example @@ -48,8 +48,8 @@ GAS_FEE_EVM=0.00001 DECIMAL_TOKEN_EVM=18 # coinmarketcap -COINMARKET_KEY=233ae614-1377-40e6-83c6-c042185a5a23 -COINMARKET_URL='https://pro-api.coinmarketcap.com/v2/cryptocurrency/quotes/latest?id=1027,8646' +COINMARKET_KEY= +COINMARKET_URL= MINA_BRIDGE_SC_PRIVATE_KEY=EKF19hihcXry9QMttf719fVp56DuRB2vZySdeQ1y9BkkvWWxnJAa diff --git a/src/database/repositories/common-configuration.repository.ts b/src/database/repositories/common-configuration.repository.ts index 3cec428..8283ea3 100644 --- a/src/database/repositories/common-configuration.repository.ts +++ b/src/database/repositories/common-configuration.repository.ts @@ -1,7 +1,7 @@ -import { isArray } from 'class-validator'; +import { isArray, isNotEmpty } from 'class-validator'; import { UpdateCommonConfigBodyDto } from 'modules/users/dto/common-config-request.dto.js'; import { EntityRepository } from 'nestjs-typeorm-custom-repository'; -import { In } from 'typeorm'; +import { Brackets, ILike, In } from 'typeorm'; import { ETableName } from '../../constants/entity.constant.js'; import { BaseRepository } from '../../core/base-repository.js'; @@ -22,6 +22,17 @@ export class CommonConfigRepository extends BaseRepository { if (isArray(dto.statuses)) { qb.andWhere({ status: In(dto.statuses) }); } + if (isNotEmpty(dto.assetName)) { + qb.andWhere({ asset: ILike(`${dto.assetName}%`) }); + } + if (isNotEmpty(dto.tokenAddress)) { + qb.andWhere( + new Brackets(qb => { + qb.orWhere({ fromAddress: ILike(dto.tokenAddress) }); + qb.orWhere({ toAddress: ILike(dto.tokenAddress) }); + }), + ); + } this.queryBuilderAddPagination(qb, dto); return qb.getManyAndCount(); } diff --git a/src/modules/users/dto/user-request.dto.ts b/src/modules/users/dto/user-request.dto.ts index 855057a..96afc95 100644 --- a/src/modules/users/dto/user-request.dto.ts +++ b/src/modules/users/dto/user-request.dto.ts @@ -32,4 +32,10 @@ export class GetProtocolFeeBodyDto { export class GetTokensReqDto extends BasePaginationRequestDto { @StringField({ isArray: true, example: ETokenPairStatus.CREATED, required: false }) statuses: ETokenPairStatus[]; + + @StringField({ required: false }) + assetName: string; + + @StringField({ required: false }) + tokenAddress: string; } From b3e4a236af79104156fa08d0ceea2b91708da27d Mon Sep 17 00:00:00 2001 From: Kenneth Hoang Date: Tue, 7 Jan 2025 16:08:26 +0700 Subject: [PATCH 06/26] feat: job deploy token --- src/constants/blockchain.constant.ts | 1 + src/constants/env.constant.ts | 1 + .../1736238200965-unique-token-address.ts | 17 ++ ...154189-add-toggle-visibility-token-pair.ts | 17 ++ src/modules/crawler/crawler.console.ts | 37 ++-- src/modules/crawler/deploy-token.ts | 117 +++++++++-- .../crawler/entities/common-config.entity.ts | 3 + .../crawler/interfaces/job.interface.ts | 7 + src/modules/crawler/job-unlock.provider.ts | 9 +- src/modules/users/admin.controller.ts | 17 +- src/modules/users/admin.service.ts | 11 +- .../users/dto/common-config-request.dto.ts | 6 +- src/modules/users/users.module.ts | 3 +- src/modules/users/users.service.ts | 9 +- src/shared/decorators/transform.decorator.ts | 5 +- src/shared/modules/web3/abis/erc-20.js | 189 ++++++++++++++++++ src/shared/modules/web3/web3.module.ts | 5 +- src/shared/modules/web3/web3.service.ts | 70 ++----- 18 files changed, 422 insertions(+), 102 deletions(-) create mode 100644 src/database/migrations/1736238200965-unique-token-address.ts create mode 100644 src/database/migrations/1736240154189-add-toggle-visibility-token-pair.ts create mode 100644 src/shared/modules/web3/abis/erc-20.js diff --git a/src/constants/blockchain.constant.ts b/src/constants/blockchain.constant.ts index 200f6bf..6f5f314 100644 --- a/src/constants/blockchain.constant.ts +++ b/src/constants/blockchain.constant.ts @@ -23,6 +23,7 @@ export enum ETokenPairStatus { DISABLE = 'disable', CREATED = 'created', DEPLOYING = 'deploying', + DEPLOY_FAILED = 'deploy_failed', } export enum EMinaChainEnviroment { diff --git a/src/constants/env.constant.ts b/src/constants/env.constant.ts index 56aca8d..b5b525f 100644 --- a/src/constants/env.constant.ts +++ b/src/constants/env.constant.ts @@ -31,6 +31,7 @@ export enum EEnvKey { MINA_TOKEN_BRIDGE_ADDRESS = 'MINA_TOKEN_BRIDGE_ADDRESS', MINA_BRIDGE_SC_PRIVATE_KEY = 'MINA_BRIDGE_SC_PRIVATE_KEY', SIGNER_PRIVATE_KEY = 'SIGNER_PRIVATE_KEY', + ADMIN_EVM_PRIVATE_KEY = 'ADMIN_EVM_PRIVATE_KEY', NUMBER_OF_BLOCK_PER_JOB = 'NUMBER_OF_BLOCK_PER_JOB', ADMIN_MESSAGE_FOR_SIGN = 'ADMIN_MESSAGE_FOR_SIGN', SIGNER_MINA_PRIVATE_KEY = 'SIGNER_MINA_PRIVATE_KEY', diff --git a/src/database/migrations/1736238200965-unique-token-address.ts b/src/database/migrations/1736238200965-unique-token-address.ts new file mode 100644 index 0000000..b928050 --- /dev/null +++ b/src/database/migrations/1736238200965-unique-token-address.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner, TableUnique } from 'typeorm'; + +export class UniqueTokenAddress1736238200965 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.createUniqueConstraint( + 'common_configuration', + new TableUnique({ + name: 'unique_from_address', + columnNames: ['from_address'], + }), + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropUniqueConstraint('common_configuration', 'unique-from-address'); + } +} diff --git a/src/database/migrations/1736240154189-add-toggle-visibility-token-pair.ts b/src/database/migrations/1736240154189-add-toggle-visibility-token-pair.ts new file mode 100644 index 0000000..4d955ea --- /dev/null +++ b/src/database/migrations/1736240154189-add-toggle-visibility-token-pair.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm'; + +export class AddToggleVisibilityTokenPair1736240154189 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + return queryRunner.addColumns('common_configuration', [ + new TableColumn({ + name: 'is_hidden', + type: 'boolean', + default: false, + }), + ]); + } + + public async down(queryRunner: QueryRunner): Promise { + return queryRunner.dropColumns('common_configuration', ['is_hidden']); + } +} diff --git a/src/modules/crawler/crawler.console.ts b/src/modules/crawler/crawler.console.ts index 17b9125..7a0782f 100644 --- a/src/modules/crawler/crawler.console.ts +++ b/src/modules/crawler/crawler.console.ts @@ -9,7 +9,7 @@ import { sleep } from '../../shared/utils/promise.js'; import { BlockchainEVMCrawler } from './crawler.evmbridge.js'; import { SCBridgeMinaCrawler } from './crawler.minabridge.js'; import { TokenDeployer } from './deploy-token.js'; -import { IGenerateSignature, IUnlockToken } from './interfaces/job.interface.js'; +import { IDeployToken, IGenerateSignature, ISenderJobPayload, IUnlockToken } from './interfaces/job.interface.js'; import { JobUnlockProvider } from './job-unlock.provider.js'; import { SenderEVMBridge } from './sender.evmbridge.js'; import { SenderMinaBridge } from './sender.minabridge.js'; @@ -86,13 +86,17 @@ export class CrawlerConsole { }) async handleSenderETHBridgeUnlock() { this.logger.info('ETH_SENDER_JOB: started'); - await this.queueService.handleQueueJob(EQueueName.EVM_SENDER_QUEUE, async (data: IUnlockToken) => { - const result = await this.senderEVMBridge.handleUnlockEVM(data.eventLogId); - if (result.error) { - // catch in queueService - throw result.error; - } - }); + await this.queueService.handleQueueJob( + EQueueName.EVM_SENDER_QUEUE, + async (data: ISenderJobPayload) => { + switch (data.type) { + case 'deploy-token': + return this.tokenDeployerService.deployTokenEth((data.payload as IDeployToken).tokenPairId); + case 'unlock': + return this.senderEVMBridge.handleUnlockEVM((data.payload as IUnlockToken).eventLogId); + } + }, + ); } @Command({ @@ -117,8 +121,13 @@ export class CrawlerConsole { }) async handleSenderMinaBridgeUnlock() { this.logger.info('MINA_SENDER_JOB: started'); - await this.queueService.handleQueueJob(EQueueName.MINA_SENDER_QUEUE, (data: IUnlockToken) => { - return this.senderMinaBridge.handleUnlockMina(data.eventLogId); + await this.queueService.handleQueueJob(EQueueName.MINA_SENDER_QUEUE, (data: ISenderJobPayload) => { + switch (data.type) { + case 'deploy-token': + return this.tokenDeployerService.deployTokenMina((data.payload as IDeployToken).tokenPairId); + case 'unlock': + return this.senderMinaBridge.handleUnlockMina((data.payload as IUnlockToken).eventLogId); + } }); } @Command({ @@ -138,12 +147,4 @@ export class CrawlerConsole { this.logger.info('SYNC_POA: started'); await this.poaSyncer.handleSyncPOA(); } - @Command({ - command: 'deploy-token-job', - description: 'handle deploy new token.', - }) - async handleJobDeployToken() { - this.logger.info('DEPLOY_TOKEN: started'); - await this.tokenDeployerService.deployNewToken(); - } } diff --git a/src/modules/crawler/deploy-token.ts b/src/modules/crawler/deploy-token.ts index 3be5fd3..3c5263f 100644 --- a/src/modules/crawler/deploy-token.ts +++ b/src/modules/crawler/deploy-token.ts @@ -1,26 +1,68 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; +import { isNotEmpty } from 'class-validator'; import { FungibleToken, FungibleTokenAdmin } from 'mina-fungible-token'; import { AccountUpdate, Bool, fetchAccount, Mina, PrivateKey, PublicKey, UInt8 } from 'o1js'; +import { IsNull } from 'typeorm'; +import { ETokenPairStatus } from '../../constants/blockchain.constant.js'; import { EEnvKey } from '../../constants/env.constant.js'; +import { EQueueName } from '../../constants/queue.constant.js'; +import { CommonConfigRepository } from '../../database/repositories/common-configuration.repository.js'; +import { LoggerService } from '../../shared/modules/logger/logger.service.js'; +import { QueueService } from '../../shared/modules/queue/queue.service.js'; import { ETHBridgeContract } from '../../shared/modules/web3/web3.service.js'; +import { ISenderJobPayload } from './interfaces/job.interface.js'; @Injectable() export class TokenDeployer { constructor( private readonly ethBridgeContract: ETHBridgeContract, private readonly configService: ConfigService, + private readonly queueService: QueueService, + private readonly tokenPairRepo: CommonConfigRepository, + private readonly loggerService: LoggerService, ) {} - private async deployTokenEth(tokenAddress: string): Promise { - const res = await this.ethBridgeContract.whitelistToken(tokenAddress); - - return res.tokenAddress; + private readonly logger = this.loggerService.getLogger('DEPLOY_TOKEN_SERVICE'); + public async deployTokenEth(tokenPairId: number) { + const tokenInfo = await this.tokenPairRepo.findOneBy({ id: tokenPairId }); + if (!tokenInfo) { + this.logger.info('Token not found', tokenPairId); + return; + } + try { + await this.ethBridgeContract.whitelistToken(tokenInfo.fromAddress); + await this.tokenPairRepo.update( + { id: tokenPairId }, + { + status: ETokenPairStatus.ENABLE, + }, + ); + } catch (error) { + this.logger.error(error); + await this.tokenPairRepo.update( + { id: tokenPairId }, + { + status: ETokenPairStatus.DEPLOY_FAILED, + }, + ); + } } - private async deployTokenMina() { + public async deployTokenMina(tokenPairId: number) { + const tokenInfo = await this.tokenPairRepo.findOneBy({ id: tokenPairId }); + if (!tokenInfo) { + this.logger.info('Token not found', tokenPairId); + return; + } + if (isNotEmpty(tokenInfo.toAddress)) { + this.logger.info(`token is already deployed`); + // must try invoke eth sender job + await this.addJobWhitelistTokenEth(tokenPairId); + return; + } const src = 'https://github.com/MinaFoundation/mina-fungible-token/blob/main/FungibleToken.ts'; - const MINAURL = 'https://proxy.devnet.minaexplorer.com/graphql'; - const ARCHIVEURL = 'https://api.minascan.io/archive/devnet/v1/graphql/'; + const MINAURL = this.configService.get(EEnvKey.MINA_BRIDGE_RPC_OPTIONS); + const ARCHIVEURL = this.configService.get(EEnvKey.MINA_BRIDGE_ARCHIVE_RPC_OPTIONS); const network = Mina.Network({ mina: MINAURL, @@ -43,32 +85,36 @@ export class TokenDeployer { const token = new FungibleToken(tokenPrivateKey.toPublicKey()); const tokenAdminContract = new FungibleTokenAdmin(tokenAdminContractPrivateKey.toPublicKey()); - console.log('feePayerPrivateKey', feePayerPrivateKey.toPublicKey().toBase58()); + this.logger.info('feePayerPrivateKey', feePayerPrivateKey.toPublicKey().toBase58()); let sentTx; try { - // call update() and send transaction - console.log('Deploying...'); const tx = await Mina.transaction({ sender: feePayerPrivateKey.toPublicKey(), fee }, async () => { AccountUpdate.fundNewAccount(feePayerPrivateKey.toPublicKey(), 3); await tokenAdminContract.deploy({ adminPublicKey: PublicKey.fromBase58(this.configService.get(EEnvKey.MINA_BRIDGE_CONTRACT_ADDRESS)!), }); await token.deploy({ - symbol: 'symbol', + symbol: tokenInfo.asset, src: src, }); await token.initialize(tokenAdminContractPrivateKey.toPublicKey(), UInt8.from(9), Bool(false)); }); - console.log('prove transaction...'); await tx.prove(); - console.log('send transaction...'); sentTx = await tx.sign([feePayerPrivateKey, tokenAdminContractPrivateKey, tokenPrivateKey]).send(); + this.logger.info('=====================txhash: ', sentTx?.hash); + await sentTx?.wait({ maxAttempts: 300 }); } catch (err) { - console.log(err); + this.logger.error(err); + await this.tokenPairRepo.update( + { id: tokenPairId, toAddress: IsNull() }, + { + status: ETokenPairStatus.DEPLOY_FAILED, + }, + ); + return; // terminate the job } - console.log('=====================txhash: ', sentTx?.hash); - await sentTx?.wait(); + // Save all private and public keys to a single JSON file const keysToSave = [ { name: 'token', privateKey: tokenPrivateKey.toBase58(), publicKey: tokenPrivateKey.toPublicKey().toBase58() }, @@ -78,10 +124,43 @@ export class TokenDeployer { publicKey: tokenAdminContractPrivateKey.toPublicKey().toBase58(), }, ]; - console.log(keysToSave); + this.logger.info(keysToSave); + // save to db + await this.tokenPairRepo.update( + { id: tokenPairId, toAddress: IsNull() }, + { + toScAddress: tokenPrivateKey.toPublicKey().toBase58(), + }, + ); + await this.addJobWhitelistTokenEth(tokenPairId); } - public async deployNewToken() { - await this.deployTokenMina(); + public addJobDeployTokenMina(tokenPairId: number) { + return this.queueService.addJobToQueue( + EQueueName.MINA_SENDER_QUEUE, + { + type: 'deploy-token', + payload: { + tokenPairId, + }, + }, + { + jobId: `deploy-token-${tokenPairId}`, + removeOnComplete: true, + removeOnFail: true, + }, + ); + } + private addJobWhitelistTokenEth(tokenPairId: number) { + return this.queueService.addJobToQueue( + EQueueName.EVM_SENDER_QUEUE, + { + type: 'deploy-token', + payload: { + tokenPairId, + }, + }, + { jobId: `deploy-token-${tokenPairId}`, removeOnComplete: true, removeOnFail: true }, + ); } } diff --git a/src/modules/crawler/entities/common-config.entity.ts b/src/modules/crawler/entities/common-config.entity.ts index 6db57b5..dc5ad52 100644 --- a/src/modules/crawler/entities/common-config.entity.ts +++ b/src/modules/crawler/entities/common-config.entity.ts @@ -78,6 +78,9 @@ export class CommonConfig extends BaseEntityIncludeTime { @Column({ name: 'to_sc_address', type: 'varchar', nullable: true }) toScAddress: string; + @Column({ name: 'is_hidden', default: false }) + isHidden: boolean; + @Column({ name: 'status', type: 'varchar', diff --git a/src/modules/crawler/interfaces/job.interface.ts b/src/modules/crawler/interfaces/job.interface.ts index a64e496..26ef005 100644 --- a/src/modules/crawler/interfaces/job.interface.ts +++ b/src/modules/crawler/interfaces/job.interface.ts @@ -12,6 +12,13 @@ export interface IReceiveVerifiedSignature { export interface IGenerateSignature { eventLogId: number; } +export interface ISenderJobPayload { + type: 'unlock' | 'deploy-token'; + payload: IUnlockToken | IDeployToken; +} +export interface IDeployToken { + tokenPairId: number; +} export interface IUnlockToken { eventLogId: number; } diff --git a/src/modules/crawler/job-unlock.provider.ts b/src/modules/crawler/job-unlock.provider.ts index 63c7545..dc6d150 100644 --- a/src/modules/crawler/job-unlock.provider.ts +++ b/src/modules/crawler/job-unlock.provider.ts @@ -18,7 +18,7 @@ import { sleep } from '../../shared/utils/promise.js'; import { getNextDayInUnix, getTimeInFutureInMinutes } from '../../shared/utils/time.js'; import { BatchJobGetPriceToken } from './batch.tokenprice.js'; import { EventLog } from './entities/event-logs.entity.js'; -import { IGenerateSignature, IJobUnlockPayload, IUnlockToken } from './interfaces/job.interface.js'; +import { IGenerateSignature, IJobUnlockPayload, ISenderJobPayload } from './interfaces/job.interface.js'; @Injectable() export class JobUnlockProvider { @@ -176,10 +176,13 @@ export class JobUnlockProvider { await this.eventLogRepository.update(data.eventLogId, { nextSendTxJobTime: getNextDayInUnix().toString() }); return; } - await this.queueService.addJobToQueue( + await this.queueService.addJobToQueue( this.getSenderQueueName(data.network), { - eventLogId: data.eventLogId, + type: 'unlock', + payload: { + eventLogId: data.eventLogId, + }, }, { jobId: `send-unlock-${data.eventLogId}`, diff --git a/src/modules/users/admin.controller.ts b/src/modules/users/admin.controller.ts index 3caa8cd..d597171 100644 --- a/src/modules/users/admin.controller.ts +++ b/src/modules/users/admin.controller.ts @@ -6,7 +6,7 @@ import { GuardPublic } from '../../guards/guard.decorator.js'; import { AuthAdminGuard } from '../../shared/decorators/http.decorator.js'; import { AdminService } from './admin.service.js'; import { CreateTokenReqDto } from './dto/admin-request.dto.js'; -import { UpdateCommonConfigBodyDto } from './dto/common-config-request.dto.js'; +import { UpdateCommonConfigBodyDto, UpdateTokenPairVisibilityReqDto } from './dto/common-config-request.dto.js'; import { GetCommonConfigResponseDto } from './dto/common-config-response.dto.js'; import { GetHistoryDto, GetHistoryOfUserResponseDto } from './dto/history-response.dto.js'; import { GetTokensReqDto } from './dto/user-request.dto.js'; @@ -30,8 +30,8 @@ export class AdminController { @Get('tokens') @GuardPublic() - // @AuthAdminGuard() - // @UseGuards(AuthGuard('jwt')) + @AuthAdminGuard() + @UseGuards(AuthGuard('jwt')) @ApiOkResponse({ type: GetCommonConfigResponseDto }) getCommonConfig(@Query() query: GetTokensReqDto) { return this.adminService.getListToken(query); @@ -40,12 +40,21 @@ export class AdminController { @Put('token/:id') @AuthAdminGuard() @UseGuards(AuthGuard('jwt')) - updateCommonConfig(@Param('id') id: number, @Body() updateConfig: UpdateCommonConfigBodyDto) { + updateTokenPair(@Param('id') id: number, @Body() updateConfig: UpdateCommonConfigBodyDto) { return this.userService.updateTokenConfig(id, updateConfig); } + + @Put('token/visibility/:id') + @AuthAdminGuard() + @UseGuards(AuthGuard('jwt')) + updateTokenPairVisibility(@Param('id') id: number, @Body() updateConfig: UpdateTokenPairVisibilityReqDto) { + return this.userService.updateTokenVisibility(id, updateConfig); + } + @Post('new-token') @AuthAdminGuard() @UseGuards(AuthGuard('jwt')) + @GuardPublic() addNewToken(@Body() payload: CreateTokenReqDto) { return this.adminService.createNewToken(payload); } diff --git a/src/modules/users/admin.service.ts b/src/modules/users/admin.service.ts index 0d18703..50a9eea 100644 --- a/src/modules/users/admin.service.ts +++ b/src/modules/users/admin.service.ts @@ -1,11 +1,14 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; +import assert from 'assert'; +import { isNumber } from 'class-validator'; import { DataSource, EntityManager } from 'typeorm'; import { ENetworkName, ETokenPairStatus } from '../../constants/blockchain.constant.js'; import { EEnvKey } from '../../constants/env.constant.js'; import { toPageDto } from '../../core/paginate-typeorm.js'; import { CommonConfigRepository } from '../../database/repositories/common-configuration.repository.js'; +import { TokenDeployer } from '../../modules/crawler/deploy-token.js'; import { CommonConfig } from '../crawler/entities/common-config.entity.js'; import { CreateTokenReqDto } from './dto/admin-request.dto.js'; import { GetTokensReqDto } from './dto/user-request.dto.js'; @@ -16,10 +19,11 @@ export class AdminService { private readonly dataSource: DataSource, private readonly configService: ConfigService, private readonly commonConfigRepo: CommonConfigRepository, + private readonly tokenDeployerService: TokenDeployer, ) {} - createNewToken(payload: CreateTokenReqDto) { + async createNewToken(payload: CreateTokenReqDto) { // get erc20 metadata: decimal... - return this.dataSource.transaction(async (e: EntityManager) => { + const newTokenPair = await this.dataSource.transaction(async (e: EntityManager) => { const commonConfigRepo = e.getRepository(CommonConfig); // move create pair logic to helpers const newCommonConfig = commonConfigRepo.create(); @@ -38,6 +42,9 @@ export class AdminService { newCommonConfig.status = ETokenPairStatus.CREATED; return newCommonConfig.save(); }); + assert(isNumber(newTokenPair.id), 'Token pair invalid!'); + await this.tokenDeployerService.addJobDeployTokenMina(newTokenPair.id); + return newTokenPair; } async getListToken(payload: GetTokensReqDto) { const [tokens, count] = await this.commonConfigRepo.getManyAndPagination(payload); diff --git a/src/modules/users/dto/common-config-request.dto.ts b/src/modules/users/dto/common-config-request.dto.ts index cf19c8c..cf928fa 100644 --- a/src/modules/users/dto/common-config-request.dto.ts +++ b/src/modules/users/dto/common-config-request.dto.ts @@ -1,5 +1,5 @@ import { EEnvKey } from '../../../constants/env.constant.js'; -import { NumberField, StringField } from '../../../shared/decorators/field.decorator.js'; +import { BooleanField, NumberField, StringField } from '../../../shared/decorators/field.decorator.js'; export class UpdateCommonConfigBodyDto { @NumberField({ @@ -32,3 +32,7 @@ export class UpdateCommonConfigBodyDto { }) unlockingFee: string; } +export class UpdateTokenPairVisibilityReqDto { + @BooleanField({ required: true }) + isHidden: boolean; +} diff --git a/src/modules/users/users.module.ts b/src/modules/users/users.module.ts index 3af3c02..cf89d60 100644 --- a/src/modules/users/users.module.ts +++ b/src/modules/users/users.module.ts @@ -6,6 +6,7 @@ import { EventLogRepository } from '../../database/repositories/event-log.reposi import { TokenPairRepository } from '../../database/repositories/token-pair.repository.js'; import { TokenPriceRepository } from '../../database/repositories/token-price.repository.js'; import { UserRepository } from '../../database/repositories/user.repository.js'; +import { TokenDeployer } from '../../modules/crawler/deploy-token.js'; import { AdminController } from './admin.controller.js'; import { AdminService } from './admin.service.js'; import { UsersController } from './users.controller.js'; @@ -22,7 +23,7 @@ import { UsersService } from './users.service.js'; ]), ], controllers: [UsersController, AdminController], - providers: [UsersService, AdminService], + providers: [UsersService, AdminService, TokenDeployer], exports: [UsersService], }) export class UsersModule {} diff --git a/src/modules/users/users.service.ts b/src/modules/users/users.service.ts index c8c4478..c06a92d 100644 --- a/src/modules/users/users.service.ts +++ b/src/modules/users/users.service.ts @@ -15,7 +15,7 @@ import { UserRepository } from '../../database/repositories/user.repository.js'; import { httpBadRequest } from '../../shared/exceptions/http-exeption.js'; import { LoggerService } from '../../shared/modules/logger/logger.service.js'; import { RedisClientService } from '../../shared/modules/redis/redis-client.service.js'; -import { UpdateCommonConfigBodyDto } from './dto/common-config-request.dto.js'; +import { UpdateCommonConfigBodyDto, UpdateTokenPairVisibilityReqDto } from './dto/common-config-request.dto.js'; import { GetHistoryDto, GetHistoryOfUserDto } from './dto/history-response.dto.js'; import { GetProtocolFeeBodyDto } from './dto/user-request.dto.js'; import { GetProofOfAssetsResponseDto, GetTokensPriceResponseDto } from './dto/user-response.dto.js'; @@ -56,6 +56,12 @@ export class UsersService { await this.commonConfigRepository.updateCommonConfig(id, updateConfig); return updateConfig; } + async updateTokenVisibility(id: number, updateConfig: UpdateTokenPairVisibilityReqDto) { + await this.commonConfigRepository.update(id, { + isHidden: updateConfig.isHidden, + }); + return updateConfig; + } async getDailyQuotaOfUser(address: string) { const [dailyQuota, totalamount] = await Promise.all([ @@ -70,6 +76,7 @@ export class UsersService { return this.commonConfigRepository.find({ where: { status: ETokenPairStatus.ENABLE, + isHidden: false, }, }); } diff --git a/src/shared/decorators/transform.decorator.ts b/src/shared/decorators/transform.decorator.ts index 0c5c7b8..c0de848 100644 --- a/src/shared/decorators/transform.decorator.ts +++ b/src/shared/decorators/transform.decorator.ts @@ -1,5 +1,6 @@ import { createParamDecorator, ExecutionContext } from '@nestjs/common'; import { Transform } from 'class-transformer'; +import { isNotEmpty } from 'class-validator'; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore import pkg from 'lodash'; @@ -18,12 +19,12 @@ export function Trim(): PropertyDecorator { } function toBoolean(value: string | number | boolean): boolean { if (typeof value === 'string') return value.toLowerCase() === 'true'; - return Boolean(value).valueOf(); + return !!value; } export function ToBoolean(): PropertyDecorator { return Transform( ({ value }) => { - if (value) return toBoolean(value); + if (isNotEmpty(value)) return toBoolean(value); }, { toClassOnly: true }, ); diff --git a/src/shared/modules/web3/abis/erc-20.js b/src/shared/modules/web3/abis/erc-20.js new file mode 100644 index 0000000..d397f32 --- /dev/null +++ b/src/shared/modules/web3/abis/erc-20.js @@ -0,0 +1,189 @@ +export default [ + { + inputs: [ + { internalType: 'string', name: 'name_', type: 'string' }, + { internalType: 'string', name: 'symbol_', type: 'string' }, + { internalType: 'uint8', name: 'decimals_', type: 'uint8' }, + ], + stateMutability: 'nonpayable', + type: 'constructor', + }, + { + inputs: [ + { internalType: 'address', name: 'spender', type: 'address' }, + { internalType: 'uint256', name: 'allowance', type: 'uint256' }, + { internalType: 'uint256', name: 'needed', type: 'uint256' }, + ], + name: 'ERC20InsufficientAllowance', + type: 'error', + }, + { + inputs: [ + { internalType: 'address', name: 'sender', type: 'address' }, + { internalType: 'uint256', name: 'balance', type: 'uint256' }, + { internalType: 'uint256', name: 'needed', type: 'uint256' }, + ], + name: 'ERC20InsufficientBalance', + type: 'error', + }, + { + inputs: [{ internalType: 'address', name: 'approver', type: 'address' }], + name: 'ERC20InvalidApprover', + type: 'error', + }, + { + inputs: [{ internalType: 'address', name: 'receiver', type: 'address' }], + name: 'ERC20InvalidReceiver', + type: 'error', + }, + { + inputs: [{ internalType: 'address', name: 'sender', type: 'address' }], + name: 'ERC20InvalidSender', + type: 'error', + }, + { + inputs: [{ internalType: 'address', name: 'spender', type: 'address' }], + name: 'ERC20InvalidSpender', + type: 'error', + }, + { + inputs: [{ internalType: 'address', name: 'owner', type: 'address' }], + name: 'OwnableInvalidOwner', + type: 'error', + }, + { + inputs: [{ internalType: 'address', name: 'account', type: 'address' }], + name: 'OwnableUnauthorizedAccount', + type: 'error', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'address', name: 'owner', type: 'address' }, + { indexed: true, internalType: 'address', name: 'spender', type: 'address' }, + { indexed: false, internalType: 'uint256', name: 'value', type: 'uint256' }, + ], + name: 'Approval', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'address', name: 'previousOwner', type: 'address' }, + { indexed: true, internalType: 'address', name: 'newOwner', type: 'address' }, + ], + name: 'OwnershipTransferred', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'address', name: 'from', type: 'address' }, + { indexed: true, internalType: 'address', name: 'to', type: 'address' }, + { indexed: false, internalType: 'uint256', name: 'value', type: 'uint256' }, + ], + name: 'Transfer', + type: 'event', + }, + { + inputs: [ + { internalType: 'address', name: 'owner', type: 'address' }, + { internalType: 'address', name: 'spender', type: 'address' }, + ], + name: 'allowance', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'spender', type: 'address' }, + { internalType: 'uint256', name: 'value', type: 'uint256' }, + ], + name: 'approve', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: 'account', type: 'address' }], + name: 'balanceOf', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'decimals', + outputs: [{ internalType: 'uint8', name: '', type: 'uint8' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'to', type: 'address' }, + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + ], + name: 'mint', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'name', + outputs: [{ internalType: 'string', name: '', type: 'string' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'owner', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { inputs: [], name: 'renounceOwnership', outputs: [], stateMutability: 'nonpayable', type: 'function' }, + { + inputs: [], + name: 'symbol', + outputs: [{ internalType: 'string', name: '', type: 'string' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'totalSupply', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'to', type: 'address' }, + { internalType: 'uint256', name: 'value', type: 'uint256' }, + ], + name: 'transfer', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'from', type: 'address' }, + { internalType: 'address', name: 'to', type: 'address' }, + { internalType: 'uint256', name: 'value', type: 'uint256' }, + ], + name: 'transferFrom', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: 'newOwner', type: 'address' }], + name: 'transferOwnership', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, +]; diff --git a/src/shared/modules/web3/web3.module.ts b/src/shared/modules/web3/web3.module.ts index 0688487..869cd44 100644 --- a/src/shared/modules/web3/web3.module.ts +++ b/src/shared/modules/web3/web3.module.ts @@ -8,7 +8,7 @@ import { initializeEthContract } from '../../../config/common.config.js'; import { EEnvKey } from '../../../constants/env.constant.js'; import { ASYNC_CONNECTION } from '../../../constants/service.constant.js'; import { sleep } from '../../utils/promise.js'; -import { ETHBridgeContract } from './web3.service.js'; +import { Erc20ContractTemplate, ETHBridgeContract } from './web3.service.js'; @Global() @Module({ @@ -29,8 +29,9 @@ import { ETHBridgeContract } from './web3.service.js'; }, inject: [ASYNC_CONNECTION], }, + Erc20ContractTemplate, ], - exports: [Web3Module, ETHBridgeContract], + exports: [Web3Module, ETHBridgeContract, Erc20ContractTemplate], }) export class Web3Module {} diff --git a/src/shared/modules/web3/web3.service.ts b/src/shared/modules/web3/web3.service.ts index 97f936e..82ad648 100644 --- a/src/shared/modules/web3/web3.service.ts +++ b/src/shared/modules/web3/web3.service.ts @@ -4,7 +4,9 @@ import { TransactionReceipt } from 'web3-core'; import { Contract, EventData } from 'web3-eth-contract'; import pkg from 'web3-utils'; +import { EEnvKey } from '../../../constants/env.constant.js'; import { sleep } from '../../utils/promise.js'; +import Erc20Abi from './abis/erc-20.js'; import { EthBridgeAbi } from './abis/eth-bridge-contract.js'; import { IRpcService } from './web3.module.js'; @@ -92,8 +94,10 @@ export class DefaultContract { return gasLimit; } - public async write(method: string, param: Array): Promise { - const signer = this.rpcService.web3.eth.accounts.privateKeyToAccount(this.rpcService.privateKeys); + public async write(method: string, param: Array, customSignerPrivateKey?: string): Promise { + const signer = this.rpcService.web3.eth.accounts.privateKeyToAccount( + customSignerPrivateKey ?? this.rpcService.privateKeys, + ); const data = this.contract.methods[method](...param).encodeABI(); const gasPrice = await this.rpcService.web3.eth.getGasPrice(); @@ -117,47 +121,6 @@ export class DefaultContract { return this.rpcService.web3.eth.sendSignedTransaction(signedTx.rawTransaction); } - public async multiWrite( - writeData: any[], - specifySignerIndex?: number, - ): Promise<{ success: boolean; error: Error | null; data: TransactionReceipt[] | null }> { - try { - const signer = this.rpcService.web3.eth.accounts.privateKeyToAccount( - this.rpcService.privateKeys[specifySignerIndex ?? 0], - ); - - const response = []; - for (const element of writeData) { - // gas estimation - const nonce = await this.rpcService.getNonce(signer.address); - const { method, param } = element; - const data = this.contract.methods[method](...param).encodeABI(); - const gasPrice = await this.rpcService.web3.eth.getGasPrice(); - 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 signedTx = await signer.signTransaction({ - ...rawTx, - gasLimit: toHex(toBN(gasLimit).add(toBN(10000))), - } as any); - response.push(await this.rpcService.web3.eth.sendSignedTransaction(signedTx.rawTransaction)); - } - - return { - success: true, - error: null, - data: response, - }; - } catch (error: any) { - return { success: false, error, data: null }; - } - } public async getBlockTimeByBlockNumber(blockNumber: number) { return this.rpcService.web3.eth.getBlock(blockNumber); @@ -204,16 +167,17 @@ export class ETHBridgeContract extends DefaultContract { public getValidatorThreshold() { return this.call('threshold', []); } - public async whitelistToken(tokenAddress: string): Promise<{ isWhitelisted: boolean; tokenAddress: string }> { - const isWhitelisted = await this.call('check', [tokenAddress]); + public async whitelistToken( + tokenAddress: string, + ): Promise<{ isWhitelisted: boolean; tokenAddress: string; txHash?: string }> { + const isWhitelisted = await this.call('whitelistTokens', [tokenAddress]); if (isWhitelisted) { return { isWhitelisted: true, tokenAddress }; } - const res = await this.write('whitelistToken', [tokenAddress]); + const res = await this.write('setWhitelistToken', [tokenAddress, true], process.env[EEnvKey.ADMIN_EVM_PRIVATE_KEY]); // pooling tx hash await this.pollingTxStatus(res.transactionHash); - - return { isWhitelisted: true, tokenAddress }; + return { isWhitelisted: true, tokenAddress, txHash: res.transactionHash }; } public mintNFT(toAddress: string) { return this.write('mint', [toAddress]); @@ -242,4 +206,12 @@ export class ETHBridgeContract extends DefaultContract { } } -export class Erc20Contract extends DefaultContract {} +export class Erc20ContractTemplate { + constructor(private readonly rpcETHService: IRpcService) {} + private getErc20Contract(address: string) { + return new DefaultContract(this.rpcETHService, Erc20Abi, address, 0); + } + public getTokenSymbol(address: string) { + return this.getErc20Contract(address).call('symbol', []); + } +} From 1d87b158fd681c10276ab868624fa2fc2712c925 Mon Sep 17 00:00:00 2001 From: Kenneth Hoang Date: Tue, 7 Jan 2025 16:17:13 +0700 Subject: [PATCH 07/26] feat: job priority --- src/constants/queue.constant.ts | 6 ++++++ src/modules/crawler/deploy-token.ts | 10 ++++++++-- src/modules/crawler/job-unlock.provider.ts | 8 +++++++- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/constants/queue.constant.ts b/src/constants/queue.constant.ts index a798719..902d627 100644 --- a/src/constants/queue.constant.ts +++ b/src/constants/queue.constant.ts @@ -5,3 +5,9 @@ export enum EQueueName { } export const getEvmValidatorQueueName = (index: number) => `EVM_VALIDATOR_${index}`; export const getMinaValidatorQueueName = (index: number) => `MINA_VALIDATOR_${index}`; + +// job priority, lower index is higher priority +export enum EJobPriority { + DEPLOY_TOKEN, + UNLOCK, +} diff --git a/src/modules/crawler/deploy-token.ts b/src/modules/crawler/deploy-token.ts index 3c5263f..f3872e1 100644 --- a/src/modules/crawler/deploy-token.ts +++ b/src/modules/crawler/deploy-token.ts @@ -7,7 +7,7 @@ import { IsNull } from 'typeorm'; import { ETokenPairStatus } from '../../constants/blockchain.constant.js'; import { EEnvKey } from '../../constants/env.constant.js'; -import { EQueueName } from '../../constants/queue.constant.js'; +import { EJobPriority, EQueueName } from '../../constants/queue.constant.js'; import { CommonConfigRepository } from '../../database/repositories/common-configuration.repository.js'; import { LoggerService } from '../../shared/modules/logger/logger.service.js'; import { QueueService } from '../../shared/modules/queue/queue.service.js'; @@ -148,6 +148,7 @@ export class TokenDeployer { jobId: `deploy-token-${tokenPairId}`, removeOnComplete: true, removeOnFail: true, + priority: EJobPriority.DEPLOY_TOKEN, }, ); } @@ -160,7 +161,12 @@ export class TokenDeployer { tokenPairId, }, }, - { jobId: `deploy-token-${tokenPairId}`, removeOnComplete: true, removeOnFail: true }, + { + jobId: `deploy-token-${tokenPairId}`, + removeOnComplete: true, + removeOnFail: true, + priority: EJobPriority.DEPLOY_TOKEN, + }, ); } } diff --git a/src/modules/crawler/job-unlock.provider.ts b/src/modules/crawler/job-unlock.provider.ts index dc6d150..f8a4515 100644 --- a/src/modules/crawler/job-unlock.provider.ts +++ b/src/modules/crawler/job-unlock.provider.ts @@ -7,7 +7,12 @@ import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity import { ENetworkName } from '../../constants/blockchain.constant.js'; import { EEnvKey } from '../../constants/env.constant.js'; -import { EQueueName, getEvmValidatorQueueName, getMinaValidatorQueueName } from '../../constants/queue.constant.js'; +import { + EJobPriority, + 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'; @@ -188,6 +193,7 @@ export class JobUnlockProvider { jobId: `send-unlock-${data.eventLogId}`, removeOnComplete: true, removeOnFail: true, + priority: EJobPriority.UNLOCK, }, ); } From f6271f3641adf79dd50c34121fae0516cb67ec50 Mon Sep 17 00:00:00 2001 From: Kenneth Hoang Date: Tue, 7 Jan 2025 16:18:05 +0700 Subject: [PATCH 08/26] feat: default value token visibility --- src/modules/users/admin.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/modules/users/admin.service.ts b/src/modules/users/admin.service.ts index 50a9eea..cbf316b 100644 --- a/src/modules/users/admin.service.ts +++ b/src/modules/users/admin.service.ts @@ -40,6 +40,7 @@ export class AdminService { newCommonConfig.mintingFee = payload.mintingFee; newCommonConfig.unlockingFee = payload.unlockingFee; newCommonConfig.status = ETokenPairStatus.CREATED; + newCommonConfig.isHidden = true; return newCommonConfig.save(); }); assert(isNumber(newTokenPair.id), 'Token pair invalid!'); From 0d7b34f2fefe192fa5e679413a48ac6089ee9916 Mon Sep 17 00:00:00 2001 From: Kenneth Hoang Date: Tue, 7 Jan 2025 16:41:21 +0700 Subject: [PATCH 09/26] feat: api check token exist --- src/config/common.config.ts | 14 +- src/modules/users/admin.controller.ts | 9 +- src/modules/users/admin.service.ts | 24 +- src/modules/users/dto/admin-request.dto.ts | 5 - src/shared/modules/web3/abis/erc-20.js | 266 ++++++++++----------- src/shared/modules/web3/web3.module.ts | 14 +- src/shared/modules/web3/web3.service.ts | 7 +- 7 files changed, 184 insertions(+), 155 deletions(-) diff --git a/src/config/common.config.ts b/src/config/common.config.ts index 044ac29..9143b97 100644 --- a/src/config/common.config.ts +++ b/src/config/common.config.ts @@ -2,7 +2,7 @@ import { ConfigService } from '@nestjs/config'; 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'; +import { Erc20ContractTemplate, ETHBridgeContract } from '../shared/modules/web3/web3.service.js'; async function createRpcService(configService: ConfigService) { return await RpcFactory(configService); @@ -18,8 +18,11 @@ function getEthBridgeAddress(configService: ConfigService) { function getEthBridgeStartBlock(configService: ConfigService) { return +configService.get(EEnvKey.ETH_BRIDGE_START_BLOCK)!; } - -async function initializeEthContract(configService: ConfigService) { +export interface IRpcInit { + bridgeContract: ETHBridgeContract; + erc20Template: Erc20ContractTemplate; +} +async function initializeEthContract(configService: ConfigService): Promise { const [rpcEthService, address, _startBlock] = await Promise.all([ createRpcEthService(configService), getEthBridgeAddress(configService), @@ -27,6 +30,9 @@ async function initializeEthContract(configService: ConfigService) { ]); // Instantiate the ETHBridgeContract with the resolved dependencies - return new ETHBridgeContract(rpcEthService, address!, _startBlock); + return { + bridgeContract: new ETHBridgeContract(rpcEthService, address!, _startBlock), + erc20Template: new Erc20ContractTemplate(rpcEthService), + }; } export { createRpcService, createRpcEthService, getEthBridgeAddress, getEthBridgeStartBlock, initializeEthContract }; diff --git a/src/modules/users/admin.controller.ts b/src/modules/users/admin.controller.ts index d597171..ff812bb 100644 --- a/src/modules/users/admin.controller.ts +++ b/src/modules/users/admin.controller.ts @@ -2,7 +2,6 @@ import { Body, Controller, Get, Param, Post, Put, Query, UseGuards } from '@nest import { AuthGuard } from '@nestjs/passport'; import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; -import { GuardPublic } from '../../guards/guard.decorator.js'; import { AuthAdminGuard } from '../../shared/decorators/http.decorator.js'; import { AdminService } from './admin.service.js'; import { CreateTokenReqDto } from './dto/admin-request.dto.js'; @@ -29,13 +28,18 @@ export class AdminController { } @Get('tokens') - @GuardPublic() @AuthAdminGuard() @UseGuards(AuthGuard('jwt')) @ApiOkResponse({ type: GetCommonConfigResponseDto }) getCommonConfig(@Query() query: GetTokensReqDto) { return this.adminService.getListToken(query); } + @Get('check/:tokenAddress') + @AuthAdminGuard() + @UseGuards(AuthGuard('jwt')) + checkTokenExist(@Param('tokenAddress') tokenAddress: string) { + return this.adminService.checkTokenExist(tokenAddress); + } @Put('token/:id') @AuthAdminGuard() @@ -54,7 +58,6 @@ export class AdminController { @Post('new-token') @AuthAdminGuard() @UseGuards(AuthGuard('jwt')) - @GuardPublic() addNewToken(@Body() payload: CreateTokenReqDto) { return this.adminService.createNewToken(payload); } diff --git a/src/modules/users/admin.service.ts b/src/modules/users/admin.service.ts index cbf316b..a5f42fa 100644 --- a/src/modules/users/admin.service.ts +++ b/src/modules/users/admin.service.ts @@ -1,14 +1,17 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import assert from 'assert'; -import { isNumber } from 'class-validator'; +import { isNotEmpty, isNumber } from 'class-validator'; import { DataSource, EntityManager } from 'typeorm'; import { ENetworkName, ETokenPairStatus } from '../../constants/blockchain.constant.js'; import { EEnvKey } from '../../constants/env.constant.js'; +import { EError } from '../../constants/error.constant.js'; import { toPageDto } from '../../core/paginate-typeorm.js'; import { CommonConfigRepository } from '../../database/repositories/common-configuration.repository.js'; import { TokenDeployer } from '../../modules/crawler/deploy-token.js'; +import { httpBadRequest } from '../../shared/exceptions/http-exeption.js'; +import { Erc20ContractTemplate } from '../../shared/modules/web3/web3.service.js'; import { CommonConfig } from '../crawler/entities/common-config.entity.js'; import { CreateTokenReqDto } from './dto/admin-request.dto.js'; import { GetTokensReqDto } from './dto/user-request.dto.js'; @@ -20,19 +23,28 @@ export class AdminService { private readonly configService: ConfigService, private readonly commonConfigRepo: CommonConfigRepository, private readonly tokenDeployerService: TokenDeployer, + private readonly erc20ContractTemplate: Erc20ContractTemplate, ) {} async createNewToken(payload: CreateTokenReqDto) { - // get erc20 metadata: decimal... + if (await this.checkTokenExist(payload.assetAddress)) { + httpBadRequest(EError.DUPLICATED_ACTION); + } + const [symbol, decimals] = await Promise.all([ + this.erc20ContractTemplate.getTokenSymbol(payload.assetAddress), + this.erc20ContractTemplate.getTokenDecimals(payload.assetAddress).then(res => +res), + ]); + assert(isNotEmpty(symbol), 'symbol invalid'); + assert(isNumber(decimals) && decimals > 0, 'decimals invalid'); const newTokenPair = await this.dataSource.transaction(async (e: EntityManager) => { const commonConfigRepo = e.getRepository(CommonConfig); // move create pair logic to helpers const newCommonConfig = commonConfigRepo.create(); - newCommonConfig.asset = payload.assetName; + newCommonConfig.asset = symbol; newCommonConfig.fromAddress = payload.assetAddress; newCommonConfig.dailyQuota = +payload.dailyQuota; newCommonConfig.fromChain = ENetworkName.ETH; newCommonConfig.toChain = ENetworkName.MINA; - newCommonConfig.fromDecimal = 18; // get from network + newCommonConfig.fromDecimal = decimals; // get from network newCommonConfig.toDecimal = 9; newCommonConfig.fromScAddress = this.configService.get(EEnvKey.ETH_BRIDGE_CONTRACT_ADDRESS)!; newCommonConfig.toScAddress = this.configService.get(EEnvKey.MINA_BRIDGE_CONTRACT_ADDRESS)!; @@ -47,6 +59,10 @@ export class AdminService { await this.tokenDeployerService.addJobDeployTokenMina(newTokenPair.id); return newTokenPair; } + async checkTokenExist(fromAddress: string) { + const isExist = await this.commonConfigRepo.findOneBy({ fromAddress }); + return !!isExist; + } async getListToken(payload: GetTokensReqDto) { const [tokens, count] = await this.commonConfigRepo.getManyAndPagination(payload); return toPageDto(tokens, payload, count); diff --git a/src/modules/users/dto/admin-request.dto.ts b/src/modules/users/dto/admin-request.dto.ts index 0f5a6d8..c0d6453 100644 --- a/src/modules/users/dto/admin-request.dto.ts +++ b/src/modules/users/dto/admin-request.dto.ts @@ -6,11 +6,6 @@ export class CreateTokenReqDto { }) assetAddress: string; - @StringField({ - default: 'ETH', - }) - assetName: string; - @StringField({ minimum: 0, number: true, diff --git a/src/shared/modules/web3/abis/erc-20.js b/src/shared/modules/web3/abis/erc-20.js index d397f32..031b30d 100644 --- a/src/shared/modules/web3/abis/erc-20.js +++ b/src/shared/modules/web3/abis/erc-20.js @@ -1,189 +1,189 @@ -export default [ +export const Erc20ABI = `[ { - inputs: [ - { internalType: 'string', name: 'name_', type: 'string' }, - { internalType: 'string', name: 'symbol_', type: 'string' }, - { internalType: 'uint8', name: 'decimals_', type: 'uint8' }, + "inputs": [ + { "internalType": "string", "name": "name_", "type": "string" }, + { "internalType": "string", "name": "symbol_", "type": "string" }, + { "internalType": "uint8", "name": "decimals_", "type": "uint8" } ], - stateMutability: 'nonpayable', - type: 'constructor', + "stateMutability": "nonpayable", + "type": "constructor" }, { - inputs: [ - { internalType: 'address', name: 'spender', type: 'address' }, - { internalType: 'uint256', name: 'allowance', type: 'uint256' }, - { internalType: 'uint256', name: 'needed', type: 'uint256' }, + "inputs": [ + { "internalType": "address", "name": "spender", "type": "address" }, + { "internalType": "uint256", "name": "allowance", "type": "uint256" }, + { "internalType": "uint256", "name": "needed", "type": "uint256" } ], - name: 'ERC20InsufficientAllowance', - type: 'error', + "name": "ERC20InsufficientAllowance", + "type": "error" }, { - inputs: [ - { internalType: 'address', name: 'sender', type: 'address' }, - { internalType: 'uint256', name: 'balance', type: 'uint256' }, - { internalType: 'uint256', name: 'needed', type: 'uint256' }, + "inputs": [ + { "internalType": "address", "name": "sender", "type": "address" }, + { "internalType": "uint256", "name": "balance", "type": "uint256" }, + { "internalType": "uint256", "name": "needed", "type": "uint256" } ], - name: 'ERC20InsufficientBalance', - type: 'error', + "name": "ERC20InsufficientBalance", + "type": "error" }, { - inputs: [{ internalType: 'address', name: 'approver', type: 'address' }], - name: 'ERC20InvalidApprover', - type: 'error', + "inputs": [{ "internalType": "address", "name": "approver", "type": "address" }], + "name": "ERC20InvalidApprover", + "type": "error" }, { - inputs: [{ internalType: 'address', name: 'receiver', type: 'address' }], - name: 'ERC20InvalidReceiver', - type: 'error', + "inputs": [{ "internalType": "address", "name": "receiver", "type": "address" }], + "name": "ERC20InvalidReceiver", + "type": "error" }, { - inputs: [{ internalType: 'address', name: 'sender', type: 'address' }], - name: 'ERC20InvalidSender', - type: 'error', + "inputs": [{ "internalType": "address", "name": "sender", "type": "address" }], + "name": "ERC20InvalidSender", + "type": "error" }, { - inputs: [{ internalType: 'address', name: 'spender', type: 'address' }], - name: 'ERC20InvalidSpender', - type: 'error', + "inputs": [{ "internalType": "address", "name": "spender", "type": "address" }], + "name": "ERC20InvalidSpender", + "type": "error" }, { - inputs: [{ internalType: 'address', name: 'owner', type: 'address' }], - name: 'OwnableInvalidOwner', - type: 'error', + "inputs": [{ "internalType": "address", "name": "owner", "type": "address" }], + "name": "OwnableInvalidOwner", + "type": "error" }, { - inputs: [{ internalType: 'address', name: 'account', type: 'address' }], - name: 'OwnableUnauthorizedAccount', - type: 'error', + "inputs": [{ "internalType": "address", "name": "account", "type": "address" }], + "name": "OwnableUnauthorizedAccount", + "type": "error" }, { - anonymous: false, - inputs: [ - { indexed: true, internalType: 'address', name: 'owner', type: 'address' }, - { indexed: true, internalType: 'address', name: 'spender', type: 'address' }, - { indexed: false, internalType: 'uint256', name: 'value', type: 'uint256' }, + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "address", "name": "owner", "type": "address" }, + { "indexed": true, "internalType": "address", "name": "spender", "type": "address" }, + { "indexed": false, "internalType": "uint256", "name": "value", "type": "uint256" } ], - name: 'Approval', - type: 'event', + "name": "Approval", + "type": "event" }, { - anonymous: false, - inputs: [ - { indexed: true, internalType: 'address', name: 'previousOwner', type: 'address' }, - { indexed: true, internalType: 'address', name: 'newOwner', type: 'address' }, + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "address", "name": "previousOwner", "type": "address" }, + { "indexed": true, "internalType": "address", "name": "newOwner", "type": "address" } ], - name: 'OwnershipTransferred', - type: 'event', + "name": "OwnershipTransferred", + "type": "event" }, { - anonymous: false, - inputs: [ - { indexed: true, internalType: 'address', name: 'from', type: 'address' }, - { indexed: true, internalType: 'address', name: 'to', type: 'address' }, - { indexed: false, internalType: 'uint256', name: 'value', type: 'uint256' }, + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "address", "name": "from", "type": "address" }, + { "indexed": true, "internalType": "address", "name": "to", "type": "address" }, + { "indexed": false, "internalType": "uint256", "name": "value", "type": "uint256" } ], - name: 'Transfer', - type: 'event', + "name": "Transfer", + "type": "event" }, { - inputs: [ - { internalType: 'address', name: 'owner', type: 'address' }, - { internalType: 'address', name: 'spender', type: 'address' }, + "inputs": [ + { "internalType": "address", "name": "owner", "type": "address" }, + { "internalType": "address", "name": "spender", "type": "address" } ], - name: 'allowance', - outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], - stateMutability: 'view', - type: 'function', + "name": "allowance", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" }, { - inputs: [ - { internalType: 'address', name: 'spender', type: 'address' }, - { internalType: 'uint256', name: 'value', type: 'uint256' }, + "inputs": [ + { "internalType": "address", "name": "spender", "type": "address" }, + { "internalType": "uint256", "name": "value", "type": "uint256" } ], - name: 'approve', - outputs: [{ internalType: 'bool', name: '', type: 'bool' }], - stateMutability: 'nonpayable', - type: 'function', + "name": "approve", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" }, { - inputs: [{ internalType: 'address', name: 'account', type: 'address' }], - name: 'balanceOf', - outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], - stateMutability: 'view', - type: 'function', + "inputs": [{ "internalType": "address", "name": "account", "type": "address" }], + "name": "balanceOf", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" }, { - inputs: [], - name: 'decimals', - outputs: [{ internalType: 'uint8', name: '', type: 'uint8' }], - stateMutability: 'view', - type: 'function', + "inputs": [], + "name": "decimals", + "outputs": [{ "internalType": "uint8", "name": "", "type": "uint8" }], + "stateMutability": "view", + "type": "function" }, { - inputs: [ - { internalType: 'address', name: 'to', type: 'address' }, - { internalType: 'uint256', name: 'amount', type: 'uint256' }, + "inputs": [ + { "internalType": "address", "name": "to", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } ], - name: 'mint', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', + "name": "mint", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" }, { - inputs: [], - name: 'name', - outputs: [{ internalType: 'string', name: '', type: 'string' }], - stateMutability: 'view', - type: 'function', + "inputs": [], + "name": "name", + "outputs": [{ "internalType": "string", "name": "", "type": "string" }], + "stateMutability": "view", + "type": "function" }, { - inputs: [], - name: 'owner', - outputs: [{ internalType: 'address', name: '', type: 'address' }], - stateMutability: 'view', - type: 'function', + "inputs": [], + "name": "owner", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" }, - { inputs: [], name: 'renounceOwnership', outputs: [], stateMutability: 'nonpayable', type: 'function' }, + { "inputs": [], "name": "renounceOwnership", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { - inputs: [], - name: 'symbol', - outputs: [{ internalType: 'string', name: '', type: 'string' }], - stateMutability: 'view', - type: 'function', + "inputs": [], + "name": "symbol", + "outputs": [{ "internalType": "string", "name": "", "type": "string" }], + "stateMutability": "view", + "type": "function" }, { - inputs: [], - name: 'totalSupply', - outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], - stateMutability: 'view', - type: 'function', + "inputs": [], + "name": "totalSupply", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" }, { - inputs: [ - { internalType: 'address', name: 'to', type: 'address' }, - { internalType: 'uint256', name: 'value', type: 'uint256' }, + "inputs": [ + { "internalType": "address", "name": "to", "type": "address" }, + { "internalType": "uint256", "name": "value", "type": "uint256" } ], - name: 'transfer', - outputs: [{ internalType: 'bool', name: '', type: 'bool' }], - stateMutability: 'nonpayable', - type: 'function', + "name": "transfer", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" }, { - inputs: [ - { internalType: 'address', name: 'from', type: 'address' }, - { internalType: 'address', name: 'to', type: 'address' }, - { internalType: 'uint256', name: 'value', type: 'uint256' }, + "inputs": [ + { "internalType": "address", "name": "from", "type": "address" }, + { "internalType": "address", "name": "to", "type": "address" }, + { "internalType": "uint256", "name": "value", "type": "uint256" } ], - name: 'transferFrom', - outputs: [{ internalType: 'bool', name: '', type: 'bool' }], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [{ internalType: 'address', name: 'newOwner', type: 'address' }], - name: 'transferOwnership', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, -]; + "name": "transferFrom", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "address", "name": "newOwner", "type": "address" }], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +]`; diff --git a/src/shared/modules/web3/web3.module.ts b/src/shared/modules/web3/web3.module.ts index 869cd44..22dcc72 100644 --- a/src/shared/modules/web3/web3.module.ts +++ b/src/shared/modules/web3/web3.module.ts @@ -4,7 +4,7 @@ import { ConfigService } from '@nestjs/config'; // @ts-ignore import Web3 from 'web3/lib/index.js'; -import { initializeEthContract } from '../../../config/common.config.js'; +import { initializeEthContract, IRpcInit } from '../../../config/common.config.js'; import { EEnvKey } from '../../../constants/env.constant.js'; import { ASYNC_CONNECTION } from '../../../constants/service.constant.js'; import { sleep } from '../../utils/promise.js'; @@ -24,12 +24,18 @@ import { Erc20ContractTemplate, ETHBridgeContract } from './web3.service.js'; }, { provide: ETHBridgeContract, - useFactory: (connection: ETHBridgeContract) => { - return connection; + useFactory: ({ bridgeContract }: IRpcInit) => { + return bridgeContract; + }, + inject: [ASYNC_CONNECTION], + }, + { + provide: Erc20ContractTemplate, + useFactory: ({ erc20Template }: IRpcInit) => { + return erc20Template; }, inject: [ASYNC_CONNECTION], }, - Erc20ContractTemplate, ], exports: [Web3Module, ETHBridgeContract, Erc20ContractTemplate], }) diff --git a/src/shared/modules/web3/web3.service.ts b/src/shared/modules/web3/web3.service.ts index 82ad648..342938d 100644 --- a/src/shared/modules/web3/web3.service.ts +++ b/src/shared/modules/web3/web3.service.ts @@ -6,7 +6,7 @@ import pkg from 'web3-utils'; import { EEnvKey } from '../../../constants/env.constant.js'; import { sleep } from '../../utils/promise.js'; -import Erc20Abi from './abis/erc-20.js'; +import { Erc20ABI } from './abis/erc-20.js'; import { EthBridgeAbi } from './abis/eth-bridge-contract.js'; import { IRpcService } from './web3.module.js'; @@ -209,9 +209,12 @@ export class ETHBridgeContract extends DefaultContract { export class Erc20ContractTemplate { constructor(private readonly rpcETHService: IRpcService) {} private getErc20Contract(address: string) { - return new DefaultContract(this.rpcETHService, Erc20Abi, address, 0); + return new DefaultContract(this.rpcETHService, Erc20ABI, address, 0); } public getTokenSymbol(address: string) { return this.getErc20Contract(address).call('symbol', []); } + public getTokenDecimals(address: string) { + return this.getErc20Contract(address).call('decimals', []); + } } From d48db829022f4ea977badc7d9c7430de49ba297d Mon Sep 17 00:00:00 2001 From: Kenneth Hoang Date: Tue, 7 Jan 2025 16:49:52 +0700 Subject: [PATCH 10/26] refactor: error handling --- src/constants/error.constant.ts | 1 + src/modules/users/admin.service.ts | 13 ++++++++++++- src/shared/exceptions/http-exeption.ts | 8 ++++---- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/constants/error.constant.ts b/src/constants/error.constant.ts index e7ebaf8..adf0dfd 100644 --- a/src/constants/error.constant.ts +++ b/src/constants/error.constant.ts @@ -1,6 +1,7 @@ export enum EError { OTHER_SYSTEM_ERROR = 'OTHER_SYSTEM_ERROR', FORBIDDEN_RESOURCE = 'FORBIDDEN_RESOURCE', + ACTION_CANNOT_PROCESSED = 'ACTION_CANNOT_PROCESSED', DUPLICATED_ACTION = 'DUPLICATED_ACTION', USER_NOT_FOUND = 'USER_NOT_FOUND', WRONG_EMAIL_OR_PASS = 'WRONG_EMAIL_OR_PASS', diff --git a/src/modules/users/admin.service.ts b/src/modules/users/admin.service.ts index a5f42fa..8380e5b 100644 --- a/src/modules/users/admin.service.ts +++ b/src/modules/users/admin.service.ts @@ -27,7 +27,7 @@ export class AdminService { ) {} async createNewToken(payload: CreateTokenReqDto) { if (await this.checkTokenExist(payload.assetAddress)) { - httpBadRequest(EError.DUPLICATED_ACTION); + return httpBadRequest(EError.DUPLICATED_ACTION); } const [symbol, decimals] = await Promise.all([ this.erc20ContractTemplate.getTokenSymbol(payload.assetAddress), @@ -67,4 +67,15 @@ export class AdminService { const [tokens, count] = await this.commonConfigRepo.getManyAndPagination(payload); return toPageDto(tokens, payload, count); } + async redeployToken(tokenPairId: number) { + const tokenInfo = await this.commonConfigRepo.findOneBy({ id: tokenPairId }); + if (!tokenInfo) { + httpBadRequest(EError.RESOURCE_NOT_FOUND); + } + if (tokenInfo.status !== ETokenPairStatus.DEPLOY_FAILED) { + return httpBadRequest(EError.ACTION_CANNOT_PROCESSED); + } + + await this.tokenDeployerService.deployTokenMina(tokenInfo.id); + } } diff --git a/src/shared/exceptions/http-exeption.ts b/src/shared/exceptions/http-exeption.ts index eed3ba3..dcc7d30 100644 --- a/src/shared/exceptions/http-exeption.ts +++ b/src/shared/exceptions/http-exeption.ts @@ -9,7 +9,7 @@ import { import { EError } from '../../constants/error.constant.js'; // 400 -export function httpBadRequest(errorCode: EError, metaData?: object) { +export function httpBadRequest(errorCode: EError, metaData?: object): never { throw new BadRequestException({ statusCode: 400, errorCode, @@ -18,7 +18,7 @@ export function httpBadRequest(errorCode: EError, metaData?: object) { } // 401 -export function httpUnAuthorized(errorCode?: EError, metaData?: object) { +export function httpUnAuthorized(errorCode?: EError, metaData?: object): never { throw new UnauthorizedException({ statusCode: 401, errorCode: errorCode || EError.UNAUTHORIZED, @@ -27,7 +27,7 @@ export function httpUnAuthorized(errorCode?: EError, metaData?: object) { } // 403 -export function httpForbidden(errorCode?: EError, metaData?: object) { +export function httpForbidden(errorCode?: EError, metaData?: object): never { throw new ForbiddenException({ statusCode: 403, errorCode: errorCode || EError.FORBIDDEN_RESOURCE, @@ -36,7 +36,7 @@ export function httpForbidden(errorCode?: EError, metaData?: object) { } // 404 -export function httpNotFound(errorCode: EError, metaData?: object) { +export function httpNotFound(errorCode: EError, metaData?: object): never { throw new NotFoundException({ statusCode: 404, errorCode, From ba328aaf00409743523df749c5fa522e8645163f Mon Sep 17 00:00:00 2001 From: Kenneth Hoang Date: Tue, 7 Jan 2025 16:58:08 +0700 Subject: [PATCH 11/26] feat: api redeploy --- src/modules/users/admin.controller.ts | 9 +++++++++ src/modules/users/admin.service.ts | 5 +++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/modules/users/admin.controller.ts b/src/modules/users/admin.controller.ts index ff812bb..93e462e 100644 --- a/src/modules/users/admin.controller.ts +++ b/src/modules/users/admin.controller.ts @@ -2,6 +2,7 @@ import { Body, Controller, Get, Param, Post, Put, Query, UseGuards } from '@nest import { AuthGuard } from '@nestjs/passport'; import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; +import { GuardPublic } from '../../guards/guard.decorator.js'; import { AuthAdminGuard } from '../../shared/decorators/http.decorator.js'; import { AdminService } from './admin.service.js'; import { CreateTokenReqDto } from './dto/admin-request.dto.js'; @@ -55,6 +56,14 @@ export class AdminController { return this.userService.updateTokenVisibility(id, updateConfig); } + @Post('token/re-deploy/:id') + // @AuthAdminGuard() + @GuardPublic() + // @UseGuards(AuthGuard('jwt')) + redeployToken(@Param('id') id: number) { + return this.adminService.redeployToken(id); + } + @Post('new-token') @AuthAdminGuard() @UseGuards(AuthGuard('jwt')) diff --git a/src/modules/users/admin.service.ts b/src/modules/users/admin.service.ts index 8380e5b..29a6bc8 100644 --- a/src/modules/users/admin.service.ts +++ b/src/modules/users/admin.service.ts @@ -75,7 +75,8 @@ export class AdminService { if (tokenInfo.status !== ETokenPairStatus.DEPLOY_FAILED) { return httpBadRequest(EError.ACTION_CANNOT_PROCESSED); } - - await this.tokenDeployerService.deployTokenMina(tokenInfo.id); + await this.tokenDeployerService.addJobDeployTokenMina(tokenInfo.id); + tokenInfo.status = ETokenPairStatus.DEPLOYING; + return tokenInfo.save(); } } From 668686e544d6953267d52912756cffda798897a4 Mon Sep 17 00:00:00 2001 From: Kenneth Hoang Date: Tue, 7 Jan 2025 16:59:03 +0700 Subject: [PATCH 12/26] feat: add guard admin --- src/modules/users/admin.controller.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/modules/users/admin.controller.ts b/src/modules/users/admin.controller.ts index 93e462e..5e4fff7 100644 --- a/src/modules/users/admin.controller.ts +++ b/src/modules/users/admin.controller.ts @@ -57,9 +57,8 @@ export class AdminController { } @Post('token/re-deploy/:id') - // @AuthAdminGuard() - @GuardPublic() - // @UseGuards(AuthGuard('jwt')) + @AuthAdminGuard() + @UseGuards(AuthGuard('jwt')) redeployToken(@Param('id') id: number) { return this.adminService.redeployToken(id); } From 3f3a46aefe88365466d401cd07cd20a44e475210 Mon Sep 17 00:00:00 2001 From: Kenneth Hoang Date: Wed, 8 Jan 2025 09:19:19 +0700 Subject: [PATCH 13/26] fix: remove hardcode keys --- src/modules/crawler/deploy-token.ts | 4 ++-- src/modules/users/admin.controller.ts | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/modules/crawler/deploy-token.ts b/src/modules/crawler/deploy-token.ts index f3872e1..75607fb 100644 --- a/src/modules/crawler/deploy-token.ts +++ b/src/modules/crawler/deploy-token.ts @@ -74,7 +74,7 @@ export class TokenDeployer { await FungibleToken.compile(); await FungibleTokenAdmin.compile(); - const feePayerPrivateKey = PrivateKey.fromBase58('EKFQWW89p2oVCd8yfM5SYVUGCiSMAByQ3yWzegLVz2cvcqMPJXgQ'); // a minter + const feePayerPrivateKey = PrivateKey.fromBase58(this.configService.get(EEnvKey.SIGNER_MINA_PRIVATE_KEY)!); // a minter const tokenPrivateKey = PrivateKey.random(); const tokenAdminContractPrivateKey = PrivateKey.random(); @@ -129,7 +129,7 @@ export class TokenDeployer { await this.tokenPairRepo.update( { id: tokenPairId, toAddress: IsNull() }, { - toScAddress: tokenPrivateKey.toPublicKey().toBase58(), + toAddress: tokenPrivateKey.toPublicKey().toBase58(), }, ); await this.addJobWhitelistTokenEth(tokenPairId); diff --git a/src/modules/users/admin.controller.ts b/src/modules/users/admin.controller.ts index 5e4fff7..ecb5bf9 100644 --- a/src/modules/users/admin.controller.ts +++ b/src/modules/users/admin.controller.ts @@ -2,7 +2,6 @@ import { Body, Controller, Get, Param, Post, Put, Query, UseGuards } from '@nest import { AuthGuard } from '@nestjs/passport'; import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; -import { GuardPublic } from '../../guards/guard.decorator.js'; import { AuthAdminGuard } from '../../shared/decorators/http.decorator.js'; import { AdminService } from './admin.service.js'; import { CreateTokenReqDto } from './dto/admin-request.dto.js'; From 223dbb15f3c275980b25819794700ae70bc7b45f Mon Sep 17 00:00:00 2001 From: Kenneth Hoang Date: Wed, 8 Jan 2025 10:58:13 +0700 Subject: [PATCH 14/26] feat: add order for get tokens api --- docker-compose.dev.yaml | 56 +++++++++---------- .../common-configuration.repository.ts | 1 + src/modules/users/admin.controller.ts | 1 + src/modules/users/users.service.ts | 3 + 4 files changed, 33 insertions(+), 28 deletions(-) diff --git a/docker-compose.dev.yaml b/docker-compose.dev.yaml index f642858..3ef248a 100644 --- a/docker-compose.dev.yaml +++ b/docker-compose.dev.yaml @@ -66,34 +66,34 @@ services: networks: - myNetwork user: node - sender-mina-2: - image: mina-bridge:1.0.0 - command: > - sh -c "npm run console sender-mina-bridge-unlock" - tty: true - restart: always - environment: - SIGNER_MINA_PRIVATE_KEY : ${SIGNER_MINA_PRIVATE_KEY_2} - depends_on: - - postgres - - redis - networks: - - myNetwork - user: node - sender-mina-3: - image: mina-bridge:1.0.0 - command: > - sh -c "npm run console sender-mina-bridge-unlock" - tty: true - restart: always - environment: - SIGNER_MINA_PRIVATE_KEY : ${SIGNER_MINA_PRIVATE_KEY_3} - depends_on: - - postgres - - redis - networks: - - myNetwork - user: node + # sender-mina-2: + # image: mina-bridge:1.0.0 + # command: > + # sh -c "npm run console sender-mina-bridge-unlock" + # tty: true + # restart: always + # environment: + # SIGNER_MINA_PRIVATE_KEY : ${SIGNER_MINA_PRIVATE_KEY_2} + # depends_on: + # - postgres + # - redis + # networks: + # - myNetwork + # user: node + # sender-mina-3: + # image: mina-bridge:1.0.0 + # command: > + # sh -c "npm run console sender-mina-bridge-unlock" + # tty: true + # restart: always + # environment: + # SIGNER_MINA_PRIVATE_KEY : ${SIGNER_MINA_PRIVATE_KEY_3} + # depends_on: + # - postgres + # - redis + # networks: + # - myNetwork + # user: node validate-evm-signature-1: image: mina-bridge:1.0.0 command: > diff --git a/src/database/repositories/common-configuration.repository.ts b/src/database/repositories/common-configuration.repository.ts index 8283ea3..9f1f0ce 100644 --- a/src/database/repositories/common-configuration.repository.ts +++ b/src/database/repositories/common-configuration.repository.ts @@ -33,6 +33,7 @@ export class CommonConfigRepository extends BaseRepository { }), ); } + qb.orderBy('id', 'DESC'); this.queryBuilderAddPagination(qb, dto); return qb.getManyAndCount(); } diff --git a/src/modules/users/admin.controller.ts b/src/modules/users/admin.controller.ts index ecb5bf9..5e4fff7 100644 --- a/src/modules/users/admin.controller.ts +++ b/src/modules/users/admin.controller.ts @@ -2,6 +2,7 @@ import { Body, Controller, Get, Param, Post, Put, Query, UseGuards } from '@nest import { AuthGuard } from '@nestjs/passport'; import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; +import { GuardPublic } from '../../guards/guard.decorator.js'; import { AuthAdminGuard } from '../../shared/decorators/http.decorator.js'; import { AdminService } from './admin.service.js'; import { CreateTokenReqDto } from './dto/admin-request.dto.js'; diff --git a/src/modules/users/users.service.ts b/src/modules/users/users.service.ts index c06a92d..5258b02 100644 --- a/src/modules/users/users.service.ts +++ b/src/modules/users/users.service.ts @@ -78,6 +78,9 @@ export class UsersService { status: ETokenPairStatus.ENABLE, isHidden: false, }, + order: { + id: 'DESC', + }, }); } From ecbde01241b5b14af9446fec3694601f26b78c37 Mon Sep 17 00:00:00 2001 From: Kenneth Hoang Date: Wed, 8 Jan 2025 15:13:26 +0700 Subject: [PATCH 15/26] fix: search list token --- src/database/repositories/common-configuration.repository.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/database/repositories/common-configuration.repository.ts b/src/database/repositories/common-configuration.repository.ts index 9f1f0ce..fa61f1f 100644 --- a/src/database/repositories/common-configuration.repository.ts +++ b/src/database/repositories/common-configuration.repository.ts @@ -23,7 +23,7 @@ export class CommonConfigRepository extends BaseRepository { qb.andWhere({ status: In(dto.statuses) }); } if (isNotEmpty(dto.assetName)) { - qb.andWhere({ asset: ILike(`${dto.assetName}%`) }); + qb.andWhere({ asset: ILike(`%${dto.assetName}%`) }); } if (isNotEmpty(dto.tokenAddress)) { qb.andWhere( From caee91f2fbc1dc3b8fc82dc157232804f6b25403 Mon Sep 17 00:00:00 2001 From: Kenneth Hoang Date: Wed, 8 Jan 2025 15:18:37 +0700 Subject: [PATCH 16/26] feat: update api tokens for users --- .../repositories/common-configuration.repository.ts | 6 +++++- src/modules/users/users.controller.ts | 6 +++--- src/modules/users/users.service.ts | 8 ++++++-- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/database/repositories/common-configuration.repository.ts b/src/database/repositories/common-configuration.repository.ts index fa61f1f..b55de5b 100644 --- a/src/database/repositories/common-configuration.repository.ts +++ b/src/database/repositories/common-configuration.repository.ts @@ -16,7 +16,7 @@ export class CommonConfigRepository extends BaseRepository { return this.createQueryBuilder(`${this.alias}`).select().getOne(); } - public getManyAndPagination(dto: GetTokensReqDto) { + public getManyAndPagination(dto: GetTokensReqDto, role: 'user' | 'admin' = 'admin') { const qb = this.createQb(); qb.select(); if (isArray(dto.statuses)) { @@ -25,6 +25,9 @@ export class CommonConfigRepository extends BaseRepository { if (isNotEmpty(dto.assetName)) { qb.andWhere({ asset: ILike(`%${dto.assetName}%`) }); } + if (role === 'user') { + qb.andWhere({ isHidden: false }); + } if (isNotEmpty(dto.tokenAddress)) { qb.andWhere( new Brackets(qb => { @@ -33,6 +36,7 @@ export class CommonConfigRepository extends BaseRepository { }), ); } + qb.orderBy('id', 'DESC'); this.queryBuilderAddPagination(qb, dto); return qb.getManyAndCount(); diff --git a/src/modules/users/users.controller.ts b/src/modules/users/users.controller.ts index 3d454fc..7af09f3 100644 --- a/src/modules/users/users.controller.ts +++ b/src/modules/users/users.controller.ts @@ -5,7 +5,7 @@ import { ETableName } from '../../constants/entity.constant.js'; import { GuardPublic } from '../../guards/guard.decorator.js'; import { EstimateBridgeRequestDto } from './dto/estimate-bridge-request.dto.js'; import { GetHistoryOfUserDto, GetHistoryOfUserResponseDto } from './dto/history-response.dto.js'; -import { GetProtocolFeeBodyDto } from './dto/user-request.dto.js'; +import { GetProtocolFeeBodyDto, GetTokensReqDto } from './dto/user-request.dto.js'; import { EstimateBridgeResponseDto, GetListTokenPairResponseDto, @@ -36,8 +36,8 @@ export class UsersController { @Get('list-supported-pairs') @GuardPublic() @ApiOkResponse({ type: [GetListTokenPairResponseDto] }) - getListTokenPair() { - return this.userService.getListTokenPair(); + getListTokenPair(@Query() query: GetTokensReqDto) { + return this.userService.getListTokenPair(query); } @Post('bridge/protocol-fee') diff --git a/src/modules/users/users.service.ts b/src/modules/users/users.service.ts index 5258b02..66a19bd 100644 --- a/src/modules/users/users.service.ts +++ b/src/modules/users/users.service.ts @@ -17,7 +17,7 @@ import { LoggerService } from '../../shared/modules/logger/logger.service.js'; import { RedisClientService } from '../../shared/modules/redis/redis-client.service.js'; import { UpdateCommonConfigBodyDto, UpdateTokenPairVisibilityReqDto } from './dto/common-config-request.dto.js'; import { GetHistoryDto, GetHistoryOfUserDto } from './dto/history-response.dto.js'; -import { GetProtocolFeeBodyDto } from './dto/user-request.dto.js'; +import { GetProtocolFeeBodyDto, GetTokensReqDto } from './dto/user-request.dto.js'; import { GetProofOfAssetsResponseDto, GetTokensPriceResponseDto } from './dto/user-response.dto.js'; @Injectable() @@ -72,7 +72,11 @@ export class UsersService { return { dailyQuota, totalAmountOfToDay: totalamount?.totalamount || 0 }; } - async getListTokenPair() { + async getListTokenPair(payload: GetTokensReqDto) { + const [data] = await this.commonConfigRepository.getManyAndPagination({ + ...payload, + statuses: [ETokenPairStatus.ENABLE], + }); return this.commonConfigRepository.find({ where: { status: ETokenPairStatus.ENABLE, From b200aeec664d7fc27cb06eed923168ba2915ddf0 Mon Sep 17 00:00:00 2001 From: Kenneth Hoang Date: Wed, 8 Jan 2025 15:22:25 +0700 Subject: [PATCH 17/26] fix: get list token users --- src/modules/users/users.service.ts | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/modules/users/users.service.ts b/src/modules/users/users.service.ts index 66a19bd..6b5c93e 100644 --- a/src/modules/users/users.service.ts +++ b/src/modules/users/users.service.ts @@ -73,19 +73,14 @@ export class UsersService { } async getListTokenPair(payload: GetTokensReqDto) { - const [data] = await this.commonConfigRepository.getManyAndPagination({ - ...payload, - statuses: [ETokenPairStatus.ENABLE], - }); - return this.commonConfigRepository.find({ - where: { - status: ETokenPairStatus.ENABLE, - isHidden: false, - }, - order: { - id: 'DESC', + const [data] = await this.commonConfigRepository.getManyAndPagination( + { + ...payload, + statuses: [ETokenPairStatus.ENABLE], }, - }); + 'user', + ); + return data; } async getProtocolFee(dto: GetProtocolFeeBodyDto) { From f23bf44b81cfc0eecb4312cc042f691b0ed9adba Mon Sep 17 00:00:00 2001 From: Kenneth Hoang Date: Wed, 8 Jan 2025 15:52:37 +0700 Subject: [PATCH 18/26] feat: update seed --- .github/workflows/auto-deploy-test.yml | 2 +- src/database/seeds/common_config.seed.ts | 5 ++++- src/modules/crawler/crawler.evmbridge.ts | 2 +- src/modules/crawler/crawler.minabridge.ts | 8 ++++---- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/.github/workflows/auto-deploy-test.yml b/.github/workflows/auto-deploy-test.yml index 958e677..bf809cc 100644 --- a/.github/workflows/auto-deploy-test.yml +++ b/.github/workflows/auto-deploy-test.yml @@ -13,6 +13,6 @@ jobs: with: node-version: v18.20.4 - run: | - echo "${{ vars.MINA_BRIDGE_BE_TEST }}" >> .env + echo "${{ secrets.MINA_BRIDGE_BE_TEST }}" >> .env docker build . -t mina-bridge:1.0.0 docker compose -f docker-compose.dev.yaml up -d --remove-orphans diff --git a/src/database/seeds/common_config.seed.ts b/src/database/seeds/common_config.seed.ts index 205e1c5..48df73e 100644 --- a/src/database/seeds/common_config.seed.ts +++ b/src/database/seeds/common_config.seed.ts @@ -3,7 +3,7 @@ import { DataSource } from 'typeorm'; import { Seeder } from 'typeorm-extension'; import { COMMOM_CONFIG_TIP, COMMON__CONFIG_DAILY_QUOTA, EAsset } from '../../constants/api.constant.js'; -import { ENetworkName } from '../../constants/blockchain.constant.js'; +import { ENetworkName, ETokenPairStatus } from '../../constants/blockchain.constant.js'; import { EEnvKey } from '../../constants/env.constant.js'; import { CommonConfig } from '../../modules/crawler/entities/common-config.entity.js'; @@ -21,12 +21,15 @@ export default class CommonConfigSeeder implements Seeder { asset: EAsset.ETH, fromAddress: '0x0000000000000000000000000000000000000000', toAddress: process.env[EEnvKey.MINA_TOKEN_BRIDGE_ADDRESS], + fromScAddress: process.env[EEnvKey.ETH_BRIDGE_CONTRACT_ADDRESS], + toScAddress: process.env[EEnvKey.MINA_BRIDGE_CONTRACT_ADDRESS], fromDecimal: 18, toDecimal: 9, fromSymbol: 'ETH', toSymbol: 'WETH', fromChain: ENetworkName.ETH, toChain: ENetworkName.MINA, + status:ETokenPairStatus.ENABLE }), ); } diff --git a/src/modules/crawler/crawler.evmbridge.ts b/src/modules/crawler/crawler.evmbridge.ts index 7172693..e7f1ac0 100644 --- a/src/modules/crawler/crawler.evmbridge.ts +++ b/src/modules/crawler/crawler.evmbridge.ts @@ -109,7 +109,7 @@ export class BlockchainEVMCrawler { networkFrom: ENetworkName.ETH, networkReceived: ENetworkName.MINA, tokenFromName: event.returnValues.tokenName, - tokenReceivedAddress: this.configService.get(EEnvKey.MINA_TOKEN_BRIDGE_ADDRESS), + tokenReceivedAddress: config.toAddress, txHashLock: event.transactionHash, receiveAddress: event.returnValues.receipt, blockNumber: event.blockNumber, diff --git a/src/modules/crawler/crawler.minabridge.ts b/src/modules/crawler/crawler.minabridge.ts index b56a0fc..1609284 100644 --- a/src/modules/crawler/crawler.minabridge.ts +++ b/src/modules/crawler/crawler.minabridge.ts @@ -113,7 +113,7 @@ export class SCBridgeMinaCrawler { }); // update total weth minted - const currentConfig = await configRepo.findOneBy({}); + const currentConfig = await configRepo.findOneBy({ toAddress: event.event.data.tokenAddress.toBase58() }); assert(currentConfig, 'comomn config not exist'); const newTotalEthMinted = new BigNumber(currentConfig.totalWethMinted).plus(existLockTx.amountReceived).toString(); @@ -142,7 +142,7 @@ export class SCBridgeMinaCrawler { } const fromTokenDecimal = this.configService.get(EEnvKey.DECIMAL_TOKEN_MINA), toTokenDecimal = this.configService.get(EEnvKey.DECIMAL_TOKEN_EVM); - const config = await configRepo.findOneBy({ fromAddress: event.event.data.tokenAddress.toBase58() }); + const config = await configRepo.findOneBy({ toAddress: event.event.data.tokenAddress.toBase58() }); assert(!!config?.bridgeFee, 'tip config undefined'); @@ -162,11 +162,11 @@ export class SCBridgeMinaCrawler { const eventUnlock: Partial = { senderAddress: JSON.parse(JSON.stringify(event.event.data.locker)), amountFrom: inputAmount, - tokenFromAddress: this.configService.get(EEnvKey.MINA_TOKEN_BRIDGE_ADDRESS), + tokenFromAddress: config.fromAddress, networkFrom: ENetworkName.MINA, networkReceived: ENetworkName.ETH, tokenFromName: EAsset.WETH, - tokenReceivedAddress: this.configService.get(EEnvKey.ETH_TOKEN_BRIDGE_ADDRESS), + tokenReceivedAddress: config.toAddress, txHashLock, receiveAddress: receiveAddress, blockNumber: +event.blockHeight.toString(), From 898597472058a46e66c56c43ff5ad0f4624acb97 Mon Sep 17 00:00:00 2001 From: Kenneth Hoang Date: Wed, 8 Jan 2025 15:54:22 +0700 Subject: [PATCH 19/26] fix: seed token --- src/database/seeds/common_config.seed.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/database/seeds/common_config.seed.ts b/src/database/seeds/common_config.seed.ts index 48df73e..fb2b175 100644 --- a/src/database/seeds/common_config.seed.ts +++ b/src/database/seeds/common_config.seed.ts @@ -29,7 +29,7 @@ export default class CommonConfigSeeder implements Seeder { toSymbol: 'WETH', fromChain: ENetworkName.ETH, toChain: ENetworkName.MINA, - status:ETokenPairStatus.ENABLE + status: ETokenPairStatus.ENABLE, }), ); } From 5553c59eaeeb5cbd4ba4b8dbc97898ca7fd44989 Mon Sep 17 00:00:00 2001 From: Kenneth Hoang Date: Wed, 8 Jan 2025 17:07:13 +0700 Subject: [PATCH 20/26] feat: update daily quota api --- src/database/repositories/event-log.repository.ts | 7 ++++++- src/modules/crawler/interfaces/job.interface.ts | 1 + src/modules/crawler/job-unlock.provider.ts | 6 +++--- src/modules/users/users.controller.ts | 6 +++--- src/modules/users/users.service.ts | 6 +++--- 5 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/database/repositories/event-log.repository.ts b/src/database/repositories/event-log.repository.ts index ef6440d..3e9691c 100644 --- a/src/database/repositories/event-log.repository.ts +++ b/src/database/repositories/event-log.repository.ts @@ -51,6 +51,7 @@ export class EventLogRepository extends BaseRepository { `${this.alias}.id as "eventLogId"`, `${this.alias}.network_received as "network"`, `${this.alias}.sender_address as "senderAddress"`, + `${this.alias}.token_received_address as "tokenReceivedAddress"`, ]); qb.leftJoin(`${this.alias}.validator`, 'signature'); @@ -157,10 +158,14 @@ export class EventLogRepository extends BaseRepository { return queryBuilder.getManyAndCount(); } - public async sumAmountBridgeOfUserInDay(address: string): Promise<{ totalamount: string }> { + public async sumAmountBridgeOfUserInDay( + address: string, + tokenReceivedAddress: string, + ): Promise<{ totalamount: 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 }) + .andWhere({ tokenReceivedAddress }) .andWhere(`${this.alias}.block_time_lock BETWEEN ${startOfDayUnix(new Date())} AND ${endOfDayUnix(new Date())}`) .groupBy(`${this.alias}.sender_address`); return qb.getRawOne() as any; diff --git a/src/modules/crawler/interfaces/job.interface.ts b/src/modules/crawler/interfaces/job.interface.ts index 26ef005..14d01ae 100644 --- a/src/modules/crawler/interfaces/job.interface.ts +++ b/src/modules/crawler/interfaces/job.interface.ts @@ -26,4 +26,5 @@ export interface IJobUnlockPayload { eventLogId: number; network: ENetworkName; senderAddress: string; + tokenReceivedAddress: string; } diff --git a/src/modules/crawler/job-unlock.provider.ts b/src/modules/crawler/job-unlock.provider.ts index f8a4515..76567e5 100644 --- a/src/modules/crawler/job-unlock.provider.ts +++ b/src/modules/crawler/job-unlock.provider.ts @@ -176,7 +176,7 @@ 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)) { + if (await this.isPassDailyQuota(data.senderAddress, data.network, data.tokenReceivedAddress)) { this.logger.warn('this tx exceed daily quota, skip until next day', data.eventLogId); await this.eventLogRepository.update(data.eventLogId, { nextSendTxJobTime: getNextDayInUnix().toString() }); return; @@ -197,13 +197,13 @@ export class JobUnlockProvider { }, ); } - private async isPassDailyQuota(address: string, networkReceived: ENetworkName): Promise { + private async isPassDailyQuota(address: string, networkReceived: ENetworkName, token: string): Promise { const fromDecimal = this.configService.get( networkReceived === ENetworkName.MINA ? EEnvKey.DECIMAL_TOKEN_EVM : EEnvKey.DECIMAL_TOKEN_MINA, ); const [dailyQuota, todayData] = await Promise.all([ await this.commonConfigRepository.getCommonConfig(), - await this.eventLogRepository.sumAmountBridgeOfUserInDay(address), + await this.eventLogRepository.sumAmountBridgeOfUserInDay(address, token), ]); assert(!!dailyQuota, 'daily quota undefined'); if ( diff --git a/src/modules/users/users.controller.ts b/src/modules/users/users.controller.ts index 7af09f3..7b41bf7 100644 --- a/src/modules/users/users.controller.ts +++ b/src/modules/users/users.controller.ts @@ -27,10 +27,10 @@ export class UsersController { return this.userService.getHistoriesOfUser(address, query); } - @Get('daily-quota/:address') + @Get('daily-quota/:address/:token') @GuardPublic() - getDailyQuota(@Param('address') address: string) { - return this.userService.getDailyQuotaOfUser(address); + getDailyQuota(@Param('address') address: string, @Param('token') token: string) { + return this.userService.getDailyQuotaOfUser(address, token); } @Get('list-supported-pairs') diff --git a/src/modules/users/users.service.ts b/src/modules/users/users.service.ts index 6b5c93e..94f1165 100644 --- a/src/modules/users/users.service.ts +++ b/src/modules/users/users.service.ts @@ -63,10 +63,10 @@ export class UsersService { return updateConfig; } - async getDailyQuotaOfUser(address: string) { + async getDailyQuotaOfUser(senderAddress: string, tokenReceivedAddress: string) { const [dailyQuota, totalamount] = await Promise.all([ this.commonConfigRepository.getCommonConfig(), - this.eventLogRepository.sumAmountBridgeOfUserInDay(address), + this.eventLogRepository.sumAmountBridgeOfUserInDay(senderAddress, tokenReceivedAddress), ]); return { dailyQuota, totalAmountOfToDay: totalamount?.totalamount || 0 }; @@ -134,7 +134,7 @@ export class UsersService { assert(config, 'invalid config, please seed the value'); const totalWethInCirculation = new BigNumber(config.totalWethMinted) .minus(config.totalWethBurnt) - .div(BigNumber(DECIMAL_BASE).pow(+this.configService.get(EEnvKey.DECIMAL_TOKEN_MINA))) + .div(BigNumber(config.fromDecimal).pow(config.toDecimal)) .toString(); return { totalWethInCirculation, From d0f89589f3d71c62a8b3e586b64bf330605cacf3 Mon Sep 17 00:00:00 2001 From: Kenneth Hoang Date: Wed, 8 Jan 2025 17:09:06 +0700 Subject: [PATCH 21/26] fix: api daily quota --- src/modules/users/users.service.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/modules/users/users.service.ts b/src/modules/users/users.service.ts index 94f1165..d140b35 100644 --- a/src/modules/users/users.service.ts +++ b/src/modules/users/users.service.ts @@ -65,7 +65,16 @@ export class UsersService { async getDailyQuotaOfUser(senderAddress: string, tokenReceivedAddress: string) { const [dailyQuota, totalamount] = await Promise.all([ - this.commonConfigRepository.getCommonConfig(), + this.commonConfigRepository.findOne({ + where: { + fromAddress: tokenReceivedAddress, + }, + select: { + fromAddress: true, + id: true, + dailyQuota: true, + }, + }), this.eventLogRepository.sumAmountBridgeOfUserInDay(senderAddress, tokenReceivedAddress), ]); From a2972e4a5b882e0b7fbeea7033d81fed2dd3725f Mon Sep 17 00:00:00 2001 From: Kenneth Hoang Date: Wed, 8 Jan 2025 17:11:49 +0700 Subject: [PATCH 22/26] fix: job unlock provider --- .../repositories/common-configuration.repository.ts | 4 ---- src/modules/crawler/job-unlock.provider.ts | 11 ++++++++++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/database/repositories/common-configuration.repository.ts b/src/database/repositories/common-configuration.repository.ts index b55de5b..baed1c8 100644 --- a/src/database/repositories/common-configuration.repository.ts +++ b/src/database/repositories/common-configuration.repository.ts @@ -12,10 +12,6 @@ import { GetTokensReqDto } from '../../modules/users/dto/user-request.dto.js'; export class CommonConfigRepository extends BaseRepository { protected alias: ETableName = ETableName.COMMON_CONFIGURATION; - public getCommonConfig() { - return this.createQueryBuilder(`${this.alias}`).select().getOne(); - } - public getManyAndPagination(dto: GetTokensReqDto, role: 'user' | 'admin' = 'admin') { const qb = this.createQb(); qb.select(); diff --git a/src/modules/crawler/job-unlock.provider.ts b/src/modules/crawler/job-unlock.provider.ts index 76567e5..05f02f7 100644 --- a/src/modules/crawler/job-unlock.provider.ts +++ b/src/modules/crawler/job-unlock.provider.ts @@ -202,7 +202,16 @@ export class JobUnlockProvider { networkReceived === ENetworkName.MINA ? EEnvKey.DECIMAL_TOKEN_EVM : EEnvKey.DECIMAL_TOKEN_MINA, ); const [dailyQuota, todayData] = await Promise.all([ - await this.commonConfigRepository.getCommonConfig(), + await this.commonConfigRepository.findOne({ + where: { + fromAddress: token, + }, + select:{ + id:true, + fromAddress:true, + dailyQuota:true + } + }), await this.eventLogRepository.sumAmountBridgeOfUserInDay(address, token), ]); assert(!!dailyQuota, 'daily quota undefined'); From 53e467f0e6d47dd4322ca5c3c04c5332529f6e54 Mon Sep 17 00:00:00 2001 From: Kenneth Hoang Date: Wed, 8 Jan 2025 17:34:56 +0700 Subject: [PATCH 23/26] fix: check daily quota --- src/modules/crawler/job-unlock.provider.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/modules/crawler/job-unlock.provider.ts b/src/modules/crawler/job-unlock.provider.ts index 05f02f7..9ba1f6f 100644 --- a/src/modules/crawler/job-unlock.provider.ts +++ b/src/modules/crawler/job-unlock.provider.ts @@ -206,11 +206,11 @@ export class JobUnlockProvider { where: { fromAddress: token, }, - select:{ - id:true, - fromAddress:true, - dailyQuota:true - } + select: { + id: true, + fromAddress: true, + dailyQuota: true, + }, }), await this.eventLogRepository.sumAmountBridgeOfUserInDay(address, token), ]); From 2c3032cce4beb62aa57219e94324bc3dcb98a81c Mon Sep 17 00:00:00 2001 From: Kenneth Hoang Date: Wed, 8 Jan 2025 17:48:07 +0700 Subject: [PATCH 24/26] feat: update total circulation --- src/modules/crawler/entities/common-config.entity.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/modules/crawler/entities/common-config.entity.ts b/src/modules/crawler/entities/common-config.entity.ts index dc5ad52..dd0484f 100644 --- a/src/modules/crawler/entities/common-config.entity.ts +++ b/src/modules/crawler/entities/common-config.entity.ts @@ -1,3 +1,4 @@ +import { BigNumber } from 'bignumber.js'; import { Column, Entity } from 'typeorm'; import { ENetworkName, ETokenPairStatus } from '../../../constants/blockchain.constant.js'; @@ -94,4 +95,13 @@ export class CommonConfig extends BaseEntityIncludeTime { super(); Object.assign(this, value); } + toJSON() { + const totalCirculation = new BigNumber(this.totalWethMinted) + .minus(this.totalWethBurnt) + .div(BigNumber(this.fromDecimal).pow(this.toDecimal)); + if (totalCirculation.lt(0)) { + return { ...this, totalCirculation: '0' }; + } + return { ...this, totalCirculation: totalCirculation.toString() }; + } } From 35cac02fa71b512e540cc43c4bb6100ee69d26cb Mon Sep 17 00:00:00 2001 From: Kenneth Hoang Date: Wed, 8 Jan 2025 17:50:00 +0700 Subject: [PATCH 25/26] fix: default value total circulation --- src/modules/crawler/entities/common-config.entity.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/modules/crawler/entities/common-config.entity.ts b/src/modules/crawler/entities/common-config.entity.ts index dd0484f..99e8ff8 100644 --- a/src/modules/crawler/entities/common-config.entity.ts +++ b/src/modules/crawler/entities/common-config.entity.ts @@ -1,4 +1,5 @@ import { BigNumber } from 'bignumber.js'; +import { isEmpty, isNotEmpty } from 'class-validator'; import { Column, Entity } from 'typeorm'; import { ENetworkName, ETokenPairStatus } from '../../../constants/blockchain.constant.js'; @@ -96,12 +97,12 @@ export class CommonConfig extends BaseEntityIncludeTime { Object.assign(this, value); } toJSON() { + if (isEmpty(this.totalWethBurnt) || isEmpty(this.totalWethBurnt)) { + return this; + } const totalCirculation = new BigNumber(this.totalWethMinted) .minus(this.totalWethBurnt) .div(BigNumber(this.fromDecimal).pow(this.toDecimal)); - if (totalCirculation.lt(0)) { - return { ...this, totalCirculation: '0' }; - } - return { ...this, totalCirculation: totalCirculation.toString() }; + return { ...this, totalCirculation: BigNumber.maximum(totalCirculation.toString(), '0') }; } } From bfef957cc3e0717061b3e3000ec3e66d98528e7e Mon Sep 17 00:00:00 2001 From: Kenneth Hoang Date: Wed, 8 Jan 2025 17:52:52 +0700 Subject: [PATCH 26/26] fix: get daily quota --- src/modules/users/users.service.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/modules/users/users.service.ts b/src/modules/users/users.service.ts index d140b35..281ac34 100644 --- a/src/modules/users/users.service.ts +++ b/src/modules/users/users.service.ts @@ -66,9 +66,12 @@ export class UsersService { async getDailyQuotaOfUser(senderAddress: string, tokenReceivedAddress: string) { const [dailyQuota, totalamount] = await Promise.all([ this.commonConfigRepository.findOne({ - where: { - fromAddress: tokenReceivedAddress, - }, + where: [ + { + fromAddress: tokenReceivedAddress, + }, + { toAddress: tokenReceivedAddress }, + ], select: { fromAddress: true, id: true,