Skip to content

Commit

Permalink
handle all todos and check available gas fees before sending transact…
Browse files Browse the repository at this point in the history
…ion.
  • Loading branch information
raress96 committed Nov 18, 2024
1 parent 7ca3b38 commit 0359052
Show file tree
Hide file tree
Showing 15 changed files with 516 additions and 83 deletions.
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,5 @@ WALLET_MNEMONIC=

CLIENT_CERT=
CLIENT_KEY=

ENABLE_GAS_CHECK=1
2 changes: 2 additions & 0 deletions .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,5 @@ WALLET_MNEMONIC="fitness horror fluid six mutual ahead upon zone install stadium

CLIENT_CERT=test
CLIENT_KEY=test

ENABLE_GAS_CHECK=1
Original file line number Diff line number Diff line change
Expand Up @@ -209,8 +209,7 @@ export class ApprovalsProcessorService {
}

private async processExecuteTask(response: ExecuteTask, taskItemId: string) {
// TODO: Should we also save response.availableGasBalance and check if enough gas was payed before executing?
const messageApproved = await this.messageApprovedRepository.create({
await this.messageApprovedRepository.createOrUpdate({
sourceChain: response.message.sourceChain,
messageId: response.message.messageID,
status: MessageApprovedStatus.PENDING,
Expand All @@ -220,15 +219,9 @@ export class ApprovalsProcessorService {
payload: Buffer.from(response.payload, 'base64'),
retry: 0,
taskItemId,
// Only support native token for gas
availableGasBalance: !response.availableGasBalance.tokenID ? response.availableGasBalance.amount : '0',
});

if (!messageApproved) {
this.logger.warn(
`Couldn't save message approved to database, duplicate exists for source chain ${response.message.sourceChain} and message id ${response.message.messageID}`,
);

return;
}
}

private async processRefundTask(response: RefundTask) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ describe('ApprovalsProcessorService', () => {
task: {
payload: BinaryUtils.hexToBase64('0123'),
availableGasBalance: {
amount: '0',
amount: '100',
},
message: {
messageID: 'messageId',
Expand All @@ -246,8 +246,8 @@ describe('ApprovalsProcessorService', () => {

await service.handleNewTasksRaw();

expect(messageApprovedRepository.create).toHaveBeenCalledTimes(1);
expect(messageApprovedRepository.create).toHaveBeenCalledWith({
expect(messageApprovedRepository.createOrUpdate).toHaveBeenCalledTimes(1);
expect(messageApprovedRepository.createOrUpdate).toHaveBeenCalledWith({
sourceChain: 'ethereum',
messageId: 'messageId',
status: MessageApprovedStatus.PENDING,
Expand All @@ -257,11 +257,12 @@ describe('ApprovalsProcessorService', () => {
payload: Buffer.from('0123', 'hex'),
retry: 0,
taskItemId: 'UUID',
availableGasBalance: '100',
});
expect(redisCacheService.set).toHaveBeenCalledTimes(1);
});

it('Should handle execute task duplicate in database', async () => {
it('Should handle execute task invalid gas token', async () => {
axelarGmpApi.getTasks.mockReturnValueOnce(
// @ts-ignore
Promise.resolve({
Expand All @@ -270,16 +271,17 @@ describe('ApprovalsProcessorService', () => {
{
type: 'EXECUTE',
task: {
payload: '0123',
payload: BinaryUtils.hexToBase64('0123'),
availableGasBalance: {
amount: '0',
tokenID: 'other',
amount: '100',
},
message: {
messageID: 'messageId',
destinationAddress: 'destinationAddress',
sourceAddress: 'sourceAddress',
sourceChain: 'ethereum',
payloadHash: '0234',
payloadHash: BinaryUtils.hexToBase64('0234'),
},
} as ExecuteTask,
id: 'UUID',
Expand All @@ -291,11 +293,21 @@ describe('ApprovalsProcessorService', () => {
}),
);

messageApprovedRepository.create.mockReturnValueOnce(Promise.resolve(null));

await service.handleNewTasksRaw();

expect(messageApprovedRepository.create).toHaveBeenCalledTimes(1);
expect(messageApprovedRepository.createOrUpdate).toHaveBeenCalledTimes(1);
expect(messageApprovedRepository.createOrUpdate).toHaveBeenCalledWith({
sourceChain: 'ethereum',
messageId: 'messageId',
status: MessageApprovedStatus.PENDING,
sourceAddress: 'sourceAddress',
contractAddress: 'destinationAddress',
payloadHash: '0234',
payload: Buffer.from('0123', 'hex'),
retry: 0,
taskItemId: 'UUID',
availableGasBalance: '0',
});
expect(redisCacheService.set).toHaveBeenCalledTimes(1);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ describe('GatewayProcessor', () => {
createdAt: new Date(),
successTimes: null,
taskItemId: null,
availableGasBalance: '0',
};

messageApprovedRepository.findBySourceChainAndMessageId.mockReturnValueOnce(Promise.resolve(messageApproved));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@ import { TransactionsHelper } from '@mvx-monorepo/common/contracts/transactions.
import { ApiConfigService, AxelarGmpApi } from '@mvx-monorepo/common';
import { ItsContract } from '@mvx-monorepo/common/contracts/its.contract';
import { Locker } from '@multiversx/sdk-nestjs-common';
import { GasError } from '@mvx-monorepo/common/contracts/entities/gas.error';
import { GasError, NotEnoughGasError } from '@mvx-monorepo/common/contracts/entities/gas.error';
import {
CannotExecuteMessageEvent,
CannotExecuteMessageEventV2,
CannotExecuteMessageReason,
Event,
} from '@mvx-monorepo/common/api/entities/axelar.gmp.api';
import { AxiosError } from 'axios';
import { DecodingUtils } from '@mvx-monorepo/common/utils/decoding.utils';
import { CONSTANTS } from '@mvx-monorepo/common/utils/constants.enum';
import { FeeHelper } from '@mvx-monorepo/common/contracts/fee.helper';

// Support a max of 3 retries (mainly because some Interchain Token Service endpoints need to be called 2 times)
const MAX_NUMBER_OF_RETRIES: number = 3;
Expand All @@ -43,6 +43,7 @@ export class MessageApprovedProcessorService {
private readonly transactionsHelper: TransactionsHelper,
private readonly itsContract: ItsContract,
private readonly axelarGmpApi: AxelarGmpApi,
private readonly feeHelper: FeeHelper,
apiConfigService: ApiConfigService,
) {
this.logger = new Logger(MessageApprovedProcessorService.name);
Expand Down Expand Up @@ -70,7 +71,7 @@ export class MessageApprovedProcessorService {
const entriesToUpdate: MessageApproved[] = [];
const entriesWithTransactions: MessageApproved[] = [];
for (const messageApproved of entries) {
if (messageApproved.retry === MAX_NUMBER_OF_RETRIES) {
if (messageApproved.retry >= MAX_NUMBER_OF_RETRIES) {
await this.handleMessageApprovedFailed(messageApproved);

entriesToUpdate.push(messageApproved);
Expand Down Expand Up @@ -106,16 +107,20 @@ export class MessageApprovedProcessorService {

entriesWithTransactions.push(messageApproved);
} catch (e) {
this.logger.error(
`Could not build and sign execute transaction for ${messageApproved.sourceChain} ${messageApproved.messageId}`,
e,
);
// In case of NotEnoughGasError, don't retry the transaction and mark it as failed instantly
if (e instanceof NotEnoughGasError) {
messageApproved.retry = MAX_NUMBER_OF_RETRIES;
messageApproved.status = MessageApprovedStatus.FAILED;

if (e instanceof GasError) {
messageApproved.retry += 1;
await this.handleMessageApprovedFailed(messageApproved, 'INSUFFICIENT_GAS');

entriesToUpdate.push(messageApproved);
} else {
this.logger.error(
`Could not build and sign execute transaction for ${messageApproved.sourceChain} ${messageApproved.messageId}`,
e,
);

throw e;
}
}
Expand Down Expand Up @@ -160,8 +165,24 @@ export class MessageApprovedProcessorService {
.withChainID(this.chainId)
.buildTransaction();

const gas = await this.transactionsHelper.getTransactionGas(transaction, messageApproved.retry);
transaction.setGasLimit(gas);
try {
const gas = await this.transactionsHelper.getTransactionGas(transaction, messageApproved.retry);
transaction.setGasLimit(gas);

this.feeHelper.checkGasCost(gas, transaction.getValue(), transaction.getData(), messageApproved);
} catch (e) {
// In case the gas estimation fails, the transaction will fail on chain, but we will still send it
// for transparency with the full gas available, but don't try to retry it
if (e instanceof GasError) {
transaction.setGasLimit(
this.feeHelper.getGasLimitFromEgldFee(BigInt(messageApproved.availableGasBalance), transaction.getData()),
);

messageApproved.retry = MAX_NUMBER_OF_RETRIES - 1;
} else {
throw e;
}
}

const signature = await this.walletSigner.sign(transaction.serializeForSigning());
transaction.applySignature(signature);
Expand Down Expand Up @@ -201,24 +222,23 @@ export class MessageApprovedProcessorService {
);
}

private async handleMessageApprovedFailed(messageApproved: MessageApproved) {
private async handleMessageApprovedFailed(
messageApproved: MessageApproved,
reason: CannotExecuteMessageReason = 'ERROR',
) {
this.logger.error(
`Could not execute MessageApproved from ${messageApproved.sourceChain} with message id ${messageApproved.messageId} after ${messageApproved.retry} retries`,
);

messageApproved.status = MessageApprovedStatus.FAILED;

const cannotExecuteEvent: CannotExecuteMessageEventV2 = {
const cannotExecuteEvent: CannotExecuteMessageEvent = {
eventID: messageApproved.executeTxHash
? DecodingUtils.getEventId(messageApproved.executeTxHash, 0)
: messageApproved.messageId,
messageID: messageApproved.messageId,
sourceChain: CONSTANTS.SOURCE_CHAIN_NAME,
reason: 'ERROR',
reason,
details: '',
meta: {
taskItemID: messageApproved.taskItemId || '',
},
taskItemID: messageApproved.taskItemId || '',
};

try {
Expand Down
Loading

0 comments on commit 0359052

Please sign in to comment.