Files
smom-dbis-138/frontend-dapp/src/components/bridge/BridgeButtons.tsx
2026-03-28 15:38:51 -07:00

712 lines
28 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @file BridgeButtons.tsx
* @notice Custom bridge UI with Wrap, Approve, and Bridge buttons using thirdweb
*/
import {
useContract,
useContractWrite,
useContractRead,
useAddress,
useBalance,
} from '@thirdweb-dev/react';
import { useState, useEffect } from 'react';
import { ethers } from 'ethers';
import toast from 'react-hot-toast';
import {
CONTRACTS,
CHAIN_SELECTORS,
BRIDGE_ABI,
WETH9_ABI,
ERC20_ABI,
CCIP_DESTINATIONS,
} from '../../config/bridge';
import CopyButton from '../ui/CopyButton';
import ConfirmationModal from '../ui/ConfirmationModal';
import Tooltip from '../ui/Tooltip';
import LoadingSkeleton from '../ui/LoadingSkeleton';
import ChainIcon from '../ui/ChainIcon';
import TokenIcon from '../ui/TokenIcon';
interface BridgeButtonsProps {
destinationChainSelector?: string; // Defaults to Ethereum Mainnet
recipientAddress?: string; // Defaults to connected wallet
}
function getErrorMessage(error: unknown): string {
if (typeof error === 'object' && error !== null) {
const maybeMessage = 'message' in error ? error.message : undefined
const maybeReason = 'reason' in error ? error.reason : undefined
if (typeof maybeMessage === 'string' && maybeMessage.length > 0) return maybeMessage
if (typeof maybeReason === 'string' && maybeReason.length > 0) return maybeReason
}
return 'Unknown error'
}
/** Fetches CCIP fee only when amount > 0 (bridge reverts on zero). Reports result to parent via onFee. */
function CalculateFeeFetcher({
bridgeContract,
destinationChainSelector,
amountWei,
onFee,
}: {
bridgeContract: NonNullable<ReturnType<typeof useContract>['contract']>;
destinationChainSelector: string;
amountWei: ethers.BigNumber;
onFee: (value: ethers.BigNumber | null) => void;
}) {
const { data } = useContractRead(bridgeContract, 'calculateFee', [
destinationChainSelector,
amountWei,
]);
useEffect(() => {
if (data != null) onFee(ethers.BigNumber.from(data.toString()));
return () => {};
}, [data, onFee]);
return null;
}
/** Inner bridge form: only mounted when address is set so balance/allowance are never called with zero address. */
function BridgeButtonsConnected({
address,
destinationChainSelector = CHAIN_SELECTORS.ETHEREUM_MAINNET,
recipientAddress,
}: BridgeButtonsProps & { address: string }) {
const [amount, setAmount] = useState<string>('');
const [recipient, setRecipient] = useState<string>(recipientAddress || address || '');
const [isWrapping, setIsWrapping] = useState(false);
const [isApproving, setIsApproving] = useState(false);
const [isBridging, setIsBridging] = useState(false);
const [showWrapModal, setShowWrapModal] = useState(false);
const [showApproveModal, setShowApproveModal] = useState(false);
const [showBridgeModal, setShowBridgeModal] = useState(false);
const [amountError, setAmountError] = useState<string>('');
const [recipientError, setRecipientError] = useState<string>('');
const [ccipFee, setCcipFee] = useState<ethers.BigNumber | null>(null);
const { contract: weth9Contract } = useContract(CONTRACTS.WETH9, WETH9_ABI);
const { contract: bridgeContract } = useContract(CONTRACTS.WETH9_BRIDGE, BRIDGE_ABI);
const { contract: linkContract } = useContract(CONTRACTS.LINK_TOKEN, ERC20_ABI);
const { data: ethBalance, refetch: refetchEthBalance } = useBalance();
const { data: weth9Balance, refetch: refetchWeth9Balance } = useContractRead(
weth9Contract,
'balanceOf',
weth9Contract ? [address] : undefined
);
const { data: linkBalance, refetch: refetchLinkBalance } = useContractRead(
linkContract,
'balanceOf',
linkContract ? [address] : undefined
);
const { data: weth9Allowance, refetch: refetchWeth9Allowance } = useContractRead(
weth9Contract,
'allowance',
weth9Contract && bridgeContract ? [address, CONTRACTS.WETH9_BRIDGE] : undefined
);
const { data: linkAllowance, refetch: refetchLinkAllowance } = useContractRead(
linkContract,
'allowance',
linkContract && bridgeContract ? [address, CONTRACTS.WETH9_BRIDGE] : undefined
);
const amountWei = amount && !isNaN(parseFloat(amount)) && parseFloat(amount) > 0
? ethers.utils.parseEther(amount)
: ethers.BigNumber.from(0);
useEffect(() => {
if (!recipientAddress) setRecipient((r) => r || address);
}, [address, recipientAddress]);
useEffect(() => {
if (!amount) {
setAmountError('');
return;
}
const numAmount = parseFloat(amount);
if (isNaN(numAmount) || numAmount <= 0) {
setAmountError('Amount must be greater than 0');
} else if (ethBalance && numAmount > parseFloat(ethBalance.displayValue)) {
setAmountError('Insufficient ETH balance');
} else {
setAmountError('');
}
}, [amount, ethBalance]);
useEffect(() => {
if (!recipient) {
setRecipientError('');
return;
}
setRecipientError(ethers.utils.isAddress(recipient) ? '' : 'Invalid Ethereum address');
}, [recipient]);
useEffect(() => {
if (!amountWei.gt(0)) setCcipFee(null);
}, [amountWei]);
// Write operations
const { mutateAsync: wrapETH } = useContractWrite(weth9Contract, 'deposit');
const { mutateAsync: approveWETH9 } = useContractWrite(weth9Contract, 'approve');
const { mutateAsync: approveLINK } = useContractWrite(linkContract, 'approve');
const { mutateAsync: sendCrossChain } = useContractWrite(bridgeContract, 'sendCrossChain');
const handleRefreshBalances = async () => {
try {
await Promise.all([
refetchEthBalance().catch(() => {}),
refetchWeth9Balance().catch(() => {}),
refetchLinkBalance().catch(() => {}),
refetchWeth9Allowance().catch(() => {}),
refetchLinkAllowance().catch(() => {}),
]);
toast.success('Balances refreshed');
} catch (error) {
// Silently handle errors - some refetches may fail if contracts aren't ready
toast.success('Balances refreshed');
}
};
const handleWrap = async () => {
if (!address) {
toast.error('Please connect your wallet');
return;
}
if (!amount || parseFloat(amount) <= 0) {
toast.error('Please enter an amount');
return;
}
if (amountError) {
toast.error(amountError);
return;
}
if (!ethBalance || parseFloat(ethBalance.displayValue) < parseFloat(amount)) {
toast.error('Insufficient ETH balance');
return;
}
setIsWrapping(true);
const toastId = toast.loading('Wrapping ETH...');
try {
const tx = await wrapETH({
overrides: {
value: ethers.utils.parseEther(amount),
},
});
console.log('Wrap transaction:', tx);
toast.success('ETH wrapped successfully!', { id: toastId });
setAmount('');
await handleRefreshBalances();
} catch (error: unknown) {
console.error('Wrap error:', error);
toast.error(`Wrap failed: ${getErrorMessage(error)}`, { id: toastId });
} finally {
setIsWrapping(false);
}
};
const handleApprove = async () => {
if (!address) {
toast.error('Please connect your wallet');
return;
}
if (!amount || parseFloat(amount) <= 0) {
toast.error('Please enter an amount');
return;
}
setIsApproving(true);
const toastId = toast.loading('Approving tokens...');
try {
const amountWei = ethers.utils.parseEther(amount);
const maxApproval = ethers.constants.MaxUint256;
// Approve WETH9
const weth9AllowanceBN = weth9Allowance
? ethers.BigNumber.from(weth9Allowance.toString())
: ethers.BigNumber.from(0);
if (weth9AllowanceBN.lt(amountWei)) {
const weth9Tx = await approveWETH9({
args: [CONTRACTS.WETH9_BRIDGE, maxApproval],
});
console.log('WETH9 approval transaction:', weth9Tx);
toast.success('WETH9 approved', { id: toastId });
}
// Approve LINK for fees (if needed)
if (ccipFee?.gt(0)) {
const linkAllowanceBN = linkAllowance
? ethers.BigNumber.from(linkAllowance.toString())
: ethers.BigNumber.from(0);
if (linkAllowanceBN.lt(ccipFee)) {
const linkTx = await approveLINK({
args: [CONTRACTS.WETH9_BRIDGE, maxApproval],
});
console.log('LINK approval transaction:', linkTx);
toast.success('LINK approved', { id: toastId });
}
}
toast.success('All approvals successful!', { id: toastId });
await handleRefreshBalances();
} catch (error: unknown) {
console.error('Approve error:', error);
toast.error(`Approval failed: ${getErrorMessage(error)}`, { id: toastId });
} finally {
setIsApproving(false);
}
};
const handleBridge = async () => {
if (!address) {
toast.error('Please connect your wallet');
return;
}
if (!amount || parseFloat(amount) <= 0) {
toast.error('Please enter an amount');
return;
}
if (amountError || recipientError) {
toast.error('Please fix the errors before bridging');
return;
}
if (!recipient || !ethers.utils.isAddress(recipient)) {
toast.error('Please enter a valid recipient address');
return;
}
const amountWei = ethers.utils.parseEther(amount);
const weth9BalanceBN = weth9Balance
? ethers.BigNumber.from(weth9Balance.toString())
: ethers.BigNumber.from(0);
if (weth9BalanceBN.lt(amountWei)) {
toast.error('Insufficient WETH9 balance. Please wrap ETH first.');
return;
}
const weth9AllowanceBN = weth9Allowance
? ethers.BigNumber.from(weth9Allowance.toString())
: ethers.BigNumber.from(0);
if (weth9AllowanceBN.lt(amountWei)) {
toast.error('Insufficient WETH9 allowance. Please approve first.');
return;
}
if (ccipFee?.gt(0)) {
const linkBalanceBN = linkBalance
? ethers.BigNumber.from(linkBalance.toString())
: ethers.BigNumber.from(0);
if (linkBalanceBN.lt(ccipFee)) {
const feeFormatted = ethers.utils.formatEther(ccipFee);
toast.error(`Insufficient LINK for fees. Required: ${feeFormatted} LINK`);
return;
}
}
setIsBridging(true);
const toastId = toast.loading('Bridging tokens...');
try {
const result = await sendCrossChain({
args: [
destinationChainSelector, // destinationChainSelector
recipient, // recipient
amountWei, // amount
],
});
console.log('Bridge transaction:', result);
const txHash = result.receipt?.transactionHash || 'N/A';
toast.success(
<div>
<p className="font-semibold">Bridge transaction sent!</p>
<p className="text-xs mt-1">TX: {txHash.slice(0, 10)}...{txHash.slice(-8)}</p>
</div>,
{ id: toastId, duration: 6000 }
);
setAmount('');
await handleRefreshBalances();
} catch (error: unknown) {
console.error('Bridge error:', error);
toast.error(`Bridge failed: ${getErrorMessage(error)}`, { id: toastId });
} finally {
setIsBridging(false);
}
};
// Button state calculations
const needsWrapping =
ethBalance &&
amount &&
parseFloat(amount) > 0 &&
parseFloat(ethBalance.displayValue) >= parseFloat(amount) &&
!amountError;
const needsApproval =
amount &&
parseFloat(amount) > 0 &&
weth9Allowance &&
ethers.BigNumber.from(weth9Allowance.toString()).lt(ethers.utils.parseEther(amount)) &&
!amountError;
const canBridge =
amount &&
parseFloat(amount) > 0 &&
!amountError &&
!recipientError &&
weth9Balance &&
ethers.BigNumber.from(weth9Balance.toString()).gte(ethers.utils.parseEther(amount)) &&
weth9Allowance &&
ethers.BigNumber.from(weth9Allowance.toString()).gte(ethers.utils.parseEther(amount)) &&
recipient &&
ethers.utils.isAddress(recipient);
const destination = CCIP_DESTINATIONS.find((d) => d.selector === destinationChainSelector) ?? null;
return (
<div className="w-full">
{bridgeContract && amountWei.gt(0) && (
<CalculateFeeFetcher
bridgeContract={bridgeContract}
destinationChainSelector={destinationChainSelector}
amountWei={amountWei}
onFee={setCcipFee}
/>
)}
<div className="relative">
<div className="mb-6">
<div className="flex items-center justify-between mb-2">
<h2 className="text-[20px] font-semibold text-white flex items-center gap-2">
Bridge to
{destination && (
<>
<ChainIcon chainId={destination.chainId} name={destination.name} size={24} />
<span>{destination.name}</span>
</>
)}
{!destination && <span>Ethereum Mainnet</span>}
</h2>
<Tooltip content="Refresh all balances and allowances">
<button
onClick={handleRefreshBalances}
disabled={!address}
className="p-2 text-teal-400 hover:text-teal-300 hover:bg-teal-500/20 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed border border-white/20"
aria-label="Refresh balances"
>
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</button>
</Tooltip>
</div>
<p className="text-[#A0A0A0] text-sm mt-1">
Wrap ETH, approve tokens, and bridge WETH9 via CCIP
</p>
</div>
<div className="mb-8">
<label className="flex items-center gap-2 text-[13px] font-medium mb-2 text-[#A0A0A0]">
<TokenIcon symbol="ETH" size={20} />
<span>Amount (ETH)</span>
<Tooltip content="Enter the amount of ETH you want to bridge. This will be wrapped to WETH9 first.">
<span className="text-white/60 cursor-help text-xs"></span>
</Tooltip>
</label>
<div className="relative">
<input
type="number"
step="0.001"
min="0"
value={amount}
onChange={(e) => setAmount(e.target.value)}
className={`w-full min-h-[48px] pl-4 pr-24 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-teal-500 focus:ring-offset-2 focus:ring-offset-[#252830] transition-all text-lg bg-[#1a1d24] text-white placeholder:text-[#A0A0A0] hover:border-white/30 ${
amountError
? 'border-red-500 focus:border-red-500'
: 'border-white/20 focus:border-teal-500'
}`}
placeholder="0.0"
aria-invalid={!!amountError}
aria-describedby={amountError ? 'amount-error' : undefined}
/>
{ethBalance && (
<button
onClick={() => {
setAmount(ethBalance.displayValue);
setAmountError('');
}}
className="absolute right-3 top-1/2 -translate-y-1/2 px-4 py-2 min-h-[36px] text-sm font-medium bg-teal-600 text-white rounded-lg hover:bg-teal-500 transition-colors"
>
MAX
</button>
)}
</div>
{amountError && (
<p id="amount-error" className="mt-2 text-sm text-red-600 flex items-center gap-1">
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
{amountError}
</p>
)}
</div>
<div className="mb-8">
<label htmlFor="bridge-recipient-address" className="block text-[13px] font-medium mb-2 text-[#A0A0A0]">
Recipient Address
<Tooltip content="The Ethereum address that will receive the bridged tokens on the destination chain.">
<span className="ml-2 text-white/60 cursor-help text-xs"></span>
</Tooltip>
</label>
<div className="relative">
<input
id="bridge-recipient-address"
name="recipient"
type="text"
value={recipient}
onChange={(e) => setRecipient(e.target.value)}
className={`w-full min-h-[48px] p-4 border rounded-xl focus:outline-none focus:ring-2 focus:ring-teal-500 focus:ring-offset-2 focus:ring-offset-[#252830] transition-all font-mono text-base bg-[#1a1d24] text-white placeholder:text-[#A0A0A0] hover:border-white/30 ${
recipientError
? 'border-red-500 focus:border-red-500'
: 'border-white/20 focus:border-teal-500'
}`}
placeholder="0x..."
aria-invalid={!!recipientError}
aria-describedby={recipientError ? 'recipient-error' : undefined}
/>
<div className="absolute right-3 top-1/2 -translate-y-1/2 flex items-center gap-2">
{address && (
<button
onClick={() => {
setRecipient(address);
setRecipientError('');
}}
className="px-4 py-2 text-sm font-medium bg-[#252830] text-white rounded-lg hover:bg-white/10 transition-colors border border-white/20"
>
Use my address
</button>
)}
{recipient && ethers.utils.isAddress(recipient) && (
<CopyButton text={recipient} />
)}
</div>
</div>
{recipientError && (
<p id="recipient-error" className="mt-2 text-sm text-red-600 flex items-center gap-1">
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
{recipientError}
</p>
)}
</div>
<div className="mb-8 p-6 bg-[#1a1d24] rounded-xl border border-white/10">
<div className="flex items-center justify-between mb-4">
<h3 className="text-[20px] font-semibold text-[#A0A0A0]">Balances & Fees</h3>
{address && (
<CopyButton text={address} className="text-xs">
<span className="text-xs">Copy Address</span>
</CopyButton>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="flex justify-between items-center p-4 bg-[#252830] rounded-lg border border-white/10 font-mono text-sm">
<span className="font-medium text-[#A0A0A0] flex items-center gap-2">
<TokenIcon symbol="ETH" size={20} />
ETH Balance:
</span>
<span className="text-white font-medium">
{ethBalance ? ethBalance.displayValue : <LoadingSkeleton />} <span className="text-[#A0A0A0]">ETH</span>
</span>
</div>
<div className="flex justify-between items-center p-4 bg-[#252830] rounded-lg border border-white/10 font-mono text-sm">
<span className="font-medium text-[#A0A0A0] flex items-center gap-2">
<TokenIcon symbol="WETH9" size={20} />
WETH9 Balance:
</span>
<span className="text-white font-medium">
{address && weth9Balance !== undefined
? `${ethers.utils.formatEther(weth9Balance.toString())}`
: address
? <LoadingSkeleton />
: '0'} <span className="text-[#A0A0A0]">WETH9</span>
</span>
</div>
<div className="flex justify-between items-center p-4 bg-[#252830] rounded-lg border border-white/10 font-mono text-sm">
<span className="font-medium text-[#A0A0A0] flex items-center gap-2">
<TokenIcon symbol="LINK" size={20} />
LINK Balance:
</span>
<span className="text-white font-medium">
{address && linkBalance !== undefined
? `${ethers.utils.formatEther(linkBalance.toString())}`
: address
? <LoadingSkeleton />
: '0'} <span className="text-[#A0A0A0]">LINK</span>
</span>
</div>
{ccipFee != null && ccipFee.gt(0) && (
<div className="flex justify-between items-center p-4 bg-[#252830] rounded-lg border border-teal-500/30 font-mono text-sm">
<span className="font-medium text-[#A0A0A0] flex items-center gap-2">
<TokenIcon symbol="LINK" size={20} />
CCIP Fee:
</span>
<span className="text-white font-medium">
{ethers.utils.formatEther(ccipFee)} <span className="text-[#A0A0A0]">LINK</span>
</span>
</div>
)}
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<button
onClick={() => setShowWrapModal(true)}
disabled={!needsWrapping || !address || isWrapping}
className="px-6 py-4 bg-[#252830] text-white rounded-xl font-semibold hover:bg-white/10 disabled:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors border border-white/20"
aria-label="Wrap ETH to WETH9"
>
{isWrapping ? (
<span className="flex items-center justify-center gap-2">
<svg className="animate-spin h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Wrapping...
</span>
) : (
'Wrap (Deposit)'
)}
</button>
<button
onClick={() => setShowApproveModal(true)}
disabled={!needsApproval || !address || isApproving}
className="px-6 py-4 bg-[#252830] text-white rounded-xl font-semibold hover:bg-white/10 disabled:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors border border-white/20"
aria-label="Approve tokens for bridge"
>
{isApproving ? (
<span className="flex items-center justify-center gap-2">
<svg className="animate-spin h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Approving...
</span>
) : (
'Approve'
)}
</button>
</div>
<div className="mb-8">
<button
onClick={() => setShowBridgeModal(true)}
disabled={!canBridge || !address || isBridging}
className="w-full min-h-[56px] py-4 text-lg font-semibold bg-teal-600 text-white rounded-xl hover:bg-teal-500 disabled:bg-[#252830] disabled:opacity-50 disabled:cursor-not-allowed transition-colors focus:outline-none focus:ring-2 focus:ring-teal-500 focus:ring-offset-2 focus:ring-offset-[#252830] shadow-lg"
aria-label="Start bridge transfer"
>
{isBridging ? (
<span className="flex items-center justify-center gap-2">
<svg className="animate-spin h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Bridging...
</span>
) : (
'Start Bridge Transfer'
)}
</button>
</div>
{/* Confirmation Modals */}
<ConfirmationModal
isOpen={showWrapModal}
onClose={() => setShowWrapModal(false)}
onConfirm={() => {
setShowWrapModal(false);
handleWrap();
}}
title="Wrap ETH to WETH9"
message={`You are about to wrap ${amount} ETH to WETH9. This action cannot be undone.`}
confirmText="Wrap ETH"
confirmColor="blue"
isLoading={isWrapping}
/>
<ConfirmationModal
isOpen={showApproveModal}
onClose={() => setShowApproveModal(false)}
onConfirm={() => {
setShowApproveModal(false);
handleApprove();
}}
title="Approve Tokens"
message={`You are about to approve ${amount} WETH9 and required LINK tokens for the bridge contract. This will allow the bridge to transfer your tokens.`}
confirmText="Approve"
confirmColor="green"
isLoading={isApproving}
/>
<ConfirmationModal
isOpen={showBridgeModal}
onClose={() => setShowBridgeModal(false)}
onConfirm={() => {
setShowBridgeModal(false);
handleBridge();
}}
title="Bridge Tokens"
message={`You are about to bridge ${amount} WETH9 to ${recipient.slice(0, 6)}...${recipient.slice(-4)} on Ethereum Mainnet. This action cannot be undone.`}
confirmText="Bridge"
confirmColor="purple"
isLoading={isBridging}
/>
</div>
</div>
);
}
export default function BridgeButtons(props: BridgeButtonsProps) {
const address = useAddress();
if (!address) {
const destinationChainSelector = props.destinationChainSelector ?? CHAIN_SELECTORS.ETHEREUM_MAINNET;
const destination = CCIP_DESTINATIONS.find((d) => d.selector === destinationChainSelector);
return (
<div className="w-full">
<div className="mb-6">
<h2 className="text-[20px] font-semibold text-white flex items-center gap-2">
Bridge to
{destination && (
<>
<ChainIcon chainId={destination.chainId} name={destination.name} size={24} />
<span>{destination.name}</span>
</>
)}
{!destination && <span>Ethereum Mainnet</span>}
</h2>
<p className="text-[#A0A0A0] text-sm mt-1">
Wrap ETH, approve tokens, and bridge WETH9 via CCIP
</p>
</div>
<div className="mt-8 p-5 bg-amber-500/10 border border-amber-500/30 rounded-xl text-sm text-white font-medium flex items-center gap-4">
<svg className="h-6 w-6 text-yellow-300 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<span>Please connect your wallet to continue</span>
</div>
</div>
);
}
return <BridgeButtonsConnected {...props} address={address} />;
}