Programmatically query and recover GMP transactions

Transactions can occasionally get stuck in the pipeline from a source to destination chain, mostly for one of the two following reasons:

  • The transaction is not relayed from the source chain into the Axelar network for processing.
  • The transaction fails to get executed on the destination chain due to flawed contract logic.

The AxelarGMPRecoveryAPI module in the AxelarJS SDK can be used to troubleshoot:

  • Query the status of any General Message Passing (GMP) transaction for either callContract() or callContractWithToken() on the gateway contract of a source chain.
  • Retry a stuck transaction at any step including the gas payment step, relaying, execution step.
  • Add gas to an underfunded transaction.

💡

Transaction recovery can also be invoked through the Axelarscan UI.

Install the AxelarGMPRecoveryAPI module

Install the AxelarJS SDK:

Terminal window
npm i @axelar-network/axelarjs-sdk

Instantiate the AxelarGMPRecoveryAPI module:

import {
AxelarGMPRecoveryAPI,
Environment,
} from "@axelar-network/axelarjs-sdk";
const sdk = new AxelarGMPRecoveryAPI({
environment: Environment.TESTNET,
});

Query transaction status by txHash

See the status of a transaction by passing its txHash into queryTransactionStatus():

It takes two parameters

  1. txHash: The transaction hash you are querying
  2. eventIndex: This is an optional paramter, useful for separating multiple internal transactions from one another that may have the same transaction hash.
const txHash: string =
"0xfb6fb85f11496ef58b088116cb611497e87e9c72ff0c9333aa21491e4cdd397a";
const eventIndex: number = 97
const txStatus: GMPStatusResponse = await sdk.queryTransactionStatus(txHash, eventIndex);

The following are possible status responses:

interface GMPStatusResponse {
status: GMPStatus | string;
timeSpent?: Record<string, number>;
gasPaidInfo?: GasPaidInfo;
error?: GMPError;
callTx?: any;
executed?: any;
expressExecuted?: any;
approved?: any;
callback?: any;
}
enum GMPStatus {
SRC_GATEWAY_CALLED = "source_gateway_called",
DEST_GATEWAY_APPROVED = "destination_gateway_approved",
DEST_EXECUTED = "destination_executed",
EXPRESS_EXECUTED = "express_executed",
DEST_EXECUTE_ERROR = "error",
DEST_EXECUTING = "executing",
APPROVING = "approving",
FORECALLED = "forecalled",
FORECALLED_WITHOUT_GAS_PAID = "forecalled_without_gas_paid",
NOT_EXECUTED = "not_executed",
NOT_EXECUTED_WITHOUT_GAS_PAID = "not_executed_without_gas_paid",
INSUFFICIENT_FEE = "insufficient_fee",
UNKNOWN_ERROR = "unknown_error",
CANNOT_FETCH_STATUS = "cannot_fetch_status",
SRC_GATEWAY_CONFIRMED = "confirmed"
}
interface GasPaidInfo {
status: GasPaidStatus;
details?: any;
}
enum GasPaidStatus {
GAS_UNPAID = "gas_unpaid",
GAS_PAID = "gas_paid",
GAS_PAID_NOT_ENOUGH_GAS = "gas_paid_not_enough_gas",
GAS_PAID_ENOUGH_GAS = "gas_paid_enough_gas",
}

Manually relay the transaction through the Axelar network

Use manualRelayToDestChain() to dislodge a transaction stuck at the Confirm step. This function will manually relay a transaction to the destination chain through the Axelar network, query the current transaction status, and recover from source to destination if needed.

The only required parameter is:

  1. sourceTxHash: The hash of the transaction that needs to be unblocked

Additional optional parameters are:

  1. txLogIndex: Unique identifier for internal transactions.
  2. txEventIndex: Used to confirm events on the network.
  3. evmWalletDetails: Wallet that will sign the transaction.
  4. messageId: Id used to recover transactions for GMP transactions from Cosmos source chains
const sourceTxHash = "0x..";
const provider = new ethers.providers.JsonRpcProvider(
"https://sepolia.infura.io/v3/projectId"
);
// Optional
// By default, The sdk uses `window.ethereum` wallet as a sender wallet e.g. MetaMask.
// This option allows caller to pass `privateKey` or `provider` to the sdk directly
const senderOptions = { privateKey: "0x", provider };
const response = await sdk.manualRelayToDestChain(
sourceTxHash,
senderOptions, /* can be skipped */
);

Possible response values are:

export interface GMPRecoveryResponse {
success: boolean;
error?: ApproveGatewayError | string;
confirmTx?: AxelarTxResponse;
signCommandTx?: AxelarTxResponse;
routeMessageTx?: AxelarTxResponse;
approveTx?: any;
infoLogs?: string[];
}

If success == false, you can either execute the transaction manually or increase the gas payment.

Manually execute a transaction

When invoking this method, you will manually execute and pay for the executable method on your specified contract on the destination chain of your cross-chain transaction.

The only required parameter is

  1. sourceTxHash: The hash of the transaction that needs to be unblocked

Additional optional parameters are:

  1. srcTxLogIndex: The log index of the transaction on the source chain.
  2. evmWalletDetails: The wallet details to use for executing the transaction.
const sourceTxHash = "0x..";
const provider = new ethers.providers.JsonRpcProvider(
"https://sepolia.infura.io/v3/projectId"
);
// Optional
// By default, The sdk uses `window.ethereum` wallet as a sender wallet e.g. MetaMask.
// This option allows caller to pass `privateKey` or `provider` to the sdk directly
const senderOptions = { privateKey: "0x", provider };
const response = await sdk.execute(
sourceTxHash,
senderOptions /* can be skipped */,
);

Possible response values are:

{
success: "success" | "failed",
data: ethers.ContractReceipt | undefined,
error: string | undefined
}

Increase gas payment

Call addNativeGas() to increase the gas payment using the source chain’s native token. The amount to be added will be automatically calculated based on factors such as the token price of the source and destination chains and the current gas price at the destination chain. This can be overridden by specifying the amount in the options.

The only required parameter is

  1. chain: Source chain needing extra gas
  2. sourceTxHash: The hash of the transaction that needs to be unblocked
  3. estimatedGasUsed: Estimated gas used

An additional optional parameter is:

  1. options: Consists of AddGasOptions to specify additional options
import {
AxelarGMPRecoveryAPI,
Environment,
AddGasOptions,
} from "@axelar-network/axelarjs-sdk";
// Optional
const options: AddGasOptions = {
amount: "10000000", // Amount of gas to be added. If not specified, the sdk will calculate the amount automatically.
refundAddress: "", // If not specified, the default value is the sender address.
estimatedGasUsed: 700000, // An amount of gas to execute `executeWithToken` or `execute` function of the custom destination contract. If not specified, the default value is 700000.
evmWalletDetails: { useWindowEthereum: true, privateKey: "0x" }, // A wallet to send an `addNativeGas` transaction. If not specified, the default value is { useWindowEthereum: true}.
};
const txHash: string = "0x...";
const { success, transaction, error } = await api.addNativeGas(
EvmChain.AVALANCHE,
txHash,
estimatedGasUsed,
options
);
if (success) {
console.log("Added native gas tx:", transaction?.transactionHash);
} else {
console.log("Cannot add native gas", error);
}

Possible response values are:

{
success: "success" | "failed",
data: ethers.ContractReceipt | undefined,
error: string | undefined
}

Edit on GitHub