From b5ac05156696ecacd20c071e842a3079d878fe49 Mon Sep 17 00:00:00 2001 From: nicktytarenko Date: Tue, 20 Jan 2026 00:21:14 +0200 Subject: [PATCH 01/10] Refactor Deposit and Withdraw Modals to Support Network Selection and Enhance User Experience - Introduced a NetworkSelector component for selecting between BASE and ETHEREUM networks in both DepositModal and WithdrawModal. - Updated transaction fee fetching to be network-specific. - Enhanced user feedback with success and error messages in WithdrawalSuccessView. - Improved modal structure by replacing Dialog components with BaseModal for consistency. - Added loading states and error handling for wallet balance and transaction processes. - Refactored input handling and validation for deposit and withdrawal amounts. --- .../modals/ResearchCoin/DepositModal.tsx | 442 +++++++------ .../modals/ResearchCoin/WithdrawModal.tsx | 587 ++++++++++-------- .../ResearchCoin/WithdrawalSuccessView.tsx | 117 ++++ components/ui/NetworkSelector.tsx | 139 +++++ constants/tokens.ts | 52 ++ hooks/useWalletRSCBalance.ts | 21 +- hooks/useWithdrawRSC.ts | 23 +- public/ethereum-logo.svg | 9 + services/transaction.service.ts | 7 +- 9 files changed, 899 insertions(+), 498 deletions(-) create mode 100644 components/modals/ResearchCoin/WithdrawalSuccessView.tsx create mode 100644 components/ui/NetworkSelector.tsx create mode 100644 public/ethereum-logo.svg diff --git a/components/modals/ResearchCoin/DepositModal.tsx b/components/modals/ResearchCoin/DepositModal.tsx index 5df52d200..bb6bf781b 100644 --- a/components/modals/ResearchCoin/DepositModal.tsx +++ b/components/modals/ResearchCoin/DepositModal.tsx @@ -1,8 +1,9 @@ 'use client'; -import { Dialog, Transition, DialogPanel, DialogTitle } from '@headlessui/react'; -import { Fragment, useCallback, useMemo, useState, useEffect, useRef } from 'react'; -import { X as XIcon, Check, AlertCircle } from 'lucide-react'; +import { useCallback, useMemo, useState, useEffect, useRef } from 'react'; +import { Check, AlertCircle, Loader2 } from 'lucide-react'; +import Image from 'next/image'; +import { BaseModal } from '@/components/ui/BaseModal'; import { formatRSC } from '@/utils/number'; import { ResearchCoinIcon } from '@/components/ui/icons/ResearchCoinIcon'; import { useAccount } from 'wagmi'; @@ -11,7 +12,11 @@ import { useIsMobile } from '@/hooks/useIsMobile'; import { Transaction, TransactionButton } from '@coinbase/onchainkit/transaction'; import { Interface } from 'ethers'; import { TransactionService } from '@/services/transaction.service'; -import { RSC, TRANSFER_ABI } from '@/constants/tokens'; +import { getRSCForNetwork, NetworkType, TRANSFER_ABI, NETWORK_CONFIG } from '@/constants/tokens'; +import { NetworkSelector } from '@/components/ui/NetworkSelector'; +import { Alert } from '@/components/ui/Alert'; +import { Button } from '@/components/ui/Button'; +import { cn } from '@/utils/styles'; const HOT_WALLET_ADDRESS_ENV = process.env.NEXT_PUBLIC_WEB3_WALLET_ADDRESS; if (!HOT_WALLET_ADDRESS_ENV || HOT_WALLET_ADDRESS_ENV.trim() === '') { @@ -19,13 +24,6 @@ if (!HOT_WALLET_ADDRESS_ENV || HOT_WALLET_ADDRESS_ENV.trim() === '') { } const HOT_WALLET_ADDRESS = HOT_WALLET_ADDRESS_ENV as `0x${string}`; -// Network configuration based on environment -const IS_PRODUCTION = process.env.NEXT_PUBLIC_VERCEL_ENV === 'production'; -const NETWORK_NAME = IS_PRODUCTION ? 'Base' : 'Base Sepolia'; -const NETWORK_DESCRIPTION = IS_PRODUCTION - ? 'Deposits are processed on Base L2' - : 'Deposits are processed on Base Sepolia testnet'; - // Define types for blockchain transaction call type Call = { to: `0x${string}`; @@ -50,20 +48,26 @@ type TransactionStatus = export function DepositModal({ isOpen, onClose, currentBalance, onSuccess }: DepositModalProps) { const [amount, setAmount] = useState(''); + const [selectedNetwork, setSelectedNetwork] = useState('BASE'); const [isInitiating, isDepositButtonDisabled] = useState(false); + const contentRef = useRef(null); const { address } = useAccount(); - const { balance: walletBalance } = useWalletRSCBalance(); + const { balance: walletBalance, isLoading: isWalletBalanceLoading } = useWalletRSCBalance({ + network: selectedNetwork, + }); const isMobile = useIsMobile(); const [txStatus, setTxStatus] = useState({ state: 'idle' }); const hasCalledSuccessRef = useRef(false); const hasProcessedDepositRef = useRef(false); const processedTxHashRef = useRef(null); - // Reset transaction status when modal opens + const rscToken = useMemo(() => getRSCForNetwork(selectedNetwork), [selectedNetwork]); + useEffect(() => { if (isOpen) { setTxStatus({ state: 'idle' }); setAmount(''); + setSelectedNetwork('BASE'); isDepositButtonDisabled(false); hasCalledSuccessRef.current = false; hasProcessedDepositRef.current = false; @@ -71,23 +75,32 @@ export function DepositModal({ isOpen, onClose, currentBalance, onSuccess }: Dep } }, [isOpen]); - // Handle custom close with state reset + useEffect(() => { + if (txStatus.state === 'error') { + if (contentRef.current) { + const scrollableParent = contentRef.current.closest('[class*="overflow-y-auto"]'); + if (scrollableParent) { + scrollableParent.scrollTo({ top: 0, behavior: 'smooth' }); + } else { + contentRef.current.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } + } + } + }, [txStatus]); + const handleClose = useCallback(() => { setTxStatus({ state: 'idle' }); setAmount(''); onClose(); }, [onClose]); - // Handle amount input change with validation const handleAmountChange = useCallback((e: React.ChangeEvent) => { const value = e.target.value; - // Only allow positive integers if (value === '' || /^\d+$/.test(value)) { setAmount(value); } }, []); - // Memoize derived values const depositAmount = useMemo(() => parseInt(amount || '0', 10), [amount]); const calculateNewBalance = useCallback( @@ -106,7 +119,6 @@ export function DepositModal({ isOpen, onClose, currentBalance, onSuccess }: Dep [address, amount, depositAmount, walletBalance, isInitiating, isMobile] ); - // Function to check if inputs should be disabled const isInputDisabled = useCallback(() => { return ( !address || @@ -166,7 +178,7 @@ export function DepositModal({ isOpen, onClose, currentBalance, onSuccess }: Dep amount: depositAmount, transaction_hash: txHash, from_address: address!, - network: 'BASE', + network: selectedNetwork, }).catch((error) => { console.error('Failed to record deposit:', error); }); @@ -190,7 +202,7 @@ export function DepositModal({ isOpen, onClose, currentBalance, onSuccess }: Dep isDepositButtonDisabled(false); } }, - [depositAmount, address, onSuccess] + [depositAmount, address, onSuccess, selectedNetwork] ); const callsCallback = useCallback(async () => { @@ -211,220 +223,206 @@ export function DepositModal({ isOpen, onClose, currentBalance, onSuccess }: Dep // Cast the result to Call type with proper hex type const transferCall: Call = { - to: RSC.address as `0x${string}`, + to: rscToken.address as `0x${string}`, data: encodedData as `0x${string}`, }; return [transferCall]; - }, [amount, depositAmount, walletBalance]); + }, [amount, depositAmount, walletBalance, rscToken.address]); // If no wallet is connected, show nothing - assuming modal shouldn't open in this state if (!address) { return null; } + const footer = useMemo(() => { + return ( + +
+ +
+
+ ); + }, [ + rscToken.chainId, + callsCallback, + handleOnStatus, + setButtonDisabledOnClick, + isButtonDisabled, + txStatus.state, + ]); + return ( - <> - - - -
- - -
-
- - -
- - Deposit RSC - - -
- -
- {isMobile && ( -
-
- -

- Deposits are temporarily unavailable on mobile devices. Please use a - desktop browser to make deposits. -

-
-
- )} - - {/* Network Info */} -
-
- {`${NETWORK_NAME} -
- {NETWORK_NAME} - {NETWORK_DESCRIPTION} -
-
-
- - {/* Wallet RSC Balance */} -
-
- Wallet Balance: -
- - - {walletBalance.toFixed(2)} - - RSC -
-
-
- - {/* Amount Input */} -
-
- Amount to Deposit - -
-
- -
- RSC -
-
- {depositAmount > walletBalance && ( -

- Deposit amount exceeds your wallet balance. -

- )} -
- - {/* Balance Display */} -
-
- Current Balance: -
-
- - - {formatRSC({ amount: currentBalance })} - - RSC -
-
-
- -
-
- After Deposit: -
-
- - 0 && depositAmount <= walletBalance ? 'text-green-600' : 'text-gray-900'}`} - > - {depositAmount > 0 && depositAmount <= walletBalance - ? formatRSC({ amount: calculateNewBalance() }) - : formatRSC({ amount: currentBalance })} - - RSC -
-
-
-
- - {/* Transaction Button */} - -
- -
-
- - {/* Transaction Status Display */} - {(txStatus.state === 'success' || txStatus.state === 'error') && ( -
- {txStatus.state === 'success' && ( -
-
- - Deposit successful! -
-

- It can take up to 10-20 minutes for the deposit to appear in your - account. -

-
- )} - - {txStatus.state === 'error' && ( -
-
- - Deposit failed -
-

{txStatus.message}

-
- )} -
- )} -
- - + +
+ {/* Transaction Status Display */} + {txStatus.state === 'success' && ( + +
+
Deposit successful!
+
+ It can take up to 10-20 minutes for the deposit to appear in your account. +
+
+
+ )} + + {txStatus.state === 'error' && ( + +
+
Deposit failed
+ {'message' in txStatus && txStatus.message && ( +
{txStatus.message}
+ )} +
+
+ )} + + {isMobile && ( + + Deposits are temporarily unavailable on mobile devices. Please use a desktop browser to + make deposits. + + )} + + {/* Network Selector */} +
+
+ Network +
+ {(Object.keys(NETWORK_CONFIG) as NetworkType[]).map((network) => { + const config = NETWORK_CONFIG[network]; + return ( + {`${config.name} + ); + })} +
+
+ +
+ + {/* Wallet RSC Balance */} +
+
+ Wallet Balance: +
+ + {isWalletBalanceLoading ? ( + Loading... + ) : ( + <> + + {walletBalance.toFixed(2)} + + RSC + + )} +
+
+
+ + {/* Amount Input */} +
+
+ Amount to Deposit + +
+
+ +
+ RSC +
+
+ {depositAmount > walletBalance && ( +

+ Deposit amount exceeds your wallet balance. +

+ )} +
+ + {/* Balance Display */} +
+
+ Current Balance: +
+
+ + + {formatRSC({ amount: currentBalance })} + + RSC +
+
+
+ +
+
+ After Deposit: +
+
+ + 0 && depositAmount <= walletBalance ? 'text-green-600' : 'text-gray-900'}`} + > + {depositAmount > 0 && depositAmount <= walletBalance + ? formatRSC({ amount: calculateNewBalance() }) + : formatRSC({ amount: currentBalance })} + + RSC +
-
-
- + + + ); } diff --git a/components/modals/ResearchCoin/WithdrawModal.tsx b/components/modals/ResearchCoin/WithdrawModal.tsx index 715dea414..fa7936909 100644 --- a/components/modals/ResearchCoin/WithdrawModal.tsx +++ b/components/modals/ResearchCoin/WithdrawModal.tsx @@ -1,21 +1,22 @@ 'use client'; -import { Dialog, Transition, DialogPanel, DialogTitle } from '@headlessui/react'; -import { Fragment, useCallback, useMemo, useState, useEffect } from 'react'; -import { X as XIcon, Check, AlertCircle, ExternalLink, Loader2 } from 'lucide-react'; +import { useCallback, useMemo, useState, useEffect, useRef } from 'react'; +import { Check, AlertCircle, ExternalLink, Loader2, Copy } from 'lucide-react'; +import Image from 'next/image'; +import { BaseModal } from '@/components/ui/BaseModal'; import { formatRSC } from '@/utils/number'; import { ResearchCoinIcon } from '@/components/ui/icons/ResearchCoinIcon'; import { useAccount } from 'wagmi'; import { useWithdrawRSC } from '@/hooks/useWithdrawRSC'; import { cn } from '@/utils/styles'; - -// Network configuration based on environment -const IS_PRODUCTION = process.env.NEXT_PUBLIC_VERCEL_ENV === 'production'; -const NETWORK_NAME = IS_PRODUCTION ? 'Base' : 'Base Sepolia'; -const NETWORK_DESCRIPTION = IS_PRODUCTION - ? 'Withdrawals are processed on Base L2' - : 'Withdrawals are processed on Base Sepolia testnet'; -const BLOCK_EXPLORER_URL = IS_PRODUCTION ? 'https://basescan.org' : 'https://sepolia.basescan.org'; +import { NetworkSelector } from '@/components/ui/NetworkSelector'; +import { NETWORK_CONFIG, NetworkType } from '@/constants/tokens'; +import { Skeleton } from '@/components/ui/Skeleton'; +import { Input } from '@/components/ui/form/Input'; +import { Button } from '@/components/ui/Button'; +import { Alert } from '@/components/ui/Alert'; +import toast from 'react-hot-toast'; +import { WithdrawalSuccessView } from './WithdrawalSuccessView'; // Minimum withdrawal amount in RSC const MIN_WITHDRAWAL_AMOUNT = 150; @@ -34,35 +35,59 @@ export function WithdrawModal({ onSuccess, }: WithdrawModalProps) { const [amount, setAmount] = useState(''); + const [selectedNetwork, setSelectedNetwork] = useState('BASE'); + const [isCopied, setIsCopied] = useState(false); + const contentRef = useRef(null); const { address } = useAccount(); - const [{ txStatus, isLoading, fee, isFeeLoading, feeError }, withdrawRSC] = useWithdrawRSC(); + const [{ txStatus, isLoading, fee, isFeeLoading, feeError }, withdrawRSC, resetTransaction] = + useWithdrawRSC({ + network: selectedNetwork, + }); + + const networkConfig = NETWORK_CONFIG[selectedNetwork]; + const blockExplorerUrl = networkConfig.explorerUrl; - // Reset state when modal is closed useEffect(() => { if (!isOpen) { - setAmount(''); + // Delay reset to ensure modal closing animation completes + const timeoutId = setTimeout(() => { + setAmount(''); + setSelectedNetwork('BASE'); + setIsCopied(false); + resetTransaction(); + }, 300); + + return () => clearTimeout(timeoutId); } - }, [isOpen]); + }, [isOpen, resetTransaction]); + + useEffect(() => { + if (txStatus.state === 'error') { + if (contentRef.current) { + const scrollableParent = contentRef.current.closest('[class*="overflow-y-auto"]'); + if (scrollableParent) { + scrollableParent.scrollTo({ top: 0, behavior: 'smooth' }); + } else { + contentRef.current.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } + } + } + }, [txStatus]); - // Handle amount input change with validation const handleAmountChange = useCallback((e: React.ChangeEvent) => { const value = e.target.value; - // Only allow positive integers if (value === '' || /^\d+$/.test(value)) { setAmount(value); } }, []); - // Memoize derived values const withdrawAmount = useMemo(() => parseInt(amount || '0', 10), [amount]); - // Calculate the amount the user will actually receive const amountUserWillReceive = useMemo((): number => { if (!fee) return 0; return Math.max(0, withdrawAmount - fee); }, [withdrawAmount, fee]); - // Check if the withdrawal amount is below minimum const isBelowMinimum = useMemo( () => withdrawAmount > 0 && withdrawAmount < MIN_WITHDRAWAL_AMOUNT, [withdrawAmount] @@ -106,7 +131,6 @@ export function WithdrawModal({ ] ); - // Function to check if inputs should be disabled const isInputDisabled = useCallback(() => { return !address || txStatus.state === 'pending' || txStatus.state === 'success'; }, [address, txStatus.state]); @@ -126,275 +150,310 @@ export function WithdrawModal({ to_address: address, agreed_to_terms: true, amount: amount, - network: 'BASE', + network: selectedNetwork, }); - // Call onSuccess callback when withdrawal is successful if (result && txStatus.state === 'success' && onSuccess) { onSuccess(); } - }, [address, amount, isButtonDisabled, withdrawRSC, txStatus.state, onSuccess, fee]); + }, [ + address, + amount, + isButtonDisabled, + withdrawRSC, + txStatus.state, + onSuccess, + fee, + selectedNetwork, + ]); + + const handleCopyAddress = useCallback(() => { + if (!address) return; + navigator.clipboard.writeText(address).then( + () => { + setIsCopied(true); + toast.success('Address copied to clipboard!'); + setTimeout(() => setIsCopied(false), 2000); + }, + (err) => { + console.error('Failed to copy address: ', err); + toast.error('Failed to copy address.'); + } + ); + }, [address]); - // If no wallet is connected, show nothing if (!address) { return null; } - return ( - - - -
- - -
-
- - -
- - Withdraw RSC - - -
+ const footer = useMemo(() => { + if (txStatus.state === 'success') { + const txHash = 'txHash' in txStatus ? txStatus.txHash : undefined; + if (txHash) { + const normalizedTxHash = txHash.startsWith('0x') ? txHash : `0x${txHash}`; + return ( + + + + ); + } + } -
- {/* Network Info */} -
-
- {`${NETWORK_NAME} -
- {NETWORK_NAME} - {NETWORK_DESCRIPTION} -
-
-
+ return ( + + ); + }, [txStatus, blockExplorerUrl, isButtonDisabled, handleWithdraw, isFeeLoading]); - {/* Amount Input */} -
-
- Amount to Withdraw - -
-
- +
+ {txStatus.state === 'success' ? ( + + ) : ( + /* Form View */ + <> + {txStatus.state === 'error' && ( + +
+
Withdrawal failed
+ {'message' in txStatus && txStatus.message && ( +
{txStatus.message}
+ )} +
+
+ )} + + {/* Network Selector */} +
+
+ Network +
+ {(Object.keys(NETWORK_CONFIG) as NetworkType[]).map((network) => { + const config = NETWORK_CONFIG[network]; + return ( + {`${config.name} -
- RSC -
-
- {isBelowMinimum && ( -

- Minimum withdrawal amount is {MIN_WITHDRAWAL_AMOUNT} RSC. -

- )} - {hasInsufficientBalance && ( -

- Withdrawal amount exceeds your available balance. -

- )} -
- - {/* Fee Display */} -
- {isFeeLoading ? ( -

- - Calculating network fee... -

- ) : feeError ? ( -

- - Unable to fetch fee: {feeError} -

- ) : ( -
-

- A network fee of{' '} - {fee} RSC will be deducted from your withdrawal amount. -

- - {/* Added "You will receive" row */} - {withdrawAmount > 0 && fee && ( -
-
- - You will receive: - -
- - - {formatRSC({ amount: amountUserWillReceive })} - - RSC -
-
- {amountUserWillReceive <= 0 && withdrawAmount > 0 && ( -

- Withdrawal amount must be greater than the network fee. -

- )} -
- )} -
- )} -
- - {/* Withdrawal Address Display */} -
-
- Withdrawal Address -
-
- {address} + ); + })} +
+
+ +
+ + {/* Amount Input */} +
+
+ Amount to Withdraw + +
+
+ +
+ RSC +
+
+ {isBelowMinimum && ( +

+ Minimum withdrawal amount is {MIN_WITHDRAWAL_AMOUNT} RSC. +

+ )} + {hasInsufficientBalance && ( +

+ Withdrawal amount exceeds your available balance. +

+ )} +
+ + {/* Fee Display */} +
+ {feeError ? ( +

+ + Unable to fetch fee: {feeError} +

+ ) : ( +
+
+
+ {isFeeLoading ? ( + + ) : ( + + )}
+

+ A network fee of{' '} + {isFeeLoading ? ( + + ) : ( + {fee} + )}{' '} + RSC will be deducted from your withdrawal amount. +

- {/* Balance Display */} -
-
- Current Balance: -
-
+ {withdrawAmount > 0 && (fee || isFeeLoading) && ( +
+
+ You will receive: +
- - {formatRSC({ amount: availableBalance })} - + {isFeeLoading ? ( + + ) : ( + + {formatRSC({ amount: amountUserWillReceive })} + + )} RSC
+ {!isFeeLoading && amountUserWillReceive <= 0 && withdrawAmount > 0 && fee && ( +

+ Withdrawal amount must be greater than the network fee. +

+ )}
+ )} +
+ )} +
+ + {/* Withdrawal Address Display */} +
+ + {isCopied ? ( + + ) : ( + + )} + + } + /> -
-
- After Withdrawal: -
-
- - 0 ? 'text-red-600' : 'text-gray-900' - )} - > - {withdrawAmount > 0 - ? formatRSC({ amount: calculateNewBalance() }) - : formatRSC({ amount: availableBalance })} - - RSC -
-
-
+ {/* Network Compatibility Warning */} + +
+ Ensure your wallet supports {NETWORK_CONFIG[selectedNetwork].name} +
+
+
+ + {/* Balance Display */} +
+
+ Current Balance: +
+
+ + + {formatRSC({ amount: availableBalance })} + + RSC
- - {/* Action Button */} - {txStatus.state === 'success' ? ( - - View Transaction - - - ) : ( -
+ +
+
+ After Withdrawal: +
+
+ + 0 ? 'text-red-600' : 'text-gray-900' )} > - {isFeeLoading - ? 'Loading fee...' - : txStatus.state === 'pending' - ? 'Processing...' - : 'Withdraw RSC'} - - )} - - {/* Transaction Status Display */} - {txStatus.state !== 'idle' && ( -
- {txStatus.state === 'pending' && ( -
- - Withdrawal in progress... -
- )} - - {txStatus.state === 'success' && ( -
- - Withdrawal successful! -
- )} - - {txStatus.state === 'error' && ( -
-
- - Withdrawal failed -
-

{txStatus.message}

-
- )} -
- )} + {withdrawAmount > 0 + ? formatRSC({ amount: calculateNewBalance() }) + : formatRSC({ amount: availableBalance })} +
+ RSC +
- - -
-
-
-
+ + + + )} + + ); } diff --git a/components/modals/ResearchCoin/WithdrawalSuccessView.tsx b/components/modals/ResearchCoin/WithdrawalSuccessView.tsx new file mode 100644 index 000000000..eca026f6d --- /dev/null +++ b/components/modals/ResearchCoin/WithdrawalSuccessView.tsx @@ -0,0 +1,117 @@ +import { useState, useCallback } from 'react'; +import Image from 'next/image'; +import { CheckCircle2, Check, Copy } from 'lucide-react'; +import { formatRSC } from '@/utils/number'; +import { ResearchCoinIcon } from '@/components/ui/icons/ResearchCoinIcon'; +import { NetworkConfig } from '@/constants/tokens'; +import toast from 'react-hot-toast'; + +interface WithdrawalSuccessViewProps { + withdrawAmount: number; + fee: number; + amountReceived: number; + networkConfig: NetworkConfig; + address: string; +} + +export function WithdrawalSuccessView({ + withdrawAmount, + fee, + amountReceived, + networkConfig, + address, +}: WithdrawalSuccessViewProps) { + const [isCopied, setIsCopied] = useState(false); + + const handleCopyAddress = useCallback(() => { + if (!address) return; + navigator.clipboard.writeText(address).then( + () => { + setIsCopied(true); + toast.success('Address copied to clipboard!'); + setTimeout(() => setIsCopied(false), 2000); + }, + (err) => { + console.error('Failed to copy address: ', err); + toast.error('Failed to copy address.'); + } + ); + }, [address]); + + const maskedAddress = address ? `${address.slice(0, 6)}...${address.slice(-4)}` : ''; + + return ( +
+
+
+ +
+

Withdrawal Successful!

+

Your RSC has been sent to your wallet

+
+ +
+
+ Amount Withdrawn +
+ + + {formatRSC({ amount: withdrawAmount })} + + RSC +
+
+ +
+ Network Fee +
+ -{fee} + RSC +
+
+ +
+
+ You Received +
+ + + {formatRSC({ amount: amountReceived })} + + RSC +
+
+
+
+ +
+
+ Network +
+ {networkConfig.name} + {networkConfig.name} +
+
+ +
+ To Address +
+ {maskedAddress} + +
+
+
+
+ ); +} diff --git a/components/ui/NetworkSelector.tsx b/components/ui/NetworkSelector.tsx new file mode 100644 index 000000000..123378c04 --- /dev/null +++ b/components/ui/NetworkSelector.tsx @@ -0,0 +1,139 @@ +'use client'; + +import { useMemo } from 'react'; +import Image from 'next/image'; +import { BaseMenu, BaseMenuItem } from './form/BaseMenu'; +import { NETWORK_CONFIG, NetworkType } from '@/constants/tokens'; +import { cn } from '@/utils/styles'; + +interface NetworkSelectorProps { + value: NetworkType; + onChange: (network: NetworkType) => void; + disabled?: boolean; + className?: string; +} + +export function NetworkSelector({ + value, + onChange, + disabled = false, + className, +}: NetworkSelectorProps) { + const selectedNetwork = NETWORK_CONFIG[value]; + + const trigger = useMemo( + () => ( +
+ {`${selectedNetwork.name} +
+
+ {selectedNetwork.name} + + {selectedNetwork.badge} + +
+ {selectedNetwork.description} +
+ + + +
+ ), + [selectedNetwork, value, disabled, className] + ); + + return ( + + {(Object.keys(NETWORK_CONFIG) as NetworkType[]).map((network) => { + const config = NETWORK_CONFIG[network]; + const isSelected = network === value; + + return ( + { + if (!disabled) { + onChange(network); + } + }} + className={cn( + 'flex items-start gap-3 px-3 py-3 rounded-md', + isSelected && 'bg-primary-50' + )} + > + {`${config.name} +
+
+ {config.name} + + {config.badge} + +
+ + {config.description} + +
+ {isSelected && ( + + + + )} +
+ ); + })} +
+ ); +} diff --git a/constants/tokens.ts b/constants/tokens.ts index 9e257b157..3bde7b618 100644 --- a/constants/tokens.ts +++ b/constants/tokens.ts @@ -5,6 +5,43 @@ const CHAIN_ID = IS_PRODUCTION ? 8453 // Base mainnet : 84532; // Base Sepolia testnet +export type NetworkType = 'BASE' | 'ETHEREUM'; + +export interface NetworkConfig { + chainId: number; + name: string; + rscAddress: string; + explorerUrl: string; + badge: string; + description: string; + icon: string; +} + +export const NETWORK_CONFIG: Record = { + BASE: { + chainId: IS_PRODUCTION ? 8453 : 84532, + name: IS_PRODUCTION ? 'Base' : 'Base Sepolia', + rscAddress: IS_PRODUCTION + ? '0xFbB75A59193A3525a8825BeBe7D4b56899E2f7e1' + : '0xdAf43508D785939D6C2d97c2df73d65c9359dBEa', + explorerUrl: IS_PRODUCTION ? 'https://basescan.org' : 'https://sepolia.basescan.org', + badge: 'Lower Fees', + description: 'Recommended network for low fees', + icon: '/base-logo.svg', + }, + ETHEREUM: { + chainId: IS_PRODUCTION ? 1 : 11155111, + name: IS_PRODUCTION ? 'Ethereum' : 'Sepolia', + rscAddress: IS_PRODUCTION + ? '0xd101dcc414f310268c37eeb4cd376ccfa507f571' + : '0xEe8D932a66aDA39E4EF08046734F601D04B6a3DA', + explorerUrl: IS_PRODUCTION ? 'https://etherscan.io' : 'https://sepolia.etherscan.io', + badge: 'Higher Fees', + description: 'Higher fees, with increased decentralization', + icon: '/ethereum-logo.svg', + }, +}; + export const RSC: Token = { name: 'ResearchCoin', symbol: 'RSC', @@ -16,6 +53,21 @@ export const RSC: Token = { chainId: CHAIN_ID, }; +/** + * Get RSC token configuration for a specific network + */ +export function getRSCForNetwork(network: NetworkType): Token { + const config = NETWORK_CONFIG[network]; + return { + name: 'ResearchCoin', + symbol: 'RSC', + decimals: 18, + address: config.rscAddress as `0x${string}`, + image: 'RSC.webp', + chainId: config.chainId, + }; +} + export const ETH: Token = { name: 'ETH', address: '', diff --git a/hooks/useWalletRSCBalance.ts b/hooks/useWalletRSCBalance.ts index 05c92b949..0dfe94436 100644 --- a/hooks/useWalletRSCBalance.ts +++ b/hooks/useWalletRSCBalance.ts @@ -2,7 +2,7 @@ import { useAccount, useBalance } from 'wagmi'; import { useMemo } from 'react'; -import { RSC } from '@/constants/tokens'; +import { getRSCForNetwork, NetworkType } from '@/constants/tokens'; interface UseWalletRSCBalanceReturn { balance: number; @@ -11,21 +11,34 @@ interface UseWalletRSCBalanceReturn { error: Error | null; } +interface UseWalletRSCBalanceOptions { + network?: NetworkType; +} + /** * Hook to check the connected wallet's RSC balance + * @param options - Options including network selection * @returns An object containing the wallet's RSC balance and related states */ -export function useWalletRSCBalance(): UseWalletRSCBalanceReturn { +export function useWalletRSCBalance( + options: UseWalletRSCBalanceOptions = {} +): UseWalletRSCBalanceReturn { + const { network = 'BASE' } = options; const { address } = useAccount(); + const rscToken = useMemo(() => getRSCForNetwork(network), [network]); + const { data: balanceData, isLoading, error, } = useBalance({ address, - token: RSC.address && RSC.address.startsWith('0x') ? (RSC.address as `0x${string}`) : undefined, - chainId: RSC.chainId, + token: + rscToken.address && rscToken.address.startsWith('0x') + ? (rscToken.address as `0x${string}`) + : undefined, + chainId: rscToken.chainId, }); const balance = useMemo( diff --git a/hooks/useWithdrawRSC.ts b/hooks/useWithdrawRSC.ts index f2123e89d..6faa9da5c 100644 --- a/hooks/useWithdrawRSC.ts +++ b/hooks/useWithdrawRSC.ts @@ -6,6 +6,7 @@ import { WithdrawalRequest, WithdrawalResponse, } from '@/services/transaction.service'; +import { NetworkType } from '@/constants/tokens'; // Define transaction status type export type TransactionStatus = @@ -25,27 +26,34 @@ interface UseWithdrawRSCState { type WithdrawRSCFn = ( withdrawalData: Omit ) => Promise; -type UseWithdrawRSCReturn = [UseWithdrawRSCState, WithdrawRSCFn]; +type ResetFn = () => void; +type UseWithdrawRSCReturn = [UseWithdrawRSCState, WithdrawRSCFn, ResetFn]; + +interface UseWithdrawRSCOptions { + network?: NetworkType; +} /** * Hook for withdrawing RSC to a wallet address. * Manages transaction state and handles API interaction. * + * @param options - Options including network selection * @returns A tuple containing withdrawal state and function */ -export function useWithdrawRSC(): UseWithdrawRSCReturn { +export function useWithdrawRSC(options: UseWithdrawRSCOptions = {}): UseWithdrawRSCReturn { + const { network = 'BASE' } = options; const [txStatus, setTxStatus] = useState({ state: 'idle' }); const [fee, setFee] = useState(null); const [isFeeLoading, setIsFeeLoading] = useState(true); const [feeError, setFeeError] = useState(null); - // Fetch the transaction fee when the hook is initialized + // Fetch the transaction fee when the hook is initialized or network changes useEffect(() => { const fetchTransactionFee = async () => { try { setIsFeeLoading(true); setFeeError(null); - const feeAmount = await TransactionService.getWithdrawalFee(); + const feeAmount = await TransactionService.getWithdrawalFee(network); setFee(feeAmount); } catch (error) { let errorMessage = 'Failed to fetch transaction fee'; @@ -63,7 +71,7 @@ export function useWithdrawRSC(): UseWithdrawRSCReturn { }; fetchTransactionFee(); - }, []); + }, [network]); const withdrawRSC = async ( withdrawalData: Omit @@ -108,6 +116,10 @@ export function useWithdrawRSC(): UseWithdrawRSCReturn { } }; + const reset = () => { + setTxStatus({ state: 'idle' }); + }; + return [ { txStatus, @@ -117,5 +129,6 @@ export function useWithdrawRSC(): UseWithdrawRSCReturn { feeError, }, withdrawRSC, + reset, ]; } diff --git a/public/ethereum-logo.svg b/public/ethereum-logo.svg new file mode 100644 index 000000000..c9a9a8d0b --- /dev/null +++ b/public/ethereum-logo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/services/transaction.service.ts b/services/transaction.service.ts index b46dbd56a..5b6dd3a13 100644 --- a/services/transaction.service.ts +++ b/services/transaction.service.ts @@ -89,14 +89,15 @@ export class TransactionService { private static readonly PURCHASE_PATH = '/api/purchase/'; // Define purchase path /** - * Fetches the current transaction fee for withdrawal on the BASE network + * Fetches the current transaction fee for withdrawal on the specified network + * @param network - The network to fetch the fee for ('BASE' or 'ETHEREUM') * @returns The transaction fee amount in RSC * @throws Error when the API request fails */ - static async getWithdrawalFee(): Promise { + static async getWithdrawalFee(network: 'BASE' | 'ETHEREUM' = 'BASE'): Promise { try { const response = await fetch( - `${process.env.NEXT_PUBLIC_API_URL}${this.WITHDRAWAL_PATH}/transaction_fee/?network=BASE`, + `${process.env.NEXT_PUBLIC_API_URL}${this.WITHDRAWAL_PATH}/transaction_fee/?network=${network}`, { method: 'GET', headers: { From f2bb12ba5e7e385d05827c84b7a88485f30eb642 Mon Sep 17 00:00:00 2001 From: nicktytarenko Date: Tue, 20 Jan 2026 00:45:50 +0200 Subject: [PATCH 02/10] deposit? --- contexts/OnchainContext.tsx | 97 +++++++++++++++++++++++++++++-------- 1 file changed, 77 insertions(+), 20 deletions(-) diff --git a/contexts/OnchainContext.tsx b/contexts/OnchainContext.tsx index dc7031af8..ee6da5779 100644 --- a/contexts/OnchainContext.tsx +++ b/contexts/OnchainContext.tsx @@ -2,7 +2,58 @@ import type { ReactNode } from 'react'; import { OnchainKitProvider } from '@coinbase/onchainkit'; -import { base } from 'wagmi/chains'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { WagmiProvider, createConfig, http } from 'wagmi'; +import { base, baseSepolia, mainnet, sepolia } from 'wagmi/chains'; +import { coinbaseWallet, metaMask } from 'wagmi/connectors'; + +const IS_PRODUCTION = process.env.NEXT_PUBLIC_VERCEL_ENV === 'production'; + +const productionConfig = createConfig({ + chains: [base, mainnet], + connectors: [ + coinbaseWallet({ + appName: 'ResearchHub', + preference: 'all', + }), + metaMask({ + dappMetadata: { + name: 'ResearchHub', + url: typeof window !== 'undefined' ? window.location.origin : 'https://researchhub.com', + }, + }), + ], + transports: { + [base.id]: http(), + [mainnet.id]: http(), + }, + ssr: true, +}); + +const developmentConfig = createConfig({ + chains: [baseSepolia, sepolia], + connectors: [ + coinbaseWallet({ + appName: 'ResearchHub', + preference: 'all', + }), + metaMask({ + dappMetadata: { + name: 'ResearchHub', + url: typeof window !== 'undefined' ? window.location.origin : 'https://researchhub.com', + }, + }), + ], + transports: { + [baseSepolia.id]: http(), + [sepolia.id]: http(), + }, + ssr: true, +}); + +const wagmiConfig = IS_PRODUCTION ? productionConfig : developmentConfig; + +const queryClient = new QueryClient(); interface OnchainProviderProps { children: ReactNode; @@ -10,27 +61,33 @@ interface OnchainProviderProps { /** * Provider component for blockchain interactions using Coinbase's OnchainKit - * Configures wallet connections and blockchain interactions + * Configures wallet connections and blockchain interactions for Base and Ethereum networks */ export function OnchainProvider({ children }: OnchainProviderProps) { + const primaryChain = IS_PRODUCTION ? base : baseSepolia; + return ( - - {children} - + + + + {children} + + + ); } From dc3a9a8be559a2bfb790f4a5bedfbdc63fd3bc18 Mon Sep 17 00:00:00 2001 From: nicktytarenko Date: Tue, 20 Jan 2026 01:12:53 +0200 Subject: [PATCH 03/10] small fixes --- .../modals/ResearchCoin/DepositModal.tsx | 324 ++++++++++-------- .../ResearchCoin/DepositSuccessView.tsx | 104 ++++++ 2 files changed, 284 insertions(+), 144 deletions(-) create mode 100644 components/modals/ResearchCoin/DepositSuccessView.tsx diff --git a/components/modals/ResearchCoin/DepositModal.tsx b/components/modals/ResearchCoin/DepositModal.tsx index bb6bf781b..7ab3f83c8 100644 --- a/components/modals/ResearchCoin/DepositModal.tsx +++ b/components/modals/ResearchCoin/DepositModal.tsx @@ -1,7 +1,7 @@ 'use client'; import { useCallback, useMemo, useState, useEffect, useRef } from 'react'; -import { Check, AlertCircle, Loader2 } from 'lucide-react'; +import { ExternalLink, Loader2 } from 'lucide-react'; import Image from 'next/image'; import { BaseModal } from '@/components/ui/BaseModal'; import { formatRSC } from '@/utils/number'; @@ -16,7 +16,7 @@ import { getRSCForNetwork, NetworkType, TRANSFER_ABI, NETWORK_CONFIG } from '@/c import { NetworkSelector } from '@/components/ui/NetworkSelector'; import { Alert } from '@/components/ui/Alert'; import { Button } from '@/components/ui/Button'; -import { cn } from '@/utils/styles'; +import { DepositSuccessView } from './DepositSuccessView'; const HOT_WALLET_ADDRESS_ENV = process.env.NEXT_PUBLIC_WEB3_WALLET_ADDRESS; if (!HOT_WALLET_ADDRESS_ENV || HOT_WALLET_ADDRESS_ENV.trim() === '') { @@ -62,6 +62,8 @@ export function DepositModal({ isOpen, onClose, currentBalance, onSuccess }: Dep const processedTxHashRef = useRef(null); const rscToken = useMemo(() => getRSCForNetwork(selectedNetwork), [selectedNetwork]); + const networkConfig = NETWORK_CONFIG[selectedNetwork]; + const blockExplorerUrl = networkConfig.explorerUrl; useEffect(() => { if (isOpen) { @@ -72,6 +74,19 @@ export function DepositModal({ isOpen, onClose, currentBalance, onSuccess }: Dep hasCalledSuccessRef.current = false; hasProcessedDepositRef.current = false; processedTxHashRef.current = null; + } else { + // Delay reset to ensure modal closing animation completes + const timeoutId = setTimeout(() => { + setTxStatus({ state: 'idle' }); + setAmount(''); + setSelectedNetwork('BASE'); + isDepositButtonDisabled(false); + hasCalledSuccessRef.current = false; + hasProcessedDepositRef.current = false; + processedTxHashRef.current = null; + }, 300); + + return () => clearTimeout(timeoutId); } }, [isOpen]); @@ -236,6 +251,26 @@ export function DepositModal({ isOpen, onClose, currentBalance, onSuccess }: Dep } const footer = useMemo(() => { + if (txStatus.state === 'success') { + const txHash = 'txHash' in txStatus ? txStatus.txHash : undefined; + if (txHash) { + const normalizedTxHash = txHash.startsWith('0x') ? txHash : `0x${txHash}`; + return ( + + + + ); + } + } + return ( + + {txStatus.state === 'pending' ? 'Processing...' : 'Building transaction...'} + + ), + }} /> @@ -265,6 +302,7 @@ export function DepositModal({ isOpen, onClose, currentBalance, onSuccess }: Dep setButtonDisabledOnClick, isButtonDisabled, txStatus.state, + blockExplorerUrl, ]); return ( @@ -277,151 +315,149 @@ export function DepositModal({ isOpen, onClose, currentBalance, onSuccess }: Dep className="md:!w-[500px]" >
- {/* Transaction Status Display */} - {txStatus.state === 'success' && ( - -
-
Deposit successful!
-
- It can take up to 10-20 minutes for the deposit to appear in your account. + {txStatus.state === 'success' ? ( + + ) : ( + <> + {txStatus.state === 'error' && ( + +
+
Deposit failed
+ {'message' in txStatus && txStatus.message && ( +
{txStatus.message}
+ )} +
+
+ )} + + {isMobile && ( + + Deposits are temporarily unavailable on mobile devices. Please use a desktop browser + to make deposits. + + )} + + {/* Network Selector */} +
+
+ Network +
+ {(Object.keys(NETWORK_CONFIG) as NetworkType[]).map((network) => { + const config = NETWORK_CONFIG[network]; + return ( + {`${config.name} + ); + })} +
+
- - )} - {txStatus.state === 'error' && ( - -
-
Deposit failed
- {'message' in txStatus && txStatus.message && ( -
{txStatus.message}
- )} + {/* Wallet RSC Balance */} +
+
+ Wallet Balance: +
+ + {isWalletBalanceLoading ? ( + Loading... + ) : ( + <> + + {walletBalance.toFixed(2)} + + RSC + + )} +
+
- - )} - - {isMobile && ( - - Deposits are temporarily unavailable on mobile devices. Please use a desktop browser to - make deposits. - - )} - {/* Network Selector */} -
-
- Network -
- {(Object.keys(NETWORK_CONFIG) as NetworkType[]).map((network) => { - const config = NETWORK_CONFIG[network]; - return ( - {`${config.name} - ); - })} -
-
- -
- - {/* Wallet RSC Balance */} -
-
- Wallet Balance: -
- - {isWalletBalanceLoading ? ( - Loading... - ) : ( - <> - - {walletBalance.toFixed(2)} - - RSC - + {/* Amount Input */} +
+
+ Amount to Deposit + +
+
+ +
+ RSC +
+
+ {depositAmount > walletBalance && ( +

+ Deposit amount exceeds your wallet balance. +

)}
-
-
- - {/* Amount Input */} -
-
- Amount to Deposit - -
-
- -
- RSC -
-
- {depositAmount > walletBalance && ( -

- Deposit amount exceeds your wallet balance. -

- )} -
- {/* Balance Display */} -
-
- Current Balance: -
-
- - - {formatRSC({ amount: currentBalance })} - - RSC + {/* Balance Display */} +
+
+ Current Balance: +
+
+ + + {formatRSC({ amount: currentBalance })} + + RSC +
+
-
-
- -
-
- After Deposit: -
-
- - 0 && depositAmount <= walletBalance ? 'text-green-600' : 'text-gray-900'}`} - > - {depositAmount > 0 && depositAmount <= walletBalance - ? formatRSC({ amount: calculateNewBalance() }) - : formatRSC({ amount: currentBalance })} - - RSC + +
+
+ After Deposit: +
+
+ + 0 && depositAmount <= walletBalance ? 'text-green-600' : 'text-gray-900'}`} + > + {depositAmount > 0 && depositAmount <= walletBalance + ? formatRSC({ amount: calculateNewBalance() }) + : formatRSC({ amount: currentBalance })} + + RSC +
+
-
-
+ + )}
); diff --git a/components/modals/ResearchCoin/DepositSuccessView.tsx b/components/modals/ResearchCoin/DepositSuccessView.tsx new file mode 100644 index 000000000..7ca76c900 --- /dev/null +++ b/components/modals/ResearchCoin/DepositSuccessView.tsx @@ -0,0 +1,104 @@ +import { useState, useCallback } from 'react'; +import Image from 'next/image'; +import { CheckCircle2, Check, Copy, Clock } from 'lucide-react'; +import { formatRSC } from '@/utils/number'; +import { ResearchCoinIcon } from '@/components/ui/icons/ResearchCoinIcon'; +import { NetworkConfig } from '@/constants/tokens'; +import toast from 'react-hot-toast'; + +interface DepositSuccessViewProps { + depositAmount: number; + networkConfig: NetworkConfig; + address: string; +} + +export function DepositSuccessView({ + depositAmount, + networkConfig, + address, +}: DepositSuccessViewProps) { + const [isCopied, setIsCopied] = useState(false); + + const handleCopyAddress = useCallback(() => { + if (!address) return; + navigator.clipboard.writeText(address).then( + () => { + setIsCopied(true); + toast.success('Address copied to clipboard!'); + setTimeout(() => setIsCopied(false), 2000); + }, + (err) => { + console.error('Failed to copy address: ', err); + toast.error('Failed to copy address.'); + } + ); + }, [address]); + + const maskedAddress = address ? `${address.slice(0, 6)}...${address.slice(-4)}` : ''; + + return ( +
+
+
+ +
+

Deposit Successful!

+

Your RSC is being processed

+
+ +
+
+ Amount Deposited +
+ + + {formatRSC({ amount: depositAmount })} + + RSC +
+
+
+ +
+
+ Network +
+ {networkConfig.name} + {networkConfig.name} +
+
+ +
+ From Address +
+ {maskedAddress} + +
+
+
+ +
+
+ +
+

Processing Time

+

+ It can take up to 10-20 minutes for the deposit to appear in your ResearchHub balance. +

+
+
+
+
+ ); +} From 9fad469bba7a846cd5f09494f886786097cd98b5 Mon Sep 17 00:00:00 2001 From: nicktytarenko Date: Tue, 20 Jan 2026 21:03:41 +0200 Subject: [PATCH 04/10] fromatting + refactoring deposit modal --- app/api/og/route.tsx | 216 +++++++++--------- app/globals.css | 2 +- components/Comment/CommentItem.tsx | 5 +- .../modals/ResearchCoin/DepositModal.tsx | 8 +- .../modals/ResearchCoin/WithdrawModal.tsx | 147 +++++++++--- components/ui/Badge.tsx | 3 +- components/ui/Button.tsx | 3 +- components/ui/NetworkSelector.tsx | 62 ++--- components/ui/form/FileUpload.tsx | 2 +- contexts/OnchainContext.tsx | 8 +- public/sw.js | 8 +- public/workbox-4754cb34.js | 177 +++++++------- utils/stringUtils.ts | 9 + 13 files changed, 375 insertions(+), 275 deletions(-) diff --git a/app/api/og/route.tsx b/app/api/og/route.tsx index 10809fca2..e101c4234 100644 --- a/app/api/og/route.tsx +++ b/app/api/og/route.tsx @@ -11,140 +11,138 @@ export async function GET(request: NextRequest) { const type = searchParams.get('type') || 'article'; return new ImageResponse( - ( +
+ {/* Background pattern */} +
+ + {/* Logo and brand */}
- {/* Background pattern */} -
+
- {/* Logo and brand */} + {/* Content type badge */} + {type && (
- ResearchHub + {type}
+ )} - {/* Content type badge */} - {type && ( -
- {type} -
- )} - - {/* Title */} - {title && ( -

- {title} -

- )} + {/* Title */} + {title && ( +

+ {title} +

+ )} - {/* Description */} - {description && ( -

- {description} -

- )} + {/* Description */} + {description && ( +

+ {description} +

+ )} - {/* Author */} - {author && ( + {/* Author */} + {author && ( +
-
- {author.charAt(0).toUpperCase()} -
- By {author} + {author.charAt(0).toUpperCase()}
- )} -
- ), + By {author} +
+ )} +
, { width: 1200, height: 630, diff --git a/app/globals.css b/app/globals.css index df0986026..b5afd3b9f 100644 --- a/app/globals.css +++ b/app/globals.css @@ -96,7 +96,7 @@ body { /* Search result highlighting */ mark { - background-color: #FEF9C3; + background-color: #fef9c3; color: inherit; padding: 0.125rem 0; border-radius: 0.125rem; diff --git a/components/Comment/CommentItem.tsx b/components/Comment/CommentItem.tsx index 64114a678..d413127b2 100644 --- a/components/Comment/CommentItem.tsx +++ b/components/Comment/CommentItem.tsx @@ -381,8 +381,9 @@ export const CommentItem = ({ } .prose pre code { - font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', - 'Courier New', monospace; + font-family: + ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', + monospace; font-size: 0.875rem; line-height: 1.5; background: none; diff --git a/components/modals/ResearchCoin/DepositModal.tsx b/components/modals/ResearchCoin/DepositModal.tsx index 7ab3f83c8..6056ca269 100644 --- a/components/modals/ResearchCoin/DepositModal.tsx +++ b/components/modals/ResearchCoin/DepositModal.tsx @@ -271,9 +271,11 @@ export function DepositModal({ isOpen, onClose, currentBalance, onSuccess }: Dep } } + const isSponsored = selectedNetwork === 'BASE'; + return ( -
+
Network
{(Object.keys(NETWORK_CONFIG) as NetworkType[]).map((network) => { @@ -365,6 +368,7 @@ export function DepositModal({ isOpen, onClose, currentBalance, onSuccess }: Dep value={selectedNetwork} onChange={setSelectedNetwork} disabled={isInputDisabled()} + showBadges={false} />
diff --git a/components/modals/ResearchCoin/WithdrawModal.tsx b/components/modals/ResearchCoin/WithdrawModal.tsx index fa7936909..30cc6e076 100644 --- a/components/modals/ResearchCoin/WithdrawModal.tsx +++ b/components/modals/ResearchCoin/WithdrawModal.tsx @@ -13,10 +13,12 @@ import { NetworkSelector } from '@/components/ui/NetworkSelector'; import { NETWORK_CONFIG, NetworkType } from '@/constants/tokens'; import { Skeleton } from '@/components/ui/Skeleton'; import { Input } from '@/components/ui/form/Input'; +import { Checkbox } from '@/components/ui/form/Checkbox'; import { Button } from '@/components/ui/Button'; import { Alert } from '@/components/ui/Alert'; import toast from 'react-hot-toast'; import { WithdrawalSuccessView } from './WithdrawalSuccessView'; +import { isValidEthereumAddress } from '@/utils/stringUtils'; // Minimum withdrawal amount in RSC const MIN_WITHDRAWAL_AMOUNT = 150; @@ -37,6 +39,8 @@ export function WithdrawModal({ const [amount, setAmount] = useState(''); const [selectedNetwork, setSelectedNetwork] = useState('BASE'); const [isCopied, setIsCopied] = useState(false); + const [addressMode, setAddressMode] = useState<'connected' | 'custom'>('connected'); + const [customAddress, setCustomAddress] = useState(''); const contentRef = useRef(null); const { address } = useAccount(); const [{ txStatus, isLoading, fee, isFeeLoading, feeError }, withdrawRSC, resetTransaction] = @@ -54,6 +58,8 @@ export function WithdrawModal({ setAmount(''); setSelectedNetwork('BASE'); setIsCopied(false); + setAddressMode('connected'); + setCustomAddress(''); resetTransaction(); }, 300); @@ -61,6 +67,15 @@ export function WithdrawModal({ } }, [isOpen, resetTransaction]); + const withdrawalAddress = useMemo(() => { + return addressMode === 'connected' ? address : customAddress; + }, [addressMode, address, customAddress]); + + const isCustomAddressValid = useMemo(() => { + if (addressMode === 'connected') return true; + return isValidEthereumAddress(customAddress); + }, [addressMode, customAddress]); + useEffect(() => { if (txStatus.state === 'error') { if (contentRef.current) { @@ -118,7 +133,9 @@ export function WithdrawModal({ !fee || hasInsufficientBalance || isBelowMinimum || - amountUserWillReceive <= 0, + amountUserWillReceive <= 0 || + !isCustomAddressValid || + !withdrawalAddress, [ amount, withdrawAmount, @@ -128,6 +145,8 @@ export function WithdrawModal({ hasInsufficientBalance, isBelowMinimum, amountUserWillReceive, + isCustomAddressValid, + withdrawalAddress, ] ); @@ -142,12 +161,12 @@ export function WithdrawModal({ }, [availableBalance, isInputDisabled, fee]); const handleWithdraw = useCallback(async () => { - if (!address || !amount || isButtonDisabled || !fee) { + if (!withdrawalAddress || !amount || isButtonDisabled || !fee) { return; } const result = await withdrawRSC({ - to_address: address, + to_address: withdrawalAddress, agreed_to_terms: true, amount: amount, network: selectedNetwork, @@ -157,7 +176,7 @@ export function WithdrawModal({ onSuccess(); } }, [ - address, + withdrawalAddress, amount, isButtonDisabled, withdrawRSC, @@ -168,8 +187,8 @@ export function WithdrawModal({ ]); const handleCopyAddress = useCallback(() => { - if (!address) return; - navigator.clipboard.writeText(address).then( + if (!withdrawalAddress) return; + navigator.clipboard.writeText(withdrawalAddress).then( () => { setIsCopied(true); toast.success('Address copied to clipboard!'); @@ -180,7 +199,11 @@ export function WithdrawModal({ toast.error('Failed to copy address.'); } ); - }, [address]); + }, [withdrawalAddress]); + + const handleCustomAddressChange = useCallback((e: React.ChangeEvent) => { + setCustomAddress(e.target.value); + }, []); if (!address) { return null; @@ -237,7 +260,7 @@ export function WithdrawModal({ fee={fee || 0} amountReceived={amountUserWillReceive} networkConfig={networkConfig} - address={address || ''} + address={withdrawalAddress || ''} /> ) : ( /* Form View */ @@ -255,7 +278,7 @@ export function WithdrawModal({ {/* Network Selector */}
-
+
Network
{(Object.keys(NETWORK_CONFIG) as NetworkType[]).map((network) => { @@ -379,38 +402,90 @@ export function WithdrawModal({ )}
- {/* Withdrawal Address Display */} -
- + Withdrawal Address + + {/* Address Mode Toggle */} + { + if (!isInputDisabled()) { + setAddressMode(checked ? 'connected' : 'custom'); + } }} - rightElement={ - - } + disabled={isInputDisabled()} /> + {/* Address Input */} + {addressMode === 'connected' ? ( + + {isCopied ? ( + + ) : ( + + )} + + } + /> + ) : ( +
+ + {isCopied ? ( + + ) : ( + + )} + + ) + } + /> + {customAddress && !isCustomAddressValid && ( +

+ Please enter a valid Ethereum address (0x followed by 40 hex characters). +

+ )} +
+ )} + {/* Network Compatibility Warning */} - +
- Ensure your wallet supports {NETWORK_CONFIG[selectedNetwork].name} + Ensure the destination wallet supports {NETWORK_CONFIG[selectedNetwork].name}
diff --git a/components/ui/Badge.tsx b/components/ui/Badge.tsx index d980fafd0..2ffeb1b09 100644 --- a/components/ui/Badge.tsx +++ b/components/ui/Badge.tsx @@ -29,8 +29,7 @@ const badgeVariants = cva( ); export interface BadgeProps - extends React.HTMLAttributes, - VariantProps { + extends React.HTMLAttributes, VariantProps { children: React.ReactNode; } diff --git a/components/ui/Button.tsx b/components/ui/Button.tsx index 39bae66e6..fc7852a65 100644 --- a/components/ui/Button.tsx +++ b/components/ui/Button.tsx @@ -37,8 +37,7 @@ const buttonVariants = cva( ); export interface ButtonProps - extends ButtonHTMLAttributes, - VariantProps { + extends ButtonHTMLAttributes, VariantProps { asChild?: boolean; tooltip?: string; } diff --git a/components/ui/NetworkSelector.tsx b/components/ui/NetworkSelector.tsx index 123378c04..b8bfa5d14 100644 --- a/components/ui/NetworkSelector.tsx +++ b/components/ui/NetworkSelector.tsx @@ -11,6 +11,7 @@ interface NetworkSelectorProps { onChange: (network: NetworkType) => void; disabled?: boolean; className?: string; + showBadges?: boolean; } export function NetworkSelector({ @@ -18,6 +19,7 @@ export function NetworkSelector({ onChange, disabled = false, className, + showBadges = true, }: NetworkSelectorProps) { const selectedNetwork = NETWORK_CONFIG[value]; @@ -41,16 +43,20 @@ export function NetworkSelector({
{selectedNetwork.name} - - {selectedNetwork.badge} - + {showBadges && selectedNetwork.badge && ( + + {selectedNetwork.badge} + + )}
- {selectedNetwork.description} + {showBadges && selectedNetwork.description && ( + {selectedNetwork.description} + )}
), - [selectedNetwork, value, disabled, className] + [selectedNetwork, value, disabled, className, showBadges] ); return ( @@ -87,7 +93,7 @@ export function NetworkSelector({ } }} className={cn( - 'flex items-start gap-3 px-3 py-3 rounded-md', + 'flex items-center gap-3 px-3 py-3 rounded-md', isSelected && 'bg-primary-50' )} > @@ -96,29 +102,33 @@ export function NetworkSelector({ alt={`${config.name} logo`} width={30} height={30} - className="flex-shrink-0 mt-0.5" + className="flex-shrink-0" />
{config.name} - - {config.badge} - + {showBadges && config.badge && ( + + {config.badge} + + )}
- - {config.description} - + {showBadges && config.description && ( + + {config.description} + + )}
{isSelected && ( diff --git a/public/sw.js b/public/sw.js index f66066964..fc699ed4b 100644 --- a/public/sw.js +++ b/public/sw.js @@ -7,8 +7,8 @@ if (!self.define) { new Promise((i) => { if ('document' in self) { const e = document.createElement('script'); - (e.src = t), (e.onload = i), document.head.appendChild(e); - } else (e = t), importScripts(t), i(); + ((e.src = t), (e.onload = i), document.head.appendChild(e)); + } else ((e = t), importScripts(t), i()); }).then(() => { let e = i[t]; if (!e) throw new Error(`Module ${t} didn’t register its module`); @@ -26,7 +26,7 @@ if (!self.define) { } define(['./workbox-4754cb34'], function (e) { 'use strict'; - importScripts(), + (importScripts(), self.skipWaiting(), e.clientsClaim(), e.precacheAndRoute( @@ -656,5 +656,5 @@ define(['./workbox-4754cb34'], function (e) { plugins: [new e.ExpirationPlugin({ maxEntries: 32, maxAgeSeconds: 3600 })], }), 'GET' - ); + )); }); diff --git a/public/workbox-4754cb34.js b/public/workbox-4754cb34.js index 0db7e50dd..4b93c1176 100644 --- a/public/workbox-4754cb34.js +++ b/public/workbox-4754cb34.js @@ -5,11 +5,11 @@ define(['exports'], function (t) { } catch (t) {} const e = (t, ...e) => { let s = t; - return e.length > 0 && (s += ` :: ${JSON.stringify(e)}`), s; + return (e.length > 0 && (s += ` :: ${JSON.stringify(e)}`), s); }; class s extends Error { constructor(t, s) { - super(e(t, s)), (this.name = t), (this.details = s); + (super(e(t, s)), (this.name = t), (this.details = s)); } } try { @@ -18,7 +18,7 @@ define(['exports'], function (t) { const n = (t) => (t && 'object' == typeof t ? t : { handle: t }); class r { constructor(t, e, s = 'GET') { - (this.handler = n(e)), (this.match = t), (this.method = s); + ((this.handler = n(e)), (this.match = t), (this.method = s)); } setCatchHandler(t) { this.catchHandler = n(t); @@ -38,7 +38,7 @@ define(['exports'], function (t) { } class a { constructor() { - (this.t = new Map()), (this.i = new Map()); + ((this.t = new Map()), (this.i = new Map())); } get routes() { return this.t; @@ -61,7 +61,7 @@ define(['exports'], function (t) { return this.handleRequest({ request: s, event: t }); }) ); - t.waitUntil(s), t.ports && t.ports[0] && s.then(() => t.ports[0].postMessage(!0)); + (t.waitUntil(s), t.ports && t.ports[0] && s.then(() => t.ports[0].postMessage(!0))); } }); } @@ -125,7 +125,7 @@ define(['exports'], function (t) { this.o = n(t); } registerRoute(t) { - this.t.has(t.method) || this.t.set(t.method, []), this.t.get(t.method).push(t); + (this.t.has(t.method) || this.t.set(t.method, []), this.t.get(t.method).push(t)); } unregisterRoute(t) { if (!this.t.has(t.method)) @@ -153,7 +153,7 @@ define(['exports'], function (t) { }); a = t; } - return c().registerRoute(a), a; + return (c().registerRoute(a), a); } try { self['workbox:strategies:6.5.4'] && _(); @@ -179,7 +179,7 @@ define(['exports'], function (t) { class y { constructor() { this.promise = new Promise((t, e) => { - (this.resolve = t), (this.reject = e); + ((this.resolve = t), (this.reject = e)); }); } } @@ -189,14 +189,14 @@ define(['exports'], function (t) { } class v { constructor(t, e) { - (this.h = {}), + ((this.h = {}), Object.assign(this, e), (this.event = e.event), (this.u = t), (this.l = new y()), (this.p = []), (this.m = [...t.plugins]), - (this.v = new Map()); + (this.v = new Map())); for (const t of this.m) this.v.set(t, {}); this.event.waitUntil(this.l.promise); } @@ -224,21 +224,21 @@ define(['exports'], function (t) { return t; } catch (t) { throw ( - (r && + r && (await this.runCallbacks('fetchDidFail', { error: t, event: e, originalRequest: r.clone(), request: i.clone(), })), - t) + t ); } } async fetchAndCachePut(t) { const e = await this.fetch(t), s = e.clone(); - return this.waitUntil(this.cachePut(t, s)), e; + return (this.waitUntil(this.cachePut(t, s)), e); } async cacheMatch(t) { const e = m(t); @@ -289,11 +289,11 @@ define(['exports'], function (t) { } catch (t) { if (t instanceof Error) throw ( - ('QuotaExceededError' === t.name && + 'QuotaExceededError' === t.name && (await (async function () { for (const t of g) await t(); })()), - t) + t ); } for (const t of this.iterateCallbacks('cacheDidUpdate')) @@ -335,7 +335,7 @@ define(['exports'], function (t) { } } waitUntil(t) { - return this.p.push(t), t; + return (this.p.push(t), t); } async doneWaiting() { let t; @@ -354,15 +354,15 @@ define(['exports'], function (t) { !e) ) break; - return s || (e && 200 !== e.status && (e = void 0)), e; + return (s || (e && 200 !== e.status && (e = void 0)), e); } } class R { constructor(t = {}) { - (this.cacheName = d(t.cacheName)), + ((this.cacheName = d(t.cacheName)), (this.plugins = t.plugins || []), (this.fetchOptions = t.fetchOptions), - (this.matchOptions = t.matchOptions); + (this.matchOptions = t.matchOptions)); } handle(t) { const [e] = this.handleAll(t); @@ -399,8 +399,8 @@ define(['exports'], function (t) { r = await t; } catch (i) {} try { - await e.runCallbacks('handlerDidRespond', { event: n, request: s, response: r }), - await e.doneWaiting(); + (await e.runCallbacks('handlerDidRespond', { event: n, request: s, response: r }), + await e.doneWaiting()); } catch (t) { t instanceof Error && (i = t); } @@ -464,14 +464,14 @@ define(['exports'], function (t) { ]) ).includes(t) ? function (...e) { - return t.apply(B(this), e), k(x.get(this)); + return (t.apply(B(this), e), k(x.get(this))); } : function (...e) { return k(t.apply(B(this), e)); } : function (e, ...s) { const n = t.call(B(this), e, ...s); - return I.set(n, e.sort ? e.sort() : [e]), k(n); + return (I.set(n, e.sort ? e.sort() : [e]), k(n)); }; } function T(t) { @@ -482,19 +482,19 @@ define(['exports'], function (t) { if (L.has(t)) return; const e = new Promise((e, s) => { const n = () => { - t.removeEventListener('complete', r), + (t.removeEventListener('complete', r), t.removeEventListener('error', i), - t.removeEventListener('abort', i); + t.removeEventListener('abort', i)); }, r = () => { - e(), n(); + (e(), n()); }, i = () => { - s(t.error || new DOMException('AbortError', 'AbortError')), n(); + (s(t.error || new DOMException('AbortError', 'AbortError')), n()); }; - t.addEventListener('complete', r), + (t.addEventListener('complete', r), t.addEventListener('error', i), - t.addEventListener('abort', i); + t.addEventListener('abort', i)); }); L.set(t, e); })(t), @@ -511,15 +511,15 @@ define(['exports'], function (t) { return (function (t) { const e = new Promise((e, s) => { const n = () => { - t.removeEventListener('success', r), t.removeEventListener('error', i); + (t.removeEventListener('success', r), t.removeEventListener('error', i)); }, r = () => { - e(k(t.result)), n(); + (e(k(t.result)), n()); }, i = () => { - s(t.error), n(); + (s(t.error), n()); }; - t.addEventListener('success', r), t.addEventListener('error', i); + (t.addEventListener('success', r), t.addEventListener('error', i)); }); return ( e @@ -533,7 +533,7 @@ define(['exports'], function (t) { })(t); if (C.has(t)) return C.get(t); const e = T(t); - return e !== t && (C.set(t, e), E.set(e, t)), e; + return (e !== t && (C.set(t, e), E.set(e, t)), e); } const B = (t) => E.get(t); const P = ['get', 'getKey', 'getAll', 'getAllKeys', 'count'], @@ -549,9 +549,9 @@ define(['exports'], function (t) { const i = async function (t, ...e) { const i = this.transaction(t, r ? 'readwrite' : 'readonly'); let a = i.store; - return n && (a = a.index(e.shift())), (await Promise.all([a[s](...e), r && i.done]))[0]; + return (n && (a = a.index(e.shift())), (await Promise.all([a[s](...e), r && i.done]))[0]); }; - return W.set(e, i), i; + return (W.set(e, i), i); } N = ((t) => q({}, t, { @@ -564,29 +564,29 @@ define(['exports'], function (t) { const S = 'cache-entries', K = (t) => { const e = new URL(t, location.href); - return (e.hash = ''), e.href; + return ((e.hash = ''), e.href); }; class A { constructor(t) { - (this._ = null), (this.L = t); + ((this._ = null), (this.L = t)); } I(t) { const e = t.createObjectStore(S, { keyPath: 'id' }); - e.createIndex('cacheName', 'cacheName', { unique: !1 }), - e.createIndex('timestamp', 'timestamp', { unique: !1 }); + (e.createIndex('cacheName', 'cacheName', { unique: !1 }), + e.createIndex('timestamp', 'timestamp', { unique: !1 })); } C(t) { - this.I(t), + (this.I(t), this.L && (function (t, { blocked: e } = {}) { const s = indexedDB.deleteDatabase(t); - e && s.addEventListener('blocked', (t) => e(t.oldVersion, t)), k(s).then(() => {}); - })(this.L); + (e && s.addEventListener('blocked', (t) => e(t.oldVersion, t)), k(s).then(() => {})); + })(this.L)); } async setTimestamp(t, e) { const s = { url: (t = K(t)), timestamp: e, cacheName: this.L, id: this.N(t) }, n = (await this.getDb()).transaction(S, 'readwrite', { durability: 'relaxed' }); - await n.store.put(s), await n.done; + (await n.store.put(s), await n.done); } async getTimestamp(t) { const e = await this.getDb(), @@ -600,11 +600,12 @@ define(['exports'], function (t) { let i = 0; for (; n; ) { const s = n.value; - s.cacheName === this.L && ((t && s.timestamp < t) || (e && i >= e) ? r.push(n.value) : i++), - (n = await n.continue()); + (s.cacheName === this.L && + ((t && s.timestamp < t) || (e && i >= e) ? r.push(n.value) : i++), + (n = await n.continue())); } const a = []; - for (const t of r) await s.delete(S, t.id), a.push(t.url); + for (const t of r) (await s.delete(S, t.id), a.push(t.url)); return a; } N(t) { @@ -628,9 +629,9 @@ define(['exports'], function (t) { s && a.addEventListener('blocked', (t) => s(t.oldVersion, t.newVersion, t)), o .then((t) => { - i && t.addEventListener('close', () => i()), + (i && t.addEventListener('close', () => i()), r && - t.addEventListener('versionchange', (t) => r(t.oldVersion, t.newVersion, t)); + t.addEventListener('versionchange', (t) => r(t.oldVersion, t.newVersion, t))); }) .catch(() => {}), o @@ -642,13 +643,13 @@ define(['exports'], function (t) { } class F { constructor(t, e = {}) { - (this.O = !1), + ((this.O = !1), (this.T = !1), (this.k = e.maxEntries), (this.B = e.maxAgeSeconds), (this.P = e.matchOptions), (this.L = t), - (this.M = new A(t)); + (this.M = new A(t))); } async expireEntries() { if (this.O) return void (this.T = !0); @@ -657,7 +658,7 @@ define(['exports'], function (t) { e = await this.M.expireEntries(t, this.k), s = await self.caches.open(this.L); for (const t of e) await s.delete(t, this.P); - (this.O = !1), this.T && ((this.T = !1), b(this.expireEntries())); + ((this.O = !1), this.T && ((this.T = !1), b(this.expireEntries()))); } async updateTimestamp(t) { await this.M.setTimestamp(t, Date.now()); @@ -671,7 +672,7 @@ define(['exports'], function (t) { return !1; } async delete() { - (this.T = !1), await this.M.expireEntries(1 / 0); + ((this.T = !1), await this.M.expireEntries(1 / 0)); } } try { @@ -724,7 +725,7 @@ define(['exports'], function (t) { } function $(t, e) { const s = e(); - return t.waitUntil(s), s; + return (t.waitUntil(s), s); } try { self['workbox:precaching:6.5.4'] && _(); @@ -743,11 +744,11 @@ define(['exports'], function (t) { } const r = new URL(n, location.href), i = new URL(n, location.href); - return r.searchParams.set('__WB_REVISION__', e), { cacheKey: r.href, url: i.href }; + return (r.searchParams.set('__WB_REVISION__', e), { cacheKey: r.href, url: i.href }); } class G { constructor() { - (this.updatedURLs = []), + ((this.updatedURLs = []), (this.notUpdatedURLs = []), (this.handlerWillStart = async ({ request: t, state: e }) => { e && (e.originalRequest = t); @@ -763,16 +764,16 @@ define(['exports'], function (t) { s ? this.notUpdatedURLs.push(t) : this.updatedURLs.push(t); } return s; - }); + })); } } class V { constructor({ precacheController: t }) { - (this.cacheKeyWillBeUsed = async ({ request: t, params: e }) => { + ((this.cacheKeyWillBeUsed = async ({ request: t, params: e }) => { const s = (null == e ? void 0 : e.cacheKey) || this.W.getCacheKeyForURL(t.url); return s ? new Request(s, { headers: t.headers }) : t; }), - (this.W = t); + (this.W = t)); } } let J, Q; @@ -790,7 +791,7 @@ define(['exports'], function (t) { const t = new Response(''); if ('body' in t) try { - new Response(t.body), (J = !0); + (new Response(t.body), (J = !0)); } catch (t) { J = !1; } @@ -804,10 +805,10 @@ define(['exports'], function (t) { } class Y extends R { constructor(t = {}) { - (t.cacheName = w(t.cacheName)), + ((t.cacheName = w(t.cacheName)), super(t), (this.j = !1 !== t.fallbackToNetwork), - this.plugins.push(Y.copyRedirectedCacheableResponsesPlugin); + this.plugins.push(Y.copyRedirectedCacheableResponsesPlugin)); } async U(t, e) { const s = await e.cacheMatch(t); @@ -821,8 +822,8 @@ define(['exports'], function (t) { const s = r.integrity, i = t.integrity, a = !i || i === s; - (n = await e.fetch(new Request(t, { integrity: 'no-cors' !== t.mode ? i || s : void 0 }))), - s && a && 'no-cors' !== t.mode && (this.A(), await e.cachePut(t, n.clone())); + ((n = await e.fetch(new Request(t, { integrity: 'no-cors' !== t.mode ? i || s : void 0 }))), + s && a && 'no-cors' !== t.mode && (this.A(), await e.cachePut(t, n.clone()))); } return n; } @@ -844,15 +845,15 @@ define(['exports'], function (t) { : e > 1 && null !== t && this.plugins.splice(t, 1); } } - (Y.defaultPrecacheCacheabilityPlugin = { + ((Y.defaultPrecacheCacheabilityPlugin = { cacheWillUpdate: async ({ response: t }) => (!t || t.status >= 400 ? null : t), }), (Y.copyRedirectedCacheableResponsesPlugin = { cacheWillUpdate: async ({ response: t }) => (t.redirected ? await X(t) : t), - }); + })); class Z { constructor({ cacheName: t, plugins: e = [], fallbackToNetwork: s = !0 } = {}) { - (this.F = new Map()), + ((this.F = new Map()), (this.H = new Map()), (this.$ = new Map()), (this.u = new Y({ @@ -861,17 +862,17 @@ define(['exports'], function (t) { fallbackToNetwork: s, })), (this.install = this.install.bind(this)), - (this.activate = this.activate.bind(this)); + (this.activate = this.activate.bind(this))); } get strategy() { return this.u; } precache(t) { - this.addToCacheList(t), + (this.addToCacheList(t), this.G || (self.addEventListener('install', this.install), self.addEventListener('activate', this.activate), - (this.G = !0)); + (this.G = !0))); } addToCacheList(t) { const e = []; @@ -966,7 +967,7 @@ define(['exports'], function (t) { } = {} ) { const i = new URL(t, location.href); - (i.hash = ''), yield i.href; + ((i.hash = ''), yield i.href); const a = (function (t, e = []) { for (const s of [...t.searchParams.keys()]) e.some((t) => t.test(s)) && t.searchParams.delete(s); @@ -974,11 +975,11 @@ define(['exports'], function (t) { })(i, e); if ((yield a.href, s && a.pathname.endsWith('/'))) { const t = new URL(a.href); - (t.pathname += s), yield t.href; + ((t.pathname += s), yield t.href); } if (n) { const t = new URL(a.href); - (t.pathname += '.html'), yield t.href; + ((t.pathname += '.html'), yield t.href); } if (r) { const t = r({ url: i }); @@ -993,7 +994,7 @@ define(['exports'], function (t) { }, t.strategy); } } - (t.CacheFirst = class extends R { + ((t.CacheFirst = class extends R { async U(t, e) { let n, r = await e.cacheMatch(t); @@ -1009,7 +1010,7 @@ define(['exports'], function (t) { }), (t.ExpirationPlugin = class { constructor(t = {}) { - (this.cachedResponseWillBeUsed = async ({ + ((this.cachedResponseWillBeUsed = async ({ event: t, request: e, cacheName: s, @@ -1028,7 +1029,7 @@ define(['exports'], function (t) { }), (this.cacheDidUpdate = async ({ cacheName: t, request: e }) => { const s = this.J(t); - await s.updateTimestamp(e.url), await s.expireEntries(); + (await s.updateTimestamp(e.url), await s.expireEntries()); }), (this.X = t), (this.B = t.maxAgeSeconds), @@ -1036,12 +1037,12 @@ define(['exports'], function (t) { t.purgeOnQuotaError && (function (t) { g.add(t); - })(() => this.deleteCacheAndMetadata()); + })(() => this.deleteCacheAndMetadata())); } J(t) { if (t === d()) throw new s('expire-custom-caches-only'); let e = this.Y.get(t); - return e || ((e = new F(t, this.X)), this.Y.set(t, e)), e; + return (e || ((e = new F(t, this.X)), this.Y.set(t, e)), e); } V(t) { if (!this.B) return !0; @@ -1056,15 +1057,15 @@ define(['exports'], function (t) { return isNaN(s) ? null : s; } async deleteCacheAndMetadata() { - for (const [t, e] of this.Y) await self.caches.delete(t), await e.delete(); + for (const [t, e] of this.Y) (await self.caches.delete(t), await e.delete()); this.Y = new Map(); } }), (t.NetworkFirst = class extends R { constructor(t = {}) { - super(t), + (super(t), this.plugins.some((t) => 'cacheWillUpdate' in t) || this.plugins.unshift(u), - (this.tt = t.networkTimeoutSeconds || 0); + (this.tt = t.networkTimeoutSeconds || 0)); } async U(t, e) { const n = [], @@ -1072,7 +1073,7 @@ define(['exports'], function (t) { let i; if (this.tt) { const { id: s, promise: a } = this.et({ request: t, logs: n, handler: e }); - (i = s), r.push(a); + ((i = s), r.push(a)); } const a = this.st({ timeoutId: i, request: t, logs: n, handler: e }); r.push(a); @@ -1100,7 +1101,7 @@ define(['exports'], function (t) { } catch (t) { t instanceof Error && (r = t); } - return t && clearTimeout(t), (!r && i) || (i = await n.cacheMatch(e)), i; + return (t && clearTimeout(t), (!r && i) || (i = await n.cacheMatch(e)), i); } }), (t.RangeRequestsPlugin = class { @@ -1111,7 +1112,7 @@ define(['exports'], function (t) { }), (t.StaleWhileRevalidate = class extends R { constructor(t = {}) { - super(t), this.plugins.some((t) => 'cacheWillUpdate' in t) || this.plugins.unshift(u); + (super(t), this.plugins.some((t) => 'cacheWillUpdate' in t) || this.plugins.unshift(u)); } async U(t, e) { const n = e.fetchAndCachePut(t).catch(() => {}); @@ -1137,7 +1138,7 @@ define(['exports'], function (t) { const s = (await self.caches.keys()).filter( (s) => s.includes(e) && s.includes(self.registration.scope) && s !== t ); - return await Promise.all(s.map((t) => self.caches.delete(t))), s; + return (await Promise.all(s.map((t) => self.caches.delete(t))), s); })(e).then((t) => {}) ); }); @@ -1146,13 +1147,13 @@ define(['exports'], function (t) { self.addEventListener('activate', () => self.clients.claim()); }), (t.precacheAndRoute = function (t, e) { - !(function (t) { + (!(function (t) { tt().precache(t); })(t), (function (t) { const e = tt(); h(new et(e, t)); - })(e); + })(e)); }), - (t.registerRoute = h); + (t.registerRoute = h)); }); diff --git a/utils/stringUtils.ts b/utils/stringUtils.ts index d055ab88c..2cf8085be 100644 --- a/utils/stringUtils.ts +++ b/utils/stringUtils.ts @@ -64,3 +64,12 @@ export const countWords = (text: string): number => { const trimmed = text.trim(); return trimmed ? trimmed.split(/\s+/).length : 0; }; + +/** + * Validates if a string is a valid Ethereum address format + * @param address The address string to validate + * @returns true if the address is a valid Ethereum address (0x followed by 40 hex characters) + */ +export const isValidEthereumAddress = (address: string): boolean => { + return /^0x[a-fA-F0-9]{40}$/.test(address); +}; From d26e3724f591e56b90c874b64189a66da1b59564 Mon Sep 17 00:00:00 2001 From: nicktytarenko Date: Wed, 21 Jan 2026 19:27:29 +0200 Subject: [PATCH 05/10] addressing comments --- .../modals/ResearchCoin/DepositModal.tsx | 46 +++++++++++++++++-- components/ui/NetworkSelector.tsx | 25 +++++++--- 2 files changed, 59 insertions(+), 12 deletions(-) diff --git a/components/modals/ResearchCoin/DepositModal.tsx b/components/modals/ResearchCoin/DepositModal.tsx index 6056ca269..5515e040d 100644 --- a/components/modals/ResearchCoin/DepositModal.tsx +++ b/components/modals/ResearchCoin/DepositModal.tsx @@ -52,14 +52,26 @@ export function DepositModal({ isOpen, onClose, currentBalance, onSuccess }: Dep const [isInitiating, isDepositButtonDisabled] = useState(false); const contentRef = useRef(null); const { address } = useAccount(); - const { balance: walletBalance, isLoading: isWalletBalanceLoading } = useWalletRSCBalance({ - network: selectedNetwork, + + // Fetch balances for both networks to determine smart default + const { balance: baseBalance, isLoading: isBaseBalanceLoading } = useWalletRSCBalance({ + network: 'BASE', + }); + const { balance: ethereumBalance, isLoading: isEthereumBalanceLoading } = useWalletRSCBalance({ + network: 'ETHEREUM', }); + + // Use selected network's balance for display and validation + const walletBalance = selectedNetwork === 'BASE' ? baseBalance : ethereumBalance; + const isWalletBalanceLoading = + selectedNetwork === 'BASE' ? isBaseBalanceLoading : isEthereumBalanceLoading; + const isMobile = useIsMobile(); const [txStatus, setTxStatus] = useState({ state: 'idle' }); const hasCalledSuccessRef = useRef(false); const hasProcessedDepositRef = useRef(false); const processedTxHashRef = useRef(null); + const hasSetDefaultRef = useRef(false); const rscToken = useMemo(() => getRSCForNetwork(selectedNetwork), [selectedNetwork]); const networkConfig = NETWORK_CONFIG[selectedNetwork]; @@ -69,7 +81,8 @@ export function DepositModal({ isOpen, onClose, currentBalance, onSuccess }: Dep if (isOpen) { setTxStatus({ state: 'idle' }); setAmount(''); - setSelectedNetwork('BASE'); + // Reset default flag to allow smart default to run + hasSetDefaultRef.current = false; isDepositButtonDisabled(false); hasCalledSuccessRef.current = false; hasProcessedDepositRef.current = false; @@ -79,7 +92,7 @@ export function DepositModal({ isOpen, onClose, currentBalance, onSuccess }: Dep const timeoutId = setTimeout(() => { setTxStatus({ state: 'idle' }); setAmount(''); - setSelectedNetwork('BASE'); + hasSetDefaultRef.current = false; isDepositButtonDisabled(false); hasCalledSuccessRef.current = false; hasProcessedDepositRef.current = false; @@ -90,6 +103,24 @@ export function DepositModal({ isOpen, onClose, currentBalance, onSuccess }: Dep } }, [isOpen]); + // Smart default selection based on wallet balances + useEffect(() => { + if (!isOpen || hasSetDefaultRef.current) return; + + if (!isBaseBalanceLoading && !isEthereumBalanceLoading) { + const baseHasBalance = baseBalance > 0; + const ethereumHasBalance = ethereumBalance > 0; + + if (ethereumHasBalance && !baseHasBalance) { + setSelectedNetwork('ETHEREUM'); + } else { + setSelectedNetwork('BASE'); // Default to Base if both have balance or neither has balance + } + + hasSetDefaultRef.current = true; + } + }, [isOpen, baseBalance, ethereumBalance, isBaseBalanceLoading, isEthereumBalanceLoading]); + useEffect(() => { if (txStatus.state === 'error') { if (contentRef.current) { @@ -368,7 +399,12 @@ export function DepositModal({ isOpen, onClose, currentBalance, onSuccess }: Dep value={selectedNetwork} onChange={setSelectedNetwork} disabled={isInputDisabled()} - showBadges={false} + showBadges={true} + showDescription={false} + customBadges={{ + BASE: 'Sponsored', + ETHEREUM: 'Network Fee', + }} />
diff --git a/components/ui/NetworkSelector.tsx b/components/ui/NetworkSelector.tsx index b8bfa5d14..f45ff2db5 100644 --- a/components/ui/NetworkSelector.tsx +++ b/components/ui/NetworkSelector.tsx @@ -12,6 +12,8 @@ interface NetworkSelectorProps { disabled?: boolean; className?: string; showBadges?: boolean; + customBadges?: Partial>; + showDescription?: boolean; } export function NetworkSelector({ @@ -20,9 +22,18 @@ export function NetworkSelector({ disabled = false, className, showBadges = true, + customBadges, + showDescription = true, }: NetworkSelectorProps) { const selectedNetwork = NETWORK_CONFIG[value]; + const getBadge = (network: NetworkType): string | undefined => { + if (customBadges && customBadges[network]) { + return customBadges[network]; + } + return NETWORK_CONFIG[network].badge; + }; + const trigger = useMemo( () => (
{selectedNetwork.name} - {showBadges && selectedNetwork.badge && ( + {showBadges && getBadge(value) && ( - {selectedNetwork.badge} + {getBadge(value)} )}
- {showBadges && selectedNetwork.description && ( + {showDescription && showBadges && selectedNetwork.description && ( {selectedNetwork.description} )}
@@ -68,7 +79,7 @@ export function NetworkSelector({
), - [selectedNetwork, value, disabled, className, showBadges] + [selectedNetwork, value, disabled, className, showBadges, showDescription, customBadges] ); return ( @@ -107,7 +118,7 @@ export function NetworkSelector({
{config.name} - {showBadges && config.badge && ( + {showBadges && getBadge(network) && ( - {config.badge} + {getBadge(network)} )}
- {showBadges && config.description && ( + {showDescription && showBadges && config.description && ( {config.description} From 56f9f8c7b2beb8f65395c5c3fe13d476d23b7bab Mon Sep 17 00:00:00 2001 From: nicktytarenko Date: Wed, 21 Jan 2026 19:39:48 +0200 Subject: [PATCH 06/10] reverting some files --- app/globals.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/globals.css b/app/globals.css index b5afd3b9f..df0986026 100644 --- a/app/globals.css +++ b/app/globals.css @@ -96,7 +96,7 @@ body { /* Search result highlighting */ mark { - background-color: #fef9c3; + background-color: #FEF9C3; color: inherit; padding: 0.125rem 0; border-radius: 0.125rem; From 76f5edb58c67dc98bdebc5c851bb7fdee3dc0882 Mon Sep 17 00:00:00 2001 From: nicktytarenko Date: Wed, 21 Jan 2026 19:44:42 +0200 Subject: [PATCH 07/10] reverting files --- app/api/og/route.tsx | 216 +++++++++++++++-------------- components/Comment/CommentItem.tsx | 5 +- components/ui/Badge.tsx | 3 +- components/ui/Button.tsx | 3 +- components/ui/form/FileUpload.tsx | 2 +- public/workbox-4754cb34.js | 177 ++++++++++++----------- 6 files changed, 204 insertions(+), 202 deletions(-) diff --git a/app/api/og/route.tsx b/app/api/og/route.tsx index e101c4234..10809fca2 100644 --- a/app/api/og/route.tsx +++ b/app/api/og/route.tsx @@ -11,138 +11,140 @@ export async function GET(request: NextRequest) { const type = searchParams.get('type') || 'article'; return new ImageResponse( -
- {/* Background pattern */} -
- - {/* Logo and brand */} + (
- ResearchHub -
- {/* Content type badge */} - {type && ( + {/* Logo and brand */}
- {type} + ResearchHub
- )} - {/* Title */} - {title && ( -

- {title} -

- )} + {/* Content type badge */} + {type && ( +
+ {type} +
+ )} - {/* Description */} - {description && ( -

- {description} -

- )} + {/* Title */} + {title && ( +

+ {title} +

+ )} - {/* Author */} - {author && ( -
+ {/* Description */} + {description && ( +

+ {description} +

+ )} + + {/* Author */} + {author && (
- {author.charAt(0).toUpperCase()} +
+ {author.charAt(0).toUpperCase()} +
+ By {author}
- By {author} -
- )} -
, + )} +
+ ), { width: 1200, height: 630, diff --git a/components/Comment/CommentItem.tsx b/components/Comment/CommentItem.tsx index d413127b2..64114a678 100644 --- a/components/Comment/CommentItem.tsx +++ b/components/Comment/CommentItem.tsx @@ -381,9 +381,8 @@ export const CommentItem = ({ } .prose pre code { - font-family: - ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', - monospace; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', + 'Courier New', monospace; font-size: 0.875rem; line-height: 1.5; background: none; diff --git a/components/ui/Badge.tsx b/components/ui/Badge.tsx index 2ffeb1b09..d980fafd0 100644 --- a/components/ui/Badge.tsx +++ b/components/ui/Badge.tsx @@ -29,7 +29,8 @@ const badgeVariants = cva( ); export interface BadgeProps - extends React.HTMLAttributes, VariantProps { + extends React.HTMLAttributes, + VariantProps { children: React.ReactNode; } diff --git a/components/ui/Button.tsx b/components/ui/Button.tsx index fc7852a65..39bae66e6 100644 --- a/components/ui/Button.tsx +++ b/components/ui/Button.tsx @@ -37,7 +37,8 @@ const buttonVariants = cva( ); export interface ButtonProps - extends ButtonHTMLAttributes, VariantProps { + extends ButtonHTMLAttributes, + VariantProps { asChild?: boolean; tooltip?: string; } diff --git a/components/ui/form/FileUpload.tsx b/components/ui/form/FileUpload.tsx index 8b4b31327..1e3e83f72 100644 --- a/components/ui/form/FileUpload.tsx +++ b/components/ui/form/FileUpload.tsx @@ -170,7 +170,7 @@ export function FileUpload({ const hasImage = Boolean( (selectedFile && selectedFile.type && selectedFile.type.startsWith('image/')) || - (existingImageUrl && !selectedFile) + (existingImageUrl && !selectedFile) ); return ( diff --git a/public/workbox-4754cb34.js b/public/workbox-4754cb34.js index 4b93c1176..0db7e50dd 100644 --- a/public/workbox-4754cb34.js +++ b/public/workbox-4754cb34.js @@ -5,11 +5,11 @@ define(['exports'], function (t) { } catch (t) {} const e = (t, ...e) => { let s = t; - return (e.length > 0 && (s += ` :: ${JSON.stringify(e)}`), s); + return e.length > 0 && (s += ` :: ${JSON.stringify(e)}`), s; }; class s extends Error { constructor(t, s) { - (super(e(t, s)), (this.name = t), (this.details = s)); + super(e(t, s)), (this.name = t), (this.details = s); } } try { @@ -18,7 +18,7 @@ define(['exports'], function (t) { const n = (t) => (t && 'object' == typeof t ? t : { handle: t }); class r { constructor(t, e, s = 'GET') { - ((this.handler = n(e)), (this.match = t), (this.method = s)); + (this.handler = n(e)), (this.match = t), (this.method = s); } setCatchHandler(t) { this.catchHandler = n(t); @@ -38,7 +38,7 @@ define(['exports'], function (t) { } class a { constructor() { - ((this.t = new Map()), (this.i = new Map())); + (this.t = new Map()), (this.i = new Map()); } get routes() { return this.t; @@ -61,7 +61,7 @@ define(['exports'], function (t) { return this.handleRequest({ request: s, event: t }); }) ); - (t.waitUntil(s), t.ports && t.ports[0] && s.then(() => t.ports[0].postMessage(!0))); + t.waitUntil(s), t.ports && t.ports[0] && s.then(() => t.ports[0].postMessage(!0)); } }); } @@ -125,7 +125,7 @@ define(['exports'], function (t) { this.o = n(t); } registerRoute(t) { - (this.t.has(t.method) || this.t.set(t.method, []), this.t.get(t.method).push(t)); + this.t.has(t.method) || this.t.set(t.method, []), this.t.get(t.method).push(t); } unregisterRoute(t) { if (!this.t.has(t.method)) @@ -153,7 +153,7 @@ define(['exports'], function (t) { }); a = t; } - return (c().registerRoute(a), a); + return c().registerRoute(a), a; } try { self['workbox:strategies:6.5.4'] && _(); @@ -179,7 +179,7 @@ define(['exports'], function (t) { class y { constructor() { this.promise = new Promise((t, e) => { - ((this.resolve = t), (this.reject = e)); + (this.resolve = t), (this.reject = e); }); } } @@ -189,14 +189,14 @@ define(['exports'], function (t) { } class v { constructor(t, e) { - ((this.h = {}), + (this.h = {}), Object.assign(this, e), (this.event = e.event), (this.u = t), (this.l = new y()), (this.p = []), (this.m = [...t.plugins]), - (this.v = new Map())); + (this.v = new Map()); for (const t of this.m) this.v.set(t, {}); this.event.waitUntil(this.l.promise); } @@ -224,21 +224,21 @@ define(['exports'], function (t) { return t; } catch (t) { throw ( - r && + (r && (await this.runCallbacks('fetchDidFail', { error: t, event: e, originalRequest: r.clone(), request: i.clone(), })), - t + t) ); } } async fetchAndCachePut(t) { const e = await this.fetch(t), s = e.clone(); - return (this.waitUntil(this.cachePut(t, s)), e); + return this.waitUntil(this.cachePut(t, s)), e; } async cacheMatch(t) { const e = m(t); @@ -289,11 +289,11 @@ define(['exports'], function (t) { } catch (t) { if (t instanceof Error) throw ( - 'QuotaExceededError' === t.name && + ('QuotaExceededError' === t.name && (await (async function () { for (const t of g) await t(); })()), - t + t) ); } for (const t of this.iterateCallbacks('cacheDidUpdate')) @@ -335,7 +335,7 @@ define(['exports'], function (t) { } } waitUntil(t) { - return (this.p.push(t), t); + return this.p.push(t), t; } async doneWaiting() { let t; @@ -354,15 +354,15 @@ define(['exports'], function (t) { !e) ) break; - return (s || (e && 200 !== e.status && (e = void 0)), e); + return s || (e && 200 !== e.status && (e = void 0)), e; } } class R { constructor(t = {}) { - ((this.cacheName = d(t.cacheName)), + (this.cacheName = d(t.cacheName)), (this.plugins = t.plugins || []), (this.fetchOptions = t.fetchOptions), - (this.matchOptions = t.matchOptions)); + (this.matchOptions = t.matchOptions); } handle(t) { const [e] = this.handleAll(t); @@ -399,8 +399,8 @@ define(['exports'], function (t) { r = await t; } catch (i) {} try { - (await e.runCallbacks('handlerDidRespond', { event: n, request: s, response: r }), - await e.doneWaiting()); + await e.runCallbacks('handlerDidRespond', { event: n, request: s, response: r }), + await e.doneWaiting(); } catch (t) { t instanceof Error && (i = t); } @@ -464,14 +464,14 @@ define(['exports'], function (t) { ]) ).includes(t) ? function (...e) { - return (t.apply(B(this), e), k(x.get(this))); + return t.apply(B(this), e), k(x.get(this)); } : function (...e) { return k(t.apply(B(this), e)); } : function (e, ...s) { const n = t.call(B(this), e, ...s); - return (I.set(n, e.sort ? e.sort() : [e]), k(n)); + return I.set(n, e.sort ? e.sort() : [e]), k(n); }; } function T(t) { @@ -482,19 +482,19 @@ define(['exports'], function (t) { if (L.has(t)) return; const e = new Promise((e, s) => { const n = () => { - (t.removeEventListener('complete', r), + t.removeEventListener('complete', r), t.removeEventListener('error', i), - t.removeEventListener('abort', i)); + t.removeEventListener('abort', i); }, r = () => { - (e(), n()); + e(), n(); }, i = () => { - (s(t.error || new DOMException('AbortError', 'AbortError')), n()); + s(t.error || new DOMException('AbortError', 'AbortError')), n(); }; - (t.addEventListener('complete', r), + t.addEventListener('complete', r), t.addEventListener('error', i), - t.addEventListener('abort', i)); + t.addEventListener('abort', i); }); L.set(t, e); })(t), @@ -511,15 +511,15 @@ define(['exports'], function (t) { return (function (t) { const e = new Promise((e, s) => { const n = () => { - (t.removeEventListener('success', r), t.removeEventListener('error', i)); + t.removeEventListener('success', r), t.removeEventListener('error', i); }, r = () => { - (e(k(t.result)), n()); + e(k(t.result)), n(); }, i = () => { - (s(t.error), n()); + s(t.error), n(); }; - (t.addEventListener('success', r), t.addEventListener('error', i)); + t.addEventListener('success', r), t.addEventListener('error', i); }); return ( e @@ -533,7 +533,7 @@ define(['exports'], function (t) { })(t); if (C.has(t)) return C.get(t); const e = T(t); - return (e !== t && (C.set(t, e), E.set(e, t)), e); + return e !== t && (C.set(t, e), E.set(e, t)), e; } const B = (t) => E.get(t); const P = ['get', 'getKey', 'getAll', 'getAllKeys', 'count'], @@ -549,9 +549,9 @@ define(['exports'], function (t) { const i = async function (t, ...e) { const i = this.transaction(t, r ? 'readwrite' : 'readonly'); let a = i.store; - return (n && (a = a.index(e.shift())), (await Promise.all([a[s](...e), r && i.done]))[0]); + return n && (a = a.index(e.shift())), (await Promise.all([a[s](...e), r && i.done]))[0]; }; - return (W.set(e, i), i); + return W.set(e, i), i; } N = ((t) => q({}, t, { @@ -564,29 +564,29 @@ define(['exports'], function (t) { const S = 'cache-entries', K = (t) => { const e = new URL(t, location.href); - return ((e.hash = ''), e.href); + return (e.hash = ''), e.href; }; class A { constructor(t) { - ((this._ = null), (this.L = t)); + (this._ = null), (this.L = t); } I(t) { const e = t.createObjectStore(S, { keyPath: 'id' }); - (e.createIndex('cacheName', 'cacheName', { unique: !1 }), - e.createIndex('timestamp', 'timestamp', { unique: !1 })); + e.createIndex('cacheName', 'cacheName', { unique: !1 }), + e.createIndex('timestamp', 'timestamp', { unique: !1 }); } C(t) { - (this.I(t), + this.I(t), this.L && (function (t, { blocked: e } = {}) { const s = indexedDB.deleteDatabase(t); - (e && s.addEventListener('blocked', (t) => e(t.oldVersion, t)), k(s).then(() => {})); - })(this.L)); + e && s.addEventListener('blocked', (t) => e(t.oldVersion, t)), k(s).then(() => {}); + })(this.L); } async setTimestamp(t, e) { const s = { url: (t = K(t)), timestamp: e, cacheName: this.L, id: this.N(t) }, n = (await this.getDb()).transaction(S, 'readwrite', { durability: 'relaxed' }); - (await n.store.put(s), await n.done); + await n.store.put(s), await n.done; } async getTimestamp(t) { const e = await this.getDb(), @@ -600,12 +600,11 @@ define(['exports'], function (t) { let i = 0; for (; n; ) { const s = n.value; - (s.cacheName === this.L && - ((t && s.timestamp < t) || (e && i >= e) ? r.push(n.value) : i++), - (n = await n.continue())); + s.cacheName === this.L && ((t && s.timestamp < t) || (e && i >= e) ? r.push(n.value) : i++), + (n = await n.continue()); } const a = []; - for (const t of r) (await s.delete(S, t.id), a.push(t.url)); + for (const t of r) await s.delete(S, t.id), a.push(t.url); return a; } N(t) { @@ -629,9 +628,9 @@ define(['exports'], function (t) { s && a.addEventListener('blocked', (t) => s(t.oldVersion, t.newVersion, t)), o .then((t) => { - (i && t.addEventListener('close', () => i()), + i && t.addEventListener('close', () => i()), r && - t.addEventListener('versionchange', (t) => r(t.oldVersion, t.newVersion, t))); + t.addEventListener('versionchange', (t) => r(t.oldVersion, t.newVersion, t)); }) .catch(() => {}), o @@ -643,13 +642,13 @@ define(['exports'], function (t) { } class F { constructor(t, e = {}) { - ((this.O = !1), + (this.O = !1), (this.T = !1), (this.k = e.maxEntries), (this.B = e.maxAgeSeconds), (this.P = e.matchOptions), (this.L = t), - (this.M = new A(t))); + (this.M = new A(t)); } async expireEntries() { if (this.O) return void (this.T = !0); @@ -658,7 +657,7 @@ define(['exports'], function (t) { e = await this.M.expireEntries(t, this.k), s = await self.caches.open(this.L); for (const t of e) await s.delete(t, this.P); - ((this.O = !1), this.T && ((this.T = !1), b(this.expireEntries()))); + (this.O = !1), this.T && ((this.T = !1), b(this.expireEntries())); } async updateTimestamp(t) { await this.M.setTimestamp(t, Date.now()); @@ -672,7 +671,7 @@ define(['exports'], function (t) { return !1; } async delete() { - ((this.T = !1), await this.M.expireEntries(1 / 0)); + (this.T = !1), await this.M.expireEntries(1 / 0); } } try { @@ -725,7 +724,7 @@ define(['exports'], function (t) { } function $(t, e) { const s = e(); - return (t.waitUntil(s), s); + return t.waitUntil(s), s; } try { self['workbox:precaching:6.5.4'] && _(); @@ -744,11 +743,11 @@ define(['exports'], function (t) { } const r = new URL(n, location.href), i = new URL(n, location.href); - return (r.searchParams.set('__WB_REVISION__', e), { cacheKey: r.href, url: i.href }); + return r.searchParams.set('__WB_REVISION__', e), { cacheKey: r.href, url: i.href }; } class G { constructor() { - ((this.updatedURLs = []), + (this.updatedURLs = []), (this.notUpdatedURLs = []), (this.handlerWillStart = async ({ request: t, state: e }) => { e && (e.originalRequest = t); @@ -764,16 +763,16 @@ define(['exports'], function (t) { s ? this.notUpdatedURLs.push(t) : this.updatedURLs.push(t); } return s; - })); + }); } } class V { constructor({ precacheController: t }) { - ((this.cacheKeyWillBeUsed = async ({ request: t, params: e }) => { + (this.cacheKeyWillBeUsed = async ({ request: t, params: e }) => { const s = (null == e ? void 0 : e.cacheKey) || this.W.getCacheKeyForURL(t.url); return s ? new Request(s, { headers: t.headers }) : t; }), - (this.W = t)); + (this.W = t); } } let J, Q; @@ -791,7 +790,7 @@ define(['exports'], function (t) { const t = new Response(''); if ('body' in t) try { - (new Response(t.body), (J = !0)); + new Response(t.body), (J = !0); } catch (t) { J = !1; } @@ -805,10 +804,10 @@ define(['exports'], function (t) { } class Y extends R { constructor(t = {}) { - ((t.cacheName = w(t.cacheName)), + (t.cacheName = w(t.cacheName)), super(t), (this.j = !1 !== t.fallbackToNetwork), - this.plugins.push(Y.copyRedirectedCacheableResponsesPlugin)); + this.plugins.push(Y.copyRedirectedCacheableResponsesPlugin); } async U(t, e) { const s = await e.cacheMatch(t); @@ -822,8 +821,8 @@ define(['exports'], function (t) { const s = r.integrity, i = t.integrity, a = !i || i === s; - ((n = await e.fetch(new Request(t, { integrity: 'no-cors' !== t.mode ? i || s : void 0 }))), - s && a && 'no-cors' !== t.mode && (this.A(), await e.cachePut(t, n.clone()))); + (n = await e.fetch(new Request(t, { integrity: 'no-cors' !== t.mode ? i || s : void 0 }))), + s && a && 'no-cors' !== t.mode && (this.A(), await e.cachePut(t, n.clone())); } return n; } @@ -845,15 +844,15 @@ define(['exports'], function (t) { : e > 1 && null !== t && this.plugins.splice(t, 1); } } - ((Y.defaultPrecacheCacheabilityPlugin = { + (Y.defaultPrecacheCacheabilityPlugin = { cacheWillUpdate: async ({ response: t }) => (!t || t.status >= 400 ? null : t), }), (Y.copyRedirectedCacheableResponsesPlugin = { cacheWillUpdate: async ({ response: t }) => (t.redirected ? await X(t) : t), - })); + }); class Z { constructor({ cacheName: t, plugins: e = [], fallbackToNetwork: s = !0 } = {}) { - ((this.F = new Map()), + (this.F = new Map()), (this.H = new Map()), (this.$ = new Map()), (this.u = new Y({ @@ -862,17 +861,17 @@ define(['exports'], function (t) { fallbackToNetwork: s, })), (this.install = this.install.bind(this)), - (this.activate = this.activate.bind(this))); + (this.activate = this.activate.bind(this)); } get strategy() { return this.u; } precache(t) { - (this.addToCacheList(t), + this.addToCacheList(t), this.G || (self.addEventListener('install', this.install), self.addEventListener('activate', this.activate), - (this.G = !0))); + (this.G = !0)); } addToCacheList(t) { const e = []; @@ -967,7 +966,7 @@ define(['exports'], function (t) { } = {} ) { const i = new URL(t, location.href); - ((i.hash = ''), yield i.href); + (i.hash = ''), yield i.href; const a = (function (t, e = []) { for (const s of [...t.searchParams.keys()]) e.some((t) => t.test(s)) && t.searchParams.delete(s); @@ -975,11 +974,11 @@ define(['exports'], function (t) { })(i, e); if ((yield a.href, s && a.pathname.endsWith('/'))) { const t = new URL(a.href); - ((t.pathname += s), yield t.href); + (t.pathname += s), yield t.href; } if (n) { const t = new URL(a.href); - ((t.pathname += '.html'), yield t.href); + (t.pathname += '.html'), yield t.href; } if (r) { const t = r({ url: i }); @@ -994,7 +993,7 @@ define(['exports'], function (t) { }, t.strategy); } } - ((t.CacheFirst = class extends R { + (t.CacheFirst = class extends R { async U(t, e) { let n, r = await e.cacheMatch(t); @@ -1010,7 +1009,7 @@ define(['exports'], function (t) { }), (t.ExpirationPlugin = class { constructor(t = {}) { - ((this.cachedResponseWillBeUsed = async ({ + (this.cachedResponseWillBeUsed = async ({ event: t, request: e, cacheName: s, @@ -1029,7 +1028,7 @@ define(['exports'], function (t) { }), (this.cacheDidUpdate = async ({ cacheName: t, request: e }) => { const s = this.J(t); - (await s.updateTimestamp(e.url), await s.expireEntries()); + await s.updateTimestamp(e.url), await s.expireEntries(); }), (this.X = t), (this.B = t.maxAgeSeconds), @@ -1037,12 +1036,12 @@ define(['exports'], function (t) { t.purgeOnQuotaError && (function (t) { g.add(t); - })(() => this.deleteCacheAndMetadata())); + })(() => this.deleteCacheAndMetadata()); } J(t) { if (t === d()) throw new s('expire-custom-caches-only'); let e = this.Y.get(t); - return (e || ((e = new F(t, this.X)), this.Y.set(t, e)), e); + return e || ((e = new F(t, this.X)), this.Y.set(t, e)), e; } V(t) { if (!this.B) return !0; @@ -1057,15 +1056,15 @@ define(['exports'], function (t) { return isNaN(s) ? null : s; } async deleteCacheAndMetadata() { - for (const [t, e] of this.Y) (await self.caches.delete(t), await e.delete()); + for (const [t, e] of this.Y) await self.caches.delete(t), await e.delete(); this.Y = new Map(); } }), (t.NetworkFirst = class extends R { constructor(t = {}) { - (super(t), + super(t), this.plugins.some((t) => 'cacheWillUpdate' in t) || this.plugins.unshift(u), - (this.tt = t.networkTimeoutSeconds || 0)); + (this.tt = t.networkTimeoutSeconds || 0); } async U(t, e) { const n = [], @@ -1073,7 +1072,7 @@ define(['exports'], function (t) { let i; if (this.tt) { const { id: s, promise: a } = this.et({ request: t, logs: n, handler: e }); - ((i = s), r.push(a)); + (i = s), r.push(a); } const a = this.st({ timeoutId: i, request: t, logs: n, handler: e }); r.push(a); @@ -1101,7 +1100,7 @@ define(['exports'], function (t) { } catch (t) { t instanceof Error && (r = t); } - return (t && clearTimeout(t), (!r && i) || (i = await n.cacheMatch(e)), i); + return t && clearTimeout(t), (!r && i) || (i = await n.cacheMatch(e)), i; } }), (t.RangeRequestsPlugin = class { @@ -1112,7 +1111,7 @@ define(['exports'], function (t) { }), (t.StaleWhileRevalidate = class extends R { constructor(t = {}) { - (super(t), this.plugins.some((t) => 'cacheWillUpdate' in t) || this.plugins.unshift(u)); + super(t), this.plugins.some((t) => 'cacheWillUpdate' in t) || this.plugins.unshift(u); } async U(t, e) { const n = e.fetchAndCachePut(t).catch(() => {}); @@ -1138,7 +1137,7 @@ define(['exports'], function (t) { const s = (await self.caches.keys()).filter( (s) => s.includes(e) && s.includes(self.registration.scope) && s !== t ); - return (await Promise.all(s.map((t) => self.caches.delete(t))), s); + return await Promise.all(s.map((t) => self.caches.delete(t))), s; })(e).then((t) => {}) ); }); @@ -1147,13 +1146,13 @@ define(['exports'], function (t) { self.addEventListener('activate', () => self.clients.claim()); }), (t.precacheAndRoute = function (t, e) { - (!(function (t) { + !(function (t) { tt().precache(t); })(t), (function (t) { const e = tt(); h(new et(e, t)); - })(e)); + })(e); }), - (t.registerRoute = h)); + (t.registerRoute = h); }); From a9e54c8cd81daef747e19e2eb11f620455f887ae Mon Sep 17 00:00:00 2001 From: nicktytarenko Date: Wed, 21 Jan 2026 20:41:24 +0200 Subject: [PATCH 08/10] small refactoring and cleanup --- components/Nonprofit/NonprofitInfoPopover.tsx | 3 ++- .../modals/ResearchCoin/DepositModal.tsx | 23 ++++------------ .../ResearchCoin/DepositSuccessView.tsx | 3 ++- .../modals/ResearchCoin/WithdrawModal.tsx | 16 ++++++------ .../ResearchCoin/WithdrawalSuccessView.tsx | 3 ++- components/modals/WalletModal.tsx | 9 +++---- components/ui/NetworkSelector.tsx | 26 +++---------------- constants/tokens.ts | 15 +++++------ utils/stringUtils.ts | 17 ++++++++++++ 9 files changed, 49 insertions(+), 66 deletions(-) diff --git a/components/Nonprofit/NonprofitInfoPopover.tsx b/components/Nonprofit/NonprofitInfoPopover.tsx index a564f4ae1..6dc02593c 100644 --- a/components/Nonprofit/NonprofitInfoPopover.tsx +++ b/components/Nonprofit/NonprofitInfoPopover.tsx @@ -5,6 +5,7 @@ import { ExternalLink, X } from 'lucide-react'; import { useEffect, useState, useRef } from 'react'; import { Button } from '@/components/ui/Button'; import { CHAIN_IDS } from '@/constants/chains'; +import { maskAddress } from '@/utils/stringUtils'; interface NonprofitInfoPopoverProps { nonprofit: NonprofitOrg; @@ -100,7 +101,7 @@ export function NonprofitInfoPopover({ nonprofit, position, onClose }: Nonprofit className="text-primary-600 hover:underline flex items-center gap-1 break-all" onClick={(e) => e.stopPropagation()} > - {baseDeployment.address.slice(0, 8)}...{baseDeployment.address.slice(-6)} + {maskAddress(baseDeployment.address, 8, 6)} diff --git a/components/modals/ResearchCoin/DepositModal.tsx b/components/modals/ResearchCoin/DepositModal.tsx index 5515e040d..d56494314 100644 --- a/components/modals/ResearchCoin/DepositModal.tsx +++ b/components/modals/ResearchCoin/DepositModal.tsx @@ -17,6 +17,7 @@ import { NetworkSelector } from '@/components/ui/NetworkSelector'; import { Alert } from '@/components/ui/Alert'; import { Button } from '@/components/ui/Button'; import { DepositSuccessView } from './DepositSuccessView'; +import toast from 'react-hot-toast'; const HOT_WALLET_ADDRESS_ENV = process.env.NEXT_PUBLIC_WEB3_WALLET_ADDRESS; if (!HOT_WALLET_ADDRESS_ENV || HOT_WALLET_ADDRESS_ENV.trim() === '') { @@ -53,7 +54,6 @@ export function DepositModal({ isOpen, onClose, currentBalance, onSuccess }: Dep const contentRef = useRef(null); const { address } = useAccount(); - // Fetch balances for both networks to determine smart default const { balance: baseBalance, isLoading: isBaseBalanceLoading } = useWalletRSCBalance({ network: 'BASE', }); @@ -61,7 +61,6 @@ export function DepositModal({ isOpen, onClose, currentBalance, onSuccess }: Dep network: 'ETHEREUM', }); - // Use selected network's balance for display and validation const walletBalance = selectedNetwork === 'BASE' ? baseBalance : ethereumBalance; const isWalletBalanceLoading = selectedNetwork === 'BASE' ? isBaseBalanceLoading : isEthereumBalanceLoading; @@ -81,7 +80,6 @@ export function DepositModal({ isOpen, onClose, currentBalance, onSuccess }: Dep if (isOpen) { setTxStatus({ state: 'idle' }); setAmount(''); - // Reset default flag to allow smart default to run hasSetDefaultRef.current = false; isDepositButtonDisabled(false); hasCalledSuccessRef.current = false; @@ -114,26 +112,13 @@ export function DepositModal({ isOpen, onClose, currentBalance, onSuccess }: Dep if (ethereumHasBalance && !baseHasBalance) { setSelectedNetwork('ETHEREUM'); } else { - setSelectedNetwork('BASE'); // Default to Base if both have balance or neither has balance + setSelectedNetwork('BASE'); } hasSetDefaultRef.current = true; } }, [isOpen, baseBalance, ethereumBalance, isBaseBalanceLoading, isEthereumBalanceLoading]); - useEffect(() => { - if (txStatus.state === 'error') { - if (contentRef.current) { - const scrollableParent = contentRef.current.closest('[class*="overflow-y-auto"]'); - if (scrollableParent) { - scrollableParent.scrollTo({ top: 0, behavior: 'smooth' }); - } else { - contentRef.current.scrollIntoView({ behavior: 'smooth', block: 'start' }); - } - } - } - }, [txStatus]); - const handleClose = useCallback(() => { setTxStatus({ state: 'idle' }); setAmount(''); @@ -241,10 +226,12 @@ export function DepositModal({ isOpen, onClose, currentBalance, onSuccess }: Dep if (status.statusName === 'error') { console.error('Transaction error full status:', JSON.stringify(status, null, 2)); + const errorMessage = status.statusData?.message || 'Transaction failed'; setTxStatus({ state: 'error', - message: status.statusData?.message || 'Transaction failed', + message: errorMessage, }); + toast.error(errorMessage); isDepositButtonDisabled(false); } }, diff --git a/components/modals/ResearchCoin/DepositSuccessView.tsx b/components/modals/ResearchCoin/DepositSuccessView.tsx index 7ca76c900..7d96ef23c 100644 --- a/components/modals/ResearchCoin/DepositSuccessView.tsx +++ b/components/modals/ResearchCoin/DepositSuccessView.tsx @@ -5,6 +5,7 @@ import { formatRSC } from '@/utils/number'; import { ResearchCoinIcon } from '@/components/ui/icons/ResearchCoinIcon'; import { NetworkConfig } from '@/constants/tokens'; import toast from 'react-hot-toast'; +import { maskAddress } from '@/utils/stringUtils'; interface DepositSuccessViewProps { depositAmount: number; @@ -34,7 +35,7 @@ export function DepositSuccessView({ ); }, [address]); - const maskedAddress = address ? `${address.slice(0, 6)}...${address.slice(-4)}` : ''; + const maskedAddress = maskAddress(address); return (
diff --git a/components/modals/ResearchCoin/WithdrawModal.tsx b/components/modals/ResearchCoin/WithdrawModal.tsx index 30cc6e076..c8d82ba70 100644 --- a/components/modals/ResearchCoin/WithdrawModal.tsx +++ b/components/modals/ResearchCoin/WithdrawModal.tsx @@ -78,17 +78,17 @@ export function WithdrawModal({ useEffect(() => { if (txStatus.state === 'error') { - if (contentRef.current) { - const scrollableParent = contentRef.current.closest('[class*="overflow-y-auto"]'); - if (scrollableParent) { - scrollableParent.scrollTo({ top: 0, behavior: 'smooth' }); - } else { - contentRef.current.scrollIntoView({ behavior: 'smooth', block: 'start' }); - } - } + const errorMessage = 'message' in txStatus ? txStatus.message : 'Transaction failed'; + toast.error(errorMessage); } }, [txStatus]); + useEffect(() => { + if (feeError) { + toast.error(`Unable to fetch fee: ${feeError}`); + } + }, [feeError]); + const handleAmountChange = useCallback((e: React.ChangeEvent) => { const value = e.target.value; if (value === '' || /^\d+$/.test(value)) { diff --git a/components/modals/ResearchCoin/WithdrawalSuccessView.tsx b/components/modals/ResearchCoin/WithdrawalSuccessView.tsx index eca026f6d..14cc530cb 100644 --- a/components/modals/ResearchCoin/WithdrawalSuccessView.tsx +++ b/components/modals/ResearchCoin/WithdrawalSuccessView.tsx @@ -5,6 +5,7 @@ import { formatRSC } from '@/utils/number'; import { ResearchCoinIcon } from '@/components/ui/icons/ResearchCoinIcon'; import { NetworkConfig } from '@/constants/tokens'; import toast from 'react-hot-toast'; +import { maskAddress } from '@/utils/stringUtils'; interface WithdrawalSuccessViewProps { withdrawAmount: number; @@ -38,7 +39,7 @@ export function WithdrawalSuccessView({ ); }, [address]); - const maskedAddress = address ? `${address.slice(0, 6)}...${address.slice(-4)}` : ''; + const maskedAddress = maskAddress(address); return (
diff --git a/components/modals/WalletModal.tsx b/components/modals/WalletModal.tsx index 69ce58c10..1a8c517be 100644 --- a/components/modals/WalletModal.tsx +++ b/components/modals/WalletModal.tsx @@ -7,6 +7,7 @@ import { X as XIcon } from 'lucide-react'; import Image from 'next/image'; import { coinbaseWallet, metaMask } from 'wagmi/connectors'; import toast from 'react-hot-toast'; +import { maskAddress } from '@/utils/stringUtils'; interface WalletModalProps { isOpen: boolean; @@ -14,10 +15,6 @@ interface WalletModalProps { onError?: (error: Error) => void; } -function truncateWalletAddress(address: string): string { - return `${address.slice(0, 4)}...${address.slice(-4)}`; -} - export function WalletModal({ isOpen, onClose, onError }: WalletModalProps) { const { connectAsync, error } = useConnect(); @@ -29,7 +26,7 @@ export function WalletModal({ isOpen, onClose, onError }: WalletModalProps) { }); const data = await connectAsync({ connector: cbConnector }); if (data?.accounts && data.accounts.length > 0) { - toast.success(`Wallet Connected (${truncateWalletAddress(data.accounts[0])})`); + toast.success(`Wallet Connected (${maskAddress(data.accounts[0], 4, 4)})`); onClose(); } else { throw new Error('No accounts returned'); @@ -51,7 +48,7 @@ export function WalletModal({ isOpen, onClose, onError }: WalletModalProps) { }); const data = await connectAsync({ connector: mmConnector }); if (data?.accounts && data.accounts.length > 0) { - toast.success(`Wallet Connected (${truncateWalletAddress(data.accounts[0])})`); + toast.success(`Wallet Connected (${maskAddress(data.accounts[0], 4, 4)})`); onClose(); } else { throw new Error('No accounts returned'); diff --git a/components/ui/NetworkSelector.tsx b/components/ui/NetworkSelector.tsx index f45ff2db5..758f9afdf 100644 --- a/components/ui/NetworkSelector.tsx +++ b/components/ui/NetworkSelector.tsx @@ -2,6 +2,7 @@ import { useMemo } from 'react'; import Image from 'next/image'; +import { Check, ChevronDown } from 'lucide-react'; import { BaseMenu, BaseMenuItem } from './form/BaseMenu'; import { NETWORK_CONFIG, NetworkType } from '@/constants/tokens'; import { cn } from '@/utils/styles'; @@ -69,14 +70,7 @@ export function NetworkSelector({ {selectedNetwork.description} )}
- - - +
), [selectedNetwork, value, disabled, className, showBadges, showDescription, customBadges] @@ -137,21 +131,7 @@ export function NetworkSelector({ )}
- {isSelected && ( - - - - )} + {isSelected && } ); })} diff --git a/constants/tokens.ts b/constants/tokens.ts index 3bde7b618..3d009b382 100644 --- a/constants/tokens.ts +++ b/constants/tokens.ts @@ -1,9 +1,8 @@ import { Token } from '@coinbase/onchainkit/token'; const IS_PRODUCTION = process.env.NEXT_PUBLIC_VERCEL_ENV === 'production'; -const CHAIN_ID = IS_PRODUCTION - ? 8453 // Base mainnet - : 84532; // Base Sepolia testnet +export const BASE_CHAIN_ID = IS_PRODUCTION ? 8453 : 84532; // Base mainnet : Base Sepolia testnet +export const ETHEREUM_CHAIN_ID = IS_PRODUCTION ? 1 : 11155111; // Ethereum mainnet : Sepolia testnet export type NetworkType = 'BASE' | 'ETHEREUM'; @@ -19,7 +18,7 @@ export interface NetworkConfig { export const NETWORK_CONFIG: Record = { BASE: { - chainId: IS_PRODUCTION ? 8453 : 84532, + chainId: BASE_CHAIN_ID, name: IS_PRODUCTION ? 'Base' : 'Base Sepolia', rscAddress: IS_PRODUCTION ? '0xFbB75A59193A3525a8825BeBe7D4b56899E2f7e1' @@ -30,7 +29,7 @@ export const NETWORK_CONFIG: Record = { icon: '/base-logo.svg', }, ETHEREUM: { - chainId: IS_PRODUCTION ? 1 : 11155111, + chainId: ETHEREUM_CHAIN_ID, name: IS_PRODUCTION ? 'Ethereum' : 'Sepolia', rscAddress: IS_PRODUCTION ? '0xd101dcc414f310268c37eeb4cd376ccfa507f571' @@ -50,7 +49,7 @@ export const RSC: Token = { ? '0xFbB75A59193A3525a8825BeBe7D4b56899E2f7e1' : '0xdAf43508D785939D6C2d97c2df73d65c9359dBEa', image: 'RSC.webp', - chainId: CHAIN_ID, + chainId: BASE_CHAIN_ID, }; /** @@ -74,7 +73,7 @@ export const ETH: Token = { symbol: 'ETH', decimals: 18, image: 'ETH.webp', - chainId: CHAIN_ID, + chainId: BASE_CHAIN_ID, }; export const USDC: Token = { @@ -85,7 +84,7 @@ export const USDC: Token = { symbol: 'USDC', decimals: 6, image: 'USDC.webp', - chainId: CHAIN_ID, + chainId: BASE_CHAIN_ID, }; export const TRANSFER_ABI = [ diff --git a/utils/stringUtils.ts b/utils/stringUtils.ts index 2cf8085be..89b96bd51 100644 --- a/utils/stringUtils.ts +++ b/utils/stringUtils.ts @@ -73,3 +73,20 @@ export const countWords = (text: string): number => { export const isValidEthereumAddress = (address: string): boolean => { return /^0x[a-fA-F0-9]{40}$/.test(address); }; + +/** + * Masks an Ethereum address by showing only the first few and last few characters + * @param address The address to mask (e.g., "0x1234567890abcdef1234567890abcdef12345678") + * @param prefixLength The number of characters to show at the start (default: 6) + * @param suffixLength The number of characters to show at the end (default: 4) + * @returns The masked address (e.g., "0x1234...5678") or empty string if address is invalid + */ +export const maskAddress = ( + address: string | null | undefined, + prefixLength: number = 6, + suffixLength: number = 4 +): string => { + if (!address) return ''; + if (address.length <= prefixLength + suffixLength) return address; + return `${address.slice(0, prefixLength)}...${address.slice(-suffixLength)}`; +}; From 4655bae603ece0c6b97f712b2f8e21d3091de092 Mon Sep 17 00:00:00 2001 From: nicktytarenko Date: Wed, 21 Jan 2026 21:07:57 +0200 Subject: [PATCH 09/10] address comments to refactor the duplicated code --- .../modals/ResearchCoin/DepositModal.tsx | 164 ++++++------------ .../ResearchCoin/DepositSuccessView.tsx | 102 ++--------- .../modals/ResearchCoin/WithdrawModal.tsx | 146 ++++------------ .../ResearchCoin/WithdrawalSuccessView.tsx | 111 ++---------- .../ResearchCoin/shared/BalanceDisplay.tsx | 61 +++++++ .../shared/NetworkSelectorSection.tsx | 50 ++++++ .../ResearchCoin/shared/TransactionFooter.tsx | 29 ++++ .../shared/TransactionSuccessView.tsx | 137 +++++++++++++++ hooks/useCopyAddress.ts | 27 +++ 9 files changed, 423 insertions(+), 404 deletions(-) create mode 100644 components/modals/ResearchCoin/shared/BalanceDisplay.tsx create mode 100644 components/modals/ResearchCoin/shared/NetworkSelectorSection.tsx create mode 100644 components/modals/ResearchCoin/shared/TransactionFooter.tsx create mode 100644 components/modals/ResearchCoin/shared/TransactionSuccessView.tsx create mode 100644 hooks/useCopyAddress.ts diff --git a/components/modals/ResearchCoin/DepositModal.tsx b/components/modals/ResearchCoin/DepositModal.tsx index d56494314..8cf3fcb42 100644 --- a/components/modals/ResearchCoin/DepositModal.tsx +++ b/components/modals/ResearchCoin/DepositModal.tsx @@ -1,10 +1,8 @@ 'use client'; import { useCallback, useMemo, useState, useEffect, useRef } from 'react'; -import { ExternalLink, Loader2 } from 'lucide-react'; -import Image from 'next/image'; +import { Loader2 } from 'lucide-react'; import { BaseModal } from '@/components/ui/BaseModal'; -import { formatRSC } from '@/utils/number'; import { ResearchCoinIcon } from '@/components/ui/icons/ResearchCoinIcon'; import { useAccount } from 'wagmi'; import { useWalletRSCBalance } from '@/hooks/useWalletRSCBalance'; @@ -13,10 +11,11 @@ import { Transaction, TransactionButton } from '@coinbase/onchainkit/transaction import { Interface } from 'ethers'; import { TransactionService } from '@/services/transaction.service'; import { getRSCForNetwork, NetworkType, TRANSFER_ABI, NETWORK_CONFIG } from '@/constants/tokens'; -import { NetworkSelector } from '@/components/ui/NetworkSelector'; import { Alert } from '@/components/ui/Alert'; -import { Button } from '@/components/ui/Button'; import { DepositSuccessView } from './DepositSuccessView'; +import { NetworkSelectorSection } from './shared/NetworkSelectorSection'; +import { BalanceDisplay } from './shared/BalanceDisplay'; +import { TransactionFooter } from './shared/TransactionFooter'; import toast from 'react-hot-toast'; const HOT_WALLET_ADDRESS_ENV = process.env.NEXT_PUBLIC_WEB3_WALLET_ADDRESS; @@ -269,51 +268,39 @@ export function DepositModal({ isOpen, onClose, currentBalance, onSuccess }: Dep } const footer = useMemo(() => { - if (txStatus.state === 'success') { - const txHash = 'txHash' in txStatus ? txStatus.txHash : undefined; - if (txHash) { - const normalizedTxHash = txHash.startsWith('0x') ? txHash : `0x${txHash}`; - return ( - - - - ); - } + const txHash = txStatus.state === 'success' ? txStatus.txHash : undefined; + + if (txHash) { + return ; } const isSponsored = selectedNetwork === 'BASE'; return ( - -
- - - {txStatus.state === 'pending' ? 'Processing...' : 'Building transaction...'} - - ), - }} - /> -
-
+ + +
+ + + {txStatus.state === 'pending' ? 'Processing...' : 'Building transaction...'} + + ), + }} + /> +
+
+
); }, [ rscToken.chainId, @@ -321,7 +308,7 @@ export function DepositModal({ isOpen, onClose, currentBalance, onSuccess }: Dep handleOnStatus, setButtonDisabledOnClick, isButtonDisabled, - txStatus.state, + txStatus, blockExplorerUrl, selectedNetwork, ]); @@ -363,37 +350,16 @@ export function DepositModal({ isOpen, onClose, currentBalance, onSuccess }: Dep )} {/* Network Selector */} -
-
- Network -
- {(Object.keys(NETWORK_CONFIG) as NetworkType[]).map((network) => { - const config = NETWORK_CONFIG[network]; - return ( - {`${config.name} - ); - })} -
-
- -
+ {/* Wallet RSC Balance */}
@@ -451,38 +417,18 @@ export function DepositModal({ isOpen, onClose, currentBalance, onSuccess }: Dep
{/* Balance Display */} -
-
- Current Balance: -
-
- - - {formatRSC({ amount: currentBalance })} - - RSC -
-
-
- -
-
- After Deposit: -
-
- - 0 && depositAmount <= walletBalance ? 'text-green-600' : 'text-gray-900'}`} - > - {depositAmount > 0 && depositAmount <= walletBalance - ? formatRSC({ amount: calculateNewBalance() }) - : formatRSC({ amount: currentBalance })} - - RSC -
-
-
-
+ 0 && depositAmount <= walletBalance + ? calculateNewBalance() + : currentBalance + } + futureBalanceLabel="After Deposit" + futureBalanceColor={ + depositAmount > 0 && depositAmount <= walletBalance ? 'green' : 'gray' + } + /> )}
diff --git a/components/modals/ResearchCoin/DepositSuccessView.tsx b/components/modals/ResearchCoin/DepositSuccessView.tsx index 7d96ef23c..1309e9db8 100644 --- a/components/modals/ResearchCoin/DepositSuccessView.tsx +++ b/components/modals/ResearchCoin/DepositSuccessView.tsx @@ -1,11 +1,5 @@ -import { useState, useCallback } from 'react'; -import Image from 'next/image'; -import { CheckCircle2, Check, Copy, Clock } from 'lucide-react'; -import { formatRSC } from '@/utils/number'; -import { ResearchCoinIcon } from '@/components/ui/icons/ResearchCoinIcon'; import { NetworkConfig } from '@/constants/tokens'; -import toast from 'react-hot-toast'; -import { maskAddress } from '@/utils/stringUtils'; +import { TransactionSuccessView } from './shared/TransactionSuccessView'; interface DepositSuccessViewProps { depositAmount: number; @@ -18,88 +12,18 @@ export function DepositSuccessView({ networkConfig, address, }: DepositSuccessViewProps) { - const [isCopied, setIsCopied] = useState(false); - - const handleCopyAddress = useCallback(() => { - if (!address) return; - navigator.clipboard.writeText(address).then( - () => { - setIsCopied(true); - toast.success('Address copied to clipboard!'); - setTimeout(() => setIsCopied(false), 2000); - }, - (err) => { - console.error('Failed to copy address: ', err); - toast.error('Failed to copy address.'); - } - ); - }, [address]); - - const maskedAddress = maskAddress(address); - return ( -
-
-
- -
-

Deposit Successful!

-

Your RSC is being processed

-
- -
-
- Amount Deposited -
- - - {formatRSC({ amount: depositAmount })} - - RSC -
-
-
- -
-
- Network -
- {networkConfig.name} - {networkConfig.name} -
-
- -
- From Address -
- {maskedAddress} - -
-
-
- -
-
- -
-

Processing Time

-

- It can take up to 10-20 minutes for the deposit to appear in your ResearchHub balance. -

-
-
-
-
+ ); } diff --git a/components/modals/ResearchCoin/WithdrawModal.tsx b/components/modals/ResearchCoin/WithdrawModal.tsx index c8d82ba70..44720c222 100644 --- a/components/modals/ResearchCoin/WithdrawModal.tsx +++ b/components/modals/ResearchCoin/WithdrawModal.tsx @@ -1,16 +1,17 @@ 'use client'; import { useCallback, useMemo, useState, useEffect, useRef } from 'react'; -import { Check, AlertCircle, ExternalLink, Loader2, Copy } from 'lucide-react'; -import Image from 'next/image'; +import { Check, AlertCircle, Loader2, Copy } from 'lucide-react'; import { BaseModal } from '@/components/ui/BaseModal'; import { formatRSC } from '@/utils/number'; import { ResearchCoinIcon } from '@/components/ui/icons/ResearchCoinIcon'; import { useAccount } from 'wagmi'; import { useWithdrawRSC } from '@/hooks/useWithdrawRSC'; import { cn } from '@/utils/styles'; -import { NetworkSelector } from '@/components/ui/NetworkSelector'; import { NETWORK_CONFIG, NetworkType } from '@/constants/tokens'; +import { NetworkSelectorSection } from './shared/NetworkSelectorSection'; +import { BalanceDisplay } from './shared/BalanceDisplay'; +import { TransactionFooter } from './shared/TransactionFooter'; import { Skeleton } from '@/components/ui/Skeleton'; import { Input } from '@/components/ui/form/Input'; import { Checkbox } from '@/components/ui/form/Checkbox'; @@ -19,6 +20,7 @@ import { Alert } from '@/components/ui/Alert'; import toast from 'react-hot-toast'; import { WithdrawalSuccessView } from './WithdrawalSuccessView'; import { isValidEthereumAddress } from '@/utils/stringUtils'; +import { useCopyAddress } from '@/hooks/useCopyAddress'; // Minimum withdrawal amount in RSC const MIN_WITHDRAWAL_AMOUNT = 150; @@ -38,7 +40,6 @@ export function WithdrawModal({ }: WithdrawModalProps) { const [amount, setAmount] = useState(''); const [selectedNetwork, setSelectedNetwork] = useState('BASE'); - const [isCopied, setIsCopied] = useState(false); const [addressMode, setAddressMode] = useState<'connected' | 'custom'>('connected'); const [customAddress, setCustomAddress] = useState(''); const contentRef = useRef(null); @@ -57,7 +58,6 @@ export function WithdrawModal({ const timeoutId = setTimeout(() => { setAmount(''); setSelectedNetwork('BASE'); - setIsCopied(false); setAddressMode('connected'); setCustomAddress(''); resetTransaction(); @@ -186,20 +186,11 @@ export function WithdrawModal({ selectedNetwork, ]); + const { isCopied: isAddressCopied, copyAddress } = useCopyAddress(); + const handleCopyAddress = useCallback(() => { - if (!withdrawalAddress) return; - navigator.clipboard.writeText(withdrawalAddress).then( - () => { - setIsCopied(true); - toast.success('Address copied to clipboard!'); - setTimeout(() => setIsCopied(false), 2000); - }, - (err) => { - console.error('Failed to copy address: ', err); - toast.error('Failed to copy address.'); - } - ); - }, [withdrawalAddress]); + copyAddress(withdrawalAddress); + }, [withdrawalAddress, copyAddress]); const handleCustomAddressChange = useCallback((e: React.ChangeEvent) => { setCustomAddress(e.target.value); @@ -210,37 +201,25 @@ export function WithdrawModal({ } const footer = useMemo(() => { - if (txStatus.state === 'success') { - const txHash = 'txHash' in txStatus ? txStatus.txHash : undefined; - if (txHash) { - const normalizedTxHash = txHash.startsWith('0x') ? txHash : `0x${txHash}`; - return ( - - - - ); - } + const txHash = txStatus.state === 'success' ? txStatus.txHash : undefined; + + if (txHash) { + return ; } return ( - + + + ); }, [txStatus, blockExplorerUrl, isButtonDisabled, handleWithdraw, isFeeLoading]); @@ -277,31 +256,11 @@ export function WithdrawModal({ )} {/* Network Selector */} -
-
- Network -
- {(Object.keys(NETWORK_CONFIG) as NetworkType[]).map((network) => { - const config = NETWORK_CONFIG[network]; - return ( - {`${config.name} - ); - })} -
-
- -
+ {/* Amount Input */}
@@ -437,7 +396,7 @@ export function WithdrawModal({ className="flex items-center gap-2 px-4 py-2 h-full text-sm font-medium text-primary-600 hover:text-primary-700 transition-colors border-l border-gray-200 bg-gray-50 hover:bg-gray-100 rounded-r-lg flex-shrink-0" type="button" > - {isCopied ? ( + {isAddressCopied ? ( ) : ( @@ -465,7 +424,7 @@ export function WithdrawModal({ className="flex items-center gap-2 px-4 py-2 h-full text-sm font-medium text-primary-600 hover:text-primary-700 transition-colors border-l border-gray-200 bg-gray-50 hover:bg-gray-100 rounded-r-lg flex-shrink-0" type="button" > - {isCopied ? ( + {isAddressCopied ? ( ) : ( @@ -491,41 +450,12 @@ export function WithdrawModal({
{/* Balance Display */} -
-
- Current Balance: -
-
- - - {formatRSC({ amount: availableBalance })} - - RSC -
-
-
- -
-
- After Withdrawal: -
-
- - 0 ? 'text-red-600' : 'text-gray-900' - )} - > - {withdrawAmount > 0 - ? formatRSC({ amount: calculateNewBalance() }) - : formatRSC({ amount: availableBalance })} - - RSC -
-
-
-
+ 0 ? calculateNewBalance() : availableBalance} + futureBalanceLabel="After Withdrawal" + futureBalanceColor={withdrawAmount > 0 ? 'red' : 'gray'} + /> )}
diff --git a/components/modals/ResearchCoin/WithdrawalSuccessView.tsx b/components/modals/ResearchCoin/WithdrawalSuccessView.tsx index 14cc530cb..63a6e876e 100644 --- a/components/modals/ResearchCoin/WithdrawalSuccessView.tsx +++ b/components/modals/ResearchCoin/WithdrawalSuccessView.tsx @@ -1,11 +1,5 @@ -import { useState, useCallback } from 'react'; -import Image from 'next/image'; -import { CheckCircle2, Check, Copy } from 'lucide-react'; -import { formatRSC } from '@/utils/number'; -import { ResearchCoinIcon } from '@/components/ui/icons/ResearchCoinIcon'; import { NetworkConfig } from '@/constants/tokens'; -import toast from 'react-hot-toast'; -import { maskAddress } from '@/utils/stringUtils'; +import { TransactionSuccessView } from './shared/TransactionSuccessView'; interface WithdrawalSuccessViewProps { withdrawAmount: number; @@ -22,97 +16,18 @@ export function WithdrawalSuccessView({ networkConfig, address, }: WithdrawalSuccessViewProps) { - const [isCopied, setIsCopied] = useState(false); - - const handleCopyAddress = useCallback(() => { - if (!address) return; - navigator.clipboard.writeText(address).then( - () => { - setIsCopied(true); - toast.success('Address copied to clipboard!'); - setTimeout(() => setIsCopied(false), 2000); - }, - (err) => { - console.error('Failed to copy address: ', err); - toast.error('Failed to copy address.'); - } - ); - }, [address]); - - const maskedAddress = maskAddress(address); - return ( -
-
-
- -
-

Withdrawal Successful!

-

Your RSC has been sent to your wallet

-
- -
-
- Amount Withdrawn -
- - - {formatRSC({ amount: withdrawAmount })} - - RSC -
-
- -
- Network Fee -
- -{fee} - RSC -
-
- -
-
- You Received -
- - - {formatRSC({ amount: amountReceived })} - - RSC -
-
-
-
- -
-
- Network -
- {networkConfig.name} - {networkConfig.name} -
-
- -
- To Address -
- {maskedAddress} - -
-
-
-
+ ); } diff --git a/components/modals/ResearchCoin/shared/BalanceDisplay.tsx b/components/modals/ResearchCoin/shared/BalanceDisplay.tsx new file mode 100644 index 000000000..72c558d84 --- /dev/null +++ b/components/modals/ResearchCoin/shared/BalanceDisplay.tsx @@ -0,0 +1,61 @@ +import { ResearchCoinIcon } from '@/components/ui/icons/ResearchCoinIcon'; +import { formatRSC } from '@/utils/number'; +import { cn } from '@/utils/styles'; + +interface BalanceDisplayProps { + currentBalance: number; + futureBalance: number; + futureBalanceLabel: string; + futureBalanceColor?: 'green' | 'red' | 'gray'; + showFutureBalance?: boolean; +} + +export function BalanceDisplay({ + currentBalance, + futureBalance, + futureBalanceLabel, + futureBalanceColor = 'gray', + showFutureBalance = true, +}: BalanceDisplayProps) { + return ( +
+
+ Current Balance: +
+
+ + + {formatRSC({ amount: currentBalance })} + + RSC +
+
+
+ + {showFutureBalance && ( + <> +
+
+ {futureBalanceLabel}: +
+
+ + + {formatRSC({ amount: futureBalance })} + + RSC +
+
+
+ + )} +
+ ); +} diff --git a/components/modals/ResearchCoin/shared/NetworkSelectorSection.tsx b/components/modals/ResearchCoin/shared/NetworkSelectorSection.tsx new file mode 100644 index 000000000..df5a1d255 --- /dev/null +++ b/components/modals/ResearchCoin/shared/NetworkSelectorSection.tsx @@ -0,0 +1,50 @@ +import Image from 'next/image'; +import { NetworkSelector } from '@/components/ui/NetworkSelector'; +import { NETWORK_CONFIG, NetworkType } from '@/constants/tokens'; + +interface NetworkSelectorSectionProps { + selectedNetwork: NetworkType; + onNetworkChange: (network: NetworkType) => void; + disabled?: boolean; + showDescription?: boolean; + customBadges?: Partial>; +} + +export function NetworkSelectorSection({ + selectedNetwork, + onNetworkChange, + disabled = false, + showDescription = true, + customBadges, +}: NetworkSelectorSectionProps) { + return ( +
+
+ Network +
+ {(Object.keys(NETWORK_CONFIG) as NetworkType[]).map((network) => { + const config = NETWORK_CONFIG[network]; + return ( + {`${config.name} + ); + })} +
+
+ +
+ ); +} diff --git a/components/modals/ResearchCoin/shared/TransactionFooter.tsx b/components/modals/ResearchCoin/shared/TransactionFooter.tsx new file mode 100644 index 000000000..69efa6ba1 --- /dev/null +++ b/components/modals/ResearchCoin/shared/TransactionFooter.tsx @@ -0,0 +1,29 @@ +import { ExternalLink } from 'lucide-react'; +import { Button } from '@/components/ui/Button'; + +interface TransactionFooterProps { + txHash?: string; + blockExplorerUrl: string; + children?: React.ReactNode; +} + +export function TransactionFooter({ txHash, blockExplorerUrl, children }: TransactionFooterProps) { + if (txHash) { + const normalizedTxHash = txHash.startsWith('0x') ? txHash : `0x${txHash}`; + return ( + + + + ); + } + + return <>{children}; +} diff --git a/components/modals/ResearchCoin/shared/TransactionSuccessView.tsx b/components/modals/ResearchCoin/shared/TransactionSuccessView.tsx new file mode 100644 index 000000000..b16a55e82 --- /dev/null +++ b/components/modals/ResearchCoin/shared/TransactionSuccessView.tsx @@ -0,0 +1,137 @@ +import Image from 'next/image'; +import { CheckCircle2, Check, Copy, Clock } from 'lucide-react'; +import { formatRSC } from '@/utils/number'; +import { ResearchCoinIcon } from '@/components/ui/icons/ResearchCoinIcon'; +import { NetworkConfig } from '@/constants/tokens'; +import { maskAddress } from '@/utils/stringUtils'; +import { useCopyAddress } from '@/hooks/useCopyAddress'; + +interface TransactionSuccessViewProps { + title: string; + subtitle: string; + amount: number; + networkConfig: NetworkConfig; + address: string; + addressLabel: 'From Address' | 'To Address'; + amountLabel: string; + amountColor?: 'green' | 'gray'; + fee?: number; + amountReceived?: number; + showProcessingTime?: boolean; + processingTimeMessage?: string; +} + +export function TransactionSuccessView({ + title, + subtitle, + amount, + networkConfig, + address, + addressLabel, + amountLabel, + amountColor = 'green', + fee, + amountReceived, + showProcessingTime = false, + processingTimeMessage, +}: TransactionSuccessViewProps) { + const { isCopied, copyAddress } = useCopyAddress(); + const maskedAddress = maskAddress(address); + + return ( +
+
+
+ +
+

{title}

+

{subtitle}

+
+ +
+
+ {amountLabel} +
+ + + {formatRSC({ amount })} + + RSC +
+
+ + {fee !== undefined && ( + <> +
+ Network Fee +
+ -{fee} + RSC +
+
+ + {amountReceived !== undefined && ( +
+
+ You Received +
+ + + {formatRSC({ amount: amountReceived })} + + RSC +
+
+
+ )} + + )} +
+ +
+
+ Network +
+ {networkConfig.name} + {networkConfig.name} +
+
+ +
+ {addressLabel} +
+ {maskedAddress} + +
+
+
+ + {showProcessingTime && processingTimeMessage && ( +
+
+ +
+

Processing Time

+

{processingTimeMessage}

+
+
+
+ )} +
+ ); +} diff --git a/hooks/useCopyAddress.ts b/hooks/useCopyAddress.ts new file mode 100644 index 000000000..2bb003f83 --- /dev/null +++ b/hooks/useCopyAddress.ts @@ -0,0 +1,27 @@ +import { useState, useCallback } from 'react'; +import toast from 'react-hot-toast'; + +/** + * Hook for copying an address to clipboard with toast notifications + */ +export function useCopyAddress() { + const [isCopied, setIsCopied] = useState(false); + + const copyAddress = useCallback((address: string | null | undefined) => { + if (!address) return; + + navigator.clipboard.writeText(address).then( + () => { + setIsCopied(true); + toast.success('Address copied to clipboard!'); + setTimeout(() => setIsCopied(false), 2000); + }, + (err) => { + console.error('Failed to copy address: ', err); + toast.error('Failed to copy address.'); + } + ); + }, []); + + return { isCopied, copyAddress }; +} From cc0e376f9d9ec6ccd2dbe85bad34afe39fc10bd7 Mon Sep 17 00:00:00 2001 From: nicktytarenko Date: Wed, 21 Jan 2026 23:38:30 +0200 Subject: [PATCH 10/10] addressing comments --- components/modals/ResearchCoin/DepositModal.tsx | 10 +++++----- components/modals/ResearchCoin/WithdrawModal.tsx | 8 ++++---- components/ui/NetworkSelector.tsx | 2 +- hooks/useWalletRSCBalance.ts | 5 +---- 4 files changed, 11 insertions(+), 14 deletions(-) diff --git a/components/modals/ResearchCoin/DepositModal.tsx b/components/modals/ResearchCoin/DepositModal.tsx index 8cf3fcb42..e22e0c5cd 100644 --- a/components/modals/ResearchCoin/DepositModal.tsx +++ b/components/modals/ResearchCoin/DepositModal.tsx @@ -262,11 +262,6 @@ export function DepositModal({ isOpen, onClose, currentBalance, onSuccess }: Dep return [transferCall]; }, [amount, depositAmount, walletBalance, rscToken.address]); - // If no wallet is connected, show nothing - assuming modal shouldn't open in this state - if (!address) { - return null; - } - const footer = useMemo(() => { const txHash = txStatus.state === 'success' ? txStatus.txHash : undefined; @@ -313,6 +308,11 @@ export function DepositModal({ isOpen, onClose, currentBalance, onSuccess }: Dep selectedNetwork, ]); + // If no wallet is connected, show nothing - assuming modal shouldn't open in this state + if (!address) { + return null; + } + return ( { const txHash = txStatus.state === 'success' ? txStatus.txHash : undefined; @@ -223,6 +219,10 @@ export function WithdrawModal({ ); }, [txStatus, blockExplorerUrl, isButtonDisabled, handleWithdraw, isFeeLoading]); + if (!address) { + return null; + } + return ( { - if (customBadges && customBadges[network]) { + if (customBadges?.[network]) { return customBadges[network]; } return NETWORK_CONFIG[network].badge; diff --git a/hooks/useWalletRSCBalance.ts b/hooks/useWalletRSCBalance.ts index 0dfe94436..eb34e5e0d 100644 --- a/hooks/useWalletRSCBalance.ts +++ b/hooks/useWalletRSCBalance.ts @@ -34,10 +34,7 @@ export function useWalletRSCBalance( error, } = useBalance({ address, - token: - rscToken.address && rscToken.address.startsWith('0x') - ? (rscToken.address as `0x${string}`) - : undefined, + token: rscToken.address?.startsWith('0x') ? (rscToken.address as `0x${string}`) : undefined, chainId: rscToken.chainId, });