diff --git a/README.md b/README.md index 23fcc85..fcafae9 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,6 @@ the [ZetaChain Toolkit](https://github.com/zeta-chain/toolkit/). - Omnichain swaps - Token deposit and withdrawal - Cross-chain transaction tracking -- Cross-chain messaging example - Bitcoin support ## Prerequisites @@ -25,8 +24,8 @@ the [ZetaChain Toolkit](https://github.com/zeta-chain/toolkit/). - Yarn The ZetaChain Toolkit is initialized with a custom RPC endpoint for ZetaChain to -ensure that requests are not rate-limited. By default we're using an RPC endpoint -provided by [AllThatNode](https://www.allthatnode.com/zetachain.dsrv). +ensure that requests are not rate-limited. By default we're using an RPC +endpoint provided by [AllThatNode](https://www.allthatnode.com/zetachain.dsrv). Before starting the development server: diff --git a/app/index.tsx b/app/index.tsx index 66533f9..2665909 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -1,8 +1,17 @@ "use client" -import { AppProvider } from "@/context/AppContext" +import React, { createContext, useContext, useEffect, useState } from "react" +import { BalanceProvider } from "@/context/BalanceContext" +import { CCTXsProvider } from "@/context/CCTXsContext" +import { FeesProvider } from "@/context/FeesContext" +import { PricesProvider } from "@/context/PricesContext" +import { StakingProvider } from "@/context/StakingContext" +import { ValidatorsProvider } from "@/context/ValidatorsContext" +// @ts-ignore +import Cookies from "js-cookie" 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" @@ -13,17 +22,46 @@ interface RootLayoutProps { } export default function Index({ children }: RootLayoutProps) { + const { toast } = useToast() + + 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/messaging/page.tsx b/app/messaging/page.tsx deleted file mode 100644 index 91daf2d..0000000 --- a/app/messaging/page.tsx +++ /dev/null @@ -1,399 +0,0 @@ -"use client" - -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" -import { getEndpoints } from "@zetachain/networks/dist/src/getEndpoints" -import { getNetworkName } from "@zetachain/networks/dist/src/getNetworkName" -import networks from "@zetachain/networks/dist/src/networks" -import { getAddress, getNonZetaAddress } from "@zetachain/protocol-contracts" -import { ethers } from "ethers" -import { formatEther, parseEther } from "ethers/lib/utils" -import { AlertCircle, BookOpen, Check, Loader2, Send } from "lucide-react" -import { useDebounce } from "use-debounce" -import { - useContractWrite, - useNetwork, - usePrepareContractWrite, - useWaitForTransaction, -} from "wagmi" - -import { useEthersSigner } from "@/hooks/useEthersSigner" -import { Alert, AlertDescription } from "@/components/ui/alert" -import { Button } from "@/components/ui/button" -import { Card } from "@/components/ui/card" -import { Checkbox } from "@/components/ui/checkbox" -import { Input } from "@/components/ui/input" -import { Label } from "@/components/ui/label" -import { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select" - -const contracts: any = { - goerli_testnet: "0x122F9Cca5121F23b74333D5FBd0c5D9B413bc002", - mumbai_testnet: "0x392bBEC0537D48640306D36525C64442E98FA780", - bsc_testnet: "0xc5d7437DE3A8b18f6380f3B8884532206272D599", -} - -const MessagingPage = () => { - const [message, setMessage] = useState("") - const [destinationNetwork, setDestinationNetwork] = useState("") - const [destinationChainID, setDestinationChainID] = useState(null) - const [isZeta, setIsZeta] = useState(false) - const [currentNetworkName, setCurrentNetworkName] = useState("") - const [completed, setCompleted] = useState(false) - const [fee, setFee] = useState("") - const [currentChain, setCurrentChain] = useState() - - const [debouncedMessage] = useDebounce(message, 500) - - const allNetworks = Object.keys(contracts) - const signer = useEthersSigner() - - const { chain } = useNetwork() - useEffect(() => { - setCurrentNetworkName(chain ? getNetworkName(chain.network) : undefined) - if (chain) { - setCurrentChain(chain) - setIsZeta(getNetworkName(chain.network) === "zeta_testnet") - } - }, [chain]) - - useEffect(() => { - setDestinationChainID( - (networks as any)[destinationNetwork]?.chain_id ?? null - ) - }, [destinationNetwork]) - const { inbounds, setInbounds, fees } = useAppContext() - - const { - config, - error: prepareError, - isError: isPrepareError, - } = usePrepareContractWrite({ - address: contracts[currentNetworkName || ""], - abi: [ - { - inputs: [ - { - internalType: "uint256", - name: "destinationChainId", - type: "uint256", - }, - { - internalType: "string", - name: "message", - type: "string", - }, - ], - name: "sendMessage", - outputs: [], - stateMutability: "payable", - type: "function", - }, - ], - value: BigInt(parseFloat(fee) * 1e18 || 0), - functionName: "sendMessage", - args: [ - BigInt(destinationChainID !== null ? destinationChainID : 0), - debouncedMessage, - ], - }) - - const { data, write } = useContractWrite(config) - - const { isLoading, isSuccess } = useWaitForTransaction({ - hash: data?.hash, - }) - - const convertZETAtoMATIC = async (amount: string) => { - const quoterContract = new ethers.Contract( - "0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6", - Quoter.abi, - signer - ) - const quotedAmountOut = - await quoterContract.callStatic.quoteExactInputSingle( - "0x0000c9ec4042283e8139c74f4c64bcd1e0b9b54f", // WZETA - "0x9c3C9283D3e44854697Cd22D3Faa240Cfb032889", // WMATIC - 500, - parseEther(amount), - 0 - ) - return quotedAmountOut - } - - const getCCMFee = useCallback(async () => { - try { - if (!currentNetworkName || !destinationNetwork) { - throw new Error("Network is not selected") - } - const feeZETA = fees.feesCCM[destinationNetwork].totalFee - let fee - if (currentNetworkName === "mumbai_testnet") { - fee = await convertZETAtoMATIC(feeZETA) - } else { - const rpc = getEndpoints("evm", currentNetworkName)[0]?.url - const provider = new ethers.providers.JsonRpcProvider(rpc) - const routerAddress = getNonZetaAddress( - "uniswapV2Router02", - currentNetworkName - ) - const router = new ethers.Contract( - routerAddress, - UniswapV2Factory.abi, - provider - ) - const amountIn = ethers.utils.parseEther(feeZETA) - const zetaToken = getAddress("zetaToken", currentNetworkName) - const weth = getNonZetaAddress("weth9", currentNetworkName) - let zetaOut = await router.getAmountsOut(amountIn, [zetaToken, weth]) - fee = zetaOut[1] - } - fee = Math.ceil(parseFloat(formatEther(fee)) * 1.01 * 100) / 100 // 1.01 is to ensure that the fee is enough - setFee(fee.toString()) - } catch (error) { - console.error(error) - } - }, [currentNetworkName, destinationNetwork]) - - useEffect(() => { - try { - getCCMFee() - } catch (error) { - console.error(error) - } - }, [currentNetworkName, destinationNetwork, signer]) - - const explorer = - destinationNetwork && - getExplorers( - contracts[destinationNetwork], - "address", - destinationNetwork - )[0] - - useEffect(() => { - if (isSuccess && data) { - const inbound = { - inboundHash: data.hash, - desc: `Message sent to ${destinationNetwork}`, - } - setCompleted(true) - setInbounds([inbound, ...inbounds]) - } - }, [isSuccess, data]) - - useEffect(() => { - setCompleted(false) - }, [destinationNetwork, message]) - - const availableNetworks = allNetworks.filter( - (network) => network !== currentNetworkName - ) - - function extractDomain(url: string): string | null { - try { - const parsedURL = new URL(url) - const parts = parsedURL.hostname.split(".") - if (parts.length < 2) { - return null - } - return parts[parts.length - 2] - } catch (error) { - console.error("Invalid URL provided:", error) - return null - } - } - - return ( -
-

- Cross-Chain Message -

-
-
- -
{ - e.preventDefault() - write?.() - }} - > - - {isZeta && ( - - - - The protocol currently does not support sending cross-chain - messages to/from ZetaChain. Please, switch to another - network. - - - )} - - setMessage(e.target.value)} - /> -
- - setFee(e.target.value)} - /> -
- -
-
-
- -
-
-

- This is a dapp that uses ZetaChain's{" "} - cross-chain messaging for sending text messages - between smart contracts deployed on different chains. It is a - simple example of how to use the cross-chain messaging to send - arbitrary data. -

-

- You can learn how to build a dapp like this by following the - tutorial: -

- - - -

- The dapp on this page interacts with a smart contract built from - the same source code as in the tutorial. The smart contract is - deployed on the following networks: -

-
-
-            
-              {JSON.stringify(contracts, null, 2)}
-            
-          
-
-

Let's try using the dapp:

-
{" "} -
    -
  1. - - - First, select the destination network - -
  2. - {!!destinationNetwork && ( -
  3. - - You've selected {destinationNetwork} as - the destination network. - -
  4. - )} -
  5. - - - Next, write a message in the input field - -
  6. -
  7. - - - Finally, click Send message and confirm in your wallet - -
  8. - {completed && ( -
  9. - - Great! You've sent a message from {currentNetworkName} to{" "} - {destinationNetwork}. Once the cross-chain transaction with - the message is processed you will be able to see it in the  - Events tab in  - - {extractDomain(explorer)} - - . - -
  10. - )} -
-
-
-
- ) -} - -export default MessagingPage diff --git a/app/nft/burn.ts b/app/nft/burn.ts index 0d8c3e8..f106652 100644 --- a/app/nft/burn.ts +++ b/app/nft/burn.ts @@ -1,5 +1,4 @@ -import { useContext } from "react" -import { useAppContext } from "@/context/AppContext" +import { useFeesContext } from "@/context/FeesContext" import { abi } from "@zetachain/example-contracts/abi/omnichain/NFT.sol/NFT.json" import { ethers } from "ethers" import { useAccount } from "wagmi" @@ -16,7 +15,7 @@ export const useBurn = () => { setAssetsBurned, omnichainContract, } = useNFT() - const { setInbounds, inbounds } = useAppContext() + const { setInbounds, inbounds } = useFeesContext() const { address } = useAccount() const signer = useEthersSigner() const { fetchNFTs } = useFetchNFTs() diff --git a/app/nft/mint.ts b/app/nft/mint.ts index 10346f2..0380fc6 100644 --- a/app/nft/mint.ts +++ b/app/nft/mint.ts @@ -1,5 +1,5 @@ -import { useContext } from "react" -import { useAppContext } from "@/context/AppContext" +import { useBalanceContext } from "@/context/BalanceContext" +import { useCCTXsContext } from "@/context/CCTXsContext" import { networks } from "@zetachain/networks" import { getAddress } from "@zetachain/protocol-contracts" import { prepareData } from "@zetachain/toolkit/client" @@ -13,8 +13,9 @@ import { useNFT } from "./useNFT" export const useMint = () => { const { amount, setAmount, setMintingInProgress, omnichainContract } = useNFT() - const { bitcoinAddress, setInbounds, inbounds, connectBitcoin } = - useAppContext() + const { setInbounds, inbounds } = useCCTXsContext() + const { bitcoinAddress } = useBalanceContext() + const { connectBitcoin } = useBalanceContext() const { address } = useAccount() const signer = useEthersSigner() diff --git a/app/nft/page.tsx b/app/nft/page.tsx index ce64953..ff7e85c 100644 --- a/app/nft/page.tsx +++ b/app/nft/page.tsx @@ -1,7 +1,7 @@ "use client" -import { useContext, useEffect } from "react" -import { useAppContext } from "@/context/AppContext" +import { useEffect } from "react" +import { useCCTXsContext } from "@/context/CCTXsContext" import { AnimatePresence, motion } from "framer-motion" import { debounce } from "lodash" import { Flame, Loader, RefreshCw, Send, Sparkles } from "lucide-react" @@ -44,7 +44,7 @@ const NFTPage = () => { setRecipient, foreignCoins, } = useNFT() - const { cctxs } = useAppContext() + const { cctxs } = useCCTXsContext() const { switchNetwork } = useSwitchNetwork() const { chain } = useNetwork() const { transfer } = useTransfer() diff --git a/app/page.tsx b/app/page.tsx index 6a0c108..a397d5d 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,7 +1,9 @@ "use client" import { useEffect, useState } from "react" -import { useAppContext } from "@/context/AppContext" +import { useBalanceContext } from "@/context/BalanceContext" +import { usePricesContext } from "@/context/PricesContext" +import { useStakingContext } from "@/context/StakingContext" import { RefreshCw } from "lucide-react" import { useAccount } from "wagmi" @@ -35,14 +37,11 @@ const ConnectWallet = () => { } export default function IndexPage() { - const { - balances, - balancesLoading, - balancesRefreshing, - fetchBalances, - prices, - stakingDelegations, - } = useAppContext() + const { stakingDelegations } = useStakingContext() + const { prices } = usePricesContext() + + const { balances, balancesLoading, balancesRefreshing, fetchBalances } = + useBalanceContext() const [sortedBalances, setSortedBalances] = useState([]) const [showAll, setShowAll] = useState(false) diff --git a/app/pools/page.tsx b/app/pools/page.tsx deleted file mode 100644 index c3e9d6a..0000000 --- a/app/pools/page.tsx +++ /dev/null @@ -1,116 +0,0 @@ -"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" - -const PoolsPage = () => { - const { pools, balances, balancesLoading, poolsLoading, fetchPools } = - useAppContext() - const [poolsSorted, setPoolsSorted] = useState([]) - const { address, isConnected } = useAccount() - - useEffect(() => { - fetchPools() - }, [address, isConnected]) - - useEffect(() => { - const enrichPoolsData = (pools: any[], balances: any[]) => { - const balancesMap: { [key: string]: any } = {} - balances.forEach((balance: any) => { - if (balance.contract) { - balancesMap[balance.contract.toLowerCase()] = { - symbol: balance.symbol, - decimals: balance.decimals, - } - } - }) - - return pools.map((pool: any) => ({ - ...pool, - t0: { - ...pool.t0, - ...balancesMap[pool.t0.address.toLowerCase()], - }, - t1: { - ...pool.t1, - ...balancesMap[pool.t1.address.toLowerCase()], - }, - })) - } - - const enrichedPools = enrichPoolsData(pools, balances) - - enrichedPools.sort((a: any, b: any) => { - const aKnown = a.t0.symbol && a.t1.symbol - const bKnown = b.t0.symbol && b.t1.symbol - - if (aKnown && !bKnown) return -1 - if (!aKnown && bKnown) return 1 - return 0 - }) - - setPoolsSorted(enrichedPools) - }, [pools, balances]) - - return ( -
-

- Pools -

-
- {balancesLoading || poolsLoading ? ( -
- {Array(15) - .fill(null) - .map((_, index) => ( - - ))} -
- ) : ( -
- {poolsSorted.map((p: any) => ( - -
-
-
- {p.t0.symbol || "Unknown"} -
-
- {p.t1.symbol || "Unknown"} -
-
-
-
- {parseFloat( - formatUnits(p.t0.reserve, p.t0.decimals) - ).toFixed(2)} -
-
- {parseFloat( - formatUnits(p.t1.reserve, p.t1.decimals) - ).toFixed(2)} -
-
-
-
- ))} -
- )} -
-
- ) -} - -export default PoolsPage diff --git a/app/staking/page.tsx b/app/staking/page.tsx index 043f32b..28de6f1 100644 --- a/app/staking/page.tsx +++ b/app/staking/page.tsx @@ -1,8 +1,10 @@ "use client" -import { useCallback, useContext, useEffect, useState } from "react" +import { useCallback, useEffect, useState } from "react" import Link from "next/link" -import { useAppContext } from "@/context/AppContext" +import { useBalanceContext } from "@/context/BalanceContext" +import { useStakingContext } from "@/context/StakingContext" +import { useValidatorsContext } from "@/context/ValidatorsContext" import { generatePostBodyBroadcast } from "@evmos/provider" import { createTxMsgBeginRedelegate, @@ -71,20 +73,21 @@ import { useToast } from "@/components/ui/use-toast" const StakingPage = () => { const { - validators, - fetchValidators, fetchStakingDelegations, stakingDelegations, - balances, fetchStakingRewards, stakingRewards, - validatorsLoading, fetchUnbondingDelegations, unbondingDelegations, - fetchBalances, + } = useStakingContext() + const { + validators, + fetchValidators, + validatorsLoading, observers, fetchObservers, - } = useAppContext() + } = useValidatorsContext() + const { balances, fetchBalances } = useBalanceContext() const [selectedValidator, setSelectedValidator] = useState(null) const [isSending, setIsSending] = useState(false) const [isZetaChain, setIsZetaChain] = useState(false) @@ -673,8 +676,11 @@ const StakingPage = () => { } const isObserver = (address: string) => { - return observers.find( - (o: any) => convertToBech32(o.operator, "zetavaloper") === address + return ( + observers && + observers.find( + (o: any) => convertToBech32(o.operator, "zetavaloper") === address + ) ) } diff --git a/components/main-nav.tsx b/components/main-nav.tsx index f55ccc1..b3c2e4a 100644 --- a/components/main-nav.tsx +++ b/components/main-nav.tsx @@ -1,9 +1,8 @@ "use client" -import { useContext } from "react" import Link from "next/link" import { usePathname } from "next/navigation" -import { useAppContext } from "@/context/AppContext" +import { useCCTXsContext } from "@/context/CCTXsContext" import { Home, Settings } from "lucide-react" import { Button } from "@/components/ui/button" @@ -19,14 +18,13 @@ import { SheetContent, SheetDescription, SheetHeader, - SheetTitle, SheetTrigger, } from "@/components/ui/sheet" import Transactions from "@/components/transactions" export function MainNav() { const pathname = usePathname() - const { cctxs } = useAppContext() + const { cctxs } = useCCTXsContext() const inProgress = cctxs.filter( diff --git a/components/swap.tsx b/components/swap.tsx index b7f0dcb..4cd16d4 100644 --- a/components/swap.tsx +++ b/components/swap.tsx @@ -1,7 +1,9 @@ "use client" import { useEffect, useState } from "react" -import { useAppContext } from "@/context/AppContext" +import { useBalanceContext } from "@/context/BalanceContext" +import { useCCTXsContext } from "@/context/CCTXsContext" +import { useFeesContext } from "@/context/FeesContext" 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" @@ -46,14 +48,9 @@ const Swap = () => { const omnichainSwapContractAddress = "0xb459F14260D1dc6484CE56EB0826be317171e91F" const { isLoading, pendingChainId, switchNetwork } = useSwitchNetwork() - const { - balances, - balancesLoading, - bitcoinAddress, - setInbounds, - inbounds, - fees, - } = useAppContext() + const { setInbounds, inbounds } = useCCTXsContext() + const { fees } = useFeesContext() + const { balances, balancesLoading, bitcoinAddress } = useBalanceContext() const { chain } = useNetwork() const signer = useEthersSigner() diff --git a/components/transactions.tsx b/components/transactions.tsx index 9d861d2..473df10 100644 --- a/components/transactions.tsx +++ b/components/transactions.tsx @@ -1,7 +1,6 @@ "use client" -import { useContext } from "react" -import { useAppContext } from "@/context/AppContext" +import { useCCTXsContext } from "@/context/CCTXsContext" import { AlertTriangle, CheckCircle2, Loader2 } from "lucide-react" import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" @@ -9,7 +8,7 @@ import { Card } from "@/components/ui/card" import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table" const TransactionsPage = () => { - const { cctxs } = useAppContext() + const { cctxs } = useCCTXsContext() const inProgress = (status: string): boolean => { return !(status === "mined-success" || status === "mined-fail") diff --git a/context/AppContext.tsx b/context/AppContext.tsx deleted file mode 100644 index 248a25d..0000000 --- a/context/AppContext.tsx +++ /dev/null @@ -1,414 +0,0 @@ -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) diff --git a/context/BalanceContext.tsx b/context/BalanceContext.tsx new file mode 100644 index 0000000..83e0391 --- /dev/null +++ b/context/BalanceContext.tsx @@ -0,0 +1,84 @@ +"use client" + +import React, { + createContext, + useCallback, + useContext, + useEffect, + useState, +} from "react" +import debounce from "lodash/debounce" +import { useAccount } from "wagmi" + +import { useZetaChainClient } from "@/hooks/useZetaChainClient" + +const BalanceContext = createContext(null) + +export const BalanceProvider = ({ + 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 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 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) + } + } + + useEffect(() => { + fetchBalances(true) + }, [isConnected, address]) + + return ( + + {children} + + ) +} + +export const useBalanceContext = () => useContext(BalanceContext) diff --git a/context/CCTXsContext.tsx b/context/CCTXsContext.tsx new file mode 100644 index 0000000..12d60fa --- /dev/null +++ b/context/CCTXsContext.tsx @@ -0,0 +1,93 @@ +import React, { createContext, useContext, useEffect, useState } from "react" +import EventEmitter from "eventemitter3" + +import { useZetaChainClient } from "@/hooks/useZetaChainClient" + +const CCTXsContext = createContext(null) + +export const CCTXsProvider = ({ children }: { children: React.ReactNode }) => { + const { client } = useZetaChainClient() + 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]) + + return ( + + {children} + + ) +} + +export const useCCTXsContext = () => useContext(CCTXsContext) diff --git a/context/FeesContext.tsx b/context/FeesContext.tsx new file mode 100644 index 0000000..b967d89 --- /dev/null +++ b/context/FeesContext.tsx @@ -0,0 +1,43 @@ +import React, { + createContext, + useCallback, + useContext, + useEffect, + useState, +} from "react" +import debounce from "lodash/debounce" +import { useAccount } from "wagmi" + +import { useZetaChainClient } from "@/hooks/useZetaChainClient" + +const FeesContext = createContext(null) + +export const FeesProvider = ({ children }: { children: React.ReactNode }) => { + const { client } = useZetaChainClient() + const { isConnected } = useAccount() + const [fees, setFees] = useState([]) + + const fetchFeesList = useCallback( + debounce(async () => { + try { + if (!isConnected) { + return setFees([]) + } + setFees(await client.getFees(500000)) + } catch (e) { + console.error(e) + } + }, 500), + [isConnected] + ) + + useEffect(() => { + fetchFeesList() + }, [isConnected]) + + return ( + {children} + ) +} + +export const useFeesContext = () => useContext(FeesContext) diff --git a/context/PricesContext.tsx b/context/PricesContext.tsx new file mode 100644 index 0000000..1acc5f5 --- /dev/null +++ b/context/PricesContext.tsx @@ -0,0 +1,74 @@ +import React, { createContext, useCallback, useContext, useState } from "react" +import { EvmPriceServiceConnection } from "@pythnetwork/pyth-evm-js" +import { getEndpoints } from "@zetachain/networks/dist/src/getEndpoints" +import debounce from "lodash/debounce" + +const PricesContext = createContext(null) + +export const PricesProvider = ({ children }: { children: React.ReactNode }) => { + const [prices, setPrices] = useState([]) + + 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), + [] + ) + + return ( + + {children} + + ) +} + +export const usePricesContext = () => useContext(PricesContext) diff --git a/context/StakingContext.tsx b/context/StakingContext.tsx new file mode 100644 index 0000000..e21e801 --- /dev/null +++ b/context/StakingContext.tsx @@ -0,0 +1,93 @@ +import React, { createContext, useCallback, useContext, useState } from "react" +import { getEndpoints } from "@zetachain/networks/dist/src/getEndpoints" +import debounce from "lodash/debounce" +import { useAccount } from "wagmi" + +import { hexToBech32Address } from "@/lib/hexToBech32Address" + +const StakingContext = createContext(null) + +export const StakingProvider = ({ + children, +}: { + children: React.ReactNode +}) => { + const { address, isConnected } = useAccount() + const [stakingDelegations, setStakingDelegations] = useState([]) + const [stakingRewards, setStakingRewards] = useState([]) + const [unbondingDelegations, setUnbondingDelegations] = useState([]) + + 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 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] + ) + + return ( + + {children} + + ) +} + +export const useStakingContext = () => useContext(StakingContext) diff --git a/context/ValidatorsContext.tsx b/context/ValidatorsContext.tsx new file mode 100644 index 0000000..0868fcf --- /dev/null +++ b/context/ValidatorsContext.tsx @@ -0,0 +1,94 @@ +import React, { createContext, useCallback, useContext, useState } from "react" +import { getEndpoints } from "@zetachain/networks/dist/src/getEndpoints" +import debounce from "lodash/debounce" +import { useAccount } from "wagmi" + +const ValidatorsContext = createContext(null) + +export const ValidatorsProvider = ({ + children, +}: { + children: React.ReactNode +}) => { + const { address, isConnected } = useAccount() + const [validators, setValidators] = useState([]) + const [validatorsLoading, setValidatorsLoading] = useState(false) + const [observers, setObservers] = useState([]) + + 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 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), + [] + ) + + return ( + + {children} + + ) +} + +export const useValidatorsContext = () => useContext(ValidatorsContext) diff --git a/hooks/useCrossChainFee.tsx b/hooks/useCrossChainFee.tsx deleted file mode 100644 index 8eb4901..0000000 --- a/hooks/useCrossChainFee.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { useEffect, useState } from "react" -import { ZetaChainClient } from "@zetachain/toolkit/client" -import { utils } from "ethers" - -type Token = { - symbol: string - chain_name: string - coin_type: string - contract?: string - zrc20?: string - chain_id?: number - decimals?: number -} - -const useCrossChainFee = ( - client: ZetaChainClient, - sourceToken: Token | undefined, - destinationToken: Token | undefined, - sendType: string | null, - fees: any -) => { - const [crossChainFee, setCrossChainFee] = useState(null) - - useEffect(() => { - const fetchCrossChainFee = async () => { - if (!sendType) return - let fee = null - - if (sendType === "crossChainZeta" && fees) { - const dest = destinationToken?.chain_name - const toZetaChain = dest === "zeta_testnet" - const feeInfo = fees["messaging"].find( - (f: any) => f.chainID === destinationToken?.chain_id - ) - const amount = toZetaChain ? 0 : parseFloat(feeInfo.totalFee) - const formatted = amount === 0 ? "Fee: 0 ZETA" : `Fee: ~${amount} ZETA` - fee = { amount, decimals: 18, symbol: "ZETA", formatted } - } else if ( - [ - "withdrawZRC20", - "crossChainSwap", - "fromZetaChainSwapAndWithdraw", - ].includes(sendType) - ) { - const st = - sourceToken?.coin_type === "ZRC20" - ? sourceToken?.contract - : sourceToken?.zrc20 - const dt = - destinationToken?.coin_type === "ZRC20" - ? destinationToken?.contract - : destinationToken?.zrc20 - if (st && dt) { - try { - const feeInfo = await client.getWithdrawFeeInInputToken(st, dt) - const feeAmount = parseFloat( - utils.formatUnits(feeInfo.amount, feeInfo.decimals) - ) - fee = { - amount: utils.formatUnits(feeInfo.amount, feeInfo.decimals), - symbol: sourceToken?.symbol, - decimals: feeInfo.decimals, - formatted: `Fee: ${feeAmount} ${sourceToken?.symbol}`, - } - } catch (error) { - console.error("Error fetching withdraw fee:", error) - } - } - } - setCrossChainFee(fee) - } - - fetchCrossChainFee() - }, [client, sourceToken, destinationToken, sendType, fees]) - - return crossChainFee -} - -export default useCrossChainFee