diff --git a/.env.example b/.env.example index 32579d8..e970a99 100644 --- a/.env.example +++ b/.env.example @@ -4,6 +4,7 @@ NODE_ENV=test # API URLs NEXT_PUBLIC_BRIDGE_API_URL="https://turing-bridge-api.fra.avail.so" +NEXT_PUBLIC_LIQUIDITY_BRIDGE_API_URL="" NEXT_PUBLIC_BRIDGE_INDEXER_URL="https://turing-bridge-indexer.fra.avail.so" NEXT_PUBLIC_COINGECKO_API_URL="https://api.coingecko.com/api/v3/simple/price" @@ -30,6 +31,10 @@ NEXT_PUBLIC_MANAGER_ADDRESS_BASE="0xf4B55457fCD2b6eF6ffd41E5F5b0D65fbE370EA3" NEXT_PUBLIC_WORMHOLE_TRANSCEIVER_ETH="0x988140794D960fD962329751278Ef0DD2438a64C" NEXT_PUBLIC_WORMHOLE_TRANSCEIVER_BASE="0xAb9C68eD462f61Fd5fd34e6c21588513d89F603c" +NEXT_PUBLIC_LP_ADDRESS_ETH="" +NEXT_PUBLIC_LP_ADDRESS_BASE="" +NEXT_PUBLIC_LP_ADDRESS_AVAIL="" + # Datadog Configuration NEXT_PUBLIC_DATADOG_RUM_APPLICATION_ID="" NEXT_PUBLIC_DATADOG_RUM_CLIENT_TOKEN="" diff --git a/app/providers.tsx b/app/providers.tsx index f17eef1..6bf93bd 100644 --- a/app/providers.tsx +++ b/app/providers.tsx @@ -36,7 +36,7 @@ export function Providers({ children }: { children: React.ReactNode }) { useEffect(() => { (async () => { await fetchDollarAmount(); - + const interval = setInterval(async () => { await fetchDollarAmount(); }, 30000); diff --git a/components/chainselector/chainselectormodal.tsx b/components/chainselector/chainselectormodal.tsx index a6d1eda..2ed41e6 100644 --- a/components/chainselector/chainselectormodal.tsx +++ b/components/chainselector/chainselectormodal.tsx @@ -10,12 +10,6 @@ import { useCommonStore } from "@/stores/common"; import { ChevronLeft } from "lucide-react"; import useEthWallet from "@/hooks/common/useEthWallet"; import { capitalizeFirstLetter } from "@/hooks/wormhole/helper"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip"; type ChainSelectorModalProps = { isOpen: boolean; @@ -24,38 +18,20 @@ type ChainSelectorModalProps = { }; const ChainSelectorModal = ({ isOpen, onClose, type }: ChainSelectorModalProps) => { - const { setFromChain, setToChain, fromChain, toChain } = useCommonStore(); - const { validateandSwitchChain } = useEthWallet(); - - const isInvalidCombination = (chain: Chain) => { - if (type === "from") { - return (toChain === Chain.BASE && chain === Chain.AVAIL) || - (toChain === Chain.AVAIL && chain === Chain.BASE); - } - if (type === "to") { - return (fromChain === Chain.BASE && chain === Chain.AVAIL) || - (fromChain === Chain.AVAIL && chain === Chain.BASE); - } - return false; - }; + const { setFromChain, setToChain, fromChain, toChain } = useCommonStore(); + const { validateandSwitchChain } = useEthWallet(); const handleChainSelect = async (selectedChain: Chain) => { - if (isInvalidCombination(selectedChain)) return; - if (type === "from") { - const needToAdjustToChain = selectedChain === toChain || - // Add check for BASE <-> AVAIL combination - (selectedChain === Chain.AVAIL && toChain === Chain.BASE) || - (selectedChain === Chain.BASE && toChain === Chain.AVAIL); - - if (needToAdjustToChain) { - setToChain(selectedChain === Chain.ETH ? Chain.BASE : Chain.ETH); + if (selectedChain === toChain) { + // If selected source chain is same as destination, swap them + setToChain(fromChain); } - setFromChain(selectedChain); await validateandSwitchChain(selectedChain); } else { if (selectedChain === fromChain) { + // If selected destination chain is same as source, swap them await validateandSwitchChain(toChain); setFromChain(toChain); setToChain(fromChain); @@ -66,66 +42,50 @@ const ChainSelectorModal = ({ isOpen, onClose, type }: ChainSelectorModalProps) onClose(); }; - return ( - - -
- -
- - - Select chain to bridge {type} - -
-
-
+ return ( + + +
+ +
+ + + Select chain to bridge {type} + +
+
+
-
- {Object.values(Chain).map((chain) => ( - - - -
handleChainSelect(chain)} - className={` - flex items-center justify-between p-4 mb-2 last:mb-0 rounded-xl - ${isInvalidCombination(chain) - ? 'opacity-50 cursor-not-allowed bg-[#252A3C]' - : 'cursor-pointer bg-[#252A3C] hover:bg-[#2A2F41] transition-colors duration-200' - } - `} - > -
- {`${chain} - - {capitalizeFirstLetter(chain.toLocaleLowerCase())} - -
-
-
- {isInvalidCombination(chain) && ( - -

Chain combination not allowed

-
- )} -
-
- ))} -
+
+ {Object.values(Chain).map((chain) => ( +
handleChainSelect(chain)} + className="flex items-center justify-between p-4 mb-2 last:mb-0 rounded-xl cursor-pointer bg-[#252A3C] hover:bg-[#2A2F41] transition-colors duration-200" + > +
+ {`${chain} + + {capitalizeFirstLetter(chain.toLocaleLowerCase())} + +
+
+ ))} +
-
- looking for the testnet bridge, click here -
-
-
- ); +
+ looking for the testnet bridge, click here +
+
+
+ ); }; -export default ChainSelectorModal; \ No newline at end of file +export default ChainSelectorModal; diff --git a/components/common/container.tsx b/components/common/container.tsx index d985d15..89dfb40 100644 --- a/components/common/container.tsx +++ b/components/common/container.tsx @@ -8,6 +8,9 @@ import BridgeSection from "../sections/bridge"; import { useAvailAccount } from "@/stores/availwallet"; import { useAccount } from "wagmi"; import { useTransactionsStore } from "@/stores/transactions"; +import TransactionModal from "../sections/bridge/review-section"; +import { useCommonStore } from "@/stores/common"; +import AdvancedSettings from "./settings"; export default function Container() { const [activeTab, setActiveTab] = useState("bridge"); @@ -15,6 +18,7 @@ export default function Container() { const { selected } = useAvailAccount(); const { address } = useAccount(); const { fetchAllTransactions, setTransactionLoader} = useTransactionsStore(); + const { reviewDialog: { isOpen: isModalOpen, onOpenChange: setIsModalOpen } } = useCommonStore(); useEffect(()=>{ (async () => { @@ -47,8 +51,8 @@ export default function Container() { id="container" className="section_bg p-2 w-screen max-sm:rounded-none max-sm:!border-x-0 !max-w-xl " > - -
+ +

+
@@ -92,6 +97,7 @@ export default function Container() { + setIsModalOpen(false)} />
); } diff --git a/components/common/settings.tsx b/components/common/settings.tsx new file mode 100644 index 0000000..4d886ab --- /dev/null +++ b/components/common/settings.tsx @@ -0,0 +1,63 @@ +import React, { useState } from 'react'; +import { ArrowLeft, Info, Settings } from 'lucide-react'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Switch } from "@/components/ui/switch"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip'; + +const AdvancedSettings = () => { + const [isOpen, setIsOpen] = useState(false); + const [autoClaim, setAutoClaim] = useState(false); + + return ( + <> + setIsOpen(true)} + /> + + + + +
+ setIsOpen(false)} + /> + Advanced Settings +
+
+
+ + + +
+ + Automatically claim on destination
+ chain wherever possible +
+ +
+
+ +

Coming soon 👀

+
+
+
+
+
+
+ + ); +}; + +export default AdvancedSettings; \ No newline at end of file diff --git a/components/common/utils.ts b/components/common/utils.ts index 3a3cab3..0066784 100644 --- a/components/common/utils.ts +++ b/components/common/utils.ts @@ -1,4 +1,6 @@ +import { appConfig } from "@/config/default"; import { TxnLifecyle } from "@/hooks/common/useTrackTxnStatus"; +import { Chain } from "@/types/common"; export const getStepStatus = (step: number, status: TxnLifecyle) => { if (step === 1) { @@ -12,4 +14,37 @@ export const getStepStatus = (step: number, status: TxnLifecyle) => { : "waiting"; } return "waiting"; - }; \ No newline at end of file + }; + + export const chainToChainId = (chain: Chain) => { + switch (chain) { + case Chain.ETH: + return appConfig.networks.ethereum.id; + case Chain.BASE: + return appConfig.networks.base.id; + default: + throw new Error(`Unsupported chain: ${chain}`); + } + } + + export const chainToAddresses = (chain: Chain) => { + switch (chain) { + case Chain.ETH: + return { + tokenAddress: appConfig.contracts.ethereum.availToken, + bridgeAddress: appConfig.contracts.ethereum.bridge, + liquidityBridgeAddress: appConfig.contracts.ethereum.liquidityBridgeAddress, + } + case Chain.BASE: + return { + tokenAddress: appConfig.contracts.base.availToken, + liquidityBridgeAddress: appConfig.contracts.base.liquidityBridgeAddress, + } + case Chain.AVAIL: + return { + liquidityBridgeAddress: appConfig.contracts.avail.liquidityBridgeAddress, + } + default: + throw new Error(`Unsupported chain: ${chain}`); + } + } \ No newline at end of file diff --git a/components/sections/bridge/index.tsx b/components/sections/bridge/index.tsx index e1d3a28..74cf9cd 100644 --- a/components/sections/bridge/index.tsx +++ b/components/sections/bridge/index.tsx @@ -1,5 +1,5 @@ -import SubmitTransaction from "./submittransaction"; +import SubmitTransaction from "./submit-transaction"; import FromField from "./fromfield"; import ToField from "./tofield"; import ChainSwapBtn from "../../chainselector/chainswapbtn"; @@ -9,10 +9,10 @@ export default function BridgeSection() { return (
- - + + - +
); diff --git a/components/sections/bridge/review-section.tsx b/components/sections/bridge/review-section.tsx new file mode 100644 index 0000000..3734756 --- /dev/null +++ b/components/sections/bridge/review-section.tsx @@ -0,0 +1,270 @@ +import { Badge } from "@/components/ui/badge"; +import React from "react"; +import { LoadingButton } from "@/components/ui/loadingbutton"; +import useZkBridge from "@/hooks/useZkBridge"; +import useSubmitTxnState from "@/hooks/common/useSubmitTxnState"; +import { SuccessDialog, useCommonStore } from "@/stores/common"; +import { Chain } from "@/types/common"; +import { validAddress } from "@/utils/common"; +import BigNumber from "bignumber.js"; +import { useState } from "react"; +import useWormHoleBridge from "@/hooks/wormhole/useWormHoleBridge"; +import { ChainPairs } from "./types"; +import { appConfig } from "@/config/default"; +import { useBalanceStore } from "@/stores/balances"; +import { useApi } from "@/stores/api"; +import { useAvailAccount } from "@/stores/availwallet"; +import { useAccount } from "wagmi"; +import useLiquidityBridge from "@/hooks/useLiquidityBridge"; +import { motion, AnimatePresence } from "framer-motion"; + +interface TransactionModalProps { + isOpen: boolean; + onClose: () => void; +} + +const TransactionModal: React.FC = ({ + isOpen, + onClose, +}) => { + const [transactionInProgress, setTransactionInProgress] = + useState(false); + +const { + fromChain, + toChain, + fromAmount, + toAddress, + successDialog, + errorDialog: { onOpenChange: setErrorOpenDialog, setError }, +} = useCommonStore(); + +const { fetchBalance } = useBalanceStore(); +const { selected } = useAvailAccount(); +const { address: ethAddress } = useAccount(); +const { api } = useApi(); +const { initEthToAvailBridging, initAvailToEthBridging } = useZkBridge(); +const { + initAvailToERC20AutomaticBridging, + initERC20toAvailAutomaticBridging, +} = useLiquidityBridge(); +const { initWormholeBridge } = useWormHoleBridge(); +const { buttonStatus, isDisabled } = useSubmitTxnState(transactionInProgress); + +const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + successDialog.setClaimDialog(false); + + try { + let bridgeResult: SuccessDialog["details"] | null = null; + const chainPair = `${fromChain}-${toChain}` as const; + + const fromAmountAtomic = new BigNumber(fromAmount) + .multipliedBy(new BigNumber(10).pow(18)) + .toString(10); + + if (toAddress === undefined || !validAddress(toAddress, toChain)) { + throw new Error("Please enter a valid address"); + } + + setTransactionInProgress(true); + + switch (chainPair) { + case ChainPairs.ETH_TO_AVAIL: { + const blockhash = await initEthToAvailBridging({ + atomicAmount: fromAmountAtomic, + destinationAddress: toAddress!, + }); + bridgeResult = { chain: Chain.ETH, hash: blockhash }; + break; + } + case ChainPairs.AVAIL_TO_ETH: { + const init = await initAvailToEthBridging({ + atomicAmount: fromAmountAtomic, + destinationAddress: toAddress!, + }); + if (init.txHash) { + bridgeResult = { chain: Chain.AVAIL, hash: init.txHash }; + } + break; + } + case ChainPairs.BASE_TO_ETH: { + const init = await initWormholeBridge({ + whfrom: appConfig.config === "mainnet" ? "Base" : "BaseSepolia", + whto: appConfig.config === "mainnet" ? "Ethereum" : "Sepolia", + sendAmount: fromAmount, + destinationAddress: toAddress!, + switcher: Chain.BASE, + }); + if (init) { + bridgeResult = { + chain: Chain.BASE, + hash: init[1] ? init[1].txid : init[0].txid, + isWormhole: true, + }; + } + break; + } + case ChainPairs.ETH_TO_BASE: { + const init = await initWormholeBridge({ + whfrom: appConfig.config === "mainnet" ? "Ethereum" : "Sepolia", + whto: appConfig.config === "mainnet" ? "Base" : "BaseSepolia", + sendAmount: fromAmount, + destinationAddress: toAddress!, + switcher: Chain.ETH, + }); + if (init) { + bridgeResult = { + chain: Chain.ETH, + hash: init[1] ? init[1].txid : init[0].txid, + isWormhole: true, + }; + } + break; + } + case ChainPairs.AVAIL_TO_BASE: { + console.log("AVAIL TO BASE"); + const init = await initAvailToERC20AutomaticBridging({ + ERC20Chain: Chain.BASE, + atomicAmount: fromAmountAtomic, + destinationAddress: toAddress!, + }); + + if (init.hash) { + bridgeResult = { + chain: Chain.AVAIL, + hash: init.hash, + }; + } + } + case ChainPairs.BASE_TO_AVAIL: { + console.log("BASE TO AVAIL", fromAmountAtomic, toAddress); + + const init = await initERC20toAvailAutomaticBridging({ + ERC20Chain: Chain.BASE, + atomicAmount: fromAmountAtomic, + destinationAddress: toAddress!, + }); + + if (init.hash) { + bridgeResult = { + chain: Chain.BASE, + hash: init.hash, + }; + } + } + } + if (bridgeResult) { + successDialog.setDetails(bridgeResult); + successDialog.onOpenChange(true); + } + } catch (error: any) { + console.error(error); + setError(error); + setErrorOpenDialog(true); + } finally { + setTransactionInProgress(false); + onClose(); + await fetchBalance( + fromChain === Chain.AVAIL ? selected?.address! : ethAddress!, + fromChain, + api + ); + } +}; + + if (!isOpen) return null; + + return ( + + {isOpen && ( +
+ + +
+
+

+ Review Transaction Details +

+ +
+
+ + {buttonStatus} + +
+
+
+ )} +
+ ); +}; + +export default TransactionModal; + + +const Details = () => { + return ( +
+
+ Destination Gas Fee ($) + 1.66 Avail ($1.2) +
+ +
+ Estimated Time + ~5 minutes +
+ +
+ Claim Type + + Auto + +
+ +
+ User will recieve +
+ 11.34 + Avail +
+
+
+ ) +} \ No newline at end of file diff --git a/components/sections/bridge/submit-transaction.tsx b/components/sections/bridge/submit-transaction.tsx new file mode 100644 index 0000000..1f55e1a --- /dev/null +++ b/components/sections/bridge/submit-transaction.tsx @@ -0,0 +1,47 @@ +import { LoadingButton } from "@/components/ui/loadingbutton"; +import useSubmitTxnState from "@/hooks/common/useSubmitTxnState"; +import { useCommonStore } from "@/stores/common"; +import { useState } from "react"; +import { RxArrowTopRight } from "react-icons/rx"; +import { isWormholeBridge } from "./utils"; + +export default function SubmitTransaction() { + const [transactionInProgress, setTransactionInProgress] = + useState(false); + + const { + fromChain, + toChain, + reviewDialog: { onOpenChange: setShowReviewModal }, + } = useCommonStore(); + const { buttonStatus, isDisabled } = useSubmitTxnState(transactionInProgress); + + + + const handleReview = (e: React.FormEvent) => { + e.preventDefault(); + setShowReviewModal(true); + }; + + + return ( + <> + + {buttonStatus} + + + {isWormholeBridge(`${fromChain}-${toChain}`) && ( +

+ Using Third Party Wormhole Bridge{" "} + +

+ )} + + ); + }; + diff --git a/components/sections/bridge/submittransaction.tsx b/components/sections/bridge/submittransaction.tsx deleted file mode 100644 index be91a1a..0000000 --- a/components/sections/bridge/submittransaction.tsx +++ /dev/null @@ -1,153 +0,0 @@ -import { LoadingButton } from "@/components/ui/loadingbutton"; -import useBridge from "@/hooks/useBridge"; -import useSubmitTxnState from "@/hooks/common/useSubmitTxnState"; -import { SuccessDialog, useCommonStore } from "@/stores/common"; -import { Chain } from "@/types/common"; -import { validAddress } from "@/utils/common"; -import BigNumber from "bignumber.js"; -import { use, useState } from "react"; -import useWormHoleBridge from "@/hooks/wormhole/useWormHoleBridge"; -import { ChainPairs } from "./types"; -import { appConfig } from "@/config/default"; -import { RxArrowTopRight } from "react-icons/rx"; -import { useBalanceStore } from "@/stores/balances"; -import { useApi } from "@/stores/api"; -import { useAvailAccount } from "@/stores/availwallet"; -import { useAccount } from "wagmi"; -import { isWormholeBridge } from "./utils"; - -export default function SubmitTransaction() { - const [transactionInProgress, setTransactionInProgress] = - useState(false); - - const { - fromChain, - toChain, - fromAmount, - toAddress, - successDialog: { onOpenChange: setOpenDialog, setDetails, setClaimDialog }, - errorDialog: { onOpenChange: setErrorOpenDialog, setError }, - } = useCommonStore(); - - const { fetchBalance } = useBalanceStore(); - const { selected } = useAvailAccount(); - const { address: ethAddress } = useAccount(); - const { api } = useApi(); - const { initEthToAvailBridging, initAvailToEthBridging } = useBridge(); - const { initWormholeBridge } = useWormHoleBridge(); - const { buttonStatus, isDisabled } = useSubmitTxnState(transactionInProgress); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setClaimDialog(false) - - try { - let bridgeResult: SuccessDialog["details"] | null = null; - const chainPair = `${fromChain}-${toChain}` as const; - - const fromAmountAtomic = new BigNumber(fromAmount) - .multipliedBy(new BigNumber(10).pow(18)) - .toString(10); - - if (toAddress === undefined || !validAddress(toAddress, toChain)) { - throw new Error("Please enter a valid address"); - } - - setTransactionInProgress(true); - - switch (chainPair) { - case ChainPairs.ETH_TO_AVAIL: { - const blockhash = await initEthToAvailBridging({ - atomicAmount: fromAmountAtomic, - destinationAddress: toAddress!, - }); - bridgeResult = { chain: Chain.ETH, hash: blockhash }; - break; - } - case ChainPairs.AVAIL_TO_ETH: { - const init = await initAvailToEthBridging({ - atomicAmount: fromAmountAtomic, - destinationAddress: toAddress!, - }); - if (init.txHash) { - bridgeResult = { chain: Chain.AVAIL, hash: init.txHash }; - } - break; - } - case ChainPairs.BASE_TO_ETH: { - const init = await initWormholeBridge({ - whfrom: appConfig.config === "mainnet" ? "Base" : "BaseSepolia", - whto: appConfig.config === "mainnet" ? "Ethereum" : "Sepolia", - sendAmount: fromAmount, - destinationAddress: toAddress!, - switcher: Chain.BASE, - }); - if (init) { - bridgeResult = { - chain: Chain.BASE, - hash: init[1] ? init[1].txid : init[0].txid, - isWormhole: true, - }; - } - break; - } - case ChainPairs.ETH_TO_BASE: { - const init = await initWormholeBridge({ - whfrom: appConfig.config === "mainnet" ? "Ethereum" : "Sepolia", - whto: appConfig.config === "mainnet" ? "Base" : "BaseSepolia", - sendAmount: fromAmount, - destinationAddress: toAddress!, - switcher: Chain.ETH, - }); - if (init) { - bridgeResult = { - chain: Chain.ETH, - hash: init[1] ? init[1].txid : init[0].txid, - isWormhole: true, - }; - } - break; - } - default: - throw new Error("Unsupported chain combination"); - } - - if (bridgeResult) { - setDetails(bridgeResult); - setOpenDialog(true); - } - } catch (error: any) { - console.error(error); - setError(error); - setErrorOpenDialog(true); - } finally { - setTransactionInProgress(false); - await fetchBalance( - fromChain === Chain.AVAIL ? selected?.address! : ethAddress!, - fromChain, - api - ); - } - } - - return ( - <> - - {buttonStatus} - - {isWormholeBridge(`${fromChain}-${toChain}`) && ( -

- Using Third Party Wormhole Bridge{" "} - -

- )} - - ); - }; - diff --git a/components/sections/bridge/types.ts b/components/sections/bridge/types.ts index b845dec..ddb336c 100644 --- a/components/sections/bridge/types.ts +++ b/components/sections/bridge/types.ts @@ -73,7 +73,9 @@ export interface Account { ETH_TO_AVAIL: `${Chain.ETH}-${Chain.AVAIL}`, AVAIL_TO_ETH: `${Chain.AVAIL}-${Chain.ETH}`, BASE_TO_ETH: `${Chain.BASE}-${Chain.ETH}`, - ETH_TO_BASE: `${Chain.ETH}-${Chain.BASE}` + ETH_TO_BASE: `${Chain.ETH}-${Chain.BASE}`, + AVAIL_TO_BASE: `${Chain.AVAIL}-${Chain.BASE}`, + BASE_TO_AVAIL: `${Chain.BASE}-${Chain.AVAIL}` } as const; diff --git a/components/sections/transactions/index.tsx b/components/sections/transactions/index.tsx index 74f38a3..9db6cf5 100644 --- a/components/sections/transactions/index.tsx +++ b/components/sections/transactions/index.tsx @@ -117,7 +117,7 @@ export default function TransactionSection() { {/* Pagination */}
-

+ Can't find your transaction? @@ -152,7 +152,7 @@ export default function TransactionSection() {

-

+