From 7f93ccfdd062ec61ada8dd3d4e0b68b5ad5a99bb Mon Sep 17 00:00:00 2001 From: Sahil Kashetwar Date: Fri, 2 May 2025 17:24:52 +0530 Subject: [PATCH 01/10] change: - setup redirect to hub page back! --- src/components/boba/Footer/useFooter.ts | 2 +- src/layout/routes/index.tsx | 134 +++++++++++++----------- 2 files changed, 71 insertions(+), 65 deletions(-) diff --git a/src/components/boba/Footer/useFooter.ts b/src/components/boba/Footer/useFooter.ts index b25aab01..ca8a7dfe 100644 --- a/src/components/boba/Footer/useFooter.ts +++ b/src/components/boba/Footer/useFooter.ts @@ -21,7 +21,7 @@ const footerNavLinks: Array = [ }, { label: "Dev Tools", - href: "https://docs.boba.network/developer", + href: "https://docs.boba.network", target: "_blank" }, { diff --git a/src/layout/routes/index.tsx b/src/layout/routes/index.tsx index 0919c38f..2df3c748 100644 --- a/src/layout/routes/index.tsx +++ b/src/layout/routes/index.tsx @@ -1,77 +1,83 @@ +import AltL1BridgePage from "@/layout/alt-l1-bridge"; import BridgePage from "@/layout/bridge"; import DaoPage from "@/layout/dao"; import EarnPage from "@/layout/earn"; import EcosystemPage from "@/layout/ecosystem"; import HistoryPage from "@/layout/history"; import Home from "@/layout/home"; -import StakePage from "@/layout/stake"; -import TradePage from "@/layout/trade"; import HubPage from "@/layout/hub"; -import SmartAccountPage from "@/layout/smartAccount"; -import { createBrowserRouter, Navigate, type RouteObject } from "react-router-dom"; import PiBridge from "@/layout/pi-bridge"; import RewardsPage from "@/layout/rewards"; -import AltL1BridgePage from "@/layout/alt-l1-bridge"; +import SmartAccountPage from "@/layout/smartAccount"; +import StakePage from "@/layout/stake"; +import TradePage from "@/layout/trade"; +import { createBrowserRouter, Navigate, type RouteObject } from "react-router-dom"; -const RouteList: RouteObject[] = [{ - path: "/", - element: , - children: [ - { - path: "*", - element: , - }, - { - path: "", - // element: , - element: , - }, - { - path: "bridge", - element: , - }, - { - path: "history", - element: , - }, - { - path: "stake", - element: , - }, - { - path: "smartaccount", - element: , - }, - { - path: "earn", - element: , - }, - { - path: "dao", - element: , - }, - { - path: "ecosystem", - element: , - }, - { - path: "trade", - element: , - }, - { - path: "pi-bridge", - element: , - }, - { - path: "rewards", - element: , - }, - { - path: "alt-l1-bridge", - element: , - }, - ] -}] +const RouteList: RouteObject[] = [ + { + path: "/", + element: , + children: [ + { + path: "*", + element: , + }, + { + path: "", + // element: , + element: , + }, + { + path: "bridge", + element: , + }, + { + path: "history", + element: , + }, + { + path: "stake", + element: , + }, + { + path: "smartaccount", + element: , + }, + { + path: "earn", + element: , + }, + { + path: "dao", + element: , + }, + { + path: "ecosystem", + element: , + }, + { + path: "trade", + element: , + }, + { + path: "pi-bridge", + element: , + }, + { + path: "rewards", + element: , + }, + { + path: "alt-l1-bridge", + element: , + }, + ], + }, + { + path: "*", + element: , + } +] const router = createBrowserRouter(RouteList) From 920d3a85ab2d244fe4f80dc7e2c6c00cefa00221 Mon Sep 17 00:00:00 2001 From: Sahil Kashetwar Date: Fri, 2 May 2025 17:53:14 +0530 Subject: [PATCH 02/10] fix: style scrolling on trade & ecosystem --- src/components/boba/DatePickerWIthRange/index.tsx | 6 +++--- src/components/boba/History/action.tsx | 2 +- src/layout/home/index.tsx | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/boba/DatePickerWIthRange/index.tsx b/src/components/boba/DatePickerWIthRange/index.tsx index 587da9ac..c8068b41 100644 --- a/src/components/boba/DatePickerWIthRange/index.tsx +++ b/src/components/boba/DatePickerWIthRange/index.tsx @@ -47,11 +47,11 @@ export const DatePickerWithRange: React.FC = ({ {selectedDates?.from ? ( selectedDates.to ? ( <> - {format(selectedDates.from, "LLL dd, y")} -{" "} - {format(selectedDates.to, "LLL dd, y")} + {format(selectedDates.from, "LL/dd/y")} -{" "} + {format(selectedDates.to, "LL/d/y")} ) : ( - format(selectedDates.from, "LLL dd, y") + format(selectedDates.from, "LL/d/y") ) ) : ( Pick a date diff --git a/src/components/boba/History/action.tsx b/src/components/boba/History/action.tsx index 42f670b5..43a9a139 100644 --- a/src/components/boba/History/action.tsx +++ b/src/components/boba/History/action.tsx @@ -184,7 +184,7 @@ const HistoryActions: React.FC = ({ return ( <> -
+
setInputSearch(e.target.value)} />
diff --git a/src/layout/home/index.tsx b/src/layout/home/index.tsx index 6558a0eb..d783d84e 100644 --- a/src/layout/home/index.tsx +++ b/src/layout/home/index.tsx @@ -16,7 +16,7 @@ function Home() {
{/* @note changing dvh to vh review on other pages. */} -
+
From d29c200ce623042def000b0189fd21a833efcbcd Mon Sep 17 00:00:00 2001 From: Sahil Kashetwar Date: Fri, 2 May 2025 18:13:30 +0530 Subject: [PATCH 03/10] change: - fix for loading the proposal threshold and data correctly! --- src/hooks/dao/useDaoStats.ts | 50 +++++++++++++------------- src/layout/dao/CreateProposalModal.tsx | 3 +- 2 files changed, 28 insertions(+), 25 deletions(-) diff --git a/src/hooks/dao/useDaoStats.ts b/src/hooks/dao/useDaoStats.ts index 3a6da088..7f563bea 100644 --- a/src/hooks/dao/useDaoStats.ts +++ b/src/hooks/dao/useDaoStats.ts @@ -1,13 +1,13 @@ import { daoContractConfig } from '@/config/contracts' -import { publicClientConfig } from '@/wagmi/publicconfig' -import { readContracts } from '@wagmi/core' +import { getProvider } from '@/lib/viem' import { useCallback, useState } from 'react' -import { formatUnits } from 'viem' -import { useAccount } from 'wagmi' +import { formatEther } from 'viem' +import { readContract } from 'viem/actions' +import { useAccount, useChainId } from 'wagmi' export function useDaoStats() { - const { address } = useAccount() + const chainId = useChainId(); const [stats, setStats] = useState({ bobaVotes: 0, @@ -24,30 +24,32 @@ export function useDaoStats() { setStats(prev => ({ ...prev, isLoading: true, error: null, isError: false })) try { - // @todo review and test the implementation - const data = await readContracts(publicClientConfig, { - contracts: [ - { - ...daoContractConfig.bobaToken, - functionName: 'getCurrentVotes', - args: [address!], - }, + + const provider = getProvider(chainId); + + const _threshold = await readContract(provider!, { + address: daoContractConfig.governorBravo.address, + abi: daoContractConfig.governorBravo.abi, + functionName: "proposalThreshold" + }) + + const _bobaVotes = await readContract(provider!, { + ...daoContractConfig.bobaToken, + functionName: 'getCurrentVotes', + args: [address!], + }) + + const _xBobaVotes = await readContract(provider!, { ...daoContractConfig.xBobaToken, functionName: 'getCurrentVotes', args: [address!], - }, - { - ...daoContractConfig.governorBravo, - functionName: 'proposalThreshold', - } - ] as any - }) + }) - const bobaVotes = data && data[0]?.result ? Number(formatUnits(data[0].result as bigint, 18)) : 0 - const xBobaVotes = data && data[1]?.result ? Number(formatUnits(data[1].result as bigint, 18)) : 0 - const proposalThreshold = data && data[2]?.result ? Number(formatUnits(data[2].result as bigint, 18)) : 0 - const totalVotes = (bobaVotes + xBobaVotes); + const proposalThreshold = Number(formatEther(_threshold as bigint)) + const bobaVotes = Number(formatEther(_bobaVotes as bigint)) + const xBobaVotes = Number(formatEther(_xBobaVotes as bigint)) + const totalVotes = (Number(bobaVotes) + Number(xBobaVotes)); const hasSufficientVotes = totalVotes >= Number(proposalThreshold) setStats({ diff --git a/src/layout/dao/CreateProposalModal.tsx b/src/layout/dao/CreateProposalModal.tsx index 7f45ea66..b43b6c81 100644 --- a/src/layout/dao/CreateProposalModal.tsx +++ b/src/layout/dao/CreateProposalModal.tsx @@ -55,6 +55,7 @@ const CreateProposalModal: React.FC = () => { hash: proposalTxHash, }) + console.log(`proposalThreshold`, proposalThreshold) useEffect(() => { if (isOpen(ModalIds.DaoCreateProposal)) { @@ -147,7 +148,7 @@ const CreateProposalModal: React.FC = () => { {proposalType === 'changeT' ?
- The minimum number of votes required for an account to create a proposal. The current value is 100000.0. + The minimum number of votes required for an account to create a proposal. The current total votes are {totalVotes}. Dao Voting Threshold From 3a3b693ff8fa2b6af4c4e25cf25625a8a68cfbd1 Mon Sep 17 00:00:00 2001 From: Sahil Kashetwar Date: Fri, 2 May 2025 18:25:29 +0530 Subject: [PATCH 04/10] fix: - proposals to be fetch normally instead of useQuery. - showing skelton correctily on loading content --- src/hooks/dao/useProposals.ts | 105 ++++++++++++++----------- src/layout/dao/ProposalListSection.tsx | 7 +- src/layout/dao/ProposalSkeleton.tsx | 79 +++++++++++++++++++ 3 files changed, 142 insertions(+), 49 deletions(-) create mode 100644 src/layout/dao/ProposalSkeleton.tsx diff --git a/src/hooks/dao/useProposals.ts b/src/hooks/dao/useProposals.ts index ccbdd5a1..98c126fa 100644 --- a/src/hooks/dao/useProposals.ts +++ b/src/hooks/dao/useProposals.ts @@ -1,12 +1,10 @@ import { daoContractConfig } from '@/config/contracts' +import { getProvider } from '@/lib/viem' import { useDaoStore } from '@/stores/dao.store' import type { Proposal } from '@/types/dao' -import { publicClientConfig } from '@/wagmi/publicconfig' -import { useQuery } from '@tanstack/react-query' -import { readContract } from '@wagmi/core' -import { useEffect } from 'react' +import { useEffect, useState } from 'react' import { formatUnits } from 'viem' -import { useReadContract } from 'wagmi' +import { useChainId, useReadContract } from 'wagmi' const proposalStates = [ 'Pending', @@ -20,8 +18,10 @@ const proposalStates = [ ] export function useProposals() { - const { setProposals } = useDaoStore() + const chainId = useChainId() + const [isLoading, setIsLoading] = useState(true) + const [proposals, setLocalProposals] = useState([]) // Get proposal count const { data: proposalCount } = useReadContract({ @@ -29,52 +29,63 @@ export function useProposals() { functionName: 'proposalCount', }) - // Fetch all proposals - const { data: proposals, isLoading } = useQuery({ - queryKey: ['proposals', Number(proposalCount)], - queryFn: async () => { - if (!proposalCount) return [] + useEffect(() => { + async function fetchProposals() { + if (!proposalCount) { + setIsLoading(false) + return + } - const proposals: Proposal[] = [] - for (let i = 1; i <= Number(proposalCount); i++) { - const [id, , , description, _, startTimestamp, endTimestamp, forVotes, againstVotes, abstainVotes] = await readContract(publicClientConfig, { - ...daoContractConfig.governorBravo, - functionName: 'proposals', - args: [BigInt(i)], - }) as unknown as any + try { + setIsLoading(true) + const newProposals: Proposal[] = [] + const provider = getProvider(chainId) - const state = await readContract(publicClientConfig, { - ...daoContractConfig.governorBravo, - functionName: 'state', - args: [BigInt(i)], - }) as unknown as any + for (let i = 1; i <= Number(proposalCount); i++) { + try { + const [id, , , description, _, startTimestamp, endTimestamp, forVotes, againstVotes, abstainVotes] = await provider.readContract({ + ...daoContractConfig.governorBravo, + functionName: 'proposals', + args: [BigInt(i)], + }) as unknown as any - proposals.push({ - id: id.toString(), - number: Number(id), - title: description, - status: proposalStates[state], - startDate: new Date(Number(startTimestamp) * 1000).toLocaleString(), - endDate: new Date(Number(endTimestamp) * 1000).toLocaleString(), - totalVotes: Number(formatUnits(forVotes, 18)) + Number(formatUnits(againstVotes, 18)) + Number(formatUnits(abstainVotes, 18)), - votes: [ - { type: 'For', count: Number(formatUnits(forVotes, 18)), color: 'bg-green-500' }, - { type: 'Against', count: Number(formatUnits(againstVotes, 18)), color: 'bg-red-500' }, - { type: 'Abstain', count: Number(formatUnits(abstainVotes, 18)), color: 'bg-gray-300' }, - ], - isExpanded: false, - }) - } - return proposals - }, - enabled: !!proposalCount, - }) + const state = await provider.readContract({ + ...daoContractConfig.governorBravo, + functionName: 'state', + args: [BigInt(i)], + }) as unknown as any - useEffect(() => { - if (proposals) { - setProposals(proposals) + newProposals.push({ + id: id.toString(), + number: Number(id), + title: description, + status: proposalStates[state], + startDate: new Date(Number(startTimestamp) * 1000).toLocaleString(), + endDate: new Date(Number(endTimestamp) * 1000).toLocaleString(), + totalVotes: Number(formatUnits(forVotes, 18)) + Number(formatUnits(againstVotes, 18)) + Number(formatUnits(abstainVotes, 18)), + votes: [ + { type: 'For', count: Number(formatUnits(forVotes, 18)), color: 'bg-green-500' }, + { type: 'Against', count: Number(formatUnits(againstVotes, 18)), color: 'bg-red-500' }, + { type: 'Abstain', count: Number(formatUnits(abstainVotes, 18)), color: 'bg-gray-300' }, + ], + isExpanded: false, + }) + } catch (error) { + console.log(`Handling error for proposal ${i}:`, error); + } + } + + setLocalProposals(newProposals) + setProposals(newProposals) + } catch (error) { + console.error('Error fetching proposals:', error) + } finally { + setIsLoading(false) + } } - }, [proposals]) + + fetchProposals() + }, [chainId, proposalCount, setProposals]) return { proposals, diff --git a/src/layout/dao/ProposalListSection.tsx b/src/layout/dao/ProposalListSection.tsx index dba46cff..f5beafd7 100644 --- a/src/layout/dao/ProposalListSection.tsx +++ b/src/layout/dao/ProposalListSection.tsx @@ -1,7 +1,6 @@ import { type FC, Fragment, useState } from 'react'; import LinearProgress from '@/components/boba/LinearProgress'; -import LoadingCard from '@/components/common/loading'; import { Text } from '@/components/ui'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; @@ -21,6 +20,7 @@ import { useModalStore } from '@/stores/modal.store'; import type { Proposal, ProposalStatus } from '@/types/dao'; import { ModalIds } from '@/types/modal'; import { IconChevronDown, IconChevronUp } from '@tabler/icons-react'; +import { ProposalListSkeleton } from './ProposalSkeleton'; interface ProposalCardProps { @@ -183,6 +183,10 @@ const ProposalListSection: FC = () => { selectedTab === 'all' || proposal.status.toLowerCase() === selectedTab.toLowerCase() ); + if (isLoading) { + return + } + return (
{
- {filteredProposals.map((proposal) => ( ( + + +
+
+
+
+
+
+
+
+ + +) + +export const TabsSkeleton = () => ( + + + {[...Array(5)].map((_, i) => ( + +
+ + ))} + + +) + +export const ProposalExpandedSkeleton = () => ( + +
+
+
+
+
+ {[...Array(3)].map((_, i) => ( +
+ ))} +
+
+
+
+
+
+
+
+
+ +) + +export const ProposalListSkeleton = () => ( +
+ +
+ {[...Array(5)].map((_, i) => ( + + +
+
+
+
+
+
+
+
+ + {i === 0 && } + + ))} +
+
+) \ No newline at end of file From a0546d75ac3c7493656bee011bcd409cf139d123 Mon Sep 17 00:00:00 2001 From: Sahil Kashetwar Date: Fri, 2 May 2025 18:31:47 +0530 Subject: [PATCH 05/10] change: handling error efficiently! --- src/layout/dao/DelegateModal.tsx | 6 +++++- src/layout/smartAccount/ManageAccountModal.tsx | 12 ++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/layout/dao/DelegateModal.tsx b/src/layout/dao/DelegateModal.tsx index 05eb04d8..15ac61ac 100644 --- a/src/layout/dao/DelegateModal.tsx +++ b/src/layout/dao/DelegateModal.tsx @@ -59,7 +59,11 @@ const DelegateModal: React.FC = () => { setModalState('success'); } catch (error) { console.log(`error`, error); - setError((error as Error).message); + if ((error as any).message.includes('User rejected the request')) { + setError('User rejected the request') + } else { + setError((error as Error).message) + } setModalState('error'); } }; diff --git a/src/layout/smartAccount/ManageAccountModal.tsx b/src/layout/smartAccount/ManageAccountModal.tsx index 325bce7b..fd687da3 100644 --- a/src/layout/smartAccount/ManageAccountModal.tsx +++ b/src/layout/smartAccount/ManageAccountModal.tsx @@ -37,7 +37,11 @@ const ManageAccountModal: React.FC = () => { await createAccount(counter.toString()); setModalState('success'); } catch (error) { - setError((error as Error).message); + if ((error as any).message.includes('User rejected the request')) { + setError('User rejected the request') + } else { + setError((error as Error).message) + } setModalState('error'); } finally { setIsProcessing(false); @@ -49,7 +53,11 @@ const ManageAccountModal: React.FC = () => { setIsProcessing(true); await deleteAccount(accountId) } catch (error) { - setError((error as Error).message); + if ((error as any).message.includes('User rejected the request')) { + setError('User rejected the request') + } else { + setError((error as Error).message) + } setModalState('error'); } finally { setIsProcessing(false); From 5bba59f1080f6cbe05ddeac4281c54795fa3dc96 Mon Sep 17 00:00:00 2001 From: Sahil Kashetwar Date: Fri, 2 May 2025 18:41:41 +0530 Subject: [PATCH 06/10] fix: skeleton & proposal list --- src/layout/dao/ProposalListSection.tsx | 12 +++++++----- src/layout/dao/ProposalSkeleton.tsx | 8 ++++---- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/layout/dao/ProposalListSection.tsx b/src/layout/dao/ProposalListSection.tsx index f5beafd7..f842c6d9 100644 --- a/src/layout/dao/ProposalListSection.tsx +++ b/src/layout/dao/ProposalListSection.tsx @@ -119,22 +119,24 @@ const ProposalCard: FC = ({ proposal, onToggleExpand }) => {
{proposal.startDate && proposal.endDate && ( -
+ {proposal.startDate} - {proposal.endDate} -
+ )}
{proposal.totalVotes && ( -
+ Total: {proposal.totalVotes.toLocaleString()} -
+ )}
{ buttonConfig.map((config) => - ( - )} +
+ + Select Token To delegate voting power + + +
+
+ + Target address + +
setIsAddressInputHovered(true)} + onMouseLeave={() => setIsAddressInputHovered(false)} + > + setToAddress(e.target.value)} + type="text" + placeholder="Enter target Address (0x...)" + className="w-full border rounded-full px-3 py-4 h-11 text-sm" + /> + {isAddressInputHovered && ( + + )} +
-
+
From cbe196e801fc71624db4e6c43b6554e5095640b4 Mon Sep 17 00:00:00 2001 From: Sahil Kashetwar Date: Fri, 2 May 2025 19:12:50 +0530 Subject: [PATCH 10/10] change: fix for address validation in bridge --- src/components/boba/Bridge/index.tsx | 9 +++++++++ src/layout/dao/CreateProposalModal.tsx | 17 ++++++++++++++--- src/layout/dao/DelegateModal.tsx | 25 ++++++++++++++++++------- 3 files changed, 41 insertions(+), 10 deletions(-) diff --git a/src/components/boba/Bridge/index.tsx b/src/components/boba/Bridge/index.tsx index 7b772f67..d2339d39 100644 --- a/src/components/boba/Bridge/index.tsx +++ b/src/components/boba/Bridge/index.tsx @@ -12,6 +12,7 @@ import { networkDetails } from "@/utils/networks"; import { IconSwitchHorizontal } from '@tabler/icons-react'; import { useState } from 'react'; import BridgeInfoCard from "./BridgeInfoCard"; +import { isAddress } from "viem"; export const NetworkSelection = () => { @@ -133,6 +134,14 @@ export const DestinationAddress: React.FC = () => { const handlePasteAddress = async () => { try { const clipboardAddress = await navigator.clipboard.readText(); + if (!isAddress(clipboardAddress)) { + toast({ + variant: "destructive", + title: "Invalid Address", + description: "The pasted content is not a valid Ethereum address" + }); + return; + } setTargetAddress(clipboardAddress); } catch (error) { toast({ diff --git a/src/layout/dao/CreateProposalModal.tsx b/src/layout/dao/CreateProposalModal.tsx index b43b6c81..ef5e0de2 100644 --- a/src/layout/dao/CreateProposalModal.tsx +++ b/src/layout/dao/CreateProposalModal.tsx @@ -1,5 +1,6 @@ import Modal from '@/components/boba/Modal'; import { Input, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Text, Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui'; +import { useToast } from '@/hooks/common/useToast'; import { useCreateProposal } from '@/hooks/dao/useCreateProposal'; import { useDaoStats } from '@/hooks/dao/useDaoStats'; import { useModalStore } from '@/stores/modal.store'; @@ -32,7 +33,7 @@ const CreateProposalModal: React.FC = () => { const [votingThreshold, setVotingThreshold] = useState(''); const [proposalText, setProposalText] = useState(''); const [proposalUrl, setProposalUrl] = useState(''); - + const { toast } = useToast() const [isProcessing, setIsProcessing] = useState(false); const [proposalType, setProposalType] = useState<'changeT' | 'freeText'>('changeT'); @@ -156,9 +157,19 @@ const CreateProposalModal: React.FC = () => {
setVotingThreshold(e.target.value)} + onChange={(e) => { + if (Number(e.target.value) <= 0) { + toast({ + variant: "destructive", + title: "Invalid threshold value", + description: "Should be in numbers of token" + }) + return; + } + setVotingThreshold(e.target.value) + }} type="text" - placeholder="Enter target Address (0x...)" + placeholder="Enter threashold amount" className="w-full border rounded-full px-3 py-4 h-11 text-sm" />
diff --git a/src/layout/dao/DelegateModal.tsx b/src/layout/dao/DelegateModal.tsx index e54560ee..64d9f40d 100644 --- a/src/layout/dao/DelegateModal.tsx +++ b/src/layout/dao/DelegateModal.tsx @@ -8,10 +8,11 @@ import { generateAvatarURL } from '@cfx-kit/wallet-avatar'; import { IconArrowFork } from '@tabler/icons-react'; import { TabContentWrapper } from '@/components/ui/tabs.smooth'; -import { toast } from '@/hooks/common/useToast'; +import { useToast } from "@/hooks/common/useToast"; import { useDelegateVotes } from '@/hooks/dao/useDelegateVotes'; import { IconSquareCheck, IconSquareX } from '@tabler/icons-react'; import { useState } from 'react'; +import { isAddress } from "viem"; import { useAccount } from 'wagmi'; type DelegateModalProps = {} @@ -34,7 +35,7 @@ const DelegateModal: React.FC = () => { const [isAddressInputHovered, setIsAddressInputHovered] = useState(false) const [toAddress, setToAddress] = useState(''); const [error, setError] = useState(''); - const [delegateType, setDelegateType] = useState(''); + const [delegateType, setDelegateType] = useState('toSelf'); const [modalState, setModalState] = useState<'delegate' | 'success' | 'error'>('delegate'); const [delegateTokenType, setDelegateTokenType] = useState<'boba' | 'xBoba'>('boba'); @@ -42,18 +43,28 @@ const DelegateModal: React.FC = () => { const { isOpen, closeModal } = useModalStore(); const { account, shortAccount } = useBobaAccount(); const { delegateVotes, isPending } = useDelegateVotes() + const { toast } = useToast() - const handlePasteAddress = async () => { + const handlePaste = async () => { try { const clipboardAddress = await navigator.clipboard.readText(); + if (!isAddress(clipboardAddress)) { + toast({ + variant: "destructive", + title: "Invalid Address", + description: "The pasted content is not a valid Ethereum address" + }); + return; + } setToAddress(clipboardAddress); } catch (error) { toast({ - title: 'Clipboard access failed', - description: 'Unable to access clipboard' + variant: "destructive", + title: "Clipboard Error", + description: "Failed to read from clipboard" }); } - }; + } const handleVoteDelegation = async () => { try { @@ -193,7 +204,7 @@ const DelegateModal: React.FC = () => { variant="outline" className="absolute inset-y-2 right-0 mr-2 px-4 py-1 h-7 text-xs font-medium rounded-full focus:outline-none focus:ring-2 focus:ring-offset-2 transition-opacity" size="sm" - onClick={handlePasteAddress} + onClick={handlePaste} > Paste