Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
487 changes: 247 additions & 240 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

53 changes: 40 additions & 13 deletions src/hooks/bridge/useBnbFeeAlerts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,49 +4,73 @@ import { useContractStore } from '@/stores/useContractStore';
import { BridgeType } from '@/types/bridge';
import { Layer } from '@/types/network';
import { useCallback } from 'react';
import { useTokenBalance } from '../common/useTokenBalances';
import { useNetworkInfo } from '../useNetworkInfo';


export function useBnbFeeAlerts() {

const { layer, isActiveNetworkBnb, isActiveNetworkBnbTestnet } = useNetworkInfo()

const { classicExitCost, exitFee, priceRatio, feeChoiceIsBoba,
amount, bridgeType, selectedToken, removeAlert, addAlert
const {
classicExitCost,
exitFee,
priceRatio,
feeChoiceIsBoba,
amount,
bridgeType,
selectedToken,
removeAlert,
addAlert
} = useBridgeStore()
const { bridgeContracts } = useContractStore()

const bobaBalance = selectedToken?.address === bridgeContracts?.l2BOBA?.address ?
Number(selectedToken?.balance || '0') : 0
// Get BOBA balance directly using token address
const { balance: bobaBalanceStr, isError: bobaBalanceError } = useTokenBalance({
tokens: bridgeContracts?.l2BOBA?.address
}) as { balance: string; isError: boolean }
const bobaBalance = Number(bobaBalanceStr || '0')

const checkFeeAlerts = useCallback(() => {
// Early returns - same conditions as old code
if (bridgeType === BridgeType.LIGHT) return;
if (layer !== Layer.L2 || !selectedToken || !amount || !isActiveNetworkBnb || isActiveNetworkBnbTestnet) return;
if (bobaBalanceError) {
console.error('Error fetching BOBA balance');
return;
}

// Clear existing fee alerts first
removeAlert(ALERT_KEYS.INSUFFICIENT_EXIT_FEE);
removeAlert(ALERT_KEYS.INSUFFICIENT_ETH_FEE);
removeAlert(ALERT_KEYS.INSUFFICIENT_BOBA_FEE);

const balance = Number(selectedToken.balance || '0');
const selectedTokenBalance = Number(selectedToken.balance || '0');
const amountToSend = Number(amount);

const cost = Number(classicExitCost) * 1.04; // Safety margin as in old code
const bobaCost = cost * Number(priceRatio || '0');
const totalExitFee = Number(exitFee || '0')
const totalExitFee = Number(exitFee || '0');

let warning = '';

// Check conditions in the same order as old code
// First check if we have enough BOBA for xChain message relay
if (totalExitFee > bobaBalance) {
warning = `Insufficient BOBA balance to cover xChain message relay. You need at least ${totalExitFee} BOBA`;
} else if (feeChoiceIsBoba) {
if (selectedToken.symbol === 'BOBA'
&& amountToSend + bobaCost + totalExitFee > balance) {
warning = `Insufficient BOBA balance to cover Exit Fee and Relay fee`
} else if (bobaCost + totalExitFee > bobaBalance) {
warning = `Insufficient BOBA balance to cover Exit Fee and Relay fee`
}
// Then check if using BOBA for fees
else if (feeChoiceIsBoba) {
const totalBobaNeeded = bobaCost + totalExitFee;
if (selectedToken.symbol === 'BOBA') {
// For BOBA transfers, check if we have enough for transfer + fees
const totalNeeded = amountToSend + totalBobaNeeded;
if (totalNeeded > selectedTokenBalance) {
warning = `Insufficient BOBA balance to cover transfer amount and fees. You need ${totalNeeded} BOBA but have ${selectedTokenBalance} BOBA`;
}
}
// For other tokens, just check BOBA balance for fees
else if (totalBobaNeeded > bobaBalance) {
warning = `Insufficient BOBA balance to cover fees. You need ${totalBobaNeeded} BOBA but have ${bobaBalance} BOBA`;
}
}

Expand All @@ -70,7 +94,10 @@ export function useBnbFeeAlerts() {
amount,
exitFee,
bobaBalance,
bobaBalanceError,
priceRatio,
classicExitCost,
feeChoiceIsBoba,
addAlert,
removeAlert
]);
Expand Down
1 change: 0 additions & 1 deletion src/hooks/bridge/useBnbFees.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@ export function useBnbFees() {
if (selectedToken?.address && amount) {
await estimateExitCost(
selectedToken.address,
amount,
selectedToken.decimals
);
}
Expand Down
9 changes: 8 additions & 1 deletion src/hooks/bridge/useBridgeSetup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { useNetworkInfo } from "../useNetworkInfo";

export const useBridgeSetup = () => {
const { chainId, layer } = useNetworkInfo();
const { setFromNetwork, setToNetwork, reset, bridgeType, resetSelection } = useBridgeStore()
const { setFromNetwork, setToNetwork, reset, bridgeType, setLightBridgeFee, resetSelection, selectedToken, setAmount } = useBridgeStore()
useEffect(() => {
if (!chainId) return;

Expand All @@ -28,4 +28,11 @@ export const useBridgeSetup = () => {
useEffect(() => {
resetSelection()
}, [bridgeType])

// reset amount as soon as token changes.

useEffect(() => {
setAmount('')
setLightBridgeFee(null)
}, [selectedToken])
}
14 changes: 4 additions & 10 deletions src/hooks/bridge/useExitCostEstimate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,11 @@ export function useExitCostEstimate() {

const estimateExitCost = useCallback(async (
tokenAddress: Address,
amount: string,
decimals: number
) => {
if (!isActiveNetworkBnb
|| !chainId
|| !address
|| !Number(amount)) {
|| !address) {
setClassicExitCost(null);
return;
}
Expand All @@ -48,7 +46,7 @@ export function useExitCostEstimate() {
}) as bigint;

let approvalCost = 0n;
const amountWei = parseUnits(amount, decimals);
const amountWei = parseUnits("0.0001", decimals);

// Calculate approval cost for non-native tokens
if (tokenAddress !== NETWORK_NATIVE_TOKEN_ADDRESS) {
Expand All @@ -65,10 +63,7 @@ export function useExitCostEstimate() {
approvalCost = gas * gasPrice;
}

// Value to send with transaction
const value = tokenAddress === NETWORK_NATIVE_TOKEN_ADDRESS
? amountWei + exitFee // For native token, include amount
: exitFee; // For other tokens, just exit fee
const value = amountWei + exitFee;

// Simulate withdrawal transaction
const gas = await provider.estimateGas({
Expand All @@ -78,15 +73,14 @@ export function useExitCostEstimate() {
abi: bridgeContracts.discretionaryExitFee.abi,
functionName: 'payAndWithdraw',
args: [
tokenAddress,
NETWORK_NATIVE_TOKEN_ADDRESS,
amountWei,
BigInt(150000), // L1 Gas Limit
'0x' + Date.now().toString(16), // Unique identifier
]
}),
value
});

// Calculate total cost
const withdrawalCost = gas * gasPrice;
const totalCost = withdrawalCost + approvalCost;
Expand Down
100 changes: 100 additions & 0 deletions src/hooks/bridge/useLightBridgeFeeEstimate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { getProvider } from '@/lib/viem';
import { useBridgeStore } from '@/stores/bridge.store';
import { useContractStore } from '@/stores/useContractStore';
import { NETWORK_NATIVE_TOKEN_ADDRESS } from '@/utils/tokeninfo';
import { useCallback } from 'react';
import { encodeFunctionData, erc20Abi, formatEther, isAddressEqual, parseUnits, zeroAddress, type Address } from 'viem';
import { useAccount, useChainId } from 'wagmi';
import { useNetworkInfo } from '../useNetworkInfo';

const ERROR_CODE = 'LIGHT_BRIDGE_FEE_ERROR';

export function useLightBridgeFeeEstimate() {
const { address } = useAccount();
const chainId = useChainId()
const { layer } = useNetworkInfo();
const { bridgeContracts } = useContractStore();
const { setLightBridgeFee } = useBridgeStore();

const estimateLightBridgeFee = useCallback(async (
tokenAddress: Address,
amount: string,
decimals: number,
toChainId: number
) => {
if (!layer || !address || !tokenAddress) {
setLightBridgeFee(null);
return;
}

try {
const lbLayerConfig = bridgeContracts?.lightBridge[layer as 'l1' | 'l2'];
if (!lbLayerConfig?.address) {
console.error('Light bridge contract address not found');
return;
}
if (!Number(amount)) {
console.error('Amount too small');
return;
}

const provider = getProvider(chainId);
const gasPrice = await provider.getGasPrice();
const amountWei = parseUnits(amount, decimals);

let approvalCost = 0n;

// Calculate approval cost for non-native tokens

if(!isAddressEqual(tokenAddress!, zeroAddress)) {
// Simulate approval transaction
const approvalGas = await provider.estimateGas({
account: address,
to: tokenAddress,
data: encodeFunctionData({
abi: erc20Abi,
functionName: 'approve',
args: [lbLayerConfig.address, amountWei]
})
});
approvalCost = approvalGas * gasPrice;
}

// Value to send with transaction (only for native token)
let value: any
if([zeroAddress, NETWORK_NATIVE_TOKEN_ADDRESS].includes(tokenAddress!)) {
value = amountWei
};
// Simulate teleportAsset transaction
const teleportGas = await provider.estimateGas({
account: address,
to: lbLayerConfig.address,
data: encodeFunctionData({
abi: bridgeContracts?.lightBridge.abi!,
functionName: 'teleportAsset',
args: [
tokenAddress,
amountWei,
BigInt(toChainId)
]
}),
value
});


// Calculate total cost
const teleportCost = teleportGas * gasPrice;
const totalCost = teleportCost + approvalCost;
// Update store with estimated cost
setLightBridgeFee(formatEther(totalCost));

} catch (error) {
console.error(`${ERROR_CODE}: Error estimating light bridge fee:`, error);
setLightBridgeFee(null);
}
}, [layer, address, bridgeContracts, setLightBridgeFee]);

return {
estimateLightBridgeFee
};
}
26 changes: 9 additions & 17 deletions src/layout/bridge/ReceiveAmount.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { Card, CardContent, Text } from "@/components/ui";
import { useBnbFeeAlerts } from "@/hooks/bridge/useBnbFeeAlerts";
import { useBnbFees } from "@/hooks/bridge/useBnbFees";
import { useNetworkInfo } from "@/hooks/useNetworkInfo";
import { useBridgeStore } from '@/stores/bridge.store';
import { BridgeType } from "@/types/bridge";
Expand All @@ -9,28 +7,18 @@ import { formatNumberWithIntl } from '@/utils/format';
import { IconChevronDown } from '@tabler/icons-react';
import { AnimatePresence, motion } from 'motion/react';
import type React from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useAccount } from "wagmi";
import { useCallback, useMemo, useState } from 'react';

const BridgeInfoCard: React.FC = () => {
const [isOpen, setIsOpen] = useState(false)
const { selectedToken, amount, bridgeType } = useBridgeStore()
const { layer, isActiveNetworkBnb } = useNetworkInfo()
const { isConnected } = useAccount()
const { fetchBnbFees } = useBnbFees()
const { classicExitCost, exitFee, priceRatio, feeChoiceIsBoba } = useBridgeStore()
const { classicExitCost, exitFee, priceRatio, feeChoiceIsBoba, lightBridgeFee } = useBridgeStore()
const toggleOpen = () => setIsOpen(!isOpen)

// only invoke in case of bnb network
const { checkFeeAlerts } = useBnbFeeAlerts()

// Effect to handle BNB fees
useEffect(() => {
if (isConnected && isActiveNetworkBnb && layer === Layer.L2) {
fetchBnbFees();
checkFeeAlerts();
}
}, [isConnected, isActiveNetworkBnb, fetchBnbFees, layer, checkFeeAlerts]);
const isLightBridge = useMemo(() => {
return bridgeType === BridgeType.LIGHT
}, [bridgeType])

const isBNBWithdrawal = useMemo(() => {
if (isActiveNetworkBnb && bridgeType === BridgeType.CLASSIC && layer === Layer.L2) {
Expand Down Expand Up @@ -127,6 +115,10 @@ const BridgeInfoCard: React.FC = () => {
<Text variant="small">Estimated time</Text>
<Text variant="small">{estimateTime() || "1~5 minutes"}</Text>
</div>
{isLightBridge && lightBridgeFee && <div className="flex justify-between items-center">
<Text variant="small">Bridge Fee</Text>
<Text variant="small">{lightBridgeFee} {selectedToken?.symbol}</Text>
</div>}
{isBNBWithdrawal && gasFee && <div className="flex justify-between items-center">
<Text variant="small">Bridge Fee</Text>
<Text variant="small">{gasFee}</Text>
Expand Down
Loading