1
1
'use client' ;
2
- import { useEffect } from 'react' ;
3
- import { Stack , styled , Typography } from '@mui/material' ;
2
+ import { useEffect , useState } from 'react' ;
3
+ import { ExpandMore as ExpandMoreIcon } from '@mui/icons-material' ;
4
+ import { Stack , styled , Typography , IconButton , Collapse } from '@mui/material' ;
4
5
import { formatUnits , parseUnits } from 'viem' ;
5
6
import { useAccount } from 'wagmi' ;
6
7
import { ExtendedTooltip as Tooltip } from '~/components' ;
8
+ import { useQuoteContext } from '~/contexts/QuoteContext' ;
7
9
import {
8
10
useExternalServices ,
9
11
usePoolAccountsContext ,
@@ -13,9 +15,12 @@ import {
13
15
} from '~/hooks' ;
14
16
import { EventType } from '~/types' ;
15
17
import { getUsdBalance , truncateAddress } from '~/utils' ;
18
+ import { FeeBreakdown } from './FeeBreakdown' ;
16
19
17
20
export const DataSection = ( ) => {
18
21
const { address } = useAccount ( ) ;
22
+ const [ isFeeBreakdownOpen , setIsFeeBreakdownOpen ] = useState ( false ) ;
23
+ const { quoteState } = useQuoteContext ( ) ;
19
24
const {
20
25
balanceBN : { symbol, decimals } ,
21
26
price,
@@ -44,6 +49,8 @@ export const DataSection = () => {
44
49
isQuoteValid,
45
50
isExpired,
46
51
feeBPS : quoteFeesBPS ,
52
+ baseFeeBPS : quoteBaseFeeBPS ,
53
+ extraGasAmountETH : quoteExtraGasAmountETH ,
47
54
quoteCommitment,
48
55
} = useRequestQuote ( {
49
56
getQuote : getQuote || ( ( ) => Promise . reject ( new Error ( 'No relayer data' ) ) ) ,
@@ -80,9 +87,6 @@ export const DataSection = () => {
80
87
const relayerFees = ( BigInt ( effectiveFeeBPS ) * parseUnits ( amount , decimals ) ) / 100n / 100n ;
81
88
82
89
const fees = isDeposit ? aspDataFees : relayerFees ;
83
- const feeFormatted = formatUnits ( fees , decimals ) ;
84
- const feeUSD = getUsdBalance ( price , feeFormatted , decimals ) ;
85
- const feeText = `${ feeFormatted } ${ symbol } (~ ${ feeUSD } USD)` ;
86
90
87
91
// Create full precision tooltips - show complete decimal precision
88
92
const formatFullPrecision = ( value : bigint , decimals : number ) => {
@@ -92,25 +96,53 @@ export const DataSection = () => {
92
96
}
93
97
const integerPart = valueStr . slice ( 0 , - decimals ) ;
94
98
const decimalPart = valueStr . slice ( - decimals ) ;
95
- return `${ integerPart } .${ decimalPart } ` ;
96
- } ;
99
+ const result = `${ integerPart } .${ decimalPart } ` ;
100
+
101
+ // Remove trailing zeros, but keep at least 2 decimal places
102
+ const trimmed = result . replace ( / \. ? 0 + $ / , '' ) ;
103
+ if ( ! trimmed . includes ( '.' ) ) {
104
+ return `${ trimmed } .00` ;
105
+ }
106
+ const decimalIndex = trimmed . indexOf ( '.' ) ;
107
+ const currentDecimals = trimmed . length - decimalIndex - 1 ;
108
+ if ( currentDecimals < 2 ) {
109
+ return trimmed + '0' . repeat ( 2 - currentDecimals ) ;
110
+ }
97
111
98
- const feeTooltip = `${ formatFullPrecision ( fees , decimals ) } ${ symbol } ` ;
112
+ return trimmed ;
113
+ } ;
99
114
100
115
const feesCollectorAddress = isDeposit
101
116
? selectedPoolInfo . entryPointAddress
102
117
: currentSelectedRelayerData ?. relayerAddress ;
103
118
const feesCollector = `OxBow (${ truncateAddress ( feesCollectorAddress ) } )` ;
104
119
105
120
const amountUSD = getUsdBalance ( price , amount , decimals ) ;
121
+
122
+ // Value is now the actual amount being withdrawn (amount minus fees)
106
123
const amountWithFeeBN = parseUnits ( amount , decimals ) - fees ;
107
124
const amountWithFee = formatUnits ( amountWithFeeBN , decimals ) ;
108
125
const amountWithFeeUSD = getUsdBalance ( price , amountWithFee , decimals ) ;
109
-
110
- const valueText = `${ amountWithFee } ${ symbol } (~ ${ amountWithFeeUSD } USD)` ;
126
+ const valueText = `${ parseFloat ( amountWithFee ) . toString ( ) } ${ symbol } (~$${ parseFloat ( amountWithFeeUSD . replace ( '$' , '' ) ) . toFixed ( 2 ) } USD)` ;
111
127
const valueTooltip = `${ formatFullPrecision ( amountWithFeeBN , decimals ) } ${ symbol } ` ;
112
128
113
- const totalText = `~${ amount . slice ( 0 , 6 ) } ${ symbol } (~ ${ amountUSD } USD)` ;
129
+ // Net Fee calculation (includes extra gas amount if enabled)
130
+ let netFeeAmount = fees ;
131
+ if ( quoteState . extraGas && quoteExtraGasAmountETH ) {
132
+ // Convert extraGasAmountETH from wei to token amount
133
+ const extraGasETH = parseFloat ( formatUnits ( BigInt ( quoteExtraGasAmountETH ) , 18 ) ) ;
134
+ const extraGasInToken = ( extraGasETH * price ) / parseFloat ( formatUnits ( parseUnits ( '1' , decimals ) , decimals ) ) ;
135
+
136
+ // Convert to fixed decimal string to avoid scientific notation
137
+ const extraGasAmountBN = parseUnits ( extraGasInToken . toFixed ( decimals ) , decimals ) ;
138
+ netFeeAmount = fees + extraGasAmountBN ;
139
+ }
140
+ const netFeeFormatted = formatUnits ( netFeeAmount , decimals ) ;
141
+ const netFeeUSD = getUsdBalance ( price , netFeeFormatted , decimals ) ;
142
+ const netFeeText = `${ parseFloat ( netFeeFormatted ) . toString ( ) } ${ symbol } (~$${ parseFloat ( netFeeUSD . replace ( '$' , '' ) ) . toFixed ( 2 ) } USD)` ;
143
+ const netFeeTooltip = `${ formatFullPrecision ( netFeeAmount , decimals ) } ${ symbol } ` ;
144
+
145
+ const totalText = `~${ amount . slice ( 0 , 6 ) } ${ symbol } (~$${ parseFloat ( amountUSD . replace ( '$' , '' ) ) . toFixed ( 2 ) } USD)` ;
114
146
const totalAmountBN = parseUnits ( amount , decimals ) ;
115
147
const totalTooltip = `${ formatFullPrecision ( totalAmountBN , decimals ) } ${ symbol } ` ;
116
148
@@ -157,12 +189,6 @@ export const DataSection = () => {
157
189
< Value variant = 'body2' > { feesCollector } </ Value >
158
190
</ Tooltip >
159
191
</ Row >
160
- < Row >
161
- < Label variant = 'body2' > Fees:</ Label >
162
- < Tooltip title = { feeTooltip } placement = 'top' >
163
- < Value variant = 'body2' > { feeText } </ Value >
164
- </ Tooltip >
165
- </ Row >
166
192
{ actionType === EventType . WITHDRAWAL && ( isQuoteValid || isExpired ) && (
167
193
< Row >
168
194
< Label variant = 'body2' > Quote expires:</ Label >
@@ -179,6 +205,40 @@ export const DataSection = () => {
179
205
< Value variant = 'body2' > { valueText } </ Value >
180
206
</ Tooltip >
181
207
</ Row >
208
+
209
+ { /* Net Fee row with dropdown for withdrawals */ }
210
+ { actionType === EventType . WITHDRAWAL && isQuoteValid && quoteFeesBPS !== null && quoteBaseFeeBPS !== null && (
211
+ < >
212
+ < Row >
213
+ < Label variant = 'body2' > Net Fee:</ Label >
214
+ < FeeRow >
215
+ < Tooltip title = { netFeeTooltip } placement = 'top' >
216
+ < NetFeeValue isExtraGasEnabled = { quoteState . extraGas } variant = 'body2' >
217
+ { netFeeText }
218
+ </ NetFeeValue >
219
+ </ Tooltip >
220
+ < ExpandIconButton
221
+ onClick = { ( ) => setIsFeeBreakdownOpen ( ! isFeeBreakdownOpen ) }
222
+ expanded = { isFeeBreakdownOpen }
223
+ >
224
+ < ExpandMoreIcon />
225
+ </ ExpandIconButton >
226
+ </ FeeRow >
227
+ </ Row >
228
+
229
+ { /* Collapsible Fee Breakdown */ }
230
+ < Collapse in = { isFeeBreakdownOpen } >
231
+ < FeeBreakdownContainer >
232
+ < FeeBreakdown
233
+ feeBPS = { quoteFeesBPS }
234
+ baseFeeBPS = { quoteBaseFeeBPS }
235
+ extraGasAmountETH = { quoteExtraGasAmountETH }
236
+ amount = { amount }
237
+ />
238
+ </ FeeBreakdownContainer >
239
+ </ Collapse >
240
+ </ >
241
+ ) }
182
242
</ Stack >
183
243
) }
184
244
@@ -260,3 +320,36 @@ const FlashingExpiredTimer = styled(Value)(({ theme }) => ({
260
320
} ,
261
321
} ,
262
322
} ) ) ;
323
+
324
+ const FeeRow = styled ( 'div' ) ( {
325
+ display : 'flex' ,
326
+ alignItems : 'center' ,
327
+ gap : '4px' ,
328
+ } ) ;
329
+
330
+ const NetFeeValue = styled ( Value , {
331
+ shouldForwardProp : ( prop ) => prop !== 'isExtraGasEnabled' ,
332
+ } ) < { isExtraGasEnabled ?: boolean } > ( ( { theme, isExtraGasEnabled } ) => ( {
333
+ color : isExtraGasEnabled ? theme . palette . success . main : theme . palette . text . primary ,
334
+ fontWeight : isExtraGasEnabled ? 600 : 400 ,
335
+ } ) ) ;
336
+
337
+ const ExpandIconButton = styled ( IconButton , {
338
+ shouldForwardProp : ( prop ) => prop !== 'expanded' ,
339
+ } ) < { expanded ?: boolean } > ( ( { theme, expanded } ) => ( {
340
+ padding : '2px' ,
341
+ minWidth : '24px' ,
342
+ minHeight : '24px' ,
343
+ transform : expanded ? 'rotate(180deg)' : 'rotate(0deg)' ,
344
+ transition : theme . transitions . create ( 'transform' , {
345
+ duration : theme . transitions . duration . shortest ,
346
+ } ) ,
347
+ '& .MuiSvgIcon-root' : {
348
+ fontSize : '18px' ,
349
+ } ,
350
+ } ) ) ;
351
+
352
+ const FeeBreakdownContainer = styled ( 'div' ) ( {
353
+ marginTop : '8px' ,
354
+ marginLeft : '16px' ,
355
+ } ) ;
0 commit comments