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 @@ -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
Expand All @@ -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(
Expand All @@ -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);
Expand Down Expand Up @@ -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
Expand All @@ -864,7 +870,7 @@ describe('JobService', () => {
expectedWeiAmount,
jobEntity.userId.toString(),
expect.any(Object),
{ gasPrice: 1n },
{ maxFeePerGas: 1n, maxPriorityFeePerGas: 1n },
);
expect(mockJobRepository.updateOne).not.toHaveBeenCalled();

Expand Down Expand Up @@ -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()
Expand All @@ -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(),
Expand All @@ -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
Expand Down
18 changes: 9 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 @@ -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) {
Expand Down Expand Up @@ -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',
Expand All @@ -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),
);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
};
Expand Down Expand Up @@ -66,6 +67,7 @@ describe('Web3Service', () => {
}).compile();

web3Service = moduleRef.get<Web3Service>(Web3Service);
web3ConfigService = moduleRef.get<Web3ConfigService>(Web3ConfigService);
configService = moduleRef.get<ConfigService>(ConfigService);
});

Expand Down Expand Up @@ -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', () => {
Expand Down
25 changes: 20 additions & 5 deletions packages/apps/job-launcher/server/src/modules/web3/web3.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,29 @@ export class Web3Service {
}
}

public async calculateGasPrice(chainId: number): Promise<bigint> {
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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -1041,9 +1044,7 @@ describe('EscrowCompletionService', () => {
});
expect(mockCompleteEscrow).toHaveBeenCalledWith(
paidPayoutsRecord.escrowAddress,
{
gasPrice: mockGasPrice,
},
mockFees,
);
expect(mockReputationService.assessEscrowParties).toHaveBeenCalledTimes(
1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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,
},
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ describe('Web3Service', () => {
});
});

describe('calculateGasPrice', () => {
describe('calculateTxFees', () => {
const mockProvider = createMock<Provider>();
let spyOnGetSigner: jest.SpyInstance;

Expand All @@ -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,
});
});
});
});
Loading
Loading