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

feat: add query transaction status #379

Merged
merged 5 commits into from
Jun 20, 2024
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
6 changes: 6 additions & 0 deletions .changeset/eight-nails-hunt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@axelarjs/transaction-recovery": patch
"@axelarjs/api": patch
---

chore: add queryTransactionStatus to transaction-recovery package
67 changes: 55 additions & 12 deletions packages/api/src/gmp/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export type ContractMethod = (typeof VALID_CONTRACT_METHODS)[number];

export type SearchGMPParams = Omit<BaseGMPParams, "contractMethod"> & {
contractMethod?: ContractMethod[] | ContractMethod;
txHash?: `0x${string}`;
txHash: string;
txLogIndex?: number | undefined;
messageId?: string | undefined;
status?: GMPTxStatus;
Expand All @@ -74,7 +74,7 @@ type HexAmount = {
hex: string;
};

type SearchGMPCall = {
export type SearchGMPCall = {
blockNumber: number;
blockHash: `0x${string}`;
block_timestamp: number;
Expand Down Expand Up @@ -122,7 +122,7 @@ type SearchGMPTransaction = {
hash: string;
};

type SearchGMPExecuted = {
export type SearchGMPExecuted = {
chain: string;
sourceTransactionIndex: number;
sourceChain: string;
Expand Down Expand Up @@ -179,25 +179,36 @@ type SearchGMPFees = {
express_gas_overhead_fee: number;
};
};

export type SearchGMPApprove = {
transactionHash: string;
contract_address: string;
chain: string;
chain_type: string;
};

export type SearchGMPGasStatus =
| "gas_paid"
| "gas_paid_not_enough_gas"
| "gas_unpaid"
| "gas_paid_enough_gas";

type GMPTxCreatedAt = {
week: number;
hour: number;
month: number;
year: number;
ms: number;
day: number;
quarter: number;
};

export type SearchGMPGasPaid = {
axelarTransactionHash: string;
chain: string;
chain_type: string;
logIndex: number;
createdAt: {
week: number;
hour: number;
month: number;
year: number;
ms: number;
day: number;
quarter: number;
};
created_at: GMPTxCreatedAt;
transactionHash: string;
returnValues: {
refundAddress: string;
Expand Down Expand Up @@ -259,17 +270,49 @@ export type SearchGMPResponseData = {
fees: SearchGMPFees;
status: GMPTxStatus;
executed?: SearchGMPExecuted;
error?: SearchGMPDataError;
time_spent: SearchGMPTimespent;
gas_paid: SearchGMPGasPaid;
gas_status: SearchGMPGasStatus;
express_executed?: SearchGMPExpressExecuted;
express_executing_at?: number;
approved: SearchGMPApprove;
command_id?: string;
is_invalid_destination_chain: boolean;
is_call_from_relayer: boolean;
is_invalid_call: boolean;
is_insufficient_fee: boolean;
interchain_transfer?: InterchainTransferEvent;
interchain_token_deployment_started?: InterchainTokenDeploymentStartedEvent;
token_manager_deployment_started?: TokenManagerDeploymentStartedEvent;
};

export type SearchGMPTimespent = {
call_express_executed?: number;
call_confirm?: number;
call_approve?: number;
approved_executed?: number;
total: number;
};

export type SearchGMPExpressExecuted = {
sourceChain: string;
chain: string;
created_at: GMPTxCreatedAt;
};

export type SearchGMPDataError = {
chain: string;
sourceChain: string;
chain_type: string;
messageId: string;
error: {
reason: string;
message: string;
transactionHash: string;
};
};

export type SearchGMPResponse = BaseGMPResponse<{
data: SearchGMPResponseData[];
total: number;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { createGMPClient } from "@axelarjs/api/gmp";

import { queryTransactionStatus as baseQueryTransactionStatus } from "./isomorphic";
import type {
QueryTransactionStatusParams,
QueryTransactionStatusResult,
} from "./types";

/**
* Query the status of the transaction sent to the destination chain.
* @param params - The parameters to query the transaction status. The parameters are:
* - `txHash`: The hash of the source transaction.
* - `environment`: The environment to query the transaction status. The value can be `mainnet`, `testnet`.
* @returns see {@link QueryTransactionStatusResult}
*/
export function queryTransactionStatus(
params: QueryTransactionStatusParams
): Promise<QueryTransactionStatusResult> {
const { environment } = params;

return baseQueryTransactionStatus(params, {
gmpClient: createGMPClient(environment),
});
}

export default queryTransactionStatus;
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./client";
export * from "./types";
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { queryTransactionStatus } from "./client";

describe("queryTransactionStatus", () => {
const environment = "mainnet";

test("should return success false when the tx is not found", async () => {
const txHash = "0x1234567890";
const result = await queryTransactionStatus({
txHash,
environment,
});

expect(result.success).toEqual(false);
expect(result.error).toEqual("Transaction not found");
});

test("should return success true when the given tx hash for cosmos tx exists", async () => {
const txHash =
"A08331DC2FCA287B96D8F93C9FAA34A9C90BFA48A1D1A230108B7F563925AA8F";
const result = await queryTransactionStatus({
txHash,
environment,
});
expect(result.success).toEqual(true);
expect(result.data?.status).toBeDefined();
expect(result.data?.timeSpent).toBeDefined();
expect(result.data?.gasPaidInfo).toBeDefined();
expect(result.data?.callTx).toBeDefined();
expect(result.data?.executed).toBeDefined();

const resultForLowerCaseTxHash = await queryTransactionStatus({
txHash: txHash.toLowerCase(),
environment,
});
expect(resultForLowerCaseTxHash.success).toEqual(true);
});

test("should return success true when the given tx hash for evm tx exists", async () => {
const txHash =
"0x9d396b11115455ffdf32294915ccfb12e3ae48e7c89f85e1f3fc6bfb591d1a3e";
const result = await queryTransactionStatus({
txHash,
environment,
});
expect(result.success).toEqual(true);
expect(result.data?.status).toBeDefined();
expect(result.data?.timeSpent).toBeDefined();
expect(result.data?.gasPaidInfo).toBeDefined();
expect(result.data?.callTx).toBeDefined();
expect(result.data?.executed).toBeDefined();
});

test("should return expressExecuted and expressExecutedAt when the given tx hash is express tx", async () => {
const txHash =
"0xac8b22c4ae0890832adcbca19f8bfd0db734a6bf00a99fe839ca6d1055b9942b";
const result = await queryTransactionStatus({
txHash,
environment,
});
expect(result.success).toEqual(true);
expect(result.data?.expressExecuted).toBeDefined();
expect(result.data?.expressExecutedAt).toBeDefined();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import type { SearchGMPResponseData } from "@axelarjs/api";

import type {
QueryTransactionStatusDependencies,
QueryTransactionStatusError,
QueryTransactionStatusParams,
QueryTransactionStatusResult,
} from "./types";

function parseTxError(
firstTx: SearchGMPResponseData
): QueryTransactionStatusError | undefined {
const errorDetails = firstTx.error;
if (errorDetails) {
return {
message: errorDetails.error.message,
txHash: errorDetails.error.transactionHash,
chain: errorDetails.chain,
};
} else if (firstTx.is_insufficient_fee) {
return {
message: "Insufficient fee",
txHash: firstTx.call.transactionHash,
chain: firstTx.call.chain,
};
}

return undefined;
}

export async function queryTransactionStatus(
params: QueryTransactionStatusParams,
dependencies: QueryTransactionStatusDependencies
): Promise<QueryTransactionStatusResult> {
const { txHash, txLogIndex } = params;
const { gmpClient } = dependencies;

const txs = await gmpClient.searchGMP({
txHash,
txLogIndex,
});

if (txs.length === 0) {
return {
success: false,
error: "Transaction not found",
};
}

const firstTx = txs[0]!;

const {
call,
status,
gas_status,
gas_paid,
executed,
time_spent,
express_executed,
express_executing_at,
approved,
} = firstTx;

return {
success: true,
data: {
status,
error: parseTxError(firstTx),
timeSpent: time_spent,
gasPaidInfo: {
status: gas_status,
details: gas_paid,
},
callTx: call,
executed,
expressExecuted: express_executed,
expressExecutedAt: express_executing_at,
approved,
},
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type {
GMPClient,
GMPTxStatus,
SearchGMPApprove,
SearchGMPCall,
SearchGMPExecuted,
SearchGMPExpressExecuted,
SearchGMPGasPaid,
SearchGMPGasStatus,
SearchGMPTimespent,
} from "@axelarjs/api";
import type { Environment } from "@axelarjs/core";

export type QueryTransactionStatusParams = {
txHash: string;
txLogIndex?: number;
environment: Environment;
};

export type QueryTransactionStatusDependencies = {
gmpClient: GMPClient;
};

export type QueryTransactionStatusResult = {
success: boolean;
error?: string;
data?: {
status: GMPTxStatus;
error?: QueryTransactionStatusError | undefined;
timeSpent: SearchGMPTimespent;
gasPaidInfo: {
status: SearchGMPGasStatus;
details: SearchGMPGasPaid;
};
callTx: SearchGMPCall;
executed?: SearchGMPExecuted | undefined;
expressExecuted?: SearchGMPExpressExecuted | undefined;
expressExecutedAt?: number | undefined;
approved?: SearchGMPApprove | undefined;
};
};

export type QueryTransactionStatusError = {
message: string;
txHash: string;
chain: string;
};
Loading