Skip to content
Open
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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@
"@zetachain/protocol-contracts": "13.0.0",
"@zetachain/protocol-contracts-solana": "^5.0.0",
"@zetachain/protocol-contracts-ton": "1.0.0-rc4",
"@zetachain/sdk-cosmos": "0.0.7",
"@zetachain/standard-contracts": "^2.0.1",
"axios": "^1.4.0",
"bech32": "^2.0.0",
Expand All @@ -176,4 +177,4 @@
"zod": "^3.24.2"
},
"packageManager": "yarn@1.22.21+sha1.1959a18351b811cdeedbd484a8f86c3cc3bbaf72"
}
}
18 changes: 11 additions & 7 deletions packages/client/src/getBalances.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import {
ChainSDKType,
Vm,
vmToJSON,
} from "@zetachain/sdk-cosmos/zetachain/zetacore/pkg/chains/chains";

import { TokenBalance } from "../../../types/balances.types";
import { ObserverSupportedChain } from "../../../types/supportedChains.types";
import {
addZetaTokens,
collectTokensFromForeignCoins,
Expand Down Expand Up @@ -67,20 +72,19 @@ export const getBalances = async function (
const evmTokens = allTokens.filter(
(token) =>
token.chain_name &&
supportedChains.find(
(chain: ObserverSupportedChain) => chain.name === token.chain_name
)?.vm === "evm"
String(
supportedChains.find((chain) => chain.name === token.chain_name)?.vm
) === vmToJSON(Vm.evm)
);

const multicallContexts = prepareMulticallContexts(evmTokens, evmAddress);

for (const [chainName, contexts] of Object.entries(multicallContexts)) {
const chain = supportedChains.find(
(c: ObserverSupportedChain) => c.name === chainName
(c: ChainSDKType) => c.name === chainName
);
if (!chain) continue;

const rpc = getRpcUrl(parseInt(chain.chain_id));
const rpc = getRpcUrl(Number(chain.chain_id));
let chainBalances: TokenBalance[];

try {
Expand Down
22 changes: 11 additions & 11 deletions packages/client/src/getFees.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import { mainnet, testnet } from "@zetachain/protocol-contracts";
import ZRC20 from "@zetachain/protocol-contracts/abi/ZRC20.sol/ZRC20.json";
import { QueryConvertGasToZetaResponseSDKType } from "@zetachain/sdk-cosmos/zetachain/zetacore/crosschain/query";
import { ForeignCoinsSDKType } from "@zetachain/sdk-cosmos/zetachain/zetacore/fungible/foreign_coins";
import { ChainSDKType } from "@zetachain/sdk-cosmos/zetachain/zetacore/pkg/chains/chains";
import { CoinType } from "@zetachain/sdk-cosmos/zetachain/zetacore/pkg/coin/coin";
import axios from "axios";
import { BigNumberish, ethers } from "ethers";

import { ZRC20Contract } from "../../../types/contracts.types";
import { ForeignCoin } from "../../../types/foreignCoins.types";
import {
ConvertGasToZetaResponse,
FeeItem,
} from "../../../types/getFees.types";
import { FeeItem } from "../../../types/getFees.types";
import { handleError } from "../../../utils/handleError";
import { ZetaChainClient } from "./client";

const fetchZEVMFees = async (
zrc20: (typeof mainnet)[number],
rpcUrl: string,
foreignCoins: ForeignCoin[]
foreignCoins: ForeignCoinsSDKType[]
): Promise<FeeItem | undefined> => {
const provider = new ethers.JsonRpcProvider(rpcUrl);
const contract = new ethers.Contract(
Expand All @@ -40,8 +40,8 @@ const fetchZEVMFees = async (
const protocolFee = ethers.toBigInt(rawProtocolFlatFee);
const gasToken = foreignCoins.find((foreignCoin) => {
return (
foreignCoin.foreign_chain_id === zrc20.foreign_chain_id &&
foreignCoin.coin_type === "Gas"
String(foreignCoin.foreign_chain_id) === zrc20.foreign_chain_id &&
foreignCoin.coin_type === CoinType.Gas
);
});

Expand Down Expand Up @@ -77,7 +77,7 @@ const fetchCCMFees = async function (

try {
const url = `${API}/zeta-chain/crosschain/convertGasToZeta?chainId=${chainID}&gasLimit=${gas}`;
const response = await axios.get<ConvertGasToZetaResponse>(url);
const response = await axios.get<QueryConvertGasToZetaResponseSDKType>(url);
const isResponseOk = response.status >= 200 && response.status < 300;

if (!isResponseOk) {
Expand Down Expand Up @@ -119,9 +119,9 @@ export const getFees = async function (this: ZetaChainClient, gas: number) {
const zrc20Addresses = addresses.filter((a) => a.type === "zrc20");

await Promise.all(
supportedChains.map(async (n: { chain_id: string; chain_name: string }) => {
supportedChains.map(async (n: ChainSDKType) => {
try {
const fee = await fetchCCMFees.call(this, n.chain_id, gas);
const fee = await fetchCCMFees.call(this, String(n.chain_id), gas);
if (fee) fees.messaging.push(fee);
} catch (error: unknown) {
handleError({
Expand Down
6 changes: 4 additions & 2 deletions packages/client/src/getForeignCoins.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { QueryAllForeignCoinsResponseSDKType } from "@zetachain/sdk-cosmos/zetachain/zetacore/fungible/query";
import axios from "axios";

import { ForeignCoinsResponse } from "../../../types/foreignCoins.types";
import { ZetaChainClient } from "./client";

export const getForeignCoins = async function (this: ZetaChainClient) {
const api = this.getEndpoint("cosmos-http", `zeta_${this.network}`);
const endpoint = `${api}/zeta-chain/fungible/foreign_coins`;
const { data } = await axios.get<ForeignCoinsResponse>(endpoint);
const { data } = await axios.get<QueryAllForeignCoinsResponseSDKType>(
endpoint
);

return data.foreignCoins;
};
5 changes: 3 additions & 2 deletions packages/client/src/getQuote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { parseUnits } from "@ethersproject/units";
import UniswapV2RouterABI from "@uniswap/v2-periphery/build/IUniswapV2Router02.json";
import { getAddress } from "@zetachain/protocol-contracts";
import ZRC20 from "@zetachain/protocol-contracts/abi/ZRC20.sol/ZRC20.json";
import { CoinType } from "@zetachain/sdk-cosmos/zetachain/zetacore/pkg/coin/coin";
import { ethers } from "ethers";

import {
Expand Down Expand Up @@ -78,8 +79,8 @@ export const getZRC20GasToken = async function (
const foreignCoins = await this.getForeignCoins();
const token = foreignCoins.find((foreignCoin) => {
return (
foreignCoin.foreign_chain_id === chainID &&
foreignCoin.coin_type === "Gas"
String(foreignCoin.foreign_chain_id) === chainID &&
Copy link
Copy Markdown
Member

@hernan-clich hernan-clich Sep 25, 2025

Choose a reason for hiding this comment

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

I do understand why we need to do this (the new sdk type says foreign_chain_id is a bigint while our previous, custom type had it as a string), but I don't think we should be doing this, the real, runtime type is a string.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is a response from the Foreign Coins endpoint:

image

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This clearly resolves as a string.

image

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The way I see it we could:

  1. Create a runtime accurate interface:
export interface ForeignCoinsRuntimeType extends Omit<ForeignCoinsSDKType, 'foreign_chain_id'> {
  foreign_chain_id: string;
}
  1. Fix the type on the sdk types lib.

foreignCoin.coin_type === CoinType.Gas
);
});
return token?.zrc20_contract_address;
Expand Down
6 changes: 4 additions & 2 deletions packages/client/src/getSupportedChains.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { QuerySupportedChainsResponseSDKType } from "@zetachain/sdk-cosmos/zetachain/zetacore/observer/query";
import axios from "axios";

import { ObserverSupportedChainsResponse } from "../../../types/supportedChains.types";
import { ZetaChainClient } from "./client";

export const getSupportedChains = async function (this: ZetaChainClient) {
const api = this.getEndpoint("cosmos-http", `zeta_${this.network}`);
const endpoint = `${api}/zeta-chain/observer/supportedChains`;
const { data } = await axios.get<ObserverSupportedChainsResponse>(endpoint);
const { data } = await axios.get<QuerySupportedChainsResponseSDKType>(
endpoint
);

return data.chains;
};
61 changes: 43 additions & 18 deletions packages/commands/src/query/cctx.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
import type { CrossChainTxSDKType } from "@zetachain/sdk-cosmos/zetachain/zetacore/crosschain/cross_chain_tx";
import {
CctxStatus,
cctxStatusToJSON,
} from "@zetachain/sdk-cosmos/zetachain/zetacore/crosschain/cross_chain_tx";
import {
CoinType,
coinTypeToJSON,
} from "@zetachain/sdk-cosmos/zetachain/zetacore/pkg/coin/coin";
import { Command } from "commander";
import { ethers } from "ethers";
import EventEmitter from "eventemitter3";
Expand All @@ -9,30 +18,34 @@ import {
DEFAULT_TIMEOUT,
} from "../../../../src/constants/commands/cctx";
import { cctxOptionsSchema } from "../../../../src/schemas/commands/cctx";
import type { CrossChainTx } from "../../../../types/trackCCTX.types";
import { fetchFromApi, sleep } from "../../../../utils";

/**
* Event map:
* - `cctx` → emitted every polling round with the **entire** array so far.
*/
interface CctxEvents {
cctx: (allSoFar: CrossChainTx[]) => void;
cctx: (allSoFar: CrossChainTxSDKType[]) => void;
}

export const cctxEmitter = new EventEmitter<CctxEvents>();

type CctxOptions = z.infer<typeof cctxOptionsSchema>;

interface CctxResponse {
CrossChainTxs: CrossChainTx[];
CrossChainTxs: CrossChainTxSDKType[];
}

/**
* True if the CCTX is still in-flight and may mutate on‑chain.
*/
const isPending = (tx: CrossChainTx): boolean =>
["PendingOutbound", "PendingRevert"].includes(tx.cctx_status.status);
const isPending = (tx: CrossChainTxSDKType): boolean => {
if (!tx.cctx_status) {
return false;
}
const status = cctxStatusToJSON(tx.cctx_status.status);
return ["PendingOutbound", "PendingRevert"].includes(status);
};

/**
* Poll indefinitely until user terminates.
Expand All @@ -49,7 +62,7 @@ const gatherCctxs = async (
): Promise<void> => {
const startTime = Date.now();
// Latest copy of each CCTX keyed by index
const results = new Map<string, CrossChainTx>();
const results = new Map<string, CrossChainTxSDKType>();

// Hashes we need to query in the upcoming round
const frontier = new Set<string>([rootHash]);
Expand Down Expand Up @@ -91,7 +104,9 @@ const gatherCctxs = async (

// Keep querying while pending
if (isPending(tx)) {
nextFrontier.add(tx.inbound_params.observed_hash);
if (tx.inbound_params) {
nextFrontier.add(tx.inbound_params.observed_hash);
}
}

queriedOnce.add(tx.index);
Expand All @@ -113,7 +128,7 @@ const gatherCctxs = async (
}
};

const formatCCTX = (cctx: CrossChainTx) => {
const formatCCTX = (cctx: CrossChainTxSDKType) => {
let output = "";
const {
index,
Expand All @@ -123,6 +138,8 @@ const formatCCTX = (cctx: CrossChainTx) => {
revert_options,
relayed_message,
} = cctx;
if (!inbound_params || !cctx_status) return "";

const { sender_chain_id, sender, amount, coin_type } = inbound_params;
const { receiver_chainId, receiver } = outbound_params[0];
const { status, status_message, error_message = "" } = cctx_status;
Expand All @@ -135,18 +152,25 @@ const formatCCTX = (cctx: CrossChainTx) => {
} = revert_options;
let mainStatusIcon = "🔄";
if (
status === "OutboundMined" ||
(status === "PendingOutbound" && outbound_params[0].hash !== "")
String(status) === cctxStatusToJSON(CctxStatus.OutboundMined) ||
(String(status) === cctxStatusToJSON(CctxStatus.PendingOutbound) &&
outbound_params[0].hash !== "")
) {
Comment on lines 154 to 158
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Use firstOutbound.hash after guarding array access

Avoid direct indexing; rely on the guarded alias.

-    (String(status) === cctxStatusToJSON(CctxStatus.PendingOutbound) &&
-      outbound_params[0].hash !== "")
+    (String(status) === cctxStatusToJSON(CctxStatus.PendingOutbound) &&
+      firstOutbound.hash !== "")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (
status === "OutboundMined" ||
(status === "PendingOutbound" && outbound_params[0].hash !== "")
String(status) === cctxStatusToJSON(CctxStatus.OutboundMined) ||
(String(status) === cctxStatusToJSON(CctxStatus.PendingOutbound) &&
outbound_params[0].hash !== "")
) {
if (
String(status) === cctxStatusToJSON(CctxStatus.OutboundMined) ||
(String(status) === cctxStatusToJSON(CctxStatus.PendingOutbound) &&
firstOutbound.hash !== "")
) {
🤖 Prompt for AI Agents
In packages/commands/src/query/cctx.ts around lines 154 to 158, the condition
directly indexes outbound_params[0].hash after only partially guarding array
access; change it to first verify outbound_params.length > 0, create a guarded
alias (e.g. const firstOutbound = outbound_params[0]) and then use
firstOutbound.hash in the conditional, or rewrite the condition to check
outbound_params.length > 0 && firstOutbound.hash !== "" so array access is
always safe.

mainStatusIcon = "✅";
} else if (status === "PendingOutbound" || status === "PendingRevert") {
} else if (
String(status) === cctxStatusToJSON(CctxStatus.PendingOutbound) ||
String(status) === cctxStatusToJSON(CctxStatus.PendingRevert)
) {
mainStatusIcon = "🔄";
} else {
mainStatusIcon = "❌";
}

let mainStatus;
if (status === "PendingOutbound" && outbound_params[0].hash !== "") {
if (
String(status) === cctxStatusToJSON(CctxStatus.PendingOutbound) &&
outbound_params[0].hash !== ""
) {
mainStatus = "PendingOutbound (transaction broadcasted to target chain)";
} else {
mainStatus = status;
Expand All @@ -169,7 +193,7 @@ Receiver: ${receiver}
mainTx += `Message: ${relayed_message}\n`;
}

if (coin_type !== "NoAssetCall") {
if (String(coin_type) !== coinTypeToJSON(CoinType.NoAssetCall)) {
mainTx += `Amount: ${amount} ${coin_type} tokens\n`;
}
Comment on lines +196 to 198
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix coin type comparison and display

String(coin_type) will not match JSON names; use coinTypeToJSON consistently.

-  if (String(coin_type) !== coinTypeToJSON(CoinType.NoAssetCall)) {
-    mainTx += `Amount:   ${amount} ${coin_type} tokens\n`;
-  }
+  if (coinTypeToJSON(coin_type) !== coinTypeToJSON(CoinType.NoAssetCall)) {
+    mainTx += `Amount:   ${amount} ${coinTypeToJSON(coin_type)} tokens\n`;
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (String(coin_type) !== coinTypeToJSON(CoinType.NoAssetCall)) {
mainTx += `Amount: ${amount} ${coin_type} tokens\n`;
}
if (coinTypeToJSON(coin_type) !== coinTypeToJSON(CoinType.NoAssetCall)) {
mainTx += `Amount: ${amount} ${coinTypeToJSON(coin_type)} tokens\n`;
}
🤖 Prompt for AI Agents
In packages/commands/src/query/cctx.ts around lines 196 to 198, the code
compares String(coin_type) to coinTypeToJSON(CoinType.NoAssetCall) and prints
coin_type directly; change the comparison to use coinTypeToJSON(coin_type) !==
coinTypeToJSON(CoinType.NoAssetCall) and when building the display line use
coinTypeToJSON(coin_type) (or a mapped readable token name) instead of
String(coin_type), so both the equality check and the printed token name use the
same JSON name format.


Expand All @@ -188,11 +212,12 @@ Receiver: ${receiver}

if (
outbound_params[1] &&
["Reverted", "PendingRevert", "Aborted"].includes(status)
["Reverted", "PendingRevert", "Aborted"].includes(String(status))
) {
const isReverted = status === "Reverted";
const isPendingRevert = status === "PendingRevert";
const isAborted = status === "Aborted";
const isReverted = String(status) === cctxStatusToJSON(CctxStatus.Reverted);
const isPendingRevert =
String(status) === cctxStatusToJSON(CctxStatus.PendingRevert);
const isAborted = String(status) === cctxStatusToJSON(CctxStatus.Aborted);

Comment on lines 214 to 221
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Revert/abort block never executes due to wrong status includes

String(status) won’t equal "Reverted"/"PendingRevert"/"Aborted". Compute flags via cctxStatusToJSON and gate on them.

-  if (
-    outbound_params[1] &&
-    ["Reverted", "PendingRevert", "Aborted"].includes(String(status))
-  ) {
-    const isReverted = String(status) === cctxStatusToJSON(CctxStatus.Reverted);
-    const isPendingRevert =
-      String(status) === cctxStatusToJSON(CctxStatus.PendingRevert);
-    const isAborted = String(status) === cctxStatusToJSON(CctxStatus.Aborted);
+  const isReverted = cctxStatusToJSON(status) === cctxStatusToJSON(CctxStatus.Reverted);
+  const isPendingRevert = cctxStatusToJSON(status) === cctxStatusToJSON(CctxStatus.PendingRevert);
+  const isAborted = cctxStatusToJSON(status) === cctxStatusToJSON(CctxStatus.Aborted);
+  if (outbound_params[1] && (isReverted || isPendingRevert || isAborted)) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
outbound_params[1] &&
["Reverted", "PendingRevert", "Aborted"].includes(status)
["Reverted", "PendingRevert", "Aborted"].includes(String(status))
) {
const isReverted = status === "Reverted";
const isPendingRevert = status === "PendingRevert";
const isAborted = status === "Aborted";
const isReverted = String(status) === cctxStatusToJSON(CctxStatus.Reverted);
const isPendingRevert =
String(status) === cctxStatusToJSON(CctxStatus.PendingRevert);
const isAborted = String(status) === cctxStatusToJSON(CctxStatus.Aborted);
const isReverted = cctxStatusToJSON(status) === cctxStatusToJSON(CctxStatus.Reverted);
const isPendingRevert = cctxStatusToJSON(status) === cctxStatusToJSON(CctxStatus.PendingRevert);
const isAborted = cctxStatusToJSON(status) === cctxStatusToJSON(CctxStatus.Aborted);
if (outbound_params[1] && (isReverted || isPendingRevert || isAborted)) {
🤖 Prompt for AI Agents
In packages/commands/src/query/cctx.ts around lines 214 to 221, the condition
uses literal strings ("Reverted","PendingRevert","Aborted") but status is
serialized via cctxStatusToJSON, so the branch never runs; compute the expected
string constants by calling cctxStatusToJSON(CctxStatus.Reverted/ PendingRevert/
Aborted) (or compute isReverted/isPendingRevert/isAborted first) and use those
values in the includes/gate expression (or directly gate on the boolean flags)
so the revert/abort block executes correctly.

const statusIcon = isPendingRevert ? "🔄" : "✅";
let statusMessage = "Unknown";
Expand All @@ -210,7 +235,7 @@ Receiver: ${receiver}
: revert_address;

const revertMessage = revert_message
? Buffer.from(revert_message, "base64").toString("hex")
? Buffer.from(String(revert_message), "base64").toString("hex")
: "null";

let chainDetails = "Unknown";
Expand Down
Loading