From d8b687e86450782a7ef450c123070a5bf1c95fa3 Mon Sep 17 00:00:00 2001 From: portuu3 Date: Tue, 28 Jan 2025 09:55:10 +0100 Subject: [PATCH 01/14] job launcher balance refactor --- .../server/src/common/constant/index.ts | 5 +- .../config/environment-config.service.spec.ts | 4 +- .../server/src/common/constants/index.ts | 5 +- .../server/src/modules/job/job.controller.ts | 88 +++++++++---------- .../server/src/modules/job/job.dto.ts | 19 ++-- .../server/src/modules/job/job.interface.ts | 5 +- .../server/src/modules/job/job.service.ts | 81 ++++------------- .../src/modules/payment/payment.repository.ts | 3 + .../src/modules/payment/payment.service.ts | 19 +++- .../server/src/common/constants/networks.ts | 5 +- 10 files changed, 97 insertions(+), 137 deletions(-) diff --git a/packages/apps/fortune/exchange-oracle/server/src/common/constant/index.ts b/packages/apps/fortune/exchange-oracle/server/src/common/constant/index.ts index d083a54a22..f68848e847 100644 --- a/packages/apps/fortune/exchange-oracle/server/src/common/constant/index.ts +++ b/packages/apps/fortune/exchange-oracle/server/src/common/constant/index.ts @@ -11,10 +11,7 @@ export const TESTNET_CHAIN_IDS = [ ChainId.BSC_TESTNET, ChainId.SEPOLIA, ]; -export const MAINNET_CHAIN_IDS = [ - ChainId.POLYGON, - ChainId.BSC_MAINNET, -]; +export const MAINNET_CHAIN_IDS = [ChainId.POLYGON, ChainId.BSC_MAINNET]; export const JWT_KVSTORE_KEY = 'jwt_public_key'; export const KYC_APPROVED = 'approved'; diff --git a/packages/apps/human-app/server/src/common/config/environment-config.service.spec.ts b/packages/apps/human-app/server/src/common/config/environment-config.service.spec.ts index 2e0b9e0a68..449dd9743e 100644 --- a/packages/apps/human-app/server/src/common/config/environment-config.service.spec.ts +++ b/packages/apps/human-app/server/src/common/config/environment-config.service.spec.ts @@ -25,7 +25,9 @@ describe('EnvironmentConfigService', () => { }); it('should return an array of valid ChainIds when CHAIN_IDS_ENABLED is valid', () => { - (configService.getOrThrow as jest.Mock).mockReturnValue('1, 11155111, 80002'); + (configService.getOrThrow as jest.Mock).mockReturnValue( + '1, 11155111, 80002', + ); const result = service.chainIdsEnabled; diff --git a/packages/apps/job-launcher/server/src/common/constants/index.ts b/packages/apps/job-launcher/server/src/common/constants/index.ts index 0e8a052281..dc14428789 100644 --- a/packages/apps/job-launcher/server/src/common/constants/index.ts +++ b/packages/apps/job-launcher/server/src/common/constants/index.ts @@ -19,10 +19,7 @@ export const TESTNET_CHAIN_IDS = [ ChainId.POLYGON_AMOY, ChainId.SEPOLIA, ]; -export const MAINNET_CHAIN_IDS = [ - ChainId.BSC_MAINNET, - ChainId.POLYGON, -]; +export const MAINNET_CHAIN_IDS = [ChainId.BSC_MAINNET, ChainId.POLYGON]; export const SENDGRID_API_KEY_REGEX = /^SG\.[A-Za-z0-9-_]{22}\.[A-Za-z0-9-_]{43}$/; diff --git a/packages/apps/job-launcher/server/src/modules/job/job.controller.ts b/packages/apps/job-launcher/server/src/modules/job/job.controller.ts index 3d2e72cccf..7435683f71 100644 --- a/packages/apps/job-launcher/server/src/modules/job/job.controller.ts +++ b/packages/apps/job-launcher/server/src/modules/job/job.controller.ts @@ -27,7 +27,7 @@ import { JobDetailsDto, JobIdDto, FortuneFinalResultDto, - JobCaptchaDto, + // JobCaptchaDto, JobQuickLaunchDto, JobCancelDto, GetJobsDto, @@ -176,49 +176,49 @@ export class JobController { ); } - @ApiOperation({ - summary: 'Create a hCaptcha job', - description: 'Endpoint to create a new hCaptcha job.', - }) - @ApiBody({ type: JobCaptchaDto }) - @ApiResponse({ - status: 201, - description: 'ID of the created hCaptcha job.', - type: Number, - }) - @ApiResponse({ - status: 400, - description: 'Bad Request. Invalid input parameters.', - }) - @ApiResponse({ - status: 401, - description: 'Unauthorized. Missing or invalid credentials.', - }) - @ApiResponse({ - status: 409, - description: 'Conflict. Conflict with the current state of the server.', - }) - @Post('/hCaptcha') - public async createCaptchaJob( - @Body() data: JobCaptchaDto, - @Request() req: RequestWithUser, - ): Promise { - throw new ControlledError( - 'Hcaptcha jobs disabled temporally', - HttpStatus.UNAUTHORIZED, - ); - return await this.mutexManagerService.runExclusive( - { id: `user${req.user.id}` }, - MUTEX_TIMEOUT, - async () => { - return await this.jobService.createJob( - req.user, - JobRequestType.HCAPTCHA, - data, - ); - }, - ); - } + // @ApiOperation({ + // summary: 'Create a hCaptcha job', + // description: 'Endpoint to create a new hCaptcha job.', + // }) + // @ApiBody({ type: JobCaptchaDto }) + // @ApiResponse({ + // status: 201, + // description: 'ID of the created hCaptcha job.', + // type: Number, + // }) + // @ApiResponse({ + // status: 400, + // description: 'Bad Request. Invalid input parameters.', + // }) + // @ApiResponse({ + // status: 401, + // description: 'Unauthorized. Missing or invalid credentials.', + // }) + // @ApiResponse({ + // status: 409, + // description: 'Conflict. Conflict with the current state of the server.', + // }) + // @Post('/hCaptcha') + // public async createCaptchaJob( + // @Body() data: JobCaptchaDto, + // @Request() req: RequestWithUser, + // ): Promise { + // throw new ControlledError( + // 'Hcaptcha jobs disabled temporally', + // HttpStatus.UNAUTHORIZED, + // ); + // return await this.mutexManagerService.runExclusive( + // { id: `user${req.user.id}` }, + // MUTEX_TIMEOUT, + // async () => { + // return await this.jobService.createJob( + // req.user, + // JobRequestType.HCAPTCHA, + // data, + // ); + // }, + // ); + // } @ApiOperation({ summary: 'Get a list of jobs', diff --git a/packages/apps/job-launcher/server/src/modules/job/job.dto.ts b/packages/apps/job-launcher/server/src/modules/job/job.dto.ts index 2f2f557091..7f3e6c48a0 100644 --- a/packages/apps/job-launcher/server/src/modules/job/job.dto.ts +++ b/packages/apps/job-launcher/server/src/modules/job/job.dto.ts @@ -72,6 +72,10 @@ export class JobDto { @IsEthereumAddress() @IsOptional() public recordingOracle?: string; + + @ApiProperty({ enum: JobCurrency }) + @IsEnumCaseInsensitive(JobCurrency) + public currency: JobCurrency | undefined; } export class JobQuickLaunchDto extends JobDto { @@ -117,10 +121,6 @@ export class JobFortuneDto extends JobDto { @IsNumber() @IsPositive() public fundAmount: number; - - @ApiProperty({ enum: JobCurrency }) - @IsEnumCaseInsensitive(JobCurrency) - public currency: JobCurrency; } export class StorageDataDto { @@ -209,10 +209,6 @@ export class JobCvatDto extends JobDto { @IsNumber() @IsPositive() public fundAmount: number; - - @ApiProperty({ enum: JobCurrency }) - @IsEnumCaseInsensitive(JobCurrency) - public currency: JobCurrency; } export class JobCancelDto { @@ -790,8 +786,5 @@ class TaskData { datapoint_text?: DatapointText; } -export type CreateJob = - | JobQuickLaunchDto - | JobFortuneDto - | JobCvatDto - | JobCaptchaDto; +export type CreateJob = JobQuickLaunchDto | JobFortuneDto | JobCvatDto; +// | JobCaptchaDto; diff --git a/packages/apps/job-launcher/server/src/modules/job/job.interface.ts b/packages/apps/job-launcher/server/src/modules/job/job.interface.ts index 40d9d55535..46b9958080 100644 --- a/packages/apps/job-launcher/server/src/modules/job/job.interface.ts +++ b/packages/apps/job-launcher/server/src/modules/job/job.interface.ts @@ -1,11 +1,10 @@ import { JobRequestType } from '../../common/enums/job'; -import { CreateJob, CvatDataDto, StorageDataDto } from './job.dto'; +import { CvatDataDto, StorageDataDto } from './job.dto'; import { JobEntity } from './job.entity'; export interface RequestAction { - calculateFundAmount: (dto: CreateJob, rate: number) => Promise; createManifest: ( - dto: CreateJob, + dto: any, requestType: JobRequestType, fundAmount: number, ) => Promise; diff --git a/packages/apps/job-launcher/server/src/modules/job/job.service.ts b/packages/apps/job-launcher/server/src/modules/job/job.service.ts index 59bb117f4d..e4ef6af345 100644 --- a/packages/apps/job-launcher/server/src/modules/job/job.service.ts +++ b/packages/apps/job-launcher/server/src/modules/job/job.service.ts @@ -44,7 +44,6 @@ import { PaymentSource, PaymentStatus, PaymentType, - TokenId, } from '../../common/enums/payment'; import { parseUrl } from '../../common/utils'; import { add, div, lt, mul, max } from '../../common/utils/decimal'; @@ -445,15 +444,9 @@ export class JobService { private createJobSpecificActions: Record = { [JobRequestType.HCAPTCHA]: { - calculateFundAmount: async (dto: JobCaptchaDto, rate: number) => { - const dataUrl = generateBucketUrl(dto.data, JobRequestType.HCAPTCHA); - const objectsInBucket = await listObjectsInBucket(dataUrl); - return div(dto.annotations.taskBidPrice * objectsInBucket.length, rate); - }, createManifest: (dto: JobCaptchaDto) => this.createHCaptchaManifest(dto), }, [JobRequestType.FORTUNE]: { - calculateFundAmount: async (dto: JobFortuneDto) => dto.fundAmount, createManifest: async ( dto: JobFortuneDto, requestType: JobRequestType, @@ -465,7 +458,6 @@ export class JobService { }), }, [JobRequestType.IMAGE_POLYGONS]: { - calculateFundAmount: async (dto: JobCvatDto) => dto.fundAmount, createManifest: ( dto: JobCvatDto, requestType: JobRequestType, @@ -473,7 +465,6 @@ export class JobService { ) => this.createCvatManifest(dto, requestType, fundAmount), }, [JobRequestType.IMAGE_BOXES]: { - calculateFundAmount: async (dto: JobCvatDto) => dto.fundAmount, createManifest: ( dto: JobCvatDto, requestType: JobRequestType, @@ -481,7 +472,6 @@ export class JobService { ) => this.createCvatManifest(dto, requestType, fundAmount), }, [JobRequestType.IMAGE_POINTS]: { - calculateFundAmount: async (dto: JobCvatDto) => dto.fundAmount, createManifest: ( dto: JobCvatDto, requestType: JobRequestType, @@ -489,7 +479,6 @@ export class JobService { ) => this.createCvatManifest(dto, requestType, fundAmount), }, [JobRequestType.IMAGE_BOXES_FROM_POINTS]: { - calculateFundAmount: async (dto: JobCvatDto) => dto.fundAmount, createManifest: ( dto: JobCvatDto, requestType: JobRequestType, @@ -497,7 +486,6 @@ export class JobService { ) => this.createCvatManifest(dto, requestType, fundAmount), }, [JobRequestType.IMAGE_SKELETONS_FROM_BOXES]: { - calculateFundAmount: async (dto: JobCvatDto) => dto.fundAmount, createManifest: ( dto: JobCvatDto, requestType: JobRequestType, @@ -844,49 +832,25 @@ export class JobService { }); } - const rate = await this.rateService.getRate(Currency.USD, TokenId.HMT); - const { calculateFundAmount, createManifest } = - this.createJobSpecificActions[requestType]; + const { createManifest } = this.createJobSpecificActions[requestType]; - const userBalance = await this.paymentService.getUserBalance(user.id); const feePercentage = Number( await this.getOracleFee(this.web3Service.getOperatorAddress(), chainId), ); + const currency = dto.currency ?? JobCurrency.HMT; + const rate = await this.rateService.getRate(currency, Currency.USD); + const tokenFee = max( + div(this.serverConfigService.minimunFeeUsd, rate), + mul(div(feePercentage, 100), dto.fundAmount), + ); + const totalAmountToPay = add(dto.fundAmount, tokenFee); - let tokenFee, tokenTotalAmount, tokenFundAmount, usdTotalAmount; - - if (dto instanceof JobQuickLaunchDto) { - tokenFee = mul(div(feePercentage, 100), dto.fundAmount); - tokenFundAmount = dto.fundAmount; - tokenTotalAmount = add(tokenFundAmount, tokenFee); - usdTotalAmount = div(tokenTotalAmount, rate); - } else if ( - (dto instanceof JobFortuneDto || dto instanceof JobCvatDto) && - dto.currency === JobCurrency.HMT - ) { - tokenFundAmount = dto.fundAmount; - const fundAmountInUSD = div(tokenFundAmount, rate); - const feeInUSD = max( - this.serverConfigService.minimunFeeUsd, - mul(div(feePercentage, 100), fundAmountInUSD), - ); - tokenFee = mul(feeInUSD, rate); - tokenTotalAmount = add(tokenFundAmount, tokenFee); - usdTotalAmount = add(fundAmountInUSD, feeInUSD); - } else { - const fundAmount = await calculateFundAmount(dto, rate); - const fee = max( - this.serverConfigService.minimunFeeUsd, - mul(div(feePercentage, 100), fundAmount), - ); - - tokenFundAmount = mul(fundAmount, rate); - tokenFee = mul(fee, rate); - tokenTotalAmount = add(tokenFundAmount, tokenFee); - usdTotalAmount = add(fundAmount, fee); - } + const userBalance = await this.paymentService.getUserBalanceByCurrency( + user.id, + currency, + ); - if (lt(userBalance, usdTotalAmount)) { + if (lt(userBalance, totalAmountToPay)) { throw new ControlledError( ErrorJob.NotEnoughFunds, HttpStatus.BAD_REQUEST, @@ -916,7 +880,7 @@ export class JobService { const manifestOrigin = await createManifest( dto, requestType, - tokenFundAmount, + dto.fundAmount, ); const { url, hash } = await this.uploadManifest( @@ -936,7 +900,7 @@ export class JobService { jobEntity.userId = user.id; jobEntity.requestType = requestType; jobEntity.fee = tokenFee; - jobEntity.fundAmount = tokenFundAmount; + jobEntity.fundAmount = dto.fundAmount; jobEntity.status = JobStatus.PENDING; jobEntity.waitUntil = new Date(); @@ -947,18 +911,9 @@ export class JobService { paymentEntity.jobId = jobEntity.id; paymentEntity.source = PaymentSource.BALANCE; paymentEntity.type = PaymentType.WITHDRAWAL; - if ( - (dto instanceof JobFortuneDto || dto instanceof JobCvatDto) && - dto.currency === JobCurrency.USD - ) { - paymentEntity.amount = -usdTotalAmount; - paymentEntity.currency = JobCurrency.USD; - paymentEntity.rate = 1; - } else { - paymentEntity.amount = -tokenTotalAmount; - paymentEntity.currency = TokenId.HMT; - paymentEntity.rate = div(1, rate); - } + paymentEntity.amount = -dto.fundAmount; + paymentEntity.currency = currency; + paymentEntity.rate = rate; paymentEntity.status = PaymentStatus.SUCCEEDED; await this.paymentRepository.createUnique(paymentEntity); diff --git a/packages/apps/job-launcher/server/src/modules/payment/payment.repository.ts b/packages/apps/job-launcher/server/src/modules/payment/payment.repository.ts index cbf7b642ed..a7da7743fb 100644 --- a/packages/apps/job-launcher/server/src/modules/payment/payment.repository.ts +++ b/packages/apps/job-launcher/server/src/modules/payment/payment.repository.ts @@ -25,6 +25,7 @@ export class PaymentRepository extends BaseRepository { public async getUserBalancePayments( userId: number, + currency?: string, ): Promise { // Find negative amounts with status 'PENDING' or 'SUCCEEDED' const negativePayments = await this.find({ @@ -32,6 +33,7 @@ export class PaymentRepository extends BaseRepository { userId, amount: LessThan(0), status: In([PaymentStatus.PENDING, PaymentStatus.SUCCEEDED]), + ...(currency && { currency }), }, }); @@ -41,6 +43,7 @@ export class PaymentRepository extends BaseRepository { userId, amount: MoreThan(0), status: PaymentStatus.SUCCEEDED, + ...(currency && { currency }), }, }); diff --git a/packages/apps/job-launcher/server/src/modules/payment/payment.service.ts b/packages/apps/job-launcher/server/src/modules/payment/payment.service.ts index 38c48237a3..2e9a6961b7 100644 --- a/packages/apps/job-launcher/server/src/modules/payment/payment.service.ts +++ b/packages/apps/job-launcher/server/src/modules/payment/payment.service.ts @@ -400,7 +400,7 @@ export class PaymentService { return true; } - public async getUserBalance(userId: number): Promise { + public async getUserUSDBalance(userId: number): Promise { const paymentEntities = await this.paymentRepository.getUserBalancePayments(userId); @@ -426,6 +426,23 @@ export class PaymentService { return totalBalance; } + public async getUserBalanceByCurrency( + userId: number, + currency: string, + ): Promise { + const paymentEntities = await this.paymentRepository.getUserBalancePayments( + userId, + currency, + ); + + const balance = paymentEntities.reduce( + (sum, payment) => sum + Number(payment.amount), + 0, + ); + + return balance; + } + public async createRefundPayment(dto: PaymentRefundCreateDto) { const rate = await this.rateService.getRate(TokenId.HMT, Currency.USD); diff --git a/packages/apps/reputation-oracle/server/src/common/constants/networks.ts b/packages/apps/reputation-oracle/server/src/common/constants/networks.ts index 751dac3d9e..9868bed377 100644 --- a/packages/apps/reputation-oracle/server/src/common/constants/networks.ts +++ b/packages/apps/reputation-oracle/server/src/common/constants/networks.ts @@ -6,9 +6,6 @@ export const TESTNET_CHAIN_IDS = [ ChainId.SEPOLIA, ]; -export const MAINNET_CHAIN_IDS = [ - ChainId.BSC_MAINNET, - ChainId.POLYGON, -]; +export const MAINNET_CHAIN_IDS = [ChainId.BSC_MAINNET, ChainId.POLYGON]; export const LOCALHOST_CHAIN_IDS = [ChainId.LOCALHOST]; From c4d58ea401ec7921e5557f1e93b4cdc8374447ce Mon Sep 17 00:00:00 2001 From: portuu3 Date: Tue, 28 Jan 2025 10:47:45 +0100 Subject: [PATCH 02/14] balance refactor --- .../server/src/modules/payment/payment.service.ts | 2 -- .../job-launcher/server/src/modules/user/user.controller.ts | 2 +- .../server/src/modules/user/user.service.spec.ts | 6 +++--- .../job-launcher/server/src/modules/user/user.service.ts | 4 ++-- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/apps/job-launcher/server/src/modules/payment/payment.service.ts b/packages/apps/job-launcher/server/src/modules/payment/payment.service.ts index 2e9a6961b7..b17a5d3ce2 100644 --- a/packages/apps/job-launcher/server/src/modules/payment/payment.service.ts +++ b/packages/apps/job-launcher/server/src/modules/payment/payment.service.ts @@ -41,7 +41,6 @@ import { ControlledError } from '../../common/errors/controlled'; import { RateService } from './rate.service'; import { UserEntity } from '../user/user.entity'; // import { JobEntity } from '../job/job.entity'; -import { ServerConfigService } from '../../common/config/server-config.service'; import { UserRepository } from '../user/user.repository'; import { JobRepository } from '../job/job.repository'; import { PageDto } from '../../common/pagination/pagination.dto'; @@ -59,7 +58,6 @@ export class PaymentService { private readonly jobRepository: JobRepository, private stripeConfigService: StripeConfigService, private rateService: RateService, - private serverConfigService: ServerConfigService, ) { this.stripe = new Stripe(this.stripeConfigService.secretKey, { apiVersion: this.stripeConfigService.apiVersion as any, diff --git a/packages/apps/job-launcher/server/src/modules/user/user.controller.ts b/packages/apps/job-launcher/server/src/modules/user/user.controller.ts index 63998a20e5..3b5023b3a5 100644 --- a/packages/apps/job-launcher/server/src/modules/user/user.controller.ts +++ b/packages/apps/job-launcher/server/src/modules/user/user.controller.ts @@ -47,7 +47,7 @@ export class UserController { @Request() req: RequestWithUser, ): Promise { try { - return this.userService.getBalance(req.user.id); + return this.userService.getTotalUSDBalance(req.user.id); } catch (e) { this.logger.log( e.message, diff --git a/packages/apps/job-launcher/server/src/modules/user/user.service.spec.ts b/packages/apps/job-launcher/server/src/modules/user/user.service.spec.ts index 24334f9cd6..3fb969b12c 100644 --- a/packages/apps/job-launcher/server/src/modules/user/user.service.spec.ts +++ b/packages/apps/job-launcher/server/src/modules/user/user.service.spec.ts @@ -99,12 +99,12 @@ describe('UserService', () => { currency: Currency.USD, }; - jest.spyOn(paymentService, 'getUserBalance').mockResolvedValue(10); + jest.spyOn(paymentService, 'getUserUSDBalance').mockResolvedValue(10); - const balance = await userService.getBalance(userId); + const balance = await userService.getTotalUSDBalance(userId); expect(balance).toEqual(expectedBalance); - expect(paymentService.getUserBalance).toHaveBeenCalledWith(userId); + expect(paymentService.getUserUSDBalance).toHaveBeenCalledWith(userId); }); }); }); diff --git a/packages/apps/job-launcher/server/src/modules/user/user.service.ts b/packages/apps/job-launcher/server/src/modules/user/user.service.ts index 1f1b41f2fb..b5d88077eb 100644 --- a/packages/apps/job-launcher/server/src/modules/user/user.service.ts +++ b/packages/apps/job-launcher/server/src/modules/user/user.service.ts @@ -48,9 +48,9 @@ export class UserService { return this.userRepository.updateOne(userEntity); } - public async getBalance(userId: number): Promise { + public async getTotalUSDBalance(userId: number): Promise { return { - amount: await this.paymentService.getUserBalance(userId), + amount: await this.paymentService.getUserUSDBalance(userId), currency: Currency.USD, }; } From 69b94888337b39ba656bb19c05993731eaaf4de1 Mon Sep 17 00:00:00 2001 From: portuu3 Date: Wed, 29 Jan 2025 09:37:27 +0100 Subject: [PATCH 03/14] use utils operators to properly handle small decimal amounts --- .../server/src/modules/payment/payment.service.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/apps/job-launcher/server/src/modules/payment/payment.service.ts b/packages/apps/job-launcher/server/src/modules/payment/payment.service.ts index b17a5d3ce2..4c6953c83f 100644 --- a/packages/apps/job-launcher/server/src/modules/payment/payment.service.ts +++ b/packages/apps/job-launcher/server/src/modules/payment/payment.service.ts @@ -34,7 +34,7 @@ import { } from '@human-protocol/core/typechain-types'; import { Web3Service } from '../web3/web3.service'; import { CoingeckoTokenId } from '../../common/constants/payment'; -import { div, eq, mul } from '../../common/utils/decimal'; +import { div, eq, mul, add } from '../../common/utils/decimal'; import { verifySignature } from '../../common/utils/signature'; import { PaymentEntity } from './payment.entity'; import { ControlledError } from '../../common/errors/controlled'; @@ -411,14 +411,13 @@ export class PaymentService { for (const token of uniqueTokens) { const tokenAmountSum = paymentEntities .filter((payment) => payment.currency === token) - .reduce((sum, payment) => sum + Number(payment.amount), 0); + .reduce((sum, payment) => add(sum, Number(payment.amount)), 0); const rate = token === Currency.USD ? 1 : await this.rateService.getRate(token, Currency.USD); - - totalBalance += tokenAmountSum * rate; + totalBalance += mul(tokenAmountSum, rate); } return totalBalance; @@ -434,7 +433,7 @@ export class PaymentService { ); const balance = paymentEntities.reduce( - (sum, payment) => sum + Number(payment.amount), + (sum, payment) => add(sum, Number(payment.amount)), 0, ); From 5227855f02804390cee3b5770233f06f4d110d76 Mon Sep 17 00:00:00 2001 From: portuu3 Date: Thu, 30 Jan 2025 16:09:28 +0100 Subject: [PATCH 04/14] fix fee calculation --- .../apps/job-launcher/server/src/modules/job/job.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/apps/job-launcher/server/src/modules/job/job.service.ts b/packages/apps/job-launcher/server/src/modules/job/job.service.ts index e4ef6af345..1c8b4d53ad 100644 --- a/packages/apps/job-launcher/server/src/modules/job/job.service.ts +++ b/packages/apps/job-launcher/server/src/modules/job/job.service.ts @@ -840,7 +840,7 @@ export class JobService { const currency = dto.currency ?? JobCurrency.HMT; const rate = await this.rateService.getRate(currency, Currency.USD); const tokenFee = max( - div(this.serverConfigService.minimunFeeUsd, rate), + mul(this.serverConfigService.minimunFeeUsd, rate), mul(div(feePercentage, 100), dto.fundAmount), ); const totalAmountToPay = add(dto.fundAmount, tokenFee); From 2015105d560c348c719b4295e8272865c9b0f4cb Mon Sep 17 00:00:00 2001 From: portuu3 Date: Thu, 30 Jan 2025 16:13:28 +0100 Subject: [PATCH 05/14] fix payment amount --- .../apps/job-launcher/server/src/modules/job/job.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/apps/job-launcher/server/src/modules/job/job.service.ts b/packages/apps/job-launcher/server/src/modules/job/job.service.ts index 1c8b4d53ad..7e3453f1b1 100644 --- a/packages/apps/job-launcher/server/src/modules/job/job.service.ts +++ b/packages/apps/job-launcher/server/src/modules/job/job.service.ts @@ -911,7 +911,7 @@ export class JobService { paymentEntity.jobId = jobEntity.id; paymentEntity.source = PaymentSource.BALANCE; paymentEntity.type = PaymentType.WITHDRAWAL; - paymentEntity.amount = -dto.fundAmount; + paymentEntity.amount = -add(dto.fundAmount, tokenFee); paymentEntity.currency = currency; paymentEntity.rate = rate; paymentEntity.status = PaymentStatus.SUCCEEDED; From a8c0d1aff8747cf3386496592180b90374e54f12 Mon Sep 17 00:00:00 2001 From: portuu3 Date: Fri, 31 Jan 2025 15:10:40 +0100 Subject: [PATCH 06/14] job launcher balance refactor backend --- .../server/src/common/enums/job.ts | 3 +- .../server/src/common/enums/payment.ts | 98 ++++++++++--------- .../migrations/1738321689843-addTokenToJob.ts | 28 ++++++ .../src/modules/cron-job/cron-job.service.ts | 2 + .../server/src/modules/job/job.dto.ts | 33 +++---- .../server/src/modules/job/job.entity.ts | 12 ++- .../server/src/modules/job/job.service.ts | 49 +++++++--- .../server/src/modules/payment/payment.dto.ts | 20 ++-- .../src/modules/payment/payment.service.ts | 22 +++-- .../src/modules/payment/rate.service.ts | 9 +- .../server/src/modules/user/user.dto.ts | 6 +- .../server/src/modules/user/user.service.ts | 4 +- 12 files changed, 168 insertions(+), 118 deletions(-) create mode 100644 packages/apps/job-launcher/server/src/database/migrations/1738321689843-addTokenToJob.ts diff --git a/packages/apps/job-launcher/server/src/common/enums/job.ts b/packages/apps/job-launcher/server/src/common/enums/job.ts index 3c54e8d744..c0f723fbfd 100644 --- a/packages/apps/job-launcher/server/src/common/enums/job.ts +++ b/packages/apps/job-launcher/server/src/common/enums/job.ts @@ -426,7 +426,6 @@ export enum WorkerBrowser { MODERN_BROWSER = 'modern_browser', } -export enum JobCurrency { +export enum EscrowFundToken { HMT = 'hmt', - USD = 'usd', } diff --git a/packages/apps/job-launcher/server/src/common/enums/payment.ts b/packages/apps/job-launcher/server/src/common/enums/payment.ts index 4c619d277a..e874b87177 100644 --- a/packages/apps/job-launcher/server/src/common/enums/payment.ts +++ b/packages/apps/job-launcher/server/src/common/enums/payment.ts @@ -1,52 +1,58 @@ -export enum Currency { +import { EscrowFundToken } from './job'; + +export enum FiatCurrency { USD = 'usd', - AED = 'aed', - ARS = 'ars', - AUD = 'aud', - BDT = 'bdt', - BMD = 'bmd', - BRL = 'brl', - CAD = 'cad', - CHF = 'chf', - CLP = 'clp', - CNY = 'cny', - CZK = 'czk', - DKK = 'dkk', - EUR = 'eur', - GBP = 'gbp', - HKD = 'hkd', - HUF = 'huf', - IDR = 'idr', - ILS = 'ils', - INR = 'inr', - JPY = 'jpy', - KRW = 'krw', - LKR = 'lkr', - MMK = 'mmk', - MXN = 'mxn', - MYR = 'myr', - NGN = 'ngn', - NOK = 'nok', - NZD = 'nzd', - PHP = 'php', - PKR = 'pkr', - PLN = 'pln', - RUB = 'rub', - SAR = 'sar', - SEK = 'sek', - SGD = 'sgd', - THB = 'thb', - TRY = 'try', - TWD = 'twd', - UAH = 'uah', - VND = 'vnd', - ZAR = 'zar', + // AED = 'aed', + // ARS = 'ars', + // AUD = 'aud', + // BDT = 'bdt', + // BMD = 'bmd', + // BRL = 'brl', + // CAD = 'cad', + // CHF = 'chf', + // CLP = 'clp', + // CNY = 'cny', + // CZK = 'czk', + // DKK = 'dkk', + // EUR = 'eur', + // GBP = 'gbp', + // HKD = 'hkd', + // HUF = 'huf', + // IDR = 'idr', + // ILS = 'ils', + // INR = 'inr', + // JPY = 'jpy', + // KRW = 'krw', + // LKR = 'lkr', + // MMK = 'mmk', + // MXN = 'mxn', + // MYR = 'myr', + // NGN = 'ngn', + // NOK = 'nok', + // NZD = 'nzd', + // PHP = 'php', + // PKR = 'pkr', + // PLN = 'pln', + // RUB = 'rub', + // SAR = 'sar', + // SEK = 'sek', + // SGD = 'sgd', + // THB = 'thb', + // TRY = 'try', + // TWD = 'twd', + // UAH = 'uah', + // VND = 'vnd', + // ZAR = 'zar', } -export enum TokenId { - HMT = 'hmt', - USDT = 'usdt', -} +// Allowed currencies for payments +export const PaymentCurrency = { + ...FiatCurrency, + ...EscrowFundToken, +}; + +export type PaymentCurrency = + (typeof PaymentCurrency)[keyof typeof PaymentCurrency]; export enum PaymentSource { FIAT = 'fiat', diff --git a/packages/apps/job-launcher/server/src/database/migrations/1738321689843-addTokenToJob.ts b/packages/apps/job-launcher/server/src/database/migrations/1738321689843-addTokenToJob.ts new file mode 100644 index 0000000000..08dde9d83b --- /dev/null +++ b/packages/apps/job-launcher/server/src/database/migrations/1738321689843-addTokenToJob.ts @@ -0,0 +1,28 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddTokenToJob1738321689843 implements MigrationInterface { + name = 'AddTokenToJob1738321689843'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TYPE "hmt"."jobs_token_enum" AS ENUM('hmt') + `); + await queryRunner.query(` + ALTER TABLE "hmt"."jobs" + ADD "token" "hmt"."jobs_token_enum" NOT NULL DEFAULT 'hmt' + `); + await queryRunner.query(` + ALTER TABLE "hmt"."jobs" + ALTER COLUMN "token" DROP DEFAULT + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE "hmt"."jobs" DROP COLUMN "token" + `); + await queryRunner.query(` + DROP TYPE "hmt"."jobs_token_enum" + `); + } +} diff --git a/packages/apps/job-launcher/server/src/modules/cron-job/cron-job.service.ts b/packages/apps/job-launcher/server/src/modules/cron-job/cron-job.service.ts index 3e4c3b313b..7485b39b03 100644 --- a/packages/apps/job-launcher/server/src/modules/cron-job/cron-job.service.ts +++ b/packages/apps/job-launcher/server/src/modules/cron-job/cron-job.service.ts @@ -229,12 +229,14 @@ export class CronJobService { await this.jobService.processEscrowCancellation(jobEntity); await this.paymentService.createRefundPayment({ refundAmount: Number(ethers.formatEther(amountRefunded)), + refundCurrency: jobEntity.token, userId: jobEntity.userId, jobId: jobEntity.id, }); } else { await this.paymentService.createRefundPayment({ refundAmount: jobEntity.fundAmount, + refundCurrency: jobEntity.token, userId: jobEntity.userId, jobId: jobEntity.id, }); diff --git a/packages/apps/job-launcher/server/src/modules/job/job.dto.ts b/packages/apps/job-launcher/server/src/modules/job/job.dto.ts index add8414948..b99c6ac62e 100644 --- a/packages/apps/job-launcher/server/src/modules/job/job.dto.ts +++ b/packages/apps/job-launcher/server/src/modules/job/job.dto.ts @@ -24,7 +24,7 @@ import { ChainId } from '@human-protocol/sdk'; import { JobCaptchaRequestType, JobCaptchaShapeType, - JobCurrency, + EscrowFundToken, JobRequestType, JobSortField, JobStatus, @@ -37,6 +37,7 @@ import { Transform } from 'class-transformer'; import { AWSRegions, StorageProviders } from '../../common/enums/storage'; import { PageOptionsDto } from '../../common/pagination/pagination.dto'; import { IsEnumCaseInsensitive } from '../../common/decorators'; +import { PaymentCurrency } from '../../common/enums/payment'; export class JobDto { @ApiProperty({ enum: ChainId, required: false, name: 'chain_id' }) @@ -73,9 +74,18 @@ export class JobDto { @IsOptional() public recordingOracle?: string; - @ApiProperty({ enum: JobCurrency }) - @IsEnumCaseInsensitive(JobCurrency) - public currency: JobCurrency | undefined; + @ApiProperty({ enum: PaymentCurrency, name: 'payment_currency' }) + @IsEnumCaseInsensitive(PaymentCurrency) + public paymentCurrency: PaymentCurrency; + + @ApiProperty({ name: 'payment_amount' }) + @IsNumber() + @IsPositive() + public paymentAmount: number; + + @ApiProperty({ enum: EscrowFundToken, name: 'escrow_fund_token' }) + @IsEnumCaseInsensitive(EscrowFundToken) + public escrowFundToken: EscrowFundToken; } export class JobQuickLaunchDto extends JobDto { @@ -96,11 +106,6 @@ export class JobQuickLaunchDto extends JobDto { @IsString() @IsOptional() public manifestHash: string; - - @ApiProperty({ name: 'fund_amount' }) - @IsNumber() - @IsPositive() - public fundAmount: number; } export class JobFortuneDto extends JobDto { @@ -116,11 +121,6 @@ export class JobFortuneDto extends JobDto { @IsNumber() @IsPositive() public submissionsRequired: number; - - @ApiProperty({ name: 'fund_amount' }) - @IsNumber() - @IsPositive() - public fundAmount: number; } export class StorageDataDto { @@ -205,11 +205,6 @@ export class JobCvatDto extends JobDto { @ApiProperty({ enum: JobRequestType }) @IsEnumCaseInsensitive(JobRequestType) public type: JobRequestType; - - @ApiProperty({ name: 'fund_amount' }) - @IsNumber() - @IsPositive() - public fundAmount: number; } export class JobCancelDto { diff --git a/packages/apps/job-launcher/server/src/modules/job/job.entity.ts b/packages/apps/job-launcher/server/src/modules/job/job.entity.ts index cd959c6adf..fb2334ea75 100644 --- a/packages/apps/job-launcher/server/src/modules/job/job.entity.ts +++ b/packages/apps/job-launcher/server/src/modules/job/job.entity.ts @@ -2,7 +2,11 @@ import { Column, Entity, Index, ManyToOne, OneToMany } from 'typeorm'; import { NS } from '../../common/constants'; import { IJob } from '../../common/interfaces'; -import { JobRequestType, JobStatus } from '../../common/enums/job'; +import { + EscrowFundToken, + JobRequestType, + JobStatus, +} from '../../common/enums/job'; import { BaseEntity } from '../../database/base.entity'; import { UserEntity } from '../user/user.entity'; import { PaymentEntity } from '../payment/payment.entity'; @@ -31,6 +35,12 @@ export class JobEntity extends BaseEntity implements IJob { @Column({ type: 'decimal', precision: 30, scale: 18 }) public fundAmount: number; + @Column({ + type: 'enum', + enum: EscrowFundToken, + }) + public token: EscrowFundToken; + @Column({ type: 'varchar' }) public manifestUrl: string; diff --git a/packages/apps/job-launcher/server/src/modules/job/job.service.ts b/packages/apps/job-launcher/server/src/modules/job/job.service.ts index 7e3453f1b1..a83ec65834 100644 --- a/packages/apps/job-launcher/server/src/modules/job/job.service.ts +++ b/packages/apps/job-launcher/server/src/modules/job/job.service.ts @@ -37,10 +37,9 @@ import { JobCaptchaMode, JobCaptchaRequestType, JobCaptchaShapeType, - JobCurrency, } from '../../common/enums/job'; import { - Currency, + FiatCurrency, PaymentSource, PaymentStatus, PaymentType, @@ -837,26 +836,43 @@ export class JobService { const feePercentage = Number( await this.getOracleFee(this.web3Service.getOperatorAddress(), chainId), ); - const currency = dto.currency ?? JobCurrency.HMT; - const rate = await this.rateService.getRate(currency, Currency.USD); - const tokenFee = max( - mul(this.serverConfigService.minimunFeeUsd, rate), - mul(div(feePercentage, 100), dto.fundAmount), + + const paymentCurrencyRate = await this.rateService.getRate( + dto.paymentCurrency, + FiatCurrency.USD, + ); + const fundTokenRate = await this.rateService.getRate( + dto.escrowFundToken, + FiatCurrency.USD, + ); + + const paymentCurrencyFee = max( + mul(this.serverConfigService.minimunFeeUsd, paymentCurrencyRate), + mul(div(feePercentage, 100), dto.paymentAmount), ); - const totalAmountToPay = add(dto.fundAmount, tokenFee); + const totalPaymentAmount = add(dto.paymentAmount, paymentCurrencyFee); const userBalance = await this.paymentService.getUserBalanceByCurrency( user.id, - currency, + dto.paymentCurrency, ); - if (lt(userBalance, totalAmountToPay)) { + if (lt(userBalance, totalPaymentAmount)) { throw new ControlledError( ErrorJob.NotEnoughFunds, HttpStatus.BAD_REQUEST, ); } + const fundTokenFee = mul( + div(paymentCurrencyFee, paymentCurrencyRate), + fundTokenRate, + ); + const fundTokenAmount = mul( + div(dto.paymentAmount, paymentCurrencyRate), + fundTokenRate, + ); + let jobEntity = new JobEntity(); if (dto instanceof JobQuickLaunchDto) { @@ -880,7 +896,7 @@ export class JobService { const manifestOrigin = await createManifest( dto, requestType, - dto.fundAmount, + fundTokenAmount, ); const { url, hash } = await this.uploadManifest( @@ -899,8 +915,8 @@ export class JobService { jobEntity.recordingOracle = recordingOracle; jobEntity.userId = user.id; jobEntity.requestType = requestType; - jobEntity.fee = tokenFee; - jobEntity.fundAmount = dto.fundAmount; + jobEntity.fee = fundTokenFee; // Fee in the token used to funding the escrow + jobEntity.fundAmount = fundTokenAmount; // Amount in the token used to funding the escrow jobEntity.status = JobStatus.PENDING; jobEntity.waitUntil = new Date(); @@ -911,9 +927,9 @@ export class JobService { paymentEntity.jobId = jobEntity.id; paymentEntity.source = PaymentSource.BALANCE; paymentEntity.type = PaymentType.WITHDRAWAL; - paymentEntity.amount = -add(dto.fundAmount, tokenFee); - paymentEntity.currency = currency; - paymentEntity.rate = rate; + paymentEntity.amount = -totalPaymentAmount; // In the currency used for the payment. + paymentEntity.currency = dto.paymentCurrency; + paymentEntity.rate = paymentCurrencyRate; paymentEntity.status = PaymentStatus.SUCCEEDED; await this.paymentRepository.createUnique(paymentEntity); @@ -1180,6 +1196,7 @@ export class JobService { if (status === JobStatus.CANCELED) { await this.paymentService.createRefundPayment({ refundAmount: jobEntity.fundAmount, + refundCurrency: jobEntity.token, userId: jobEntity.userId, jobId: jobEntity.id, }); diff --git a/packages/apps/job-launcher/server/src/modules/payment/payment.dto.ts b/packages/apps/job-launcher/server/src/modules/payment/payment.dto.ts index b7d30b39e0..688ecfe69b 100644 --- a/packages/apps/job-launcher/server/src/modules/payment/payment.dto.ts +++ b/packages/apps/job-launcher/server/src/modules/payment/payment.dto.ts @@ -9,7 +9,8 @@ import { Min, } from 'class-validator'; import { - Currency, + FiatCurrency, + PaymentCurrency, PaymentSortField, PaymentSource, PaymentStatus, @@ -34,10 +35,10 @@ export class PaymentFiatCreateDto { public amount: number; @ApiProperty({ - enum: Currency, + enum: FiatCurrency, }) - @IsEnumCaseInsensitive(Currency) - public currency: Currency; + @IsEnumCaseInsensitive(FiatCurrency) + public currency: FiatCurrency; @ApiProperty({ name: 'payment_method_id', @@ -79,17 +80,10 @@ export class GetRateDto { public to: string; } -export class PaymentRefundCreateDto { - @ApiPropertyOptional({ name: 'refund_amount' }) - @IsNumber() +export class PaymentRefund { public refundAmount: number; - - @ApiPropertyOptional({ name: 'user_id' }) - @IsNumber() + public refundCurrency: PaymentCurrency; public userId: number; - - @ApiPropertyOptional({ name: 'job_id' }) - @IsNumber() public jobId: number; } diff --git a/packages/apps/job-launcher/server/src/modules/payment/payment.service.ts b/packages/apps/job-launcher/server/src/modules/payment/payment.service.ts index 4c6953c83f..a9f8f8c370 100644 --- a/packages/apps/job-launcher/server/src/modules/payment/payment.service.ts +++ b/packages/apps/job-launcher/server/src/modules/payment/payment.service.ts @@ -14,15 +14,14 @@ import { PaymentDto, PaymentFiatConfirmDto, PaymentFiatCreateDto, - PaymentRefundCreateDto, + PaymentRefund, } from './payment.dto'; import { - Currency, + FiatCurrency, PaymentSource, PaymentStatus, PaymentType, StripePaymentStatus, - TokenId, VatType, } from '../../common/enums/payment'; import { TX_CONFIRMATION_TRESHOLD } from '../../common/constants'; @@ -224,7 +223,7 @@ export class PaymentService { } // Record the payment details in the system. - const rate = await this.rateService.getRate(currency, Currency.USD); + const rate = await this.rateService.getRate(currency, FiatCurrency.USD); const newPaymentEntity = new PaymentEntity(); Object.assign(newPaymentEntity, { @@ -379,7 +378,7 @@ export class PaymentService { ); } - const rate = await this.rateService.getRate(tokenId, Currency.USD); + const rate = await this.rateService.getRate(tokenId, FiatCurrency.USD); const newPaymentEntity = new PaymentEntity(); Object.assign(newPaymentEntity, { @@ -414,9 +413,9 @@ export class PaymentService { .reduce((sum, payment) => add(sum, Number(payment.amount)), 0); const rate = - token === Currency.USD + token === FiatCurrency.USD ? 1 - : await this.rateService.getRate(token, Currency.USD); + : await this.rateService.getRate(token, FiatCurrency.USD); totalBalance += mul(tokenAmountSum, rate); } @@ -440,8 +439,11 @@ export class PaymentService { return balance; } - public async createRefundPayment(dto: PaymentRefundCreateDto) { - const rate = await this.rateService.getRate(TokenId.HMT, Currency.USD); + public async createRefundPayment(dto: PaymentRefund) { + const rate = await this.rateService.getRate( + dto.refundCurrency, + FiatCurrency.USD, + ); const paymentEntity = new PaymentEntity(); Object.assign(paymentEntity, { @@ -450,7 +452,7 @@ export class PaymentService { source: PaymentSource.BALANCE, type: PaymentType.REFUND, amount: dto.refundAmount, - currency: TokenId.HMT, + currency: dto.refundCurrency, rate, status: PaymentStatus.SUCCEEDED, }); diff --git a/packages/apps/job-launcher/server/src/modules/payment/rate.service.ts b/packages/apps/job-launcher/server/src/modules/payment/rate.service.ts index da86a7801d..fc58d9f096 100644 --- a/packages/apps/job-launcher/server/src/modules/payment/rate.service.ts +++ b/packages/apps/job-launcher/server/src/modules/payment/rate.service.ts @@ -4,12 +4,9 @@ import { firstValueFrom } from 'rxjs'; import { ServerConfigService } from '../../common/config/server-config.service'; import { COINGECKO_API_URL } from '../../common/constants'; import { ErrorCurrency } from '../../common/constants/errors'; -import { - // CoinMarketCupTokenId, - CoingeckoTokenId, -} from '../../common/constants/payment'; -import { TokenId } from '../../common/enums/payment'; +import { CoingeckoTokenId } from '../../common/constants/payment'; import { ControlledError } from '../../common/errors/controlled'; +import { EscrowFundToken } from '../../common/enums/job'; @Injectable() export class RateService { @@ -38,7 +35,7 @@ export class RateService { let coingeckoFrom = from; let coingeckoTo = to; - if (Object.values(TokenId).includes(to as TokenId)) { + if (Object.values(EscrowFundToken).includes(to as EscrowFundToken)) { coingeckoFrom = CoingeckoTokenId[to]; coingeckoTo = from; reversed = true; diff --git a/packages/apps/job-launcher/server/src/modules/user/user.dto.ts b/packages/apps/job-launcher/server/src/modules/user/user.dto.ts index 27aab6dde4..d419d0d025 100644 --- a/packages/apps/job-launcher/server/src/modules/user/user.dto.ts +++ b/packages/apps/job-launcher/server/src/modules/user/user.dto.ts @@ -3,7 +3,7 @@ import { IsEmail, IsOptional, IsString } from 'class-validator'; import { Transform } from 'class-transformer'; import { UserStatus, UserType } from '../../common/enums/user'; import { ValidatePasswordDto } from '../auth/auth.dto'; -import { Currency } from '../../common/enums/payment'; +import { FiatCurrency } from '../../common/enums/payment'; import { IsEnumCaseInsensitive } from '../../common/decorators'; export class UserCreateDto extends ValidatePasswordDto { @@ -42,7 +42,7 @@ export class UserBalanceDto { @ApiProperty({ description: 'Currency of the user balance', - enum: Currency, + enum: FiatCurrency, }) - currency: Currency; + currency: FiatCurrency; } diff --git a/packages/apps/job-launcher/server/src/modules/user/user.service.ts b/packages/apps/job-launcher/server/src/modules/user/user.service.ts index b5d88077eb..adc655761b 100644 --- a/packages/apps/job-launcher/server/src/modules/user/user.service.ts +++ b/packages/apps/job-launcher/server/src/modules/user/user.service.ts @@ -7,7 +7,7 @@ import { UserBalanceDto, UserCreateDto } from './user.dto'; import { UserRepository } from './user.repository'; import { ValidatePasswordDto } from '../auth/auth.dto'; import { PaymentService } from '../payment/payment.service'; -import { Currency } from '../../common/enums/payment'; +import { FiatCurrency } from '../../common/enums/payment'; @Injectable() export class UserService { @@ -51,7 +51,7 @@ export class UserService { public async getTotalUSDBalance(userId: number): Promise { return { amount: await this.paymentService.getUserUSDBalance(userId), - currency: Currency.USD, + currency: FiatCurrency.USD, }; } } From f3120827c9a2749ab32c76fe72c986f22a053001 Mon Sep 17 00:00:00 2001 From: portuu3 Date: Fri, 31 Jan 2025 15:48:46 +0100 Subject: [PATCH 07/14] set fund token for the job --- packages/apps/job-launcher/server/src/modules/job/job.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/apps/job-launcher/server/src/modules/job/job.service.ts b/packages/apps/job-launcher/server/src/modules/job/job.service.ts index a83ec65834..50125761ca 100644 --- a/packages/apps/job-launcher/server/src/modules/job/job.service.ts +++ b/packages/apps/job-launcher/server/src/modules/job/job.service.ts @@ -917,6 +917,7 @@ export class JobService { jobEntity.requestType = requestType; jobEntity.fee = fundTokenFee; // Fee in the token used to funding the escrow jobEntity.fundAmount = fundTokenAmount; // Amount in the token used to funding the escrow + jobEntity.token = dto.escrowFundToken; jobEntity.status = JobStatus.PENDING; jobEntity.waitUntil = new Date(); From b64d2ecb7abef181a29796f2795ab85db51d435e Mon Sep 17 00:00:00 2001 From: portuu3 Date: Fri, 31 Jan 2025 18:40:18 +0100 Subject: [PATCH 08/14] fix rate direction --- .../apps/job-launcher/server/src/modules/job/job.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/apps/job-launcher/server/src/modules/job/job.service.ts b/packages/apps/job-launcher/server/src/modules/job/job.service.ts index 50125761ca..3bd39d42b1 100644 --- a/packages/apps/job-launcher/server/src/modules/job/job.service.ts +++ b/packages/apps/job-launcher/server/src/modules/job/job.service.ts @@ -842,8 +842,8 @@ export class JobService { FiatCurrency.USD, ); const fundTokenRate = await this.rateService.getRate( - dto.escrowFundToken, FiatCurrency.USD, + dto.escrowFundToken, ); const paymentCurrencyFee = max( From b6ca48f5bacacc2ad310f4f20d881672a51052e3 Mon Sep 17 00:00:00 2001 From: portuu3 Date: Fri, 31 Jan 2025 19:03:24 +0100 Subject: [PATCH 09/14] fix payment logic --- .../server/src/modules/job/job.service.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/apps/job-launcher/server/src/modules/job/job.service.ts b/packages/apps/job-launcher/server/src/modules/job/job.service.ts index 3bd39d42b1..7e032bfa03 100644 --- a/packages/apps/job-launcher/server/src/modules/job/job.service.ts +++ b/packages/apps/job-launcher/server/src/modules/job/job.service.ts @@ -847,7 +847,7 @@ export class JobService { ); const paymentCurrencyFee = max( - mul(this.serverConfigService.minimunFeeUsd, paymentCurrencyRate), + div(this.serverConfigService.minimunFeeUsd, paymentCurrencyRate), mul(div(feePercentage, 100), dto.paymentAmount), ); const totalPaymentAmount = add(dto.paymentAmount, paymentCurrencyFee); @@ -864,14 +864,14 @@ export class JobService { ); } - const fundTokenFee = mul( - div(paymentCurrencyFee, paymentCurrencyRate), - fundTokenRate, - ); - const fundTokenAmount = mul( - div(dto.paymentAmount, paymentCurrencyRate), - fundTokenRate, - ); + const fundTokenFee = + dto.paymentCurrency === dto.escrowFundToken + ? paymentCurrencyFee + : mul(mul(paymentCurrencyFee, paymentCurrencyRate), fundTokenRate); + const fundTokenAmount = + dto.paymentCurrency === dto.escrowFundToken + ? dto.paymentAmount + : mul(mul(dto.paymentAmount, paymentCurrencyRate), fundTokenRate); let jobEntity = new JobEntity(); From 91eebfc2db4a735efb1929044c0d48dace946ae9 Mon Sep 17 00:00:00 2001 From: portuu3 Date: Mon, 3 Feb 2025 16:28:35 +0100 Subject: [PATCH 10/14] update frontend and tests --- .../components/Jobs/Create/CryptoPayForm.tsx | 26 +++-- .../components/Jobs/Create/FiatPayForm.tsx | 30 +++++- .../src/components/TokenSelect/index.tsx | 10 +- .../TopUpAccount/CryptoTopUpForm.tsx | 11 ++- .../client/src/constants/chains.ts | 2 +- .../job-launcher/client/src/services/job.ts | 20 ++-- .../job-launcher/client/src/types/index.ts | 10 +- .../src/modules/job/job.controller.spec.ts | 31 ++++-- .../modules/payment/payment.service.spec.ts | 70 ++++++------- .../src/modules/user/user.service.spec.ts | 4 +- yarn.lock | 97 +++++++++++++------ 11 files changed, 204 insertions(+), 107 deletions(-) diff --git a/packages/apps/job-launcher/client/src/components/Jobs/Create/CryptoPayForm.tsx b/packages/apps/job-launcher/client/src/components/Jobs/Create/CryptoPayForm.tsx index cca95b4e36..a08ffdbbee 100644 --- a/packages/apps/job-launcher/client/src/components/Jobs/Create/CryptoPayForm.tsx +++ b/packages/apps/job-launcher/client/src/components/Jobs/Create/CryptoPayForm.tsx @@ -25,7 +25,7 @@ import { usePublicClient, } from 'wagmi'; import { TokenSelect } from '../../../components/TokenSelect'; -import { CURRENCY } from '../../../constants/payment'; +import { NETWORK_TOKENS } from '../../../constants/chains'; import { useTokenRate } from '../../../hooks/useTokenRate'; import { useCreateJobPageUI } from '../../../providers/CreateJobPageUIProvider'; import * as jobService from '../../../services/job'; @@ -46,6 +46,7 @@ export const CryptoPayForm = ({ const { chain } = useAccount(); const { jobRequest, goToPrevStep } = useCreateJobPageUI(); const [tokenAddress, setTokenAddress] = useState(); + const [tokenSymbol, setTokenSymbol] = useState(); const [payWithAccountBalance, setPayWithAccountBalance] = useState(false); const [amount, setAmount] = useState(); const [isLoading, setIsLoading] = useState(false); @@ -103,7 +104,7 @@ export const CryptoPayForm = ({ }, [payWithAccountBalance, totalAmount, accountAmount]); const handlePay = async () => { - if (signer && tokenAddress && amount && jobRequest.chainId) { + if (signer && tokenAddress && amount && jobRequest.chainId && tokenSymbol) { setIsLoading(true); try { if (walletPayAmount > 0) { @@ -145,15 +146,17 @@ export const CryptoPayForm = ({ await jobService.createFortuneJob( chainId, fortuneRequest, + tokenSymbol, Number(amount), - CURRENCY.hmt, + tokenSymbol, ); } else if (jobType === JobType.CVAT && cvatRequest) { await jobService.createCvatJob( chainId, cvatRequest, + tokenSymbol, Number(amount), - CURRENCY.hmt, + tokenSymbol, ); } else if (jobType === JobType.HCAPTCHA && hCaptchaRequest) { await jobService.createHCaptchaJob(chainId, hCaptchaRequest); @@ -223,7 +226,15 @@ export const CryptoPayForm = ({ setTokenAddress(e.target.value as string)} + onChange={(e) => { + const symbol = e.target.value as string; + setTokenSymbol(symbol); + setTokenAddress( + NETWORK_TOKENS[ + jobRequest.chainId! as keyof typeof NETWORK_TOKENS + ]?.[symbol.toLowerCase()], + ); + }} /> Account Balance - {user?.balance?.amount?.toFixed(2) ?? '0'}{' '} + ~ {user?.balance?.amount?.toFixed(2) ?? '0'}{' '} {user?.balance?.currency?.toUpperCase() ?? 'USD'} @@ -265,7 +276,7 @@ export const CryptoPayForm = ({ > Fund Amount - {fundAmount?.toFixed(2)} USD + ~ {fundAmount?.toFixed(2)} USD (); useEffect(() => { const fetchJobLauncherData = async () => { @@ -181,6 +183,11 @@ export const FiatPayForm = ({ return; } + if (!tokenAddress) { + onError('Please select a token.'); + return; + } + onStart(); setIsLoading(true); @@ -215,11 +222,18 @@ export const FiatPayForm = ({ await createFortuneJob( chainId, fortuneRequest, - fundAmount, CURRENCY.usd, + fundAmount, + tokenAddress, ); } else if (jobType === JobType.CVAT && cvatRequest) { - await createCvatJob(chainId, cvatRequest, fundAmount, CURRENCY.usd); + await createCvatJob( + chainId, + cvatRequest, + CURRENCY.usd, + fundAmount, + tokenAddress, + ); } else if (jobType === JobType.HCAPTCHA && hCaptchaRequest) { await createHCaptchaJob(chainId, hCaptchaRequest); } @@ -321,6 +335,13 @@ export const FiatPayForm = ({ Add Payment Method )} + + setTokenAddress(e.target.value as string) + } + /> @@ -348,7 +369,7 @@ export const FiatPayForm = ({ Account Balance {user?.balance && ( - {user?.balance?.amount?.toFixed(2)} USD + ~ {user?.balance?.amount?.toFixed(2)} USD )} @@ -427,7 +448,8 @@ export const FiatPayForm = ({ disabled={ !amount || (!payWithAccountBalance && !selectedCard) || - hasError + hasError || + !tokenAddress } > Pay now diff --git a/packages/apps/job-launcher/client/src/components/TokenSelect/index.tsx b/packages/apps/job-launcher/client/src/components/TokenSelect/index.tsx index 111c515b56..80bfa2bfb6 100644 --- a/packages/apps/job-launcher/client/src/components/TokenSelect/index.tsx +++ b/packages/apps/job-launcher/client/src/components/TokenSelect/index.tsx @@ -28,11 +28,11 @@ export const TokenSelect: FC = (props) => { return ( - Token + Funding token