diff --git a/packages/client/src/getBalances.ts b/packages/client/src/getBalances.ts index b35c7973..67fee91e 100644 --- a/packages/client/src/getBalances.ts +++ b/packages/client/src/getBalances.ts @@ -11,6 +11,7 @@ import { getSolanaBalances, getSplTokenBalances, getSuiBalances, + getTonBalances, prepareMulticallContexts, } from "../../../utils/balances"; import { ZetaChainClient } from "./client"; @@ -23,6 +24,7 @@ import { ZetaChainClient } from "./client"; * @param options.btcAddress Bitcoin address * @param options.solanaAddress Solana address * @param options.suiAddress Sui address + * @param options.tonAddress TON address * @returns Array of token balances */ export const getBalances = async function ( @@ -32,11 +34,13 @@ export const getBalances = async function ( btcAddress, solanaAddress, suiAddress, + tonAddress, }: { btcAddress?: string; evmAddress?: string; solanaAddress?: string; suiAddress?: string; + tonAddress?: string; } ): Promise { const foreignCoins = await this.getForeignCoins(); @@ -140,5 +144,11 @@ export const getBalances = async function ( balances.push(...suiBalances); } + // Get TON balances + if (tonAddress) { + const tonBalances = await getTonBalances(allTokens, tonAddress); + balances.push(...tonBalances); + } + return balances; }; diff --git a/packages/commands/src/query/balances.ts b/packages/commands/src/query/balances.ts index 03e880e6..1524af10 100644 --- a/packages/commands/src/query/balances.ts +++ b/packages/commands/src/query/balances.ts @@ -36,6 +36,7 @@ const balancesOptionsSchema = z.object({ network: z.enum(["mainnet", "testnet"]).default("testnet"), solana: z.string().optional(), sui: z.string().optional(), + ton: z.string().optional(), }); type BalancesOptions = z.infer; @@ -91,7 +92,15 @@ const main = async (options: BalancesOptions) => { suiAddress: options.sui, }); - if (!evmAddress && !btcAddress && !solanaAddress && !suiAddress) { + const tonAddress = options.ton; + + if ( + !evmAddress && + !btcAddress && + !solanaAddress && + !suiAddress && + !tonAddress + ) { spinner.fail("No addresses provided or derivable from account name"); console.error(chalk.red(WALLET_ERROR)); return; @@ -108,6 +117,7 @@ const main = async (options: BalancesOptions) => { evmAddress, solanaAddress, suiAddress, + tonAddress, }); spinner.succeed("Successfully fetched balances"); @@ -149,6 +159,7 @@ export const balancesCommand = new Command("balances") "Fetch balances for a specific Bitcoin address" ) .option("--sui
", "Fetch balances for a specific Sui address") + .option("--ton
", "Fetch balances for a specific TON address") .option("--name ", "Account name") .addOption( new Option("--network ", "Network to use") diff --git a/utils/balances.ts b/utils/balances.ts index 4b42134e..d9a6af6b 100644 --- a/utils/balances.ts +++ b/utils/balances.ts @@ -18,6 +18,9 @@ import { ForeignCoin } from "../types/foreignCoins.types"; import { ObserverSupportedChain } from "../types/supportedChains.types"; import { handleError } from "./handleError"; +export const TON_MAINNET_API = "https://tonapi.io/v2/accounts"; +export const TON_TESTNET_API = "https://testnet.tonapi.io/v2/accounts"; + export interface UTXO { txid: string; value: number; @@ -791,3 +794,79 @@ export const hasSufficientBalanceEvm = async ( return { balance, decimals, hasEnoughBalance }; }; + +interface TonApiResponse { + address: string; + balance: string | number; + get_methods: string[]; + interfaces: string[]; + is_wallet: boolean; + last_activity: number; + status: string; +} + +/** + * Gets TON native token balances + */ +export const getTonBalances = async ( + tokens: Token[], + tonAddress: string +): Promise => { + const tonToken = tokens.find( + (token) => + token.coin_type === "Gas" && + token.chain_name && + ["ton_mainnet", "ton_testnet"].includes(token.chain_name) + ); + + if (!tonToken) { + return []; + } + + try { + const network = tonToken.chain_name?.replace("ton_", "") || "testnet"; + const API = network === "mainnet" ? TON_MAINNET_API : TON_TESTNET_API; + + const response = await axios.get(`${API}/${tonAddress}`, { + headers: { + Accept: "application/json", + }, + }); + + // Validate response structure + if (!response.data) { + throw new Error("Empty response from TON API"); + } + + const { balance } = response.data; + if (balance === undefined || balance === null) { + throw new Error( + `Missing balance in TON API response: ${JSON.stringify(response.data)}` + ); + } + + // TON API returns balance in nanoTON (1 TON = 10^9 nanoTON) + const formattedBalance = ethers.formatUnits(BigInt(balance.toString()), 9); + + return [ + { + ...tonToken, + balance: formattedBalance, + id: parseTokenId(tonToken.chain_id?.toString() || "", tonToken.symbol), + }, + ]; + } catch (error) { + if (axios.isAxiosError(error)) { + console.error( + `Failed to get TON balance for ${tonToken.chain_name}:`, + error.response?.data || error.message + ); + } else { + console.error( + `Failed to get TON balance for ${tonToken.chain_name}:`, + error instanceof Error ? error.message : String(error) + ); + } + return []; + } +};