- {address}
+
+ )}
+
+ {/* Network Selector */}
+
+
+ {/* Amount Input */}
+
+
+ Amount to Withdraw
+
+
+
+ {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 })}
-
- RSC
-
-
-
-
-
-
-
After Withdrawal:
-
-
-
- 0 ? 'text-red-600' : 'text-gray-900'
- )}
- >
- {withdrawAmount > 0
- ? formatRSC({ amount: calculateNewBalance() })
- : formatRSC({ amount: availableBalance })}
-
+ {isFeeLoading ? (
+
+ ) : (
+
+ {formatRSC({ amount: amountUserWillReceive })}
+
+ )}
RSC
+ {!isFeeLoading && amountUserWillReceive <= 0 && withdrawAmount > 0 && fee && (
+
+ Withdrawal amount must be greater than the network fee.
+
+ )}
-
-
- {/* Action Button */}
- {txStatus.state === 'success' ? (
-
- View Transaction
-
-
- ) : (
+ )}
+
+ )}
+
+
+ {/* Withdrawal Address */}
+
+
Withdrawal Address
+
+ {/* Address Mode Toggle */}
+
{
+ if (!isInputDisabled()) {
+ setAddressMode(checked ? 'connected' : 'custom');
+ }
+ }}
+ disabled={isInputDisabled()}
+ />
+
+ {/* Address Input */}
+ {addressMode === 'connected' ? (
+
- {isFeeLoading
- ? 'Loading fee...'
- : txStatus.state === 'pending'
- ? 'Processing...'
- : 'Withdraw RSC'}
+ {isAddressCopied ? (
+
+ ) : (
+
+ )}
+ }
+ />
+ ) : (
+
+
+ {isAddressCopied ? (
+
+ ) : (
+
+ )}
+
+ )
+ }
+ />
+ {customAddress && !isCustomAddressValid && (
+
+ Please enter a valid Ethereum address (0x followed by 40 hex characters).
+
)}
+
+ )}
- {/* Transaction Status Display */}
- {txStatus.state !== 'idle' && (
-
- {txStatus.state === 'pending' && (
-
-
- Withdrawal in progress...
-
- )}
-
- {txStatus.state === 'success' && (
-
-
- Withdrawal successful!
-
- )}
-
- {txStatus.state === 'error' && (
-
- )}
-
- )}
+ {/* Network Compatibility Warning */}
+
+
+ Ensure the destination wallet supports {NETWORK_CONFIG[selectedNetwork].name}
-
-
-
-
-
-
+
+
+
+ {/* Balance Display */}
+
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
new file mode 100644
index 000000000..63a6e876e
--- /dev/null
+++ b/components/modals/ResearchCoin/WithdrawalSuccessView.tsx
@@ -0,0 +1,33 @@
+import { NetworkConfig } from '@/constants/tokens';
+import { TransactionSuccessView } from './shared/TransactionSuccessView';
+
+interface WithdrawalSuccessViewProps {
+ withdrawAmount: number;
+ fee: number;
+ amountReceived: number;
+ networkConfig: NetworkConfig;
+ address: string;
+}
+
+export function WithdrawalSuccessView({
+ withdrawAmount,
+ fee,
+ amountReceived,
+ networkConfig,
+ address,
+}: WithdrawalSuccessViewProps) {
+ return (
+
+ );
+}
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 (
+
+ );
+ })}
+
+
+
+
+ );
+}
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}
+
+
+
+
+
{addressLabel}
+
+ {maskedAddress}
+
+
+
+
+
+ {showProcessingTime && processingTimeMessage && (
+
+
+
+
+
Processing Time
+
{processingTimeMessage}
+
+
+
+ )}
+
+ );
+}
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
new file mode 100644
index 000000000..bbf2e9e2e
--- /dev/null
+++ b/components/ui/NetworkSelector.tsx
@@ -0,0 +1,140 @@
+'use client';
+
+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';
+
+interface NetworkSelectorProps {
+ value: NetworkType;
+ onChange: (network: NetworkType) => void;
+ disabled?: boolean;
+ className?: string;
+ showBadges?: boolean;
+ customBadges?: Partial>;
+ showDescription?: boolean;
+}
+
+export function NetworkSelector({
+ value,
+ onChange,
+ disabled = false,
+ className,
+ showBadges = true,
+ customBadges,
+ showDescription = true,
+}: NetworkSelectorProps) {
+ const selectedNetwork = NETWORK_CONFIG[value];
+
+ const getBadge = (network: NetworkType): string | undefined => {
+ if (customBadges?.[network]) {
+ return customBadges[network];
+ }
+ return NETWORK_CONFIG[network].badge;
+ };
+
+ const trigger = useMemo(
+ () => (
+
+
+
+
+ {selectedNetwork.name}
+ {showBadges && getBadge(value) && (
+
+ {getBadge(value)}
+
+ )}
+
+ {showDescription && showBadges && selectedNetwork.description && (
+
{selectedNetwork.description}
+ )}
+
+
+
+ ),
+ [selectedNetwork, value, disabled, className, showBadges, showDescription, customBadges]
+ );
+
+ 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-center gap-3 px-3 py-3 rounded-md',
+ isSelected && 'bg-primary-50'
+ )}
+ >
+
+
+
+ {config.name}
+ {showBadges && getBadge(network) && (
+
+ {getBadge(network)}
+
+ )}
+
+ {showDescription && showBadges && config.description && (
+
+ {config.description}
+
+ )}
+
+ {isSelected && }
+
+ );
+ })}
+
+ );
+}
diff --git a/constants/tokens.ts b/constants/tokens.ts
index 9e257b157..3d009b382 100644
--- a/constants/tokens.ts
+++ b/constants/tokens.ts
@@ -1,9 +1,45 @@
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';
+
+export interface NetworkConfig {
+ chainId: number;
+ name: string;
+ rscAddress: string;
+ explorerUrl: string;
+ badge: string;
+ description: string;
+ icon: string;
+}
+
+export const NETWORK_CONFIG: Record = {
+ BASE: {
+ chainId: BASE_CHAIN_ID,
+ 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: ETHEREUM_CHAIN_ID,
+ 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',
@@ -13,16 +49,31 @@ export const RSC: Token = {
? '0xFbB75A59193A3525a8825BeBe7D4b56899E2f7e1'
: '0xdAf43508D785939D6C2d97c2df73d65c9359dBEa',
image: 'RSC.webp',
- chainId: CHAIN_ID,
+ chainId: BASE_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: '',
symbol: 'ETH',
decimals: 18,
image: 'ETH.webp',
- chainId: CHAIN_ID,
+ chainId: BASE_CHAIN_ID,
};
export const USDC: Token = {
@@ -33,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/contexts/OnchainContext.tsx b/contexts/OnchainContext.tsx
index dc7031af8..8fa56d746 100644
--- a/contexts/OnchainContext.tsx
+++ b/contexts/OnchainContext.tsx
@@ -2,7 +2,61 @@
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 PAYMASTER_URL = process.env.NEXT_PUBLIC_PAYMASTER_URL?.trim() || undefined;
+const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://researchhub.com';
+
+const productionConfig = createConfig({
+ chains: [base, mainnet],
+ connectors: [
+ coinbaseWallet({
+ appName: 'ResearchHub',
+ preference: 'all',
+ }),
+ metaMask({
+ dappMetadata: {
+ name: 'ResearchHub',
+ url: SITE_URL,
+ },
+ }),
+ ],
+ 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: SITE_URL,
+ },
+ }),
+ ],
+ 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 +64,34 @@ 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}
+
+
+
);
}
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 };
+}
diff --git a/hooks/useWalletRSCBalance.ts b/hooks/useWalletRSCBalance.ts
index 05c92b949..eb34e5e0d 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,31 @@ 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?.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/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/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: {
diff --git a/utils/stringUtils.ts b/utils/stringUtils.ts
index d055ab88c..89b96bd51 100644
--- a/utils/stringUtils.ts
+++ b/utils/stringUtils.ts
@@ -64,3 +64,29 @@ 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);
+};
+
+/**
+ * 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)}`;
+};