Skip to content

Commit

Permalink
add withdrawal L1 fee info modal (#138)
Browse files Browse the repository at this point in the history
* add withdrawal fee info

* cleanup

* small updates
  • Loading branch information
lukasrosario authored Nov 22, 2023
1 parent 6721126 commit a4b1470
Show file tree
Hide file tree
Showing 5 changed files with 263 additions and 17 deletions.
16 changes: 16 additions & 0 deletions apps/bridge/src/components/Tooltip/Tooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import Image from 'next/image';

type TooltipProps = {
children: string;
};

export function Tooltip({ children }: TooltipProps) {
return (
<div className="has-tooltip">
<span className="tooltip -mt-10 ml-6 max-w-sm rounded-lg bg-cds-background-gray-90 p-2 text-black shadow-lg">
{children}
</span>
<Image alt="tooltip" src="/icons/question-mark-circled.svg" width={16} height={16} />
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { Modal } from 'apps/bridge/src/components/Modal/Modal';
import { Asset, BridgeProtocol } from 'apps/bridge/src/types/Asset';
import { usdFormatter } from 'apps/bridge/src/utils/formatter/balance';
import { useConversionRate } from 'apps/bridge/src/utils/hooks/useConversionRate';
import { useGetWithdrawalFeeEstimates } from 'apps/bridge/src/utils/hooks/useGetWithdrawalFeeEstimates';
import { Tooltip } from 'apps/bridge/src/components/Tooltip/Tooltip';

const chainIdToNetwork: Record<number, string> = {
1: 'Ethereum Mainnet',
5: 'Goerli Testnet',
11155111: 'Sepolia Testnet',
};

const chainIdToTransferTime: Record<number, string> = {
1: 'About 7 days',
5: 'A few minutes',
11155111: 'A few minutes',
};

type TransactionSummaryModalProps = {
selectedAsset: Asset;
amount: string;
isOpen: boolean;
onClose: () => void;
onProceed: () => void;
protocol: BridgeProtocol;
};

export function TransactionSummaryModal({
selectedAsset,
amount,
isOpen,
onClose,
onProceed,
protocol,
}: TransactionSummaryModalProps) {
const { eth: feesInETH, usd: feesInUSD } = useGetWithdrawalFeeEstimates({ selectedAsset });
const assetConversionRate = useConversionRate({ asset: selectedAsset.apiId });
const fiatAmount = usdFormatter(parseFloat(amount) * (assetConversionRate ?? 0));

const proveTooltipText =
'In order to complete a withdrawal, you must submit two additional L1 transactions, each of which requires enough ETH to pay for L1 gas fees charged by the Ethereum network.';
const finalizeTooltipText =
selectedAsset.protocol === 'CCTP'
? 'In order to complete a withdrawal, you must submit an additional L1 transaction which requires enough ETH to pay for L1 gas fees charged by the Ethereum network.'
: 'In order to complete a withdrawal, you must submit two additional L1 transactions, each of which requires enough ETH to pay for L1 gas fees charged by the Ethereum network.';

const content = (
<div className="flex w-96 flex-col space-y-4">
<div className="flex w-full flex-row items-center justify-between pt-8">
<div className="flex flex-col items-start">
<span className="text-white">Receive {selectedAsset.L1symbol}</span>
<span>On {chainIdToNetwork[selectedAsset.L1chainId]}</span>
</div>
<div className="flex flex-col items-end">
<span className="text-white">
{amount} {selectedAsset.L1symbol}
</span>
<span>{fiatAmount}</span>
</div>
</div>
<div className="flex w-full flex-row items-center justify-between pt-8">
<div className="flex flex-col items-start">
<span className="text-white">Transfer time</span>
</div>
<div className="flex flex-col items-end">
<span className="text-white">
{selectedAsset.protocol === 'CCTP'
? 'A few minutes'
: chainIdToTransferTime[selectedAsset.L1chainId]}
</span>
</div>
</div>
{protocol === 'OP' && (
<div className="flex w-full flex-row items-center justify-between pt-8">
<div className="flex flex-row items-center space-x-2">
<span className="text-white">Verification fee (est.)</span>
<Tooltip>{proveTooltipText}</Tooltip>
</div>
<div className="flex flex-col items-end">
<span className="text-white">{feesInETH.prove} ETH</span>
<span>{feesInUSD.prove}</span>
</div>
</div>
)}
<div className="flex w-full flex-row items-center justify-between pt-8">
<div className="flex flex-row items-center space-x-2">
<span className="text-white">Completion fee (est.)</span>
<Tooltip>{finalizeTooltipText}</Tooltip>
</div>
<div className="flex flex-col items-end">
<span className="text-white">{feesInETH.finalize} ETH</span>
<span>{feesInUSD.finalize}</span>
</div>
</div>
<div className="flex w-full flex-row items-center justify-between pt-8">
<div className="flex flex-col items-start">
<span className="text-white">Fee total (est.)</span>
</div>
<div className="flex flex-col items-end">
<span className="text-white">{feesInETH.total} ETH</span>
<span>{feesInUSD.total}</span>
</div>
</div>
</div>
);

const footer = (
<div className="flex flex-row justify-center space-x-12">
<button
className="w-48 border border-white bg-black py-2 text-lg text-white"
type="button"
onClick={onClose}
>
Cancel
</button>
<button className="w-48 bg-white py-2 text-lg text-black" type="button" onClick={onProceed}>
Continue
</button>
</div>
);

return (
<Modal
isOpen={isOpen}
onClose={onClose}
title="Transaction Summary"
content={content}
footer={footer}
/>
);
}
61 changes: 51 additions & 10 deletions apps/bridge/src/components/WithdrawContainer/WithdrawContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { useIsContractApproved } from 'apps/bridge/src/utils/hooks/useIsContract
import { useApproveContract } from 'apps/bridge/src/utils/hooks/useApproveContract';
import { BridgeButton } from 'apps/bridge/src/components/BridgeButton/BridgeButton';
import { parseUnits } from 'viem';
import { TransactionSummaryModal } from 'apps/bridge/src/components/WithdrawContainer/TransactionSummaryModal';

const activeAssets = getWithdrawalAssetsForChainEnv();

Expand Down Expand Up @@ -137,6 +138,12 @@ export function WithdrawContainer() {
});
const { writeAsync: withdrawCCTPAssetWrite } = useContractWrite(withdrawCCTPAssetConfig);

const {
isOpen: isTransactionSummaryModalOpen,
onOpen: onOpenTransactionSummaryModal,
onClose: onCloseTransactionSummaryModal,
} = useDisclosure();

const {
isOpen: isWithdrawModalOpen,
onOpen: onOpenWithdrawModal,
Expand Down Expand Up @@ -252,8 +259,19 @@ export function WithdrawContainer() {
onCloseWithdrawModal,
]);

const handleProceedToApproval = useCallback(() => {
onCloseTransactionSummaryModal();
initiateApproval();
}, [initiateApproval, onCloseTransactionSummaryModal]);

const handleProceedToWithdraw = useCallback(() => {
onCloseTransactionSummaryModal();
initiateWithdrawal();
}, [initiateWithdrawal, onCloseTransactionSummaryModal]);

let button;
let withdrawDisabled;
let transactionSummaryModal;

if (!isWalletConnected) {
button = (
Expand All @@ -265,13 +283,23 @@ export function WithdrawContainer() {

button = (
<BridgeButton
onClick={initiateApproval}
onClick={onOpenTransactionSummaryModal}
disabled={withdrawDisabled}
className="text-md flex w-full items-center justify-center rounded-md p-4 font-sans font-bold uppercase sm:w-auto"
>
Approval
</BridgeButton>
);
transactionSummaryModal = (
<TransactionSummaryModal
isOpen={isTransactionSummaryModalOpen}
onClose={onCloseTransactionSummaryModal}
onProceed={handleProceedToApproval}
selectedAsset={selectedAsset}
protocol={selectedAsset.protocol}
amount={withdrawAmount}
/>
);
} else {
withdrawDisabled =
parseFloat(withdrawAmount) <= 0 ||
Expand All @@ -282,27 +310,40 @@ export function WithdrawContainer() {

button = (
<BaseButton
onClick={initiateWithdrawal}
onClick={onOpenTransactionSummaryModal}
disabled={withdrawDisabled}
toChainId={chainId}
className="text-md flex w-full items-center justify-center rounded-md p-4 font-sans font-bold uppercase sm:w-auto"
>
Withdraw
</BaseButton>
);
transactionSummaryModal = (
<TransactionSummaryModal
isOpen={isTransactionSummaryModalOpen}
onClose={onCloseTransactionSummaryModal}
onProceed={handleProceedToWithdraw}
selectedAsset={selectedAsset}
protocol={selectedAsset.protocol}
amount={withdrawAmount}
/>
);
}

return (
<div className="flex-col lg:flex lg:h-full lg:flex-row">
<div className="grow">
<WithdrawModal
isOpen={isWithdrawModalOpen}
onClose={handleCloseWithdrawModal}
L2ApproveTxHash={L2ApproveTxHash}
L2WithdrawTxHash={L2WithdrawTxHash}
isApprovalTx={isApprovalTx}
protocol={selectedAsset.protocol}
/>
{isTransactionSummaryModalOpen && transactionSummaryModal}
{isWithdrawModalOpen && (
<WithdrawModal
isOpen={isWithdrawModalOpen}
onClose={handleCloseWithdrawModal}
L2ApproveTxHash={L2ApproveTxHash}
L2WithdrawTxHash={L2WithdrawTxHash}
isApprovalTx={isApprovalTx}
protocol={selectedAsset.protocol}
/>
)}
<BridgeInput
inputNetwork={getL2NetworkForChainEnv()}
isWithdraw
Expand Down
21 changes: 14 additions & 7 deletions apps/bridge/src/utils/hooks/useConversionRate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,20 @@ import { request } from 'apps/bridge/src/http/fetchJSON';

type UseConversionRateParams = {
asset: string;
refetch?: boolean;
};

type CoinGeckoResponseType = Record<
string,
{
usd: number;
}
string,
{
usd: number;
}
>;

export function useConversionRate({ asset }: UseConversionRateParams): number | undefined {
export function useConversionRate({
asset,
refetch = true,
}: UseConversionRateParams): number | undefined {
const { data } = useQuery(
asset,
async () => {
Expand All @@ -25,8 +29,11 @@ export function useConversionRate({ asset }: UseConversionRateParams): number |
},
{
suspense: false,
staleTime: 15000,
refetchInterval: 1000 * 30,
staleTime: refetch ? 15000 : Infinity,
refetchInterval: refetch ? 1000 * 30 : false,
refetchOnMount: refetch,
refetchOnReconnect: refetch,
refetchIntervalInBackground: refetch,
},
);

Expand Down
50 changes: 50 additions & 0 deletions apps/bridge/src/utils/hooks/useGetWithdrawalFeeEstimates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Asset } from 'apps/bridge/src/types/Asset';
import { usdFormatter } from 'apps/bridge/src/utils/formatter/balance';
import { useConversionRate } from 'apps/bridge/src/utils/hooks/useConversionRate';
import { formatEther } from 'viem';
import { useFeeData } from 'wagmi';

// prove: ~300_000
// finalize: ~100_000 for ETH, ~215_000 for ERC20, ~170_000 for CCTP
const OP_PROVE_GAS = 300000n;
const OP_FINALIZE_ETH_GAS = 100000n;
const OP_FINALIZE_ERC20_GAS = 215000n;
const CCTP_FINALIZE_GAS = 170000n;

type UseGetWithdrawalFeeEstimatesProps = {
selectedAsset: Asset;
};

export function useGetWithdrawalFeeEstimates({ selectedAsset }: UseGetWithdrawalFeeEstimatesProps) {
const { data: feeData } = useFeeData({ chainId: selectedAsset.L1chainId });
const ethConversionRate = useConversionRate({ asset: 'ethereum', refetch: false });

const proveGas = selectedAsset.protocol === 'CCTP' ? 0n : OP_PROVE_GAS;
let finalizeGas;
if (selectedAsset.protocol === 'CCTP') {
finalizeGas = CCTP_FINALIZE_GAS;
} else {
finalizeGas = selectedAsset.L2symbol === 'ETH' ? OP_FINALIZE_ETH_GAS : OP_FINALIZE_ERC20_GAS;
}

const proveEstimate = parseFloat(formatEther((feeData?.gasPrice ?? 0n) * proveGas));
const finalizeEstimate = parseFloat(formatEther((feeData?.gasPrice ?? 0n) * finalizeGas));

const proveEstimateFormatted = proveEstimate * (ethConversionRate ?? 0);
const finalizeEstimateFormatted = finalizeEstimate * (ethConversionRate ?? 0);

const fees = {
eth: {
prove: proveEstimate.toFixed(4),
finalize: finalizeEstimate.toFixed(4),
total: (proveEstimate + finalizeEstimate).toFixed(4),
},
usd: {
prove: usdFormatter(proveEstimateFormatted),
finalize: usdFormatter(finalizeEstimateFormatted),
total: usdFormatter(proveEstimateFormatted + finalizeEstimateFormatted),
},
};

return fees;
}

0 comments on commit a4b1470

Please sign in to comment.