Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const envValidator = Joi.object({
PORT: Joi.string(),
// Web3
WEB3_PRIVATE_KEY: Joi.string().required(),
SDK_TX_TIMEOUT_MS: Joi.number(),
RPC_URL_POLYGON: Joi.string(),
RPC_URL_BSC: Joi.string(),
RPC_URL_POLYGON_AMOY: Joi.string(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,12 @@ export class Web3ConfigService {
get privateKey(): string {
return this.configService.getOrThrow<string>('WEB3_PRIVATE_KEY');
}

/**
* Timeout for web3 transactions in milliseconds.
* Default: 60000 (60 seconds)
*/
get txTimeoutMs(): number {
return +this.configService.get<number>('SDK_TX_TIMEOUT_MS', 60000);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ export class JobService {
jobSolutionUploaded.url,
jobSolutionUploaded.hash,
!lastProcessedSolution?.error ? amountToReserve : 0n,
{ timeoutMs: this.web3ConfigService.txTimeoutMs },
);

if (
Expand Down Expand Up @@ -307,6 +308,7 @@ export class JobService {
intermediateResultsURL,
intermediateResultsHash,
0n,
{ timeoutMs: this.web3ConfigService.txTimeoutMs },
);

let reputationOracleWebhook: string | null = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const envValidator = Joi.object({
// Web3
WEB3_ENV: Joi.string(),
WEB3_PRIVATE_KEY: Joi.string().required(),
SDK_TX_TIMEOUT_MS: Joi.number(),
GAS_PRICE_MULTIPLIER: Joi.number(),
APPROVE_AMOUNT_USD: Joi.number(),
REPUTATION_ORACLE_ADDRESS: Joi.string().required(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,12 @@ export class Web3ConfigService {
get approveAmountUsd(): number {
return this.configService.get<number>('APPROVE_AMOUNT_USD', 0);
}

/**
* Timeout for web3 transactions in milliseconds.
* Default: 60000 (60 seconds)
*/
get txTimeoutMs(): number {
return +this.configService.get<number>('SDK_TX_TIMEOUT_MS', 60000);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { Test } from '@nestjs/testing';
import { ethers, ZeroAddress } from 'ethers';
import { createSignerMock } from '../../../test/fixtures/web3';
import { ServerConfigService } from '../../common/config/server-config.service';
import { Web3ConfigService } from '../../common/config/web3-config.service';
import { ErrorEscrow, ErrorJob } from '../../common/constants/errors';
import { TOKEN_ADDRESSES } from '../../common/constants/tokens';
import {
Expand Down Expand Up @@ -82,6 +83,9 @@ const mockRateService = createMock<RateService>();
const mockRoutingProtocolService = createMock<RoutingProtocolService>();
const mockManifestService = createMock<ManifestService>();
const mockWhitelistService = createMock<WhitelistService>();
const mockWeb3ConfigService = {
txTimeoutMs: faker.number.int({ min: 30000, max: 120000 }),
};

const mockedEscrowClient = jest.mocked(EscrowClient);
const mockedEscrowUtils = jest.mocked(EscrowUtils);
Expand Down Expand Up @@ -129,6 +133,7 @@ describe('JobService', () => {
provide: ManifestService,
useValue: mockManifestService,
},
{ provide: Web3ConfigService, useValue: mockWeb3ConfigService },
],
}).compile();

Expand Down Expand Up @@ -794,7 +799,11 @@ describe('JobService', () => {
manifest: jobEntity.manifestUrl,
manifestHash: jobEntity.manifestHash,
}),
{ maxFeePerGas: 1n, maxPriorityFeePerGas: 1n },
{
maxFeePerGas: 1n,
maxPriorityFeePerGas: 1n,
timeoutMs: mockWeb3ConfigService.txTimeoutMs,
},
);
expect(result.status).toBe(JobStatus.LAUNCHED);
expect(result.escrowAddress).toBe(escrowAddress);
Expand Down Expand Up @@ -870,7 +879,11 @@ describe('JobService', () => {
expectedWeiAmount,
jobEntity.userId.toString(),
expect.any(Object),
{ maxFeePerGas: 1n, maxPriorityFeePerGas: 1n },
{
maxFeePerGas: 1n,
maxPriorityFeePerGas: 1n,
timeoutMs: mockWeb3ConfigService.txTimeoutMs,
},
);
expect(mockJobRepository.updateOne).not.toHaveBeenCalled();

Expand Down
23 changes: 14 additions & 9 deletions packages/apps/job-launcher/server/src/modules/job/job.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
validate,
} from 'class-validator';
import { ethers } from 'ethers';
import { Web3ConfigService } from '../../common/config/web3-config.service';
import { ServerConfigService } from '../../common/config/server-config.service';
import { CANCEL_JOB_STATUSES } from '../../common/constants';
import {
Expand Down Expand Up @@ -87,6 +88,7 @@ export class JobService {
constructor(
@Inject(Web3Service)
private readonly web3Service: Web3Service,
private readonly web3ConfigService: Web3ConfigService,
private readonly jobRepository: JobRepository,
private readonly webhookRepository: WebhookRepository,
private readonly paymentService: PaymentService,
Expand Down Expand Up @@ -345,7 +347,10 @@ export class JobService {
weiAmount,
jobEntity.userId.toString(),
escrowConfig,
await this.web3Service.calculateTxFees(jobEntity.chainId),
{
...(await this.web3Service.calculateTxFees(jobEntity.chainId)),
timeoutMs: this.web3ConfigService.txTimeoutMs,
},
);

if (!escrowAddress) {
Expand Down Expand Up @@ -607,10 +612,10 @@ export class JobService {
// Attempt requestCancellation; on any error attempt direct cancel once.
// TODO: Remove try-catch when requestCancellation is fully supported by all escrows
try {
await (escrowClient as any).requestCancellation(
escrowAddress!,
await this.web3Service.calculateTxFees(chainId),
);
await (escrowClient as any).requestCancellation(escrowAddress!, {
...(await this.web3Service.calculateTxFees(chainId)),
timeoutMs: this.web3ConfigService.txTimeoutMs,
});
} catch (error: any) {
this.logger.warn(
'requestCancellation failed, attempting cancel fallback',
Expand All @@ -621,10 +626,10 @@ export class JobService {
error,
},
);
await (escrowClient as any).cancel(
escrowAddress!,
await this.web3Service.calculateTxFees(chainId),
);
await (escrowClient as any).cancel(escrowAddress!, {
...(await this.web3Service.calculateTxFees(chainId)),
timeoutMs: this.web3ConfigService.txTimeoutMs,
});
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export const envValidator = Joi.object({
// Web3
WEB3_ENV: Joi.string().valid(...Object.values(Web3Network)),
WEB3_PRIVATE_KEY: Joi.string().required(),
SDK_TX_TIMEOUT_MS: Joi.number().integer(),
GAS_PRICE_MULTIPLIER: Joi.number().positive(),
RPC_URL_SEPOLIA: Joi.string().uri({ scheme: ['http', 'https'] }),
RPC_URL_POLYGON: Joi.string().uri({ scheme: ['http', 'https'] }),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,14 @@ export class Web3ConfigService {
return Number(this.configService.get('GAS_PRICE_MULTIPLIER')) || 1;
}

/**
* Timeout for web3 transactions in milliseconds.
* Default: 60000 (60 seconds)
*/
get txTimeoutMs(): number {
return +this.configService.get<number>('SDK_TX_TIMEOUT_MS', 60000);
}

getRpcUrlByChainId(chainId: number): string | undefined {
const rpcUrlsByChainId: Record<string, string | undefined> = {
[ChainId.POLYGON]: this.configService.get('RPC_URL_POLYGON'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,14 @@ import stringify from 'json-stable-stringify';
import _ from 'lodash';

import { CvatJobType, FortuneJobType } from '@/common/enums';
import { ServerConfigService } from '@/config';
import { ServerConfigService, Web3ConfigService } from '@/config';
import { ReputationService } from '@/modules/reputation';
import { StorageService } from '@/modules/storage';
import { WalletWithProvider, Web3Service } from '@/modules/web3';
import { generateTestnetChainId } from '@/modules/web3/fixtures';
import {
generateTestnetChainId,
mockWeb3ConfigService,
} from '@/modules/web3/fixtures';
import { OutgoingWebhookService } from '@/modules/webhook';
import { createSignerMock, type SignerMock } from '~/test/fixtures/web3';

Expand Down Expand Up @@ -99,6 +102,10 @@ describe('EscrowCompletionService', () => {
provide: StorageService,
useValue: mockStorageService,
},
{
provide: Web3ConfigService,
useValue: mockWeb3ConfigService,
},
{
provide: OutgoingWebhookService,
useValue: mockOutgoingWebhookService,
Expand Down Expand Up @@ -1044,7 +1051,10 @@ describe('EscrowCompletionService', () => {
});
expect(mockCompleteEscrow).toHaveBeenCalledWith(
paidPayoutsRecord.escrowAddress,
mockFees,
{
...mockFees,
timeoutMs: mockWeb3ConfigService.txTimeoutMs,
},
);
expect(mockReputationService.assessEscrowParties).toHaveBeenCalledTimes(
1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { v4 as uuidv4 } from 'uuid';

import { BACKOFF_INTERVAL_SECONDS } from '@/common/constants';
import { JobManifest, JobRequestType } from '@/common/types';
import { ServerConfigService } from '@/config';
import { ServerConfigService, Web3ConfigService } from '@/config';
import { isDuplicatedError } from '@/database';
import logger from '@/logger';
import { ReputationService } from '@/modules/reputation';
Expand Down Expand Up @@ -58,6 +58,7 @@ export class EscrowCompletionService {
private readonly escrowCompletionRepository: EscrowCompletionRepository,
private readonly escrowPayoutsBatchRepository: EscrowPayoutsBatchRepository,
private readonly web3Service: Web3Service,
private readonly web3ConfigService: Web3ConfigService,
private readonly storageService: StorageService,
private readonly outgoingWebhookService: OutgoingWebhookService,
private readonly reputationService: ReputationService,
Expand Down Expand Up @@ -243,10 +244,16 @@ export class EscrowCompletionService {
const feeOverrides = await this.web3Service.calculateTxFees(chainId);

if (escrowStatus === EscrowStatus.ToCancel) {
await escrowClient.cancel(escrowAddress, feeOverrides);
await escrowClient.cancel(escrowAddress, {
...feeOverrides,
timeoutMs: this.web3ConfigService.txTimeoutMs,
});
escrowStatus = EscrowStatus.Cancelled;
} else {
await escrowClient.complete(escrowAddress, feeOverrides);
await escrowClient.complete(escrowAddress, {
...feeOverrides,
timeoutMs: this.web3ConfigService.txTimeoutMs,
});
escrowStatus = EscrowStatus.Complete;
}

Expand Down Expand Up @@ -453,7 +460,10 @@ export class EscrowCompletionService {

try {
const transactionResponse = await signer.sendTransaction(rawTransaction);
await transactionResponse.wait();
await transactionResponse.wait(
undefined,
this.web3ConfigService.txTimeoutMs,
);

await this.escrowPayoutsBatchRepository.deleteOne(payoutsBatch);
} catch (error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ import {
} from './user.error';
import { UserRepository } from './user.repository';
import { UserService, OperatorStatus } from './user.service';

const mockUserRepository = createMock<UserRepository>();
const mockSiteKeyRepository = createMock<SiteKeyRepository>();
const mockHCaptchaService = createMock<HCaptchaService>();
Expand Down Expand Up @@ -472,6 +471,7 @@ describe('UserService', () => {
expect(mockedKVStoreSet).toHaveBeenCalledWith(
user.evmAddress,
OperatorStatus.ACTIVE,
{ timeoutMs: mockWeb3ConfigService.txTimeoutMs },
);
});
});
Expand Down Expand Up @@ -554,6 +554,7 @@ describe('UserService', () => {
expect(mockedKVStoreSet).toHaveBeenCalledWith(
user.evmAddress,
OperatorStatus.INACTIVE,
{ timeoutMs: mockWeb3ConfigService.txTimeoutMs },
);
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,9 @@ export class UserService {
);
}

await kvstore.set(operatorUser.evmAddress, OperatorStatus.ACTIVE);
await kvstore.set(operatorUser.evmAddress, OperatorStatus.ACTIVE, {
timeoutMs: this.web3ConfigService.txTimeoutMs,
});
}

async disableOperator(userId: number, signature: string): Promise<void> {
Expand Down Expand Up @@ -266,7 +268,9 @@ export class UserService {
);
}

await kvstore.set(operatorUser.evmAddress, OperatorStatus.INACTIVE);
await kvstore.set(operatorUser.evmAddress, OperatorStatus.INACTIVE, {
timeoutMs: this.web3ConfigService.txTimeoutMs,
});
}

async registrationInExchangeOracle(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const mockWeb3ConfigService: Omit<Web3ConfigService, 'configService'> = {
operatorAddress: testWallet.address,
network: Web3Network.TESTNET,
gasPriceMultiplier: faker.number.int({ min: 1, max: 42 }),
txTimeoutMs: faker.number.int({ min: 30000, max: 120000 }),
reputationNetworkChainId: generateTestnetChainId(),
getRpcUrlByChainId: () => faker.internet.url(),
};
Loading