Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[work-in-progress] improve manual relay #245

Merged
merged 11 commits into from
Feb 14, 2023
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Fixed `estimateGasFee` function in how it handles the `minGasPrice` parameter, which should compare the min to the destination chain gas price, whereas it was comparing to the source chain price originally.
- AxelarRecoveryAPI
- introduced wss subscription service (subscribeToTx) to invoke subscribe to specific transactions for updates
- AxelarGMPRecoveryAPI
- fixes to manualRelayToDestinationChain to first check if transaction is already confirmed but not broadcasted, and broadcast the transaction (as identified by command ID) if so

## [0.12.4] - 2023-FEBRUARY-1

Expand Down
7 changes: 6 additions & 1 deletion src/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export interface EnvironmentConfigs {
axelarRpcUrl: string;
axelarLcdUrl: string;
axelarGMPApiUrl: string;
axelarscanBaseApiUrl: string;
depositServiceUrl: string;
recoveryApiUrl: string;
axelarCrosschainApiUrl: string;
Expand All @@ -17,6 +18,7 @@ const localConfigs: EnvironmentConfigs = {
axelarRpcUrl: "https://axelar-testnet-rpc.axelar-dev.workers.dev",
axelarLcdUrl: "https://axelar-testnet-lcd.axelar-dev.workers.dev",
axelarGMPApiUrl: "https://testnet.api.gmp.axelarscan.io",
axelarscanBaseApiUrl: "",
depositServiceUrl: "https://deposit-service-devnet-release.devnet.axelar.dev",
recoveryApiUrl: "https://axelar-signing-relayer-testnet.axelar.dev",
axelarCrosschainApiUrl: "https://testnet.api.axelarscan.io/cross-chain",
Expand All @@ -28,6 +30,7 @@ const devnetConfigs: EnvironmentConfigs = {
axelarRpcUrl: "",
axelarLcdUrl: "",
axelarGMPApiUrl: "https://devnet.api.gmp.axelarscan.io",
axelarscanBaseApiUrl: "",
depositServiceUrl: "https://deposit-service-devnet-release.devnet.axelar.dev",
recoveryApiUrl: "",
axelarCrosschainApiUrl: "",
Expand All @@ -37,9 +40,10 @@ const devnetConfigs: EnvironmentConfigs = {
const testnetConfigs: EnvironmentConfigs = {
resourceUrl: "https://nest-server-testnet.axelar.dev",
axelarRpcUrl: "https://rpc-axelar-testnet.imperator.co:443", // "https://testnet.rpc.axelar.dev/chain/axelar",
axelarLcdUrl: "https://rpc-axelar-testnet.imperator.co",
axelarLcdUrl: "https://lcd-axelar-testnet.imperator.co",
depositServiceUrl: "https://deposit-service.testnet.axelar.dev",
axelarGMPApiUrl: "https://testnet.api.gmp.axelarscan.io",
axelarscanBaseApiUrl: "https://testnet.api.axelarscan.io",
recoveryApiUrl: "https://axelar-signing-relayer-testnet.axelar.dev",
axelarCrosschainApiUrl: "https://testnet.api.axelarscan.io/cross-chain",
axelarscanUrl: "https://testnet.axelarscan.io",
Expand All @@ -50,6 +54,7 @@ const mainnetConfigs: EnvironmentConfigs = {
axelarRpcUrl: "https://mainnet.rpc.axelar.dev/chain/axelar",
axelarLcdUrl: "https://lcd-axelar.imperator.co",
axelarGMPApiUrl: "https://api.gmp.axelarscan.io",
axelarscanBaseApiUrl: "https://api.axelarscan.io",
depositServiceUrl: "https://deposit-service.mainnet.axelar.dev",
recoveryApiUrl: "https://axelar-signing-relayer-mainnet.axelar.dev",
axelarCrosschainApiUrl: "https://api.axelarscan.io/cross-chain",
Expand Down
15 changes: 15 additions & 0 deletions src/libs/AxelarQueryAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,21 @@ export class AxelarQueryAPI {
});
}

public async getEVMEvent(sourceChainId: string, srcTxHash: string, srcEventId: number) {
await throwIfInvalidChainIds([sourceChainId], this.environment);
await this.initQueryClientIfNeeded();
return this.axelarQueryClient.evm.Event({
chain: sourceChainId,
eventId: `${srcTxHash}-${srcEventId}`,
});
}

public async getConfirmationHeight(chain: string) {
await throwIfInvalidChainIds([chain], this.environment);
await this.initQueryClientIfNeeded();
return this.axelarQueryClient.evm.ConfirmationHeight({ chain });
}

/**
* Gest the transfer fee for a given transaction
* example testnet query: "https://axelartest-lcd.quickapi.com/axelar/nexus/v1beta1/transfer_fee?source_chain=ethereum&destination_chain=terra&amount=100000000uusd"
Expand Down
243 changes: 183 additions & 60 deletions src/libs/TransactionRecoveryApi/AxelarGMPRecoveryAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { AxelarQueryAPI } from "../AxelarQueryAPI";
import rpcInfo from "./constants/chain";
import {
getDestinationChainFromTxReceipt,
getEventIndexFromTxReceipt,
getGasAmountFromTxReceipt,
getLogIndexFromTxReceipt,
getNativeGasAmountFromTxReceipt,
Expand All @@ -38,6 +39,8 @@ import {
ContractCallError,
ExecutionRevertedError,
GasPriceAPIError,
// GMPErrorMap,
// GMPErrorResponse,
GMPQueryError,
InsufficientFundsError,
InvalidGasTokenError,
Expand All @@ -46,11 +49,23 @@ import {
NotGMPTransactionError,
UnsupportedGasTokenError,
} from "./constants/error";
import { callExecute, CALL_EXECUTE_ERROR } from "./helpers";
import { asyncRetry, sleep, throwIfInvalidChainIds } from "../../utils";
import { BatchedCommandsResponse } from "@axelar-network/axelarjs-types/axelar/evm/v1beta1/query";
import { callExecute, CALL_EXECUTE_ERROR, getCommandId } from "./helpers";
import { sleep, throwIfInvalidChainIds } from "../../utils";
import { EventResponse } from "@axelar-network/axelarjs-types/axelar/evm/v1beta1/query";
import { Event_Status } from "@axelar-network/axelarjs-types/axelar/evm/v1beta1/types";
import { Interface } from "ethers/lib/utils";

export const GMPErrorMap: Record<string, ApproveGatewayError> = {
[GMPStatus.CANNOT_FETCH_STATUS]: ApproveGatewayError.FETCHING_STATUS_FAILED,
[GMPStatus.DEST_EXECUTED]: ApproveGatewayError.ALREADY_EXECUTED,
[GMPStatus.DEST_GATEWAY_APPROVED]: ApproveGatewayError.ALREADY_APPROVED,
};

export const GMPErrorResponse = (error: ApproveGatewayError, errorDetails?: string) => ({
success: false,
error: errorDetails || error,
});

export class AxelarGMPRecoveryAPI extends AxelarRecoveryApi {
axelarQueryApi: AxelarQueryAPI;
public constructor(config: AxelarRecoveryAPIConfig) {
Expand All @@ -77,82 +92,183 @@ export class AxelarGMPRecoveryAPI extends AxelarRecoveryApi {
});
}

public async manualRelayToDestChain(
txHash: string,
public getCidFromSrcTxHash(srcChainId: string, txHash: string, eventIndex: number) {
return getCommandId(srcChainId, txHash, eventIndex, this.environment);
}

public async doesTxMeetConfirmHt(chain: string, currHeight: number) {
return this.axelarQueryApi
.getConfirmationHeight(chain)
.then((res) => res.height.greaterThan(currHeight))
.catch((e) => undefined);
}

public isEVMEventFailed(eventResponse: EventResponse): boolean | undefined {
if (!eventResponse) return undefined;
return [Event_Status.STATUS_FAILED, Event_Status.STATUS_UNSPECIFIED].includes(
eventResponse.event?.status as Event_Status
);
}

public isEVMEventConfirmed(eventResponse: EventResponse): boolean | undefined {
if (!eventResponse) return undefined;
return eventResponse.event?.status === Event_Status.STATUS_CONFIRMED;
}
public isEVMEventCompleted(eventResponse: EventResponse): boolean | undefined {
if (!eventResponse) return undefined;
return eventResponse.event?.status === Event_Status.STATUS_COMPLETED;
}
public async getEvmEvent(
srcChainId: string,
srcTxHash: string,
evmWalletDetails?: EvmWalletDetails
): Promise<ApproveGatewayResponse> {
const _evmWalletDetails = evmWalletDetails || { useWindowEthereum: true };
): Promise<{ commandId: string; eventResponse: EventResponse }> {
let eventIndex: number;

const { callTx, status } = await this.queryTransactionStatus(txHash);
try {
eventIndex = (await this.getEventIndex(
srcChainId as EvmChain,
srcTxHash,
evmWalletDetails
)) as number;
} catch (e) {
throw `could not find event index for ${srcTxHash}`;
}
if (!eventIndex || eventIndex < 0) throw `could not find event index for ${srcTxHash}`;

let confirmTx: Nullable<AxelarTxResponse>;
let createPendingTransferTx: Nullable<AxelarTxResponse>;
let signCommandTx: Nullable<AxelarTxResponse>;
const commandId = this.getCidFromSrcTxHash(srcChainId, srcTxHash, eventIndex);

const errorResponse = (error: ApproveGatewayError, errorDetails?: string) => ({
success: false,
error: errorDetails || error,
confirmTx,
createPendingTransferTx,
signCommandTx,
});
const eventResponse = await this.axelarQueryApi.getEVMEvent(srcChainId, srcTxHash, eventIndex);
if (!eventResponse) throw `could not determine status of event: ${srcTxHash}`;

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unused create pending transfer variable above

if (status === GMPStatus.CANNOT_FETCH_STATUS)
return errorResponse(ApproveGatewayError.FETCHING_STATUS_FAILED);
if (status === GMPStatus.DEST_EXECUTED)
return errorResponse(ApproveGatewayError.ALREADY_EXECUTED);
if (status === GMPStatus.DEST_GATEWAY_APPROVED)
return errorResponse(ApproveGatewayError.ALREADY_APPROVED);
if (this.isEVMEventFailed(eventResponse))
throw `event on source chain is not successful: ${srcTxHash}`;

const srcChain = callTx.chain;
const destChain = callTx.returnValues.destinationChain;
return {
commandId,
eventResponse,
};
}

public async findBatchAndSignIfNeeded(
commandId: string,
destChainId: string,
_evmWalletDetails: EvmWalletDetails
): Promise<AxelarTxResponse | null> {
let signCommandTx = null;
if (!commandId) return signCommandTx;

try {
if (!(await this.fetchBatchData(commandId))) {
signCommandTx = await this.signCommands(destChainId);
await sleep(20);
}
} catch (e) {
console.error(e);
}
return signCommandTx;
}

public async findBatchAndBroadcastIfNeeded(
commandId: string,
destChainId: string,
_evmWalletDetails: EvmWalletDetails
): Promise<AxelarTxResponse | null> {
let approveTx = null;

if (!commandId) return approveTx;

try {
const batchData = await this.fetchBatchData(commandId);
if (!batchData) return approveTx;

const commandData = batchData.commands.find((command) => command.id === commandId);
if (!commandData) return approveTx;

if (!commandData?.executed) {
approveTx = await this.sendApproveTx(
destChainId,
batchData.execute_data,
_evmWalletDetails
);
}
} catch (e) {
console.error(e);
}
return approveTx;
}

public async findEventAndConfirmIfNeeded(
eventResponse: EventResponse,
srcChain: EvmChain,
txHash: string,
evmWalletDetails: EvmWalletDetails
) {
let confirmTx = null;

if (!this.isEVMEventCompleted(eventResponse) && !this.isEVMEventConfirmed(eventResponse)) {
/**todo, need to check whether tx is finalized */
// const confirmationHeight = await this.axelarQueryApi.getConfirmationHeight(srcChain);

confirmTx = await this.confirmGatewayTx(txHash, srcChain);
await sleep(2);
await sleep(30);

createPendingTransferTx = await this.createPendingTransfers(destChain);
await sleep(2);
const updatedEvent = await this.getEvmEvent(srcChain, txHash, evmWalletDetails);

signCommandTx = await this.signCommands(destChain);
const signEvent = signCommandTx.rawLog[0]?.events?.find(
(event: any) => event.type === "sign"
);
if (!this.isEVMEventCompleted(updatedEvent?.eventResponse))
throw `could not confirm event successfully: ${txHash}`;
}

if (!signEvent) return errorResponse(ApproveGatewayError.SIGN_COMMAND_FAILED);
return confirmTx;
}

await sleep(2);
const batchedCommandId = signEvent.attributes.find(
(attr: any) => attr.key === "batchedCommandId"
)?.value;
public async manualRelayToDestChain(
txHash: string,
evmWalletDetails?: EvmWalletDetails
): Promise<ApproveGatewayResponse | null> {
let confirmTx: AxelarTxResponse | null = null;
let signCommandTx: AxelarTxResponse | null = null;
let approveTx: any = null;
let success: boolean = false;

const batchedCommand = await asyncRetry(
() => this.queryBatchedCommands(destChain, batchedCommandId),
(res?: BatchedCommandsResponse) => !!res && res.executeData?.length > 0
);
if (!batchedCommand) return errorResponse(ApproveGatewayError.ERROR_BATCHED_COMMAND);
const _evmWalletDetails = evmWalletDetails || { useWindowEthereum: true };

const { callTx, status } = await this.queryTransactionStatus(txHash);

/**first check if transaction is already executed */
if (GMPErrorMap[status]) return GMPErrorResponse(GMPErrorMap[status]);
const srcChain = callTx.chain;
const destChain = callTx.returnValues.destinationChain;

await sleep(2);
const { commandId, eventResponse } = await this.getEvmEvent(srcChain, txHash, evmWalletDetails);

const approveTx = await this.sendApproveTx(
destChain,
batchedCommand.executeData,
/**find event and confirm if needed */
try {
confirmTx = await this.findEventAndConfirmIfNeeded(
eventResponse,
srcChain,
txHash,
_evmWalletDetails
);
} catch (e) {}

return {
success: true,
confirmTx,
createPendingTransferTx,
signCommandTx,
approveTx,
};
} catch (e: any) {
if (e.message.includes("account sequence mismatch")) {
return errorResponse(ApproveGatewayError.ERROR_ACCOUNT_SEQUENCE_MISMATCH);
}
return errorResponse(ApproveGatewayError.ERROR_UNKNOWN, e.message);
}
/**find batch and sign if needed */
try {
signCommandTx = await this.findBatchAndSignIfNeeded(commandId, destChain, _evmWalletDetails);
} catch (e) {}

/**find batch and manually execute if needed */
try {
await this.findBatchAndBroadcastIfNeeded(commandId, destChain, _evmWalletDetails);
success = true;
} catch (e) {}

return {
success,
confirmTx,
signCommandTx,
approveTx,
};
}

/**
Expand Down Expand Up @@ -217,6 +333,13 @@ export class AxelarGMPRecoveryAPI extends AxelarRecoveryApi {
return this.subtractGasFee(sourceChain, destinationChain, gasTokenSymbol, paidGasFee, options);
}

public async getEventIndex(chain: EvmChain, txHash: string, evmWalletDetails?: EvmWalletDetails) {
const signer = this.getSigner(chain, evmWalletDetails || { useWindowEthereum: true });
const receipt = await signer.provider.getTransactionReceipt(txHash);
if (!receipt) return -1;
return getEventIndexFromTxReceipt(receipt);
}

/**
* Pay native token as gas fee for the given transaction hash.
* If the transaction details is not valid, it will return an error with reason.
Expand Down
Loading