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 is transaction is already confirmed but not broadcast, and broadcast the transaction (as identified by command ID) if so
canhtrinh marked this conversation as resolved.
Show resolved Hide resolved

## [0.12.4] - 2023-FEBRUARY-1

Expand Down
5 changes: 5 additions & 0 deletions 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 @@ -40,6 +43,7 @@ const testnetConfigs: EnvironmentConfigs = {
axelarLcdUrl: "https://rpc-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
73 changes: 63 additions & 10 deletions src/libs/TransactionRecoveryApi/AxelarGMPRecoveryAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
} from "../types";
import {
AxelarRecoveryApi,
BatchedCommandsAxelarscanResponse,
ExecuteParams,
GMPStatus,
GMPStatusResponse,
Expand All @@ -25,6 +26,7 @@ import { AxelarQueryAPI } from "../AxelarQueryAPI";
import rpcInfo from "./constants/chain";
import {
getDestinationChainFromTxReceipt,
getEventIndexFromTxReceipt,
getGasAmountFromTxReceipt,
getLogIndexFromTxReceipt,
getNativeGasAmountFromTxReceipt,
Expand All @@ -46,10 +48,11 @@ import {
NotGMPTransactionError,
UnsupportedGasTokenError,
} from "./constants/error";
import { callExecute, CALL_EXECUTE_ERROR } from "./helpers";
import { callExecute, CALL_EXECUTE_ERROR, getCommandId } from "./helpers";
import { asyncRetry, sleep, throwIfInvalidChainIds } from "../../utils";
import { BatchedCommandsResponse } from "@axelar-network/axelarjs-types/axelar/evm/v1beta1/query";
import { Interface } from "ethers/lib/utils";
import { arrayify, Interface, keccak256 } from "ethers/lib/utils";
import { fromHex } from "@cosmjs/encoding";

export class AxelarGMPRecoveryAPI extends AxelarRecoveryApi {
axelarQueryApi: AxelarQueryAPI;
Expand Down Expand Up @@ -77,6 +80,10 @@ export class AxelarGMPRecoveryAPI extends AxelarRecoveryApi {
});
}

public getCommandIdFromSrcTxHash(srcChainId: number, txHash: string, eventIndex: number) {
return getCommandId(srcChainId, txHash, eventIndex);
}

public async manualRelayToDestChain(
txHash: string,
evmWalletDetails?: EvmWalletDetails
Expand All @@ -97,6 +104,9 @@ export class AxelarGMPRecoveryAPI extends AxelarRecoveryApi {
signCommandTx,
});
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


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

/**
* 1. check if transaction is already executed or approved
*/
if (status === GMPStatus.CANNOT_FETCH_STATUS)
return errorResponse(ApproveGatewayError.FETCHING_STATUS_FAILED);
if (status === GMPStatus.DEST_EXECUTED)
Expand All @@ -105,23 +115,59 @@ export class AxelarGMPRecoveryAPI extends AxelarRecoveryApi {
return errorResponse(ApproveGatewayError.ALREADY_APPROVED);

const srcChain = callTx.chain;
const destChainName = callTx.returnValues.destinationChain.toLowerCase();
const destChain = callTx.returnValues.destinationChain;

/**
* 3. check if command ID exists. if it does, no need to reconfirm. if it doesn't, then move on to confirm
* if command ID exists but command has not been executed, then execute it
*/
try {
confirmTx = await this.confirmGatewayTx(txHash, srcChain);
await sleep(2);
const destChainId = rpcInfo[this.environment].networkInfo[destChainName]?.chainId;
const eventIndex = await this.getEventIndex(srcChain, txHash, evmWalletDetails);
if (!eventIndex || eventIndex < 0) throw `could not find event index for ${txHash}`;

const commandId = this.getCommandIdFromSrcTxHash(destChainId, txHash, eventIndex);
if (commandId) {
const batchData = await this.fetchBatchData(commandId);
if (batchData) {
const command = batchData.commands.find((command) => command.id === commandId);
if (command && !command?.executed) {
Copy link
Member

Choose a reason for hiding this comment

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

not very readable, should encapsulate in a separate function, and return early on the failure condition for less nested ifs

const approveTx = await this.sendApproveTx(
destChain,
batchData.execute_data,
_evmWalletDetails
);
return {
success: true,
approveTx,
};
}
return { success: false, error: "Transaction is already confirmed" };
}
return {
success: false,
error: "Already confirmed but unable to send to destination chain",
};
}
} catch (e) {
console.error(e);
}

createPendingTransferTx = await this.createPendingTransfers(destChain);
await sleep(2);
/**
* 4. transaction was not confirmed by the network at all, so confirm it and bring through the rest of the pipeline
*/

try {
confirmTx = await this.confirmGatewayTx(txHash, srcChain);
await sleep(3);

signCommandTx = await this.signCommands(destChain);
const signEvent = signCommandTx.rawLog[0]?.events?.find(
(event: any) => event.type === "sign"
);
await sleep(3);

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

await sleep(2);
const batchedCommandId = signEvent.attributes.find(
Copy link
Member

Choose a reason for hiding this comment

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

sign commands is racing axelarons here, so you will often not get the correct batch id to relay. The logic is also duplicated. I think this should be split across a few functions, and look something like:

  1. Add a method to check if tx hash is confirmed. Use the event query for this.
  2. If the event status is failed, abort.
  3. If not confirmed yet, check if tx is finalized (this could be skipped if axelarscan only shows the Approve button after src tx is finalized, to keep the logic here simpler, otherwise this method should check if latest block is x block confirmations over tx).
  4. And then call the confirm gateway tx method. Wait for 30s for votes to go through (can take 2-3 blocks) and signing/relaying to occur.
  5. Check if event is in completed status now. Fail if not.
  6. if event was in completed status, 2-5 can be skipped.
  7. If destination chain is not EVM, fail (with cosmos gmp this will change).
  8. Check if command is executed.
  9. If the batch itself wasn't found, then sign and wait 20s
  10. If the batch wasn't found OR command wasn't executed, submit batch to gateway using user's wallet, and wait for 20s.
  11. If command is still not executed, return an error.

(attr: any) => attr.key === "batchedCommandId"
)?.value;
Expand All @@ -132,7 +178,7 @@ export class AxelarGMPRecoveryAPI extends AxelarRecoveryApi {
);
if (!batchedCommand) return errorResponse(ApproveGatewayError.ERROR_BATCHED_COMMAND);

await sleep(2);
await sleep(3);

const approveTx = await this.sendApproveTx(
destChain,
Expand Down Expand Up @@ -217,6 +263,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
37 changes: 37 additions & 0 deletions src/libs/TransactionRecoveryApi/AxelarRecoveryApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,33 @@ export interface ExecuteParamsResponse {
data?: ExecuteParams;
}

export interface CommandObj {
id: string;
type: string;
key_id: string;
max_gas_cost: number;
executed: boolean;
transactionHash: string;
transactionIndex: string;
logIndex: number;
block_timestamp: number;
}
export interface BatchedCommandsAxelarscanResponse {
data: string;
status: string;
key_id: string;
execute_data: string;
prev_batched_commands_id: string;
command_ids: string[];
proof: Record<string, string[]>;
weights: string[];
threshold: string;
signatures: string[];
batch_id: string;
chain: string;
commands: CommandObj[];
id: string;
}
export type SubscriptionStrategy =
| {
kind: "websocket";
Expand All @@ -88,6 +115,7 @@ export class AxelarRecoveryApi {
readonly environment: Environment;
readonly recoveryApiUrl: string;
readonly axelarGMPApiUrl: string;
readonly axelarscanBaseApiUrl: string;
readonly axelarRpcUrl: string;
readonly axelarLcdUrl: string;
readonly wssStatusUrl: string;
Expand All @@ -99,6 +127,7 @@ export class AxelarRecoveryApi {
const { environment } = config;
const links: EnvironmentConfigs = getConfigs(environment);
this.axelarGMPApiUrl = links.axelarGMPApiUrl;
this.axelarscanBaseApiUrl = links.axelarscanBaseApiUrl;
this.recoveryApiUrl = links.recoveryApiUrl;
this.wssStatusUrl = links.wssStatus;
this.axelarRpcUrl = config.axelarRpcUrl || links.axelarRpcUrl;
Expand All @@ -117,6 +146,14 @@ export class AxelarRecoveryApi {
.catch(() => undefined);
}

public async fetchBatchData(commandId: string): Promise<BatchedCommandsAxelarscanResponse> {
return this.execPost(this.axelarscanBaseApiUrl, "/batches", {
commandId,
})
.then((res) => res[0])
.catch(() => undefined);
}

private parseGMPStatus(response: any): GMPStatus | string {
const { error, status } = response;

Expand Down
2 changes: 1 addition & 1 deletion src/libs/TransactionRecoveryApi/client/EVMClient/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export default class EVMClient {
this.provider = provider;
} else {
this.provider =
useWindowEthereum && window?.ethereum
useWindowEthereum && typeof window !== "undefined" && window?.ethereum
? new ethers.providers.Web3Provider(window.ethereum, networkOptions)
: new ethers.providers.JsonRpcProvider(rpcUrl, networkOptions);
}
Expand Down
11 changes: 10 additions & 1 deletion src/libs/TransactionRecoveryApi/helpers/contractEventHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ export function getLogIndexFromTxReceipt(
);
}

export function getEventIndexFromTxReceipt(
receipt: ethers.providers.TransactionReceipt
): Nullable<number> {
return (
getContractCallEvent(receipt)?.eventIndex || getContractCallWithTokenEvent(receipt)?.eventIndex
);
}

export function isContractCallWithToken(receipt: ethers.providers.TransactionReceipt): boolean {
return !!getContractCallWithTokenEvent(receipt);
}
Expand Down Expand Up @@ -173,14 +181,15 @@ export function findContractEvent(
eventSignatures: string[],
abiInterface: Interface
): Nullable<EventLog> {
for (const log of receipt.logs) {
for (const [index, log] of receipt.logs.entries()) {
const eventIndex = eventSignatures.indexOf(log.topics[0]);
if (eventIndex > -1) {
const eventLog = abiInterface.parseLog(log);
return {
signature: eventSignatures[eventIndex],
eventLog,
logIndex: log.logIndex,
Copy link
Member

Choose a reason for hiding this comment

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

logIndex is the event index, right?

eventIndex: index,
};
}
}
Expand Down
15 changes: 15 additions & 0 deletions src/libs/TransactionRecoveryApi/helpers/getCommandId.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { arrayify, keccak256 } from "ethers/lib/utils";

export function getCommandId(chainID: number, txHash: string, sourceEventIndex: number) {
Copy link
Member

Choose a reason for hiding this comment

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

add a link to core for the derivation?

const seiArr = arrayify(sourceEventIndex).reverse();
const txHashWithEventIndex = new Uint8Array([
...arrayify(txHash),
...new Uint8Array(8).map((a, i) => seiArr[i] || a),
]);
console.log("chain id", chainID);
const chainIdByteArray = arrayify(chainID);
const dataToHash = new Uint8Array(txHashWithEventIndex.length + chainIdByteArray.length);
dataToHash.set(txHashWithEventIndex, 0);
dataToHash.set(chainIdByteArray, txHashWithEventIndex.length);
return keccak256(dataToHash).slice(2); // remove 0x prefix
}
1 change: 1 addition & 0 deletions src/libs/TransactionRecoveryApi/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from "./axelarHelper";
export * from "./contractEventHelper";
export * from "./providerHelper";
export * from "./contractCallHelper";
export * from "./getCommandId";
27 changes: 27 additions & 0 deletions src/libs/test/TransactionRecoveryAPI/EncodingTests.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { AxelarGMPRecoveryAPI } from "../../TransactionRecoveryApi/AxelarGMPRecoveryAPI";
import { AddGasOptions, Environment, EvmChain, EvmWalletDetails } from "../../types";
import { utils } from "@axelar-network/axelar-local-dev";

describe("AxelarDepositRecoveryAPI", () => {
const { setLogger } = utils;
setLogger(() => null);

let evmWalletDetails: EvmWalletDetails;
beforeEach(() => {
vitest.clearAllMocks();
evmWalletDetails = {
privateKey: "",
useWindowEthereum: false,
};
});

describe("creating command ID", () => {
const api = new AxelarGMPRecoveryAPI({ environment: Environment.TESTNET });
test("It should create a command ID from a tx hash and event index", async () => {
const txHash = "0xa290f800f2089535a0abb013cea9cb26e1cdb3f2a2f2a8dcef2f149eb7a4d3be";
const eventIndex = await api.getEventIndex(EvmChain.MOONBEAM, txHash, evmWalletDetails);
const res = await api.getCommandIdFromSrcTxHash(97, txHash, eventIndex as number);
expect(res).toEqual("131f5b18753a46a21b9a154818242c9dc0647c9d85faf13461bd3fefbab6c3de");
}, 60000);
});
});
1 change: 1 addition & 0 deletions src/libs/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ export interface EventLog {
signature: string;
eventLog: LogDescription;
logIndex: number;
eventIndex: number;
}

export interface ExecuteArgs {
Expand Down