@@ -94,10 +94,28 @@ export default function InteractionClient() {
9494 const [ lastSyncTime , setLastSyncTime ] = useState < Date | null > ( null ) ;
9595 const [ isSyncing , setIsSyncing ] = useState ( false ) ;
9696
97+ // Add new state for minting mode toggle
98+ // 'mint' mode: user enters amount to mint, shows amount they'll receive
99+ // 'receive' mode: user enters amount to receive, shows amount that will be minted
100+ const [ mintingMode , setMintingMode ] = useState < 'mint' | 'receive' > ( 'mint' ) ;
101+ const [ receiveAmount , setReceiveAmount ] = useState ( "" ) ;
102+ const [ calculatedMintAmount , setCalculatedMintAmount ] = useState < number > ( 0 ) ;
103+ const [ isCalculatingMintAmount , setIsCalculatingMintAmount ] = useState < boolean > ( false ) ;
97104
98105 const [ tokenAddress , setTokenAddress ] = useState < `0x${string } `> ( "0x0" ) ;
99106 const [ chainId , setChainId ] = useState < SupportedChainId | null > ( null ) ;
100107
108+ const [ tokenDetails , setTokenDetails ] = useState < TokenDetailsState > ( {
109+ tokenName : "" ,
110+ tokenSymbol : "" ,
111+ maxSupply : 0 ,
112+ thresholdSupply : 0 ,
113+ maxExpansionRate : 0 ,
114+ currentSupply : 0 ,
115+ lastMintTimestamp : 0 ,
116+ maxMintableAmount : 0 ,
117+ } ) ;
118+
101119 // Helper function to add delays between requests
102120 const delay = ( ms : number ) => new Promise ( resolve => setTimeout ( resolve , ms ) ) ;
103121
@@ -132,69 +150,43 @@ export default function InteractionClient() {
132150 } , [ ] ) ;
133151
134152 // Function to calculate user amount after fees
135- const calculateUserAmountAfterFees = useCallback ( async ( amount : string ) => {
136- if ( ! amount || ! tokenAddress || ! chainId || isNaN ( Number ( amount ) ) || Number ( amount ) <= 0 ) {
153+ // Simple math: if fee is 0.5%, then userAmount = mintAmount * 0.995
154+ const calculateUserAmountAfterFees = useCallback ( ( amount : string ) => {
155+ if ( ! amount || isNaN ( Number ( amount ) ) || Number ( amount ) <= 0 ) {
137156 setUserAmountAfterFees ( 0 ) ;
138- setIsCalculatingFees ( false ) ;
139157 return ;
140158 }
141159
142- // Validate that decimals is set and is a valid number
143- if ( decimals === undefined || decimals === null || isNaN ( decimals ) ) {
144- console . warn ( "Decimals not set yet, skipping fee calculation" ) ;
145- setIsCalculatingFees ( false ) ;
146- return ;
147- }
148-
149- setIsCalculatingFees ( true ) ;
150-
151- try {
152- const publicClient = getPublicClient ( config , { chainId } ) ;
153- if ( ! publicClient ) {
154- console . warn ( "No public client available, using fallback calculation" ) ;
155- const fallbackAmount = Number ( amount ) * 0.995 ;
156- setUserAmountAfterFees ( isNaN ( fallbackAmount ) ? 0 : fallbackAmount ) ;
157- setIsCalculatingFees ( false ) ;
158- return ;
159- }
160-
161- const userAmount = await makeContractCallWithRetry ( publicClient , {
162- address : tokenAddress ,
163- abi : CONTRIBUTION_ACCOUNTING_TOKEN_ABI ,
164- functionName : "userAmountAfterFees" ,
165- args : [ parseUnits ( amount , decimals ) ] ,
166- } ) ;
160+ const mintAmount = Number ( amount ) ;
161+
162+ // Simple calculation: userAmount = mintAmount * (1 - 0.005)
163+ const userAmount = mintAmount * 0.995 ;
164+
165+ // Round to 6 decimal places for better UX
166+ const roundedUserAmount = Math . round ( userAmount * 1000000 ) / 1000000 ;
167+
168+ setUserAmountAfterFees ( roundedUserAmount ) ;
169+ } , [ ] ) ;
167170
168- const calculatedAmount = Number ( formatUnits ( userAmount as bigint , decimals ) ) ;
169-
170- // Validate the calculated amount
171- if ( isNaN ( calculatedAmount ) || calculatedAmount < 0 ) {
172- console . warn ( "Invalid calculated amount, using fallback" ) ;
173- const fallbackAmount = Number ( amount ) * 0.995 ;
174- setUserAmountAfterFees ( isNaN ( fallbackAmount ) ? 0 : fallbackAmount ) ;
175- } else {
176- setUserAmountAfterFees ( calculatedAmount ) ;
177- }
178- } catch ( error ) {
179- console . error ( "Error calculating user amount after fees:" , error ) ;
180- // Fallback calculation
181- const fallbackAmount = Number ( amount ) * 0.995 ;
182- setUserAmountAfterFees ( isNaN ( fallbackAmount ) ? 0 : fallbackAmount ) ;
183- } finally {
184- setIsCalculatingFees ( false ) ;
171+ // Function to calculate mint amount needed to achieve desired receive amount
172+ // Simple math: if fee is 0.5%, then receiveAmount = mintAmount * 0.995
173+ // Therefore: mintAmount = receiveAmount / 0.995
174+ const calculateMintAmountFromReceive = useCallback ( ( desiredReceiveAmount : string ) => {
175+ if ( ! desiredReceiveAmount || isNaN ( Number ( desiredReceiveAmount ) ) || Number ( desiredReceiveAmount ) <= 0 ) {
176+ setCalculatedMintAmount ( 0 ) ;
177+ return ;
185178 }
186- } , [ tokenAddress , chainId , decimals , makeContractCallWithRetry ] ) ;
187179
188- const [ tokenDetails , setTokenDetails ] = useState < TokenDetailsState > ( {
189- tokenName : "" ,
190- tokenSymbol : "" ,
191- maxSupply : 0 ,
192- thresholdSupply : 0 ,
193- maxExpansionRate : 0 ,
194- currentSupply : 0 ,
195- lastMintTimestamp : 0 ,
196- maxMintableAmount : 0 ,
197- } ) ;
180+ const receiveAmount = Number ( desiredReceiveAmount ) ;
181+
182+ // Simple calculation: mintAmount = receiveAmount / (1 - 0.005)
183+ const mintAmount = receiveAmount / 0.995 ;
184+
185+ // Round to 6 decimal places for better UX
186+ const roundedMintAmount = Math . round ( mintAmount * 1000000 ) / 1000000 ;
187+
188+ setCalculatedMintAmount ( roundedMintAmount ) ;
189+ } , [ ] ) ;
198190
199191 // Add new state for transaction signing
200192 const [ isSigning , setIsSigning ] = useState ( false ) ;
@@ -682,19 +674,31 @@ export default function InteractionClient() {
682674 }
683675 } , [ revokeMinterRoleData , chainId ] ) ;
684676
685- // Calculate user amount after fees when mint amount changes
677+ // Calculate amounts based on minting mode
686678 useEffect ( ( ) => {
687- calculateUserAmountAfterFees ( mintAmount ) ;
688- } , [ mintAmount , calculateUserAmountAfterFees ] ) ;
679+ if ( mintingMode === 'mint' ) {
680+ calculateUserAmountAfterFees ( mintAmount ) ;
681+ }
682+ } , [ mintAmount , calculateUserAmountAfterFees , mintingMode ] ) ;
683+
684+ useEffect ( ( ) => {
685+ if ( mintingMode === 'receive' ) {
686+ calculateMintAmountFromReceive ( receiveAmount ) ;
687+ }
688+ } , [ receiveAmount , calculateMintAmountFromReceive , mintingMode ] ) ;
689689
690690 const handleMint = async ( ) => {
691691 try {
692692 setIsSigning ( true ) ;
693+
694+ // Use the appropriate amount based on minting mode
695+ const amountToMint = mintingMode === 'mint' ? mintAmount : calculatedMintAmount . toString ( ) ;
696+
693697 await mint ( {
694698 abi : CONTRIBUTION_ACCOUNTING_TOKEN_ABI ,
695699 address : tokenAddress ,
696700 functionName : "mint" ,
697- args : [ mintToAddress as `0x${string } `, parseUnits ( mintAmount , decimals ) ]
701+ args : [ mintToAddress as `0x${string } `, parseUnits ( amountToMint , decimals ) ]
698702 } ) ;
699703 } catch ( error ) {
700704 console . error ( "Error minting tokens:" , error ) ;
@@ -1249,59 +1253,141 @@ export default function InteractionClient() {
12491253 </ span >
12501254 </ p >
12511255 </ div >
1252- < div className = "space-y-2" >
1253- < Label htmlFor = "mintAmount" className = "text-sm font-bold text-gray-600 dark:text-yellow-200" > Amount to Mint</ Label >
1254- < div className = "flex gap-2" >
1255- < Input
1256- id = "mintAmount"
1257- type = "number"
1258- placeholder = "Enter amount"
1259- value = { mintAmount }
1260- onChange = { ( e ) => setMintAmount ( Math . min ( Number ( e . target . value ) , tokenDetails . maxMintableAmount ) . toString ( ) ) }
1261- className = "h-10 text-sm bg-white/60 dark:bg-[#2a1a00] border-2 border-gray-200 dark:border-yellow-400/20 text-gray-600 dark:text-yellow-200"
1262- />
1263- < Button
1264- type = "button"
1265- onClick = { ( ) => {
1266- // Set max mintable amount (fees will be deducted from this amount)
1267- const safeMaxAmount = Math . max ( 0 , tokenDetails . maxMintableAmount ) ;
1268- setMintAmount ( safeMaxAmount . toFixed ( 6 ) ) ;
1269- } }
1270- disabled = { tokenDetails . maxMintableAmount === 0 }
1271- className = "h-10 px-3 text-sm bg-gray-500 dark:bg-gray-600 hover:bg-gray-600 dark:hover:bg-gray-700 text-white rounded-xl whitespace-nowrap"
1272- >
1273- Max
1274- </ Button >
1256+ { /* Minting Mode Toggle */ }
1257+ < div className = "space-y-3" >
1258+ < div className = "flex items-center justify-center" >
1259+ < div className = "flex bg-gray-100 dark:bg-[#2a1a00] rounded-xl p-1 border border-gray-200 dark:border-yellow-400/20" >
1260+ < Button
1261+ type = "button"
1262+ onClick = { ( ) => setMintingMode ( 'mint' ) }
1263+ className = { `h-8 px-3 text-xs rounded-lg transition-all ${
1264+ mintingMode === 'mint'
1265+ ? 'bg-[#5cacc5] dark:bg-[#BA9901] text-white shadow-sm'
1266+ : 'bg-transparent text-gray-600 dark:text-yellow-200 hover:bg-gray-50 dark:hover:bg-[#1a1400]'
1267+ } `}
1268+ >
1269+ Mint by Amount
1270+ </ Button >
1271+ < Button
1272+ type = "button"
1273+ onClick = { ( ) => setMintingMode ( 'receive' ) }
1274+ className = { `h-8 px-3 text-xs rounded-lg transition-all ${
1275+ mintingMode === 'receive'
1276+ ? 'bg-[#5cacc5] dark:bg-[#BA9901] text-white shadow-sm'
1277+ : 'bg-transparent text-gray-600 dark:text-yellow-200 hover:bg-gray-50 dark:hover:bg-[#1a1400]'
1278+ } `}
1279+ >
1280+ Mint by Receive
1281+ </ Button >
1282+ </ div >
12751283 </ div >
1276- { mintAmount && ! isNaN ( Number ( mintAmount ) ) && Number ( mintAmount ) > 0 && (
1277- < div className = "mt-2 p-2 rounded-lg bg-blue-50 dark:bg-yellow-400/10 border border-blue-200 dark:border-yellow-400/20" >
1278- < p className = "text-xs text-blue-600 dark:text-yellow-200" >
1279- You will receive: < span
1280- className = "font-bold cursor-help"
1281- title = { `${ userAmountAfterFees || 0 } ${ tokenDetails . tokenSymbol } ` }
1284+
1285+ { /* Mint Mode: User enters amount to mint */ }
1286+ { mintingMode === 'mint' && (
1287+ < div className = "space-y-2" >
1288+ < div className = "flex gap-2" >
1289+ < Input
1290+ id = "mintAmount"
1291+ type = "number"
1292+ placeholder = "Enter amount to mint"
1293+ value = { mintAmount }
1294+ onChange = { ( e ) => setMintAmount ( Math . min ( Number ( e . target . value ) , tokenDetails . maxMintableAmount ) . toString ( ) ) }
1295+ className = "h-10 text-sm bg-white/60 dark:bg-[#2a1a00] border-2 border-gray-200 dark:border-yellow-400/20 text-gray-600 dark:text-yellow-200"
1296+ />
1297+ < Button
1298+ type = "button"
1299+ onClick = { ( ) => {
1300+ const safeMaxAmount = Math . max ( 0 , tokenDetails . maxMintableAmount ) ;
1301+ setMintAmount ( safeMaxAmount . toFixed ( 6 ) ) ;
1302+ } }
1303+ disabled = { tokenDetails . maxMintableAmount === 0 }
1304+ className = "h-10 px-3 text-sm bg-gray-500 dark:bg-gray-600 hover:bg-gray-600 dark:hover:bg-gray-700 text-white rounded-xl whitespace-nowrap"
12821305 >
1283- { isCalculatingFees ? (
1284- "Calculating..."
1285- ) : ! isNaN ( userAmountAfterFees ) && userAmountAfterFees !== null ? (
1286- formatNumber ( userAmountAfterFees )
1287- ) : (
1288- "0"
1289- ) } { tokenDetails . tokenSymbol }
1290- </ span >
1291- < br />
1292- Clowder fee: < span
1293- className = "font-bold cursor-help"
1294- title = { `${ ! isNaN ( userAmountAfterFees ) && userAmountAfterFees !== null ? Number ( mintAmount ) - userAmountAfterFees : 0 } ${ tokenDetails . tokenSymbol } ` }
1306+ Max
1307+ </ Button >
1308+ </ div >
1309+ { mintAmount && ! isNaN ( Number ( mintAmount ) ) && Number ( mintAmount ) > 0 && (
1310+ < div className = "mt-2 p-2 rounded-xl bg-blue-50 dark:bg-yellow-400/10 border border-blue-200 dark:border-yellow-400/20" >
1311+ < p className = "text-xs text-blue-600 dark:text-yellow-200" >
1312+ You will receive: < span
1313+ className = "font-bold cursor-help"
1314+ title = { `${ userAmountAfterFees || 0 } ${ tokenDetails . tokenSymbol } ` }
1315+ >
1316+ { ! isNaN ( userAmountAfterFees ) && userAmountAfterFees !== null ? (
1317+ formatNumber ( userAmountAfterFees )
1318+ ) : (
1319+ "0"
1320+ ) } { tokenDetails . tokenSymbol }
1321+ </ span >
1322+ < br />
1323+ Clowder fee: < span
1324+ className = "font-bold cursor-help"
1325+ title = { `${ ! isNaN ( userAmountAfterFees ) && userAmountAfterFees !== null ? Number ( mintAmount ) - userAmountAfterFees : 0 } ${ tokenDetails . tokenSymbol } ` }
1326+ >
1327+ { ! isNaN ( userAmountAfterFees ) && userAmountAfterFees !== null ? (
1328+ formatNumber ( Number ( mintAmount ) - userAmountAfterFees )
1329+ ) : (
1330+ "0"
1331+ ) } { tokenDetails . tokenSymbol }
1332+ </ span >
1333+ </ p >
1334+ </ div >
1335+ ) }
1336+ </ div >
1337+ ) }
1338+
1339+ { /* Receive Mode: User enters amount to receive */ }
1340+ { mintingMode === 'receive' && (
1341+ < div className = "space-y-2" >
1342+ < div className = "flex gap-2" >
1343+ < Input
1344+ id = "receiveAmount"
1345+ type = "number"
1346+ placeholder = "Enter amount recipent should receive"
1347+ value = { receiveAmount }
1348+ onChange = { ( e ) => setReceiveAmount ( e . target . value ) }
1349+ className = "h-10 text-sm bg-white/60 dark:bg-[#2a1a00] border-2 border-gray-200 dark:border-yellow-400/20 text-gray-600 dark:text-yellow-200"
1350+ />
1351+ < Button
1352+ type = "button"
1353+ onClick = { ( ) => {
1354+ // Set a reasonable max receive amount (slightly less than max mintable due to fees)
1355+ const safeMaxReceiveAmount = Math . max ( 0 , tokenDetails . maxMintableAmount * 0.99 ) ;
1356+ setReceiveAmount ( safeMaxReceiveAmount . toFixed ( 6 ) ) ;
1357+ } }
1358+ disabled = { tokenDetails . maxMintableAmount === 0 }
1359+ className = "h-10 px-3 text-sm bg-gray-500 dark:bg-gray-600 hover:bg-gray-600 dark:hover:bg-gray-700 text-white rounded-xl whitespace-nowrap"
12951360 >
1296- { isCalculatingFees ? (
1297- "Calculating..."
1298- ) : ! isNaN ( userAmountAfterFees ) && userAmountAfterFees !== null ? (
1299- formatNumber ( Number ( mintAmount ) - userAmountAfterFees )
1300- ) : (
1301- "0"
1302- ) } { tokenDetails . tokenSymbol }
1303- </ span >
1304- </ p >
1361+ Max
1362+ </ Button >
1363+ </ div >
1364+ { receiveAmount && ! isNaN ( Number ( receiveAmount ) ) && Number ( receiveAmount ) > 0 && (
1365+ < div className = "mt-2 p-2 rounded-xl bg-green-50 dark:bg-green-400/10 border border-green-200 dark:border-green-400/20" >
1366+ < p className = "text-xs text-green-600 dark:text-green-200" >
1367+ Amount to mint: < span
1368+ className = "font-bold cursor-help"
1369+ title = { `${ calculatedMintAmount || 0 } ${ tokenDetails . tokenSymbol } ` }
1370+ >
1371+ { ! isNaN ( calculatedMintAmount ) && calculatedMintAmount !== null ? (
1372+ formatNumber ( calculatedMintAmount )
1373+ ) : (
1374+ "0"
1375+ ) } { tokenDetails . tokenSymbol }
1376+ </ span >
1377+ < br />
1378+ Clowder fee: < span
1379+ className = "font-bold cursor-help"
1380+ title = { `${ ! isNaN ( calculatedMintAmount ) && calculatedMintAmount !== null ? calculatedMintAmount - Number ( receiveAmount ) : 0 } ${ tokenDetails . tokenSymbol } ` }
1381+ >
1382+ { ! isNaN ( calculatedMintAmount ) && calculatedMintAmount !== null ? (
1383+ formatNumber ( calculatedMintAmount - Number ( receiveAmount ) )
1384+ ) : (
1385+ "0"
1386+ ) } { tokenDetails . tokenSymbol }
1387+ </ span >
1388+ </ p >
1389+ </ div >
1390+ ) }
13051391 </ div >
13061392 ) }
13071393 </ div >
@@ -1320,7 +1406,14 @@ export default function InteractionClient() {
13201406 < div className = "mt-6" >
13211407 < Button
13221408 onClick = { handleMint }
1323- disabled = { ! mintAmount || ! mintToAddress || isMinting || isSigning || ( ! isUserMinter && ! isUserAdmin ) }
1409+ disabled = {
1410+ ! mintToAddress ||
1411+ isMinting ||
1412+ isSigning ||
1413+ ( ! isUserMinter && ! isUserAdmin ) ||
1414+ ( mintingMode === 'mint' && ( ! mintAmount || isNaN ( Number ( mintAmount ) ) || Number ( mintAmount ) <= 0 ) ) ||
1415+ ( mintingMode === 'receive' && ( ! receiveAmount || isNaN ( Number ( receiveAmount ) ) || Number ( receiveAmount ) <= 0 || calculatedMintAmount <= 0 ) )
1416+ }
13241417 className = "w-full h-10 text-sm bg-[#5cacc5] dark:bg-[#BA9901] hover:bg-[#4a9db5] dark:hover:bg-[#a88a01] text-white rounded-xl disabled:opacity-50 disabled:cursor-not-allowed"
13251418 >
13261419 { ! isUserMinter && ! isUserAdmin ? (
0 commit comments