import { useConfig } from "wagmi" import { Abi, formatUnits } from "viem" import { keepPreviousData, useQuery, useQueryClient, UseQueryOptions, } from "@tanstack/react-query" import { getBalance, readContracts } from "wagmi/actions" import { useCallback } from "react" import { mainnetTokenContracts, MezoChainToken, testnetTokenContracts, } from "../lib/contracts" import { useWalletAccount } from "./useWalletAccount" import { getAsset, isBitcoinLikeCryptoAsset, isTTokenCryptoAsset, isMezoCryptoAsset, } from "../utils/assets" import { convertToUsd } from "../utils/currency" import { CHAIN_ID, mezoMainnet, mezoTestnet } from "../constants" import { useAssetsConversionRates } from "./useAssetsConversionRates" import { normalizePrecision } from "../utils/numbers" import { usePassportContext } from "./usePassportContext" // Wagmi handles typesafety with ABI const assertions. TypeScript doesn't // support importing JSON as const yet so types cannot be inferred from the // imported contract. As a workaround there is minimal ABI definition that can // be asserted types from. // Ref: https://wagmi.sh/core/typescript#const-assert-abis-typed-data export const BALANCE_OF_ABI = [ { inputs: [ { internalType: "address", name: "account", type: "address", }, ], name: "balanceOf", outputs: [ { internalType: "uint256", name: "", type: "uint256", }, ], stateMutability: "view", type: "function", }, ] as const satisfies Abi const BALANCE_TOKENS: MezoChainToken[] = [ "MEZO", "MUSD", "mcbBTC", "mDAI", "mFBTC", "mSolvBTC", "mswBTC", "mT", "mUSDC", "mUSDe", "mUSDT", "mxSolvBTC", ] type UseMezoChainTokensBalancesOptions = { tokens?: T queryOptions?: Omit< UseQueryOptions>, "queryKey" | "queryFn" | "select" | "enabled" > } export type TokenBalance = { decimals: number formatted: string symbol: string value: bigint usd: { value: bigint formatted: string } } const TOKEN_BALANCES_QUERY_KEY = "passport.tokenBalances" /** * Hook to get the balance of a list of Mezo tokens for the current account * @param options.tokens The list of tokens to get the balance for. It will * fallback to all tokens if not provided. * @param options.queryOptions The query options to pass to the * `useReadContracts` * @returns Tanstack's `useQuery` returnings with balance of tokens for the * current account in form of typesafe object with token names as keys * and balances as values. * @example * const mezoTokensBalance = useTokensBalances({ * tokens: ["mT", "mxSolvBTC"], * }) * // Assuming the status is "success" * console.log(mezoTokensBalance.data.mT) // Eg. { value: 0n ... } * console.log(Object.keys(mezoTokensBalance.data)) // ["mT", "mxSolvBTC"] */ export function useTokensBalances( options: UseMezoChainTokensBalancesOptions = {}, ) { const { tokens = BALANCE_TOKENS, ...restQueryOptions } = options const walletAccount = useWalletAccount() const config = useConfig() const { data: conversionRatesData } = useAssetsConversionRates() const { environment = "mainnet" } = usePassportContext() const chainId = environment === "testnet" ? mezoTestnet.id : mezoMainnet.id return useQuery({ queryKey: [ TOKEN_BALANCES_QUERY_KEY, walletAccount?.accountAddress, options.tokens, chainId, ], enabled: !!walletAccount?.accountAddress && !!conversionRatesData, placeholderData: keepPreviousData, queryFn: async () => { const isMainnet = chainId === CHAIN_ID.mainnet const contractsMap = isMainnet ? mainnetTokenContracts : testnetTokenContracts if (!walletAccount?.accountAddress) { throw new Error("Account address is not available.") } const tokenContracts = tokens.map((token) => { const { address } = contractsMap[token] return { address, abi: BALANCE_OF_ABI, functionName: "balanceOf" as const, args: [walletAccount?.accountAddress], chainId, } }) if (!conversionRatesData) { throw new Error("Conversion rates data is not available.") } return Promise.all([ getBalance(config, { address: walletAccount?.accountAddress, chainId }), readContracts(config, { contracts: tokenContracts, }), new Promise((resolve) => { resolve(conversionRatesData) }) as Promise, ]) }, select: (data) => { const [btcBalance, tokensBalancesData, conversion] = data const parsedBtcBalance = { ...btcBalance, symbol: getAsset("BTC").symbol, usd: convertToUsd( btcBalance.value, btcBalance.decimals, conversion.rates.BTC, conversion.decimals, ), } const parsedTokensBalances = tokensBalancesData.map((item, index) => { const token = tokens[index] const { decimals, symbol } = getAsset(token) if (item.status === "failure") { throw new Error( `Failed to fetch balance of ${token} for ${walletAccount?.accountAddress}.`, ) } const tokenBalance: Omit = { value: item.result, decimals, symbol, formatted: formatUnits(item.result, decimals), } let usd = { value: normalizePrecision( tokenBalance.value, tokenBalance.decimals, conversion.decimals, ), formatted: formatUnits(tokenBalance.value, tokenBalance.decimals), } if (isBitcoinLikeCryptoAsset(tokenBalance.symbol)) { usd = convertToUsd( tokenBalance.value, tokenBalance.decimals, conversion.rates.BTC, conversion.decimals, ) } if (isTTokenCryptoAsset(tokenBalance.symbol)) { usd = convertToUsd( tokenBalance.value, tokenBalance.decimals, conversion.rates.mT, conversion.decimals, ) } // TODO: Provide MEZO oracle to get accurate token price if (isMezoCryptoAsset(tokenBalance.symbol)) { usd = { value: 0n, formatted: "0", } } return { ...tokenBalance, usd } }) return [parsedBtcBalance, ...parsedTokensBalances].reduce( (acc, token) => ({ ...acc, [token.symbol]: token, }), {} as Record, ) }, ...restQueryOptions, }) } /** * Hook for invalidating current user's token balances. Can be used to * invalidate the balances manually, which forces the data to be re-fetched. * @returns Function `invalidateTokenBalances` that invalidates token balances * @example * const { invalidateTokenBalances } = useInvalidateTokensBalances() * (...) * await invalidateTokenBalances() */ export function useInvalidateTokensBalances() { const queryClient = useQueryClient() const invalidateTokensBalancesHandler = useCallback( () => queryClient.invalidateQueries({ queryKey: [TOKEN_BALANCES_QUERY_KEY] }), [queryClient], ) return { invalidateTokensBalances: invalidateTokensBalancesHandler, } } /** * Hook for resetting current user's token balances. Can be used to reset the * balances manually, which forces the data to be re-fetched. * @returns Function `resetTokenBalances` that invalidates token balances * @example * const { resetTokenBalances } = useResetTokensBalances() * (...) * await resetTokenBalances() */ export function useResetTokensBalances() { const queryClient = useQueryClient() const resetTokenBalancesHandler = useCallback( () => queryClient.resetQueries({ queryKey: [TOKEN_BALANCES_QUERY_KEY] }), [queryClient], ) return { resetTokenBalances: resetTokenBalancesHandler, } }