diff --git a/app/index.tsx b/app/index.tsx index 9a9fd41..66533f9 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -1,430 +1,29 @@ "use client" -import { EvmPriceServiceConnection } from "@pythnetwork/pyth-evm-js" +import { AppProvider } from "@/context/AppContext" -import "@/styles/globals.css" -import { createContext, useCallback, useEffect, useState } from "react" -import { getEndpoints } from "@zetachain/networks/dist/src/getEndpoints" -import EventEmitter from "eventemitter3" -// @ts-ignore -import Cookies from "js-cookie" -import debounce from "lodash/debounce" -import { useAccount } from "wagmi" - -import { hexToBech32Address } from "@/lib/hexToBech32Address" import { Toaster } from "@/components/ui/toaster" -import { useToast } from "@/components/ui/use-toast" import { SiteHeader } from "@/components/site-header" import { ThemeProvider } from "@/components/theme-provider" -import { useZetaChainClient } from "../hooks/useZetaChainClient" import { NFTProvider } from "./nft/useNFT" interface RootLayoutProps { children: React.ReactNode } -export const AppContext = createContext(null) - export default function Index({ children }: RootLayoutProps) { - const { client } = useZetaChainClient() - - const [balances, setBalances] = useState([]) - const [balancesLoading, setBalancesLoading] = useState(true) - const [balancesRefreshing, setBalancesRefreshing] = useState(false) - const [bitcoinAddress, setBitcoinAddress] = useState("") - const [fees, setFees] = useState([]) - const [pools, setPools] = useState([]) - const [poolsLoading, setPoolsLoading] = useState(false) - const [validators, setValidators] = useState([]) - const [validatorsLoading, setValidatorsLoading] = useState(false) - const [stakingDelegations, setStakingDelegations] = useState([]) - const [stakingRewards, setStakingRewards] = useState([]) - const [unbondingDelegations, setUnbondingDelegations] = useState([]) - const [observers, setObservers] = useState([]) - const [prices, setPrices] = useState([]) - const { address, isConnected } = useAccount() - const { toast } = useToast() - - const fetchObservers = useCallback( - debounce(async () => { - try { - const api = getEndpoints("cosmos-http", "zeta_testnet")[0]?.url - const url = `${api}/zeta-chain/observer/nodeAccount` - const response = await fetch(url) - const data = await response.json() - setObservers(data.NodeAccount) - } catch (e) { - console.error(e) - } - }, 500), - [] - ) - - const fetchUnbondingDelegations = useCallback( - debounce(async () => { - try { - if (!isConnected) { - return setUnbondingDelegations([]) - } - const api = getEndpoints("cosmos-http", "zeta_testnet")[0]?.url - const addr = hexToBech32Address(address as any, "zeta") - const url = `${api}/cosmos/staking/v1beta1/delegators/${addr}/unbonding_delegations` - const response = await fetch(url) - const data = await response.json() - setUnbondingDelegations(data.unbonding_responses) - } catch (e) { - console.error(e) - } - }, 500), - [address, isConnected] - ) - - const connectBitcoin = async () => { - const w = window as any - if ("xfi" in w && w.xfi?.bitcoin) { - w.xfi.bitcoin.changeNetwork("testnet") - const btc = (await w.xfi.bitcoin.getAccounts())[0] - await setBitcoinAddress(btc) - fetchBalances(true, btc) - } - } - - const fetchStakingDelegations = useCallback( - debounce(async () => { - try { - if (!isConnected) { - return setStakingDelegations([]) - } - const api = getEndpoints("cosmos-http", "zeta_testnet")[0]?.url - const addr = hexToBech32Address(address as any, "zeta") - const url = `${api}/cosmos/staking/v1beta1/delegations/${addr}` - const response = await fetch(url) - const data = await response.json() - setStakingDelegations(data.delegation_responses) - } catch (e) { - console.error(e) - } - }, 500), - [address, isConnected] - ) - - const fetchStakingRewards = useCallback( - debounce(async () => { - try { - if (!isConnected) { - return setStakingRewards([]) - } - const api = getEndpoints("cosmos-http", "zeta_testnet")[0]?.url - const addr = hexToBech32Address(address as any, "zeta") - const url = `${api}/cosmos/distribution/v1beta1/delegators/${addr}/rewards` - const response = await fetch(url) - const data = await response.json() - setStakingRewards(data.rewards) - } catch (e) { - console.error(e) - } - }, 500), - [address, isConnected] - ) - - const fetchValidators = useCallback( - debounce(async () => { - setValidatorsLoading(true) - let allValidators: any[] = [] - let nextKey: any = null - - try { - if (!isConnected) { - setValidatorsLoading(false) - setValidators([]) - } - const api = getEndpoints("cosmos-http", "zeta_testnet")[0]?.url - - const fetchBonded = async () => { - const response = await fetch(`${api}/cosmos/staking/v1beta1/pool`) - const data = await response.json() - return data - } - - const fetchPage = async (key: string) => { - const endpoint = "/cosmos/staking/v1beta1/validators" - const query = `pagination.key=${encodeURIComponent(key)}` - const url = `${api}${endpoint}?${key && query}` - - const response = await fetch(url) - const data = await response.json() - - allValidators = allValidators.concat(data.validators) - - if (data.pagination && data.pagination.next_key) { - await fetchPage(data.pagination.next_key) - } - } - const pool = (await fetchBonded())?.pool - const tokens = parseInt(pool.bonded_tokens) - await fetchPage(nextKey) - allValidators = allValidators.map((v) => { - return { - ...v, - voting_power: tokens ? (parseInt(v.tokens) / tokens) * 100 : 0, - } - }) - } catch (e) { - console.error(e) - } finally { - setValidators(allValidators) - setValidatorsLoading(false) - } - }, 500), - [address, isConnected] - ) - - const fetchBalances = useCallback( - debounce(async (refresh: Boolean = false, btc: any = null) => { - if (refresh) setBalancesRefreshing(true) - if (balances.length === 0) setBalancesLoading(true) - try { - if (!isConnected) { - return setBalances([]) - } - const b = await client.getBalances({ - evmAddress: address, - btcAddress: btc, - }) - setBalances(b) - } catch (e) { - console.error(e) - } finally { - setBalancesRefreshing(false) - setBalancesLoading(false) - } - }, 500), - [isConnected, address] - ) - - const fetchFeesList = useCallback( - debounce(async () => { - try { - if (!isConnected) { - return setFees([]) - } - setFees(await client.getFees(500000)) - } catch (e) { - console.error(e) - } - }, 500), - [] - ) - - const fetchPools = useCallback( - debounce(async () => { - setPoolsLoading(true) - try { - setPools(await client.getPools()) - } catch (e) { - console.error(e) - } finally { - setPoolsLoading(false) - } - }, 500), - [] - ) - - useEffect(() => { - fetchBalances(true) - fetchFeesList() - fetchStakingDelegations() - fetchPrices() - }, [isConnected, address]) - - const fetchPrices = useCallback( - debounce(async () => { - let priceIds: any = [] - const api = getEndpoints("cosmos-http", "zeta_testnet")[0]?.url - - const zetaChainUrl = `${api}/zeta-chain/fungible/foreign_coins` - const pythNetworkUrl = "https://benchmarks.pyth.network/v1/price_feeds/" - - try { - const zetaResponse = await fetch(zetaChainUrl) - const zetaData = await zetaResponse.json() - const foreignCoins = zetaData.foreignCoins - const symbolsFromZeta = foreignCoins.map((coin: any) => - coin.symbol.replace(/^[tg]/, "") - ) - - const pythResponse = await fetch(pythNetworkUrl) - const pythData = await pythResponse.json() - const priceFeeds = pythData - - priceIds = priceFeeds - .filter((feed: any) => { - const base = symbolsFromZeta.includes(feed.attributes.base) - const quote = feed.attributes.quote_currency === "USD" - return base && quote - }) - .map((feed: any) => ({ - symbol: feed.attributes.base, - id: feed.id, - })) - } catch (error) { - console.error("Error fetching or processing data:", error) - return [] - } - const connection = new EvmPriceServiceConnection( - "https://hermes.pyth.network" - ) - - const priceFeeds = await connection.getLatestPriceFeeds( - priceIds.map((p: any) => p.id) - ) - - setPrices( - priceFeeds?.map((p: any) => { - const pr = p.getPriceNoOlderThan(60) - return { - id: p.id, - symbol: priceIds.find((i: any) => i.id === p.id)?.symbol, - price: parseInt(pr.price) * 10 ** pr.expo, - } - }) - ) - }, 500), - [] - ) - - const [inbounds, setInbounds] = useState([]) - const [cctxs, setCCTXs] = useState([]) - - const updateCCTX = (updatedItem: any) => { - setCCTXs((prevItems: any) => { - const index = prevItems.findIndex( - (item: any) => item.inboundHash === updatedItem.inboundHash - ) - - if (index === -1) return prevItems - - const newItems = [...prevItems] - newItems[index] = { - ...newItems[index], - ...updatedItem, - } - - return newItems - }) - } - - useEffect(() => { - const cctxList = cctxs.map((c: any) => c.inboundHash) - for (let i of inbounds) { - if (!cctxList.includes(i.inboundHash)) { - const emitter = new EventEmitter() - emitter - .on("search-add", ({ text }) => { - updateCCTX({ - inboundHash: i.inboundHash, - progress: text, - status: "searching", - }) - }) - .on("add", ({ text }) => { - updateCCTX({ - inboundHash: i.inboundHash, - progress: text, - status: "searching", - }) - }) - .on("succeed", ({ text }) => { - updateCCTX({ - inboundHash: i.inboundHash, - progress: text, - status: "succeed", - }) - }) - .on("fail", ({ text }) => { - updateCCTX({ - inboundHash: i.inboundHash, - progress: text, - status: "failed", - }) - }) - .on("mined-success", (value) => { - updateCCTX({ - inboundHash: i.inboundHash, - status: "mined-success", - ...value, - }) - }) - .on("mined-fail", (value) => { - updateCCTX({ - inboundHash: i.inboundHash, - status: "mined-fail", - ...value, - }) - }) - - client.trackCCTX(i.inboundHash, false, emitter) - setCCTXs([...cctxs, { inboundHash: i.inboundHash, desc: i.desc }]) - } - } - }, [inbounds]) - - useEffect(() => { - if (!Cookies.get("firstTimeVisit")) { - toast({ - title: "Welcome to ZetaChain Example App", - description: "This is a testnet. Please do not use real funds.", - duration: 60000, - }) - Cookies.set("firstTimeVisit", "true", { expires: 7 }) - } - }, []) - return ( - <> - - - -
- -
{children}
-
- -
-
-
- + + + +
+ +
{children}
+
+ +
+
+
) } diff --git a/app/layout.tsx b/app/layout.tsx index 61485b0..a70a960 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -9,9 +9,9 @@ import { xdefiWallet, } from "@rainbow-me/rainbowkit/wallets" +import { ZetaChainProvider } from "@/hooks/useZetaChainClient" import Index from "@/app/index" -import { ZetaChainProvider } from "../hooks/useZetaChainClient" import "@rainbow-me/rainbowkit/styles.css" import { RainbowKitProvider, @@ -56,28 +56,26 @@ const wagmiConfig = createConfig({ webSocketPublicClient, }) -export const fontSans = FontSans({ +const fontSans = FontSans({ subsets: ["latin"], variable: "--font-sans", }) export default function RootLayout({ children }: RootLayoutProps) { return ( - <> - - - - - - - {children} - - - - - - + + + + + + + {children} + + + + + ) } diff --git a/app/messaging/page.tsx b/app/messaging/page.tsx index 19cc989..91daf2d 100644 --- a/app/messaging/page.tsx +++ b/app/messaging/page.tsx @@ -2,6 +2,7 @@ import { useCallback, useContext, useEffect, useState } from "react" import Link from "next/link" +import { useAppContext } from "@/context/AppContext" import UniswapV2Factory from "@uniswap/v2-periphery/build/IUniswapV2Router02.json" import Quoter from "@uniswap/v3-periphery/artifacts/contracts/lens/Quoter.sol/Quoter.json" import { getExplorers } from "@zetachain/networks" @@ -35,7 +36,6 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select" -import { AppContext } from "@/app/index" const contracts: any = { goerli_testnet: "0x122F9Cca5121F23b74333D5FBd0c5D9B413bc002", @@ -72,7 +72,7 @@ const MessagingPage = () => { (networks as any)[destinationNetwork]?.chain_id ?? null ) }, [destinationNetwork]) - const { inbounds, setInbounds, fees } = useContext(AppContext) + const { inbounds, setInbounds, fees } = useAppContext() const { config, diff --git a/app/nft/burn.ts b/app/nft/burn.ts index 1cb5221..0d8c3e8 100644 --- a/app/nft/burn.ts +++ b/app/nft/burn.ts @@ -1,10 +1,10 @@ import { useContext } from "react" +import { useAppContext } from "@/context/AppContext" import { abi } from "@zetachain/example-contracts/abi/omnichain/NFT.sol/NFT.json" import { ethers } from "ethers" import { useAccount } from "wagmi" import { useEthersSigner } from "@/hooks/useEthersSigner" -import { AppContext } from "@/app/index" import { useFetchNFTs } from "./fetchNFTs" import { useNFT } from "./useNFT" @@ -16,7 +16,7 @@ export const useBurn = () => { setAssetsBurned, omnichainContract, } = useNFT() - const { setInbounds, inbounds } = useContext(AppContext) + const { setInbounds, inbounds } = useAppContext() const { address } = useAccount() const signer = useEthersSigner() const { fetchNFTs } = useFetchNFTs() diff --git a/app/nft/mint.ts b/app/nft/mint.ts index 570be39..10346f2 100644 --- a/app/nft/mint.ts +++ b/app/nft/mint.ts @@ -1,4 +1,5 @@ import { useContext } from "react" +import { useAppContext } from "@/context/AppContext" import { networks } from "@zetachain/networks" import { getAddress } from "@zetachain/protocol-contracts" import { prepareData } from "@zetachain/toolkit/client" @@ -6,7 +7,6 @@ import { parseEther } from "viem" import { useAccount } from "wagmi" import { useEthersSigner } from "@/hooks/useEthersSigner" -import { AppContext } from "@/app/index" import { useNFT } from "./useNFT" @@ -14,7 +14,7 @@ export const useMint = () => { const { amount, setAmount, setMintingInProgress, omnichainContract } = useNFT() const { bitcoinAddress, setInbounds, inbounds, connectBitcoin } = - useContext(AppContext) + useAppContext() const { address } = useAccount() const signer = useEthersSigner() diff --git a/app/nft/page.tsx b/app/nft/page.tsx index 0fbca11..ce64953 100644 --- a/app/nft/page.tsx +++ b/app/nft/page.tsx @@ -1,6 +1,7 @@ "use client" import { useContext, useEffect } from "react" +import { useAppContext } from "@/context/AppContext" import { AnimatePresence, motion } from "framer-motion" import { debounce } from "lodash" import { Flame, Loader, RefreshCw, Send, Sparkles } from "lucide-react" @@ -21,7 +22,6 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select" -import { AppContext } from "@/app/index" import { useBurn } from "./burn" import { useFetchNFTs } from "./fetchNFTs" @@ -44,7 +44,7 @@ const NFTPage = () => { setRecipient, foreignCoins, } = useNFT() - const { cctxs } = useContext(AppContext) + const { cctxs } = useAppContext() const { switchNetwork } = useSwitchNetwork() const { chain } = useNetwork() const { transfer } = useTransfer() diff --git a/app/page.tsx b/app/page.tsx index 2a7f030..6a0c108 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,24 +1,15 @@ "use client" -import { useContext, useEffect, useState } from "react" -import Link from "next/link" -import { ArrowBigUp, ChevronDown, ChevronUp, RefreshCw } from "lucide-react" -import { formatUnits } from "viem" +import { useEffect, useState } from "react" +import { useAppContext } from "@/context/AppContext" +import { RefreshCw } from "lucide-react" import { useAccount } from "wagmi" import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" import { Button } from "@/components/ui/button" import { Skeleton } from "@/components/ui/skeleton" -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table" +import BalancesTable from "@/components/BalancesTable" import Swap from "@/components/swap" -import { AppContext } from "@/app/index" const LoadingSkeleton = () => { return ( @@ -32,83 +23,6 @@ const LoadingSkeleton = () => { ) } -const BalancesTable = ({ - balances, - showAll, - toggleShowAll, - stakingAmountTotal, -}: any) => { - return ( -
- - - - Asset - Type - Price - Balance - - - - {balances - .slice(0, showAll ? balances.length : 5) - .map((b: any, index: any) => ( - - -
{b.ticker}
-
{b.chain_name}
-
- {b.coin_type} - - {b.price?.toFixed(2)} - - - {parseFloat(b.balance).toFixed(2) || "N/A"} - {b.ticker === "ZETA" && b.coin_type === "Gas" && ( -
- -
- )} -
-
- ))} -
-
- {balances?.length > 5 && ( -
- -
- )} -
- ) -} - const ConnectWallet = () => { return ( @@ -128,7 +42,7 @@ export default function IndexPage() { fetchBalances, prices, stakingDelegations, - } = useContext(AppContext) + } = useAppContext() const [sortedBalances, setSortedBalances] = useState([]) const [showAll, setShowAll] = useState(false) @@ -144,12 +58,10 @@ export default function IndexPage() { const balancesPrices = sortedBalances.map((balance: any) => { const normalizeSymbol = (symbol: string) => symbol.replace(/^[tg]/, "") - const normalizedSymbol = normalizeSymbol(balance.symbol) const priceObj = prices.find( (price: any) => normalizeSymbol(price.symbol) === normalizedSymbol ) - return { ...balance, price: priceObj ? priceObj.price : null, @@ -167,20 +79,18 @@ export default function IndexPage() { // Prioritize ZETA if (a.ticker === "ZETA" && a.coin_type === "Gas") return -1 if (b.ticker === "ZETA" && b.coin_type === "Gas") return 1 - if (a.coin_type === "Gas" && b.coin_type !== "Gas") return -1 if (a.coin_type !== "Gas" && b.coin_type === "Gas") return 1 return a.chain_name < b.chain_name ? -1 : 1 }) - .filter((b: any) => { - return b.balance > 0 - }) + .filter((b: any) => b.balance > 0) setSortedBalances(balance) }, [balances]) - const balancesTotal = balancesPrices.reduce((a: any, c: any) => { - return a + parseFloat(c.balance) - }, 0) + const balancesTotal = balancesPrices.reduce( + (a: any, c: any) => a + parseFloat(c.balance), + 0 + ) const formatBalanceTotal = (b: string) => { if (parseFloat(b) > 1000) { diff --git a/app/pools/page.tsx b/app/pools/page.tsx index 9613562..c3e9d6a 100644 --- a/app/pools/page.tsx +++ b/app/pools/page.tsx @@ -1,16 +1,16 @@ "use client" import { useContext, useEffect, useState } from "react" +import { useAppContext } from "@/context/AppContext" import { formatUnits } from "viem" import { useAccount } from "wagmi" import { Card } from "@/components/ui/card" import { Skeleton } from "@/components/ui/skeleton" -import { AppContext } from "@/app/index" const PoolsPage = () => { const { pools, balances, balancesLoading, poolsLoading, fetchPools } = - useContext(AppContext) + useAppContext() const [poolsSorted, setPoolsSorted] = useState([]) const { address, isConnected } = useAccount() diff --git a/app/staking/page.tsx b/app/staking/page.tsx index 99add30..043f32b 100644 --- a/app/staking/page.tsx +++ b/app/staking/page.tsx @@ -2,6 +2,7 @@ import { useCallback, useContext, useEffect, useState } from "react" import Link from "next/link" +import { useAppContext } from "@/context/AppContext" import { generatePostBodyBroadcast } from "@evmos/provider" import { createTxMsgBeginRedelegate, @@ -67,7 +68,6 @@ import { TableRow, } from "@/components/ui/table" import { useToast } from "@/components/ui/use-toast" -import { AppContext } from "@/app/index" const StakingPage = () => { const { @@ -84,7 +84,7 @@ const StakingPage = () => { fetchBalances, observers, fetchObservers, - } = useContext(AppContext) + } = useAppContext() const [selectedValidator, setSelectedValidator] = useState(null) const [isSending, setIsSending] = useState(false) const [isZetaChain, setIsZetaChain] = useState(false) diff --git a/components/BalancesTable.tsx b/components/BalancesTable.tsx new file mode 100644 index 0000000..9cd7dbf --- /dev/null +++ b/components/BalancesTable.tsx @@ -0,0 +1,92 @@ +import Link from "next/link" +import { ArrowBigUp, ChevronDown, ChevronUp } from "lucide-react" +import { formatUnits } from "viem" + +import { Button } from "@/components/ui/button" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" + +const BalancesTable = ({ + balances, + showAll, + toggleShowAll, + stakingAmountTotal, +}: any) => { + return ( +
+ + + + Asset + Type + Price + Balance + + + + {balances + .slice(0, showAll ? balances.length : 5) + .map((b: any, index: any) => ( + + +
{b.ticker}
+
{b.chain_name}
+
+ {b.coin_type} + + {b.price?.toFixed(2)} + + + {parseFloat(b.balance).toFixed(2) || "N/A"} + {b.ticker === "ZETA" && b.coin_type === "Gas" && ( +
+ +
+ )} +
+
+ ))} +
+
+ {balances?.length > 5 && ( +
+ +
+ )} +
+ ) +} + +export default BalancesTable diff --git a/components/main-nav.tsx b/components/main-nav.tsx index f35038b..f55ccc1 100644 --- a/components/main-nav.tsx +++ b/components/main-nav.tsx @@ -3,6 +3,7 @@ import { useContext } from "react" import Link from "next/link" import { usePathname } from "next/navigation" +import { useAppContext } from "@/context/AppContext" import { Home, Settings } from "lucide-react" import { Button } from "@/components/ui/button" @@ -22,11 +23,10 @@ import { SheetTrigger, } from "@/components/ui/sheet" import Transactions from "@/components/transactions" -import { AppContext } from "@/app/index" export function MainNav() { const pathname = usePathname() - const { cctxs } = useContext(AppContext) + const { cctxs } = useAppContext() const inProgress = cctxs.filter( diff --git a/components/site-header.tsx b/components/site-header.tsx index 2699573..a2786dd 100644 --- a/components/site-header.tsx +++ b/components/site-header.tsx @@ -1,4 +1,5 @@ import { useContext } from "react" +import { AppProvider } from "@/context/AppContext" import { ConnectButton } from "@rainbow-me/rainbowkit" import { Bitcoin } from "lucide-react" @@ -10,10 +11,9 @@ import { TooltipTrigger, } from "@/components/ui/tooltip" import { MainNav } from "@/components/main-nav" -import { AppContext } from "@/app/index" export function SiteHeader() { - const { bitcoinAddress, connectBitcoin } = useContext(AppContext) + // const { bitcoinAddress, connectBitcoin } = useContext(AppContext) return (
diff --git a/components/swap.tsx b/components/swap.tsx index c2940e6..f8a45cb 100644 --- a/components/swap.tsx +++ b/components/swap.tsx @@ -1,6 +1,7 @@ "use client" import { useContext, useEffect, useState } from "react" +import { useAppContext } from "@/context/AppContext" import ERC20_ABI from "@openzeppelin/contracts/build/contracts/ERC20.json" import { getAddress } from "@zetachain/protocol-contracts" import ERC20Custody from "@zetachain/protocol-contracts/abi/evm/ERC20Custody.sol/ERC20Custody.json" @@ -38,7 +39,6 @@ import { PopoverContent, PopoverTrigger, } from "@/components/ui/popover" -import { AppContext } from "@/app/index" import SwapToAnyToken from "./SwapToAnyToken.json" @@ -77,7 +77,7 @@ const Swap = () => { setInbounds, inbounds, fees, - } = useContext(AppContext) + } = useAppContext() const { chain } = useNetwork() const signer = useEthersSigner() diff --git a/components/transactions.tsx b/components/transactions.tsx index bd0bc08..9d861d2 100644 --- a/components/transactions.tsx +++ b/components/transactions.tsx @@ -1,15 +1,15 @@ "use client" import { useContext } from "react" +import { useAppContext } from "@/context/AppContext" import { AlertTriangle, CheckCircle2, Loader2 } from "lucide-react" import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" import { Card } from "@/components/ui/card" import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table" -import { AppContext } from "@/app/index" const TransactionsPage = () => { - const { cctxs } = useContext(AppContext) + const { cctxs } = useAppContext() const inProgress = (status: string): boolean => { return !(status === "mined-success" || status === "mined-fail") diff --git a/context/AppContext.tsx b/context/AppContext.tsx new file mode 100644 index 0000000..248a25d --- /dev/null +++ b/context/AppContext.tsx @@ -0,0 +1,414 @@ +import { + createContext, + useCallback, + useContext, + useEffect, + useState, +} from "react" +import { EvmPriceServiceConnection } from "@pythnetwork/pyth-evm-js" +import { getEndpoints } from "@zetachain/networks/dist/src/getEndpoints" +import EventEmitter from "eventemitter3" +// @ts-ignore +import Cookies from "js-cookie" +import debounce from "lodash/debounce" +import { useAccount } from "wagmi" + +import { hexToBech32Address } from "@/lib/hexToBech32Address" +import { useZetaChainClient } from "@/hooks/useZetaChainClient" +import { useToast } from "@/components/ui/use-toast" + +const AppContext = createContext(null) + +export const AppProvider = ({ children }: { children: React.ReactNode }) => { + const { client } = useZetaChainClient() + const { address, isConnected } = useAccount() + const [balances, setBalances] = useState([]) + const [balancesLoading, setBalancesLoading] = useState(true) + const [balancesRefreshing, setBalancesRefreshing] = useState(false) + const [bitcoinAddress, setBitcoinAddress] = useState("") + const [fees, setFees] = useState([]) + const [pools, setPools] = useState([]) + const [poolsLoading, setPoolsLoading] = useState(false) + const [validators, setValidators] = useState([]) + const [validatorsLoading, setValidatorsLoading] = useState(false) + const [stakingDelegations, setStakingDelegations] = useState([]) + const [stakingRewards, setStakingRewards] = useState([]) + const [unbondingDelegations, setUnbondingDelegations] = useState([]) + const [observers, setObservers] = useState([]) + const [prices, setPrices] = useState([]) + const { toast } = useToast() + + const fetchObservers = useCallback( + debounce(async () => { + try { + const api = getEndpoints("cosmos-http", "zeta_testnet")[0]?.url + const url = `${api}/zeta-chain/observer/nodeAccount` + const response = await fetch(url) + const data = await response.json() + setObservers(data.NodeAccount) + } catch (e) { + console.error(e) + } + }, 500), + [] + ) + + const fetchUnbondingDelegations = useCallback( + debounce(async () => { + try { + if (!isConnected) { + return setUnbondingDelegations([]) + } + const api = getEndpoints("cosmos-http", "zeta_testnet")[0]?.url + const addr = hexToBech32Address(address as any, "zeta") + const url = `${api}/cosmos/staking/v1beta1/delegators/${addr}/unbonding_delegations` + const response = await fetch(url) + const data = await response.json() + setUnbondingDelegations(data.unbonding_responses) + } catch (e) { + console.error(e) + } + }, 500), + [address, isConnected] + ) + + const connectBitcoin = async () => { + const w = window as any + if ("xfi" in w && w.xfi?.bitcoin) { + w.xfi.bitcoin.changeNetwork("testnet") + const btc = (await w.xfi.bitcoin.getAccounts())[0] + await setBitcoinAddress(btc) + fetchBalances(true, btc) + } + } + + const fetchStakingDelegations = useCallback( + debounce(async () => { + try { + if (!isConnected) { + return setStakingDelegations([]) + } + const api = getEndpoints("cosmos-http", "zeta_testnet")[0]?.url + const addr = hexToBech32Address(address as any, "zeta") + const url = `${api}/cosmos/staking/v1beta1/delegations/${addr}` + const response = await fetch(url) + const data = await response.json() + setStakingDelegations(data.delegation_responses) + } catch (e) { + console.error(e) + } + }, 500), + [address, isConnected] + ) + + const fetchStakingRewards = useCallback( + debounce(async () => { + try { + if (!isConnected) { + return setStakingRewards([]) + } + const api = getEndpoints("cosmos-http", "zeta_testnet")[0]?.url + const addr = hexToBech32Address(address as any, "zeta") + const url = `${api}/cosmos/distribution/v1beta1/delegators/${addr}/rewards` + const response = await fetch(url) + const data = await response.json() + setStakingRewards(data.rewards) + } catch (e) { + console.error(e) + } + }, 500), + [address, isConnected] + ) + + const fetchValidators = useCallback( + debounce(async () => { + setValidatorsLoading(true) + let allValidators: any[] = [] + let nextKey: any = null + + try { + if (!isConnected) { + setValidatorsLoading(false) + setValidators([]) + } + const api = getEndpoints("cosmos-http", "zeta_testnet")[0]?.url + + const fetchBonded = async () => { + const response = await fetch(`${api}/cosmos/staking/v1beta1/pool`) + const data = await response.json() + return data + } + + const fetchPage = async (key: string) => { + const endpoint = "/cosmos/staking/v1beta1/validators" + const query = `pagination.key=${encodeURIComponent(key)}` + const url = `${api}${endpoint}?${key && query}` + + const response = await fetch(url) + const data = await response.json() + + allValidators = allValidators.concat(data.validators) + + if (data.pagination && data.pagination.next_key) { + await fetchPage(data.pagination.next_key) + } + } + const pool = (await fetchBonded())?.pool + const tokens = parseInt(pool.bonded_tokens) + await fetchPage(nextKey) + allValidators = allValidators.map((v) => { + return { + ...v, + voting_power: tokens ? (parseInt(v.tokens) / tokens) * 100 : 0, + } + }) + } catch (e) { + console.error(e) + } finally { + setValidators(allValidators) + setValidatorsLoading(false) + } + }, 500), + [address, isConnected] + ) + + const fetchBalances = useCallback( + debounce(async (refresh: Boolean = false, btc: any = null) => { + if (refresh) setBalancesRefreshing(true) + if (balances.length === 0) setBalancesLoading(true) + try { + if (!isConnected) { + return setBalances([]) + } + const b = await client.getBalances({ + evmAddress: address, + btcAddress: btc, + }) + setBalances(b) + } catch (e) { + console.error(e) + } finally { + setBalancesRefreshing(false) + setBalancesLoading(false) + } + }, 500), + [isConnected, address] + ) + + const fetchFeesList = useCallback( + debounce(async () => { + try { + if (!isConnected) { + return setFees([]) + } + setFees(await client.getFees(500000)) + } catch (e) { + console.error(e) + } + }, 500), + [] + ) + + const fetchPools = useCallback( + debounce(async () => { + setPoolsLoading(true) + try { + setPools(await client.getPools()) + } catch (e) { + console.error(e) + } finally { + setPoolsLoading(false) + } + }, 500), + [] + ) + + useEffect(() => { + fetchBalances(true) + fetchFeesList() + fetchStakingDelegations() + fetchPrices() + }, [isConnected, address]) + + const fetchPrices = useCallback( + debounce(async () => { + let priceIds: any = [] + const api = getEndpoints("cosmos-http", "zeta_testnet")[0]?.url + + const zetaChainUrl = `${api}/zeta-chain/fungible/foreign_coins` + const pythNetworkUrl = "https://benchmarks.pyth.network/v1/price_feeds/" + + try { + const zetaResponse = await fetch(zetaChainUrl) + const zetaData = await zetaResponse.json() + const foreignCoins = zetaData.foreignCoins + const symbolsFromZeta = foreignCoins.map((coin: any) => + coin.symbol.replace(/^[tg]/, "") + ) + + const pythResponse = await fetch(pythNetworkUrl) + const pythData = await pythResponse.json() + const priceFeeds = pythData + + priceIds = priceFeeds + .filter((feed: any) => { + const base = symbolsFromZeta.includes(feed.attributes.base) + const quote = feed.attributes.quote_currency === "USD" + return base && quote + }) + .map((feed: any) => ({ + symbol: feed.attributes.base, + id: feed.id, + })) + } catch (error) { + console.error("Error fetching or processing data:", error) + return [] + } + const connection = new EvmPriceServiceConnection( + "https://hermes.pyth.network" + ) + + const priceFeeds = await connection.getLatestPriceFeeds( + priceIds.map((p: any) => p.id) + ) + + setPrices( + priceFeeds?.map((p: any) => { + const pr = p.getPriceNoOlderThan(60) + return { + id: p.id, + symbol: priceIds.find((i: any) => i.id === p.id)?.symbol, + price: parseInt(pr.price) * 10 ** pr.expo, + } + }) + ) + }, 500), + [] + ) + + const [inbounds, setInbounds] = useState([]) + const [cctxs, setCCTXs] = useState([]) + + const updateCCTX = (updatedItem: any) => { + setCCTXs((prevItems: any) => { + const index = prevItems.findIndex( + (item: any) => item.inboundHash === updatedItem.inboundHash + ) + + if (index === -1) return prevItems + + const newItems = [...prevItems] + newItems[index] = { + ...newItems[index], + ...updatedItem, + } + + return newItems + }) + } + + useEffect(() => { + const cctxList = cctxs.map((c: any) => c.inboundHash) + for (let i of inbounds) { + if (!cctxList.includes(i.inboundHash)) { + const emitter = new EventEmitter() + emitter + .on("search-add", ({ text }) => { + updateCCTX({ + inboundHash: i.inboundHash, + progress: text, + status: "searching", + }) + }) + .on("add", ({ text }) => { + updateCCTX({ + inboundHash: i.inboundHash, + progress: text, + status: "searching", + }) + }) + .on("succeed", ({ text }) => { + updateCCTX({ + inboundHash: i.inboundHash, + progress: text, + status: "succeed", + }) + }) + .on("fail", ({ text }) => { + updateCCTX({ + inboundHash: i.inboundHash, + progress: text, + status: "failed", + }) + }) + .on("mined-success", (value) => { + updateCCTX({ + inboundHash: i.inboundHash, + status: "mined-success", + ...value, + }) + }) + .on("mined-fail", (value) => { + updateCCTX({ + inboundHash: i.inboundHash, + status: "mined-fail", + ...value, + }) + }) + + client.trackCCTX(i.inboundHash, false, emitter) + setCCTXs([...cctxs, { inboundHash: i.inboundHash, desc: i.desc }]) + } + } + }, [inbounds]) + + useEffect(() => { + if (!Cookies.get("firstTimeVisit")) { + toast({ + title: "Welcome to ZetaChain Example App", + description: "This is a testnet. Please do not use real funds.", + duration: 60000, + }) + Cookies.set("firstTimeVisit", "true", { expires: 7 }) + } + }, []) + + return ( + + {children} + + ) +} + +export const useAppContext = () => useContext(AppContext)