Skip to content

Commit 029b60c

Browse files
Merge pull request #85 from StabilityNexus/Frontend
Added two options for minting
2 parents 9316c57 + 774ce7e commit 029b60c

File tree

1 file changed

+204
-111
lines changed

1 file changed

+204
-111
lines changed

web/src/app/[cat]/InteractionClient.tsx

Lines changed: 204 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)