From 5bcbc5e2339d4747239ff3b4997cc40e42dbf66e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20L=C3=B3pez?= Date: Fri, 13 Feb 2026 16:43:35 +0100 Subject: [PATCH] Update gas price handling to use new EIP-1559 fee parameters across services and tests --- .../src/modules/job/job.service.spec.ts | 28 ++++++--- .../server/src/modules/job/job.service.ts | 18 +++--- .../src/modules/web3/web3.service.spec.ts | 60 +++++++++++++++---- .../server/src/modules/web3/web3.service.ts | 25 ++++++-- .../escrow-completion.service.spec.ts | 11 ++-- .../escrow-completion.service.ts | 10 ++-- .../src/modules/web3/web3.service.spec.ts | 55 +++++++++++++---- .../server/src/modules/web3/web3.service.ts | 23 +++++-- 8 files changed, 168 insertions(+), 62 deletions(-) diff --git a/packages/apps/job-launcher/server/src/modules/job/job.service.spec.ts b/packages/apps/job-launcher/server/src/modules/job/job.service.spec.ts index 42f7b763b0..a179f84e47 100644 --- a/packages/apps/job-launcher/server/src/modules/job/job.service.spec.ts +++ b/packages/apps/job-launcher/server/src/modules/job/job.service.spec.ts @@ -754,7 +754,10 @@ describe('JobService', () => { } as unknown as EscrowClient); mockWeb3Service.ensureEscrowAllowance.mockResolvedValueOnce(undefined); - mockWeb3Service.calculateGasPrice.mockResolvedValueOnce(1n); + mockWeb3Service.calculateTxFees.mockResolvedValueOnce({ + maxFeePerGas: 1n, + maxPriorityFeePerGas: 1n, + }); const token = (TOKEN_ADDRESSES[jobEntity.chainId as ChainId] ?? {})[ jobEntity.token as EscrowFundToken @@ -774,7 +777,7 @@ describe('JobService', () => { expectedWeiAmount, NETWORKS[jobEntity.chainId as ChainId]!.factoryAddress, ); - expect(mockWeb3Service.calculateGasPrice).toHaveBeenCalledWith( + expect(mockWeb3Service.calculateTxFees).toHaveBeenCalledWith( jobEntity.chainId, ); expect(createFundAndSetupEscrowMock).toHaveBeenCalledWith( @@ -791,7 +794,7 @@ describe('JobService', () => { manifest: jobEntity.manifestUrl, manifestHash: jobEntity.manifestHash, }), - { gasPrice: 1n }, + { maxFeePerGas: 1n, maxPriorityFeePerGas: 1n }, ); expect(result.status).toBe(JobStatus.LAUNCHED); expect(result.escrowAddress).toBe(escrowAddress); @@ -837,7 +840,10 @@ describe('JobService', () => { } as unknown as EscrowClient); mockWeb3Service.ensureEscrowAllowance.mockResolvedValueOnce(undefined); - mockWeb3Service.calculateGasPrice.mockResolvedValueOnce(1n); + mockWeb3Service.calculateTxFees.mockResolvedValueOnce({ + maxFeePerGas: 1n, + maxPriorityFeePerGas: 1n, + }); const token = (TOKEN_ADDRESSES[jobEntity.chainId as ChainId] ?? {})[ jobEntity.token as EscrowFundToken @@ -864,7 +870,7 @@ describe('JobService', () => { expectedWeiAmount, jobEntity.userId.toString(), expect.any(Object), - { gasPrice: 1n }, + { maxFeePerGas: 1n, maxPriorityFeePerGas: 1n }, ); expect(mockJobRepository.updateOne).not.toHaveBeenCalled(); @@ -1262,7 +1268,10 @@ describe('JobService', () => { describe('processEscrowCancellation', () => { it('should process escrow cancellation', async () => { const jobEntity = createJobEntity(); - mockWeb3Service.calculateGasPrice.mockResolvedValueOnce(1n); + mockWeb3Service.calculateTxFees.mockResolvedValueOnce({ + maxFeePerGas: 1n, + maxPriorityFeePerGas: 1n, + }); const getStatusMock = jest.fn().mockResolvedValueOnce('Active'); const requestCancellationMock = jest .fn() @@ -1283,7 +1292,10 @@ describe('JobService', () => { it('should throw if escrow status is not Active', async () => { const jobEntity = createJobEntity(); - mockWeb3Service.calculateGasPrice.mockResolvedValueOnce(1n); + mockWeb3Service.calculateTxFees.mockResolvedValueOnce({ + maxFeePerGas: 1n, + maxPriorityFeePerGas: 1n, + }); mockedEscrowClient.build.mockResolvedValueOnce({ getStatus: jest.fn().mockResolvedValueOnce(EscrowStatus.Complete), requestCancellation: jest.fn(), @@ -1299,7 +1311,7 @@ describe('JobService', () => { // TODO: Re-enable when cancellation is removed from processEscrowCancellation // it('should throw if requestCancellation throws an error', async () => { // const jobEntity = createJobEntity(); - // mockWeb3Service.calculateGasPrice.mockResolvedValueOnce(1n); + // mockWeb3Service.calculateTxFees.mockResolvedValueOnce({ maxFeePerGas: 1n, maxPriorityFeePerGas: 1n }); // mockedEscrowClient.build.mockResolvedValueOnce({ // getStatus: jest.fn().mockResolvedValueOnce(EscrowStatus.Pending), // requestCancellation: jest 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 5984349463..0a426131eb 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 @@ -345,9 +345,7 @@ export class JobService { weiAmount, jobEntity.userId.toString(), escrowConfig, - { - gasPrice: await this.web3Service.calculateGasPrice(jobEntity.chainId), - }, + await this.web3Service.calculateTxFees(jobEntity.chainId), ); if (!escrowAddress) { @@ -609,9 +607,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!, { - gasPrice: await this.web3Service.calculateGasPrice(chainId), - }); + await (escrowClient as any).requestCancellation( + escrowAddress!, + await this.web3Service.calculateTxFees(chainId), + ); } catch (error: any) { this.logger.warn( 'requestCancellation failed, attempting cancel fallback', @@ -622,9 +621,10 @@ export class JobService { error, }, ); - await (escrowClient as any).cancel(escrowAddress!, { - gasPrice: await this.web3Service.calculateGasPrice(chainId), - }); + await (escrowClient as any).cancel( + escrowAddress!, + await this.web3Service.calculateTxFees(chainId), + ); } } diff --git a/packages/apps/job-launcher/server/src/modules/web3/web3.service.spec.ts b/packages/apps/job-launcher/server/src/modules/web3/web3.service.spec.ts index a08edebae1..dcaaadd94b 100644 --- a/packages/apps/job-launcher/server/src/modules/web3/web3.service.spec.ts +++ b/packages/apps/job-launcher/server/src/modules/web3/web3.service.spec.ts @@ -36,6 +36,7 @@ jest.mock('@human-protocol/sdk', () => { describe('Web3Service', () => { let configService: ConfigService; let web3Service: Web3Service; + let web3ConfigService: Web3ConfigService; const mockRateService = { getRate: jest.fn(), }; @@ -66,6 +67,7 @@ describe('Web3Service', () => { }).compile(); web3Service = moduleRef.get(Web3Service); + web3ConfigService = moduleRef.get(Web3ConfigService); configService = moduleRef.get(ConfigService); }); @@ -115,41 +117,77 @@ describe('Web3Service', () => { }); }); - describe('calculateGasPrice', () => { - it('should return gas price multiplied by the multiplier', async () => { + describe('calculateTxFees', () => { + it('should return transaction fees multiplied by the multiplier', async () => { jest.spyOn(configService, 'get').mockImplementation((key: string) => { if (key === 'GAS_PRICE_MULTIPLIER') return 1; return mockConfig[key]; }); - const mockGasPrice = BigInt(1000000000); + const mockMaxFeePerGas = faker.number.bigInt(); + const mockMaxPriorityFeePerGas = faker.number.bigInt(); web3Service.getSigner = jest.fn().mockReturnValue({ address: MOCK_ADDRESS, getNetwork: jest.fn().mockResolvedValue({ chainId: 1 }), provider: { - getFeeData: jest - .fn() - .mockResolvedValueOnce({ gasPrice: mockGasPrice }), + getFeeData: jest.fn().mockResolvedValueOnce({ + maxFeePerGas: mockMaxFeePerGas, + maxPriorityFeePerGas: mockMaxPriorityFeePerGas, + }), }, }); - const result = await web3Service.calculateGasPrice(ChainId.POLYGON_AMOY); - expect(result).toBe(mockGasPrice * BigInt(1)); + const result = await web3Service.calculateTxFees(ChainId.POLYGON_AMOY); + expect(result).toEqual({ + maxFeePerGas: + mockMaxFeePerGas * BigInt(web3ConfigService.gasPriceMultiplier), + maxPriorityFeePerGas: + mockMaxPriorityFeePerGas * + BigInt(web3ConfigService.gasPriceMultiplier), + }); }); - it('should throw an error if gasPrice is undefined', async () => { + it('should throw an error if transaction fees are missing', async () => { web3Service.getSigner = jest.fn().mockReturnValue({ address: MOCK_ADDRESS, getNetwork: jest.fn().mockResolvedValue({ chainId: 1 }), provider: { - getFeeData: jest.fn().mockResolvedValueOnce({ gasPrice: undefined }), + getFeeData: jest.fn().mockResolvedValueOnce({ + maxFeePerGas: undefined, + maxPriorityFeePerGas: undefined, + }), }, }); await expect( - web3Service.calculateGasPrice(ChainId.POLYGON_AMOY), + web3Service.calculateTxFees(ChainId.POLYGON_AMOY), ).rejects.toThrow(new ConflictError(ErrorWeb3.GasPriceError)); }); + + it('should fallback to legacy gasPrice data', async () => { + const mockGasPrice = faker.number.bigInt(); + + web3Service.getSigner = jest.fn().mockReturnValue({ + address: MOCK_ADDRESS, + getNetwork: jest.fn().mockResolvedValue({ chainId: 1 }), + provider: { + getFeeData: jest.fn().mockResolvedValueOnce({ + gasPrice: mockGasPrice, + maxFeePerGas: null, + maxPriorityFeePerGas: null, + }), + }, + }); + + await expect( + web3Service.calculateTxFees(ChainId.POLYGON_AMOY), + ).resolves.toEqual({ + maxFeePerGas: + mockGasPrice * BigInt(web3ConfigService.gasPriceMultiplier), + maxPriorityFeePerGas: + mockGasPrice * BigInt(web3ConfigService.gasPriceMultiplier), + }); + }); }); describe('validateChainId', () => { diff --git a/packages/apps/job-launcher/server/src/modules/web3/web3.service.ts b/packages/apps/job-launcher/server/src/modules/web3/web3.service.ts index a288dc2be3..10bb5fdd40 100644 --- a/packages/apps/job-launcher/server/src/modules/web3/web3.service.ts +++ b/packages/apps/job-launcher/server/src/modules/web3/web3.service.ts @@ -45,14 +45,29 @@ export class Web3Service { } } - public async calculateGasPrice(chainId: number): Promise { + public async calculateTxFees(chainId: number): Promise<{ + maxFeePerGas: bigint; + maxPriorityFeePerGas: bigint; + }> { const signer = this.getSigner(chainId); - const multiplier = this.web3ConfigService.gasPriceMultiplier; + const multiplier = BigInt(this.web3ConfigService.gasPriceMultiplier); + const feeData = await signer.provider?.getFeeData(); - const gasPrice = (await signer.provider?.getFeeData())?.gasPrice; - if (gasPrice) { - return gasPrice * BigInt(multiplier); + if (!feeData) { + throw new ConflictError(ErrorWeb3.GasPriceError); } + + const maxFeePerGas = feeData.maxFeePerGas ?? feeData.gasPrice; + const maxPriorityFeePerGas = + feeData.maxPriorityFeePerGas ?? feeData.gasPrice; + + if (maxFeePerGas && maxPriorityFeePerGas) { + return { + maxFeePerGas: maxFeePerGas * multiplier, + maxPriorityFeePerGas: maxPriorityFeePerGas * multiplier, + }; + } + throw new ConflictError(ErrorWeb3.GasPriceError); } diff --git a/packages/apps/reputation-oracle/server/src/modules/escrow-completion/escrow-completion.service.spec.ts b/packages/apps/reputation-oracle/server/src/modules/escrow-completion/escrow-completion.service.spec.ts index e13aa05392..b6af6e4211 100644 --- a/packages/apps/reputation-oracle/server/src/modules/escrow-completion/escrow-completion.service.spec.ts +++ b/packages/apps/reputation-oracle/server/src/modules/escrow-completion/escrow-completion.service.spec.ts @@ -998,8 +998,11 @@ describe('EscrowCompletionService', () => { recordingOracle: recordingOracleAddress, } as unknown as IEscrow); mockGetEscrowStatus.mockResolvedValueOnce(escrowStatus); - const mockGasPrice = faker.number.bigInt(); - mockWeb3Service.calculateGasPrice.mockResolvedValueOnce(mockGasPrice); + const mockFees = { + maxFeePerGas: faker.number.bigInt(), + maxPriorityFeePerGas: faker.number.bigInt(), + }; + mockWeb3Service.calculateTxFees.mockResolvedValueOnce(mockFees); const paidPayoutsRecord = generateEscrowCompletion( EscrowCompletionStatus.PAID, @@ -1041,9 +1044,7 @@ describe('EscrowCompletionService', () => { }); expect(mockCompleteEscrow).toHaveBeenCalledWith( paidPayoutsRecord.escrowAddress, - { - gasPrice: mockGasPrice, - }, + mockFees, ); expect(mockReputationService.assessEscrowParties).toHaveBeenCalledTimes( 1, diff --git a/packages/apps/reputation-oracle/server/src/modules/escrow-completion/escrow-completion.service.ts b/packages/apps/reputation-oracle/server/src/modules/escrow-completion/escrow-completion.service.ts index 205e915fcc..817c6904c8 100644 --- a/packages/apps/reputation-oracle/server/src/modules/escrow-completion/escrow-completion.service.ts +++ b/packages/apps/reputation-oracle/server/src/modules/escrow-completion/escrow-completion.service.ts @@ -240,13 +240,13 @@ export class EscrowCompletionService { EscrowStatus.ToCancel, ].includes(escrowStatus) ) { - const gasPrice = await this.web3Service.calculateGasPrice(chainId); + const feeOverrides = await this.web3Service.calculateTxFees(chainId); if (escrowStatus === EscrowStatus.ToCancel) { - await escrowClient.cancel(escrowAddress, { gasPrice }); + await escrowClient.cancel(escrowAddress, feeOverrides); escrowStatus = EscrowStatus.Cancelled; } else { - await escrowClient.complete(escrowAddress, { gasPrice }); + await escrowClient.complete(escrowAddress, feeOverrides); escrowStatus = EscrowStatus.Complete; } @@ -439,9 +439,9 @@ export class EscrowCompletionService { uuidv4(), // TODO obtain it from intermediate results false, { - gasPrice: await this.web3Service.calculateGasPrice( + ...(await this.web3Service.calculateTxFees( escrowCompletionEntity.chainId, - ), + )), nonce: payoutsBatch.txNonce, }, ); diff --git a/packages/apps/reputation-oracle/server/src/modules/web3/web3.service.spec.ts b/packages/apps/reputation-oracle/server/src/modules/web3/web3.service.spec.ts index adef07f9e6..f3b58e84d0 100644 --- a/packages/apps/reputation-oracle/server/src/modules/web3/web3.service.spec.ts +++ b/packages/apps/reputation-oracle/server/src/modules/web3/web3.service.spec.ts @@ -54,7 +54,7 @@ describe('Web3Service', () => { }); }); - describe('calculateGasPrice', () => { + describe('calculateTxFees', () => { const mockProvider = createMock(); let spyOnGetSigner: jest.SpyInstance; @@ -75,32 +75,61 @@ describe('Web3Service', () => { mockProvider.getFeeData.mockReset(); }); - it('should use multiplier for gas price', async () => { + it('should use multiplier for transaction fees', async () => { const testChainId = generateTestnetChainId(); - const randomGasPrice = faker.number.bigInt({ min: 1n }); + const randomMaxFeePerGas = faker.number.bigInt(); + const randomMaxPriorityFeePerGas = faker.number.bigInt(); mockProvider.getFeeData.mockResolvedValueOnce({ - gasPrice: randomGasPrice, + maxFeePerGas: randomMaxFeePerGas, + maxPriorityFeePerGas: randomMaxPriorityFeePerGas, } as FeeData); - const gasPrice = await web3Service.calculateGasPrice(testChainId); - - const expectedGasPrice = - randomGasPrice * BigInt(mockWeb3ConfigService.gasPriceMultiplier); - expect(gasPrice).toEqual(expectedGasPrice); + const fees = await web3Service.calculateTxFees(testChainId); + + const expectedMaxFeePerGas = + randomMaxFeePerGas * BigInt(mockWeb3ConfigService.gasPriceMultiplier); + const expectedMaxPriorityFeePerGas = + randomMaxPriorityFeePerGas * + BigInt(mockWeb3ConfigService.gasPriceMultiplier); + expect(fees).toEqual({ + maxFeePerGas: expectedMaxFeePerGas, + maxPriorityFeePerGas: expectedMaxPriorityFeePerGas, + }); }); - it('should throw if no gas price from provider', async () => { + it('should throw if transaction fees are missing', async () => { const testChainId = generateTestnetChainId(); mockProvider.getFeeData.mockResolvedValueOnce({ - gasPrice: null, + maxFeePerGas: null, + maxPriorityFeePerGas: null, } as FeeData); - await expect(web3Service.calculateGasPrice(testChainId)).rejects.toThrow( - `No gas price data for chain id: ${testChainId}`, + await expect(web3Service.calculateTxFees(testChainId)).rejects.toThrow( + `No transaction fee data for chain id: ${testChainId}`, ); }); + + it('should fallback to legacy gasPrice data', async () => { + const testChainId = generateTestnetChainId(); + const randomGasPrice = faker.number.bigInt(); + + mockProvider.getFeeData.mockResolvedValueOnce({ + gasPrice: randomGasPrice, + maxFeePerGas: null, + maxPriorityFeePerGas: null, + } as FeeData); + + const fees = await web3Service.calculateTxFees(testChainId); + const expectedFee = + randomGasPrice * BigInt(mockWeb3ConfigService.gasPriceMultiplier); + + expect(fees).toEqual({ + maxFeePerGas: expectedFee, + maxPriorityFeePerGas: expectedFee, + }); + }); }); }); diff --git a/packages/apps/reputation-oracle/server/src/modules/web3/web3.service.ts b/packages/apps/reputation-oracle/server/src/modules/web3/web3.service.ts index 768196b62a..5b81ab2c6f 100644 --- a/packages/apps/reputation-oracle/server/src/modules/web3/web3.service.ts +++ b/packages/apps/reputation-oracle/server/src/modules/web3/web3.service.ts @@ -85,15 +85,26 @@ export class Web3Service { throw new Error(`No signer for provided chain id: ${chainId}`); } - async calculateGasPrice(chainId: number): Promise { + async calculateTxFees(chainId: number): Promise<{ + maxFeePerGas: bigint; + maxPriorityFeePerGas: bigint; + }> { const signer = this.getSigner(chainId); - const { gasPrice } = await signer.provider.getFeeData(); - - if (gasPrice) { - return gasPrice * BigInt(this.web3ConfigService.gasPriceMultiplier); + const feeData = await signer.provider.getFeeData(); + const multiplier = BigInt(this.web3ConfigService.gasPriceMultiplier); + + const maxFeePerGas = feeData.maxFeePerGas ?? feeData.gasPrice; + const maxPriorityFeePerGas = + feeData.maxPriorityFeePerGas ?? feeData.gasPrice; + + if (maxFeePerGas && maxPriorityFeePerGas) { + return { + maxFeePerGas: maxFeePerGas * multiplier, + maxPriorityFeePerGas: maxPriorityFeePerGas * multiplier, + }; } - throw new Error(`No gas price data for chain id: ${chainId}`); + throw new Error(`No transaction fee data for chain id: ${chainId}`); } async getTokenDecimals(