diff --git a/components/factory/components/MyDenoms.tsx b/components/factory/components/MyDenoms.tsx index 4379b581..4fe18d03 100644 --- a/components/factory/components/MyDenoms.tsx +++ b/components/factory/components/MyDenoms.tsx @@ -10,6 +10,8 @@ import BurnModal from '@/components/factory/modals/BurnModal'; import { UpdateDenomMetadataModal } from '@/components/factory/modals/updateDenomMetadata'; import { PiInfo } from 'react-icons/pi'; import { ExtendedMetadataSDKType, shiftDigits } from '@/utils'; +import { MultiMintModal } from '@/components/factory/modals'; +import { MultiBurnModal } from '../modals/multiMfxBurnModal'; export default function MyDenoms({ denoms, @@ -27,11 +29,20 @@ export default function MyDenoms({ const [searchQuery, setSearchQuery] = useState(''); const router = useRouter(); const [selectedDenom, setSelectedDenom] = useState(null); - const [modalType, setModalType] = useState<'mint' | 'burn' | null>(null); + const [modalType, setModalType] = useState< + 'mint' | 'burn' | 'multimint' | 'multiburn' | 'update' | null + >(null); const handleDenomSelect = (denom: ExtendedMetadataSDKType) => { - setSelectedDenom(denom); - router.push(`/factory?denom=${denom?.base}`, undefined, { shallow: true }); + if (!modalType) { + // Only show denom info if no other modal is active + setSelectedDenom(denom); + setModalType(null); // Ensure no other modal type is set + const modal = document.getElementById('denom-info-modal') as HTMLDialogElement; + if (modal) { + modal.showModal(); + } + } }; useEffect(() => { @@ -41,9 +52,15 @@ export default function MyDenoms({ const metadata = denoms.find(d => d.base === decodedDenom); if (metadata) { setSelectedDenom(metadata); - if (action === 'mint' || action === 'burn') { - setModalType(action); + if ( + action === 'mint' || + action === 'burn' || + action === 'multimint' || + action === 'multiburn' + ) { + setModalType(action as 'mint' | 'burn' | 'multimint' | 'multiburn'); } else { + // Only show denom info if no other action is specified const modal = document.getElementById('denom-info-modal') as HTMLDialogElement; if (modal) { modal.showModal(); @@ -62,14 +79,34 @@ export default function MyDenoms({ router.push('/factory', undefined, { shallow: true }); }; - const handleUpdateModal = (denom: ExtendedMetadataSDKType) => { + const handleUpdateModal = (denom: ExtendedMetadataSDKType, e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); // Stop event from bubbling up to the row setSelectedDenom(denom); + // Important: Don't show the denom info modal + setModalType('update'); // Add this new modal type const modal = document.getElementById('update-denom-metadata-modal') as HTMLDialogElement; if (modal) { modal.showModal(); } }; + const handleSwitchToMultiMint = () => { + setModalType('multimint'); + // Update URL if needed + router.push(`/factory?denom=${selectedDenom?.base}&action=multimint`, undefined, { + shallow: true, + }); + }; + + const handleSwitchToMultiBurn = () => { + setModalType('multiburn'); // Set the modal type to multiburn + // Update URL if needed + router.push(`/factory?denom=${selectedDenom?.base}&action=multiburn`, undefined, { + shallow: true, + }); + }; + const filteredDenoms = useMemo(() => { return denoms.filter(denom => denom?.display.toLowerCase().includes(searchQuery.toLowerCase())); }, [denoms, searchQuery]); @@ -155,21 +192,17 @@ export default function MyDenoms({ key={denom.base} denom={denom} onSelectDenom={() => handleDenomSelect(denom)} - onMint={() => { + onMint={e => { + e.stopPropagation(); setSelectedDenom(denom); setModalType('mint'); - router.push(`/factory?denom=${denom.base}&action=mint`, undefined, { - shallow: true, - }); }} - onBurn={() => { + onBurn={e => { + e.stopPropagation(); setSelectedDenom(denom); setModalType('burn'); - router.push(`/factory?denom=${denom.base}&action=burn`, undefined, { - shallow: true, - }); }} - onUpdate={() => handleUpdateModal(denom)} + onUpdate={e => handleUpdateModal(denom, e)} /> ))} @@ -177,7 +210,12 @@ export default function MyDenoms({ - + + { + // ... existing update logic ... + }} + addPayoutPair={() => { + // ... existing add logic ... + }} + removePayoutPair={index => { + // ... existing remove logic ... + }} + handleMultiMint={async () => { + // ... existing multi mint logic ... + handleCloseModal(); + }} + isSigning={false} + /> + { + // Implementation will be handled in BurnForm + }} + addBurnPair={() => { + // Implementation will be handled in BurnForm + }} + removeBurnPair={index => { + // Implementation will be handled in BurnForm + }} + handleMultiBurn={async () => { + // Implementation will be handled in BurnForm + handleCloseModal(); + }} + isSigning={false} + /> ); } @@ -217,9 +295,9 @@ function TokenRow({ }: { denom: ExtendedMetadataSDKType; onSelectDenom: () => void; - onMint: () => void; - onBurn: () => void; - onUpdate: () => void; + onMint: (e: React.MouseEvent) => void; + onBurn: (e: React.MouseEvent) => void; + onUpdate: (e: React.MouseEvent) => void; }) { return ( - + e.stopPropagation()} // Stop propagation at the cell level + >
- - @@ -289,8 +358,9 @@ function TokenRow({ disabled={denom.base.includes('umfx')} className="btn btn-sm btn-square btn-outline btn-info group" onClick={e => { + e.preventDefault(); e.stopPropagation(); - onUpdate(); + onUpdate(e); }} > diff --git a/components/factory/forms/BurnForm.tsx b/components/factory/forms/BurnForm.tsx index 1975c626..8e49cd04 100644 --- a/components/factory/forms/BurnForm.tsx +++ b/components/factory/forms/BurnForm.tsx @@ -19,6 +19,17 @@ interface BurnPair { amount: string; } +interface BurnFormProps { + isAdmin: boolean; + admin: string; + denom: ExtendedMetadataSDKType; + address: string; + refetch: () => void; + balance: string; + totalSupply: string; + onMultiBurnClick: () => void; +} + export default function BurnForm({ isAdmin, admin, @@ -27,15 +38,8 @@ export default function BurnForm({ refetch, balance, totalSupply, -}: Readonly<{ - isAdmin: boolean; - admin: string; - denom: ExtendedMetadataSDKType; - address: string; - refetch: () => void; - balance: string; - totalSupply: string; -}>) { + onMultiBurnClick, +}: BurnFormProps) { const [amount, setAmount] = useState(''); const [recipient, setRecipient] = useState(address); @@ -330,26 +334,14 @@ export default function BurnForm({ {isMFX && ( )} - {isMFX && ( - setIsModalOpen(false)} - burnPairs={burnPairs} - updateBurnPair={updateBurnPair} - addBurnPair={addBurnPair} - removeBurnPair={removeBurnPair} - handleMultiBurn={handleMultiBurn} - isSigning={isSigning} - /> - )}
); } diff --git a/components/factory/forms/MintForm.tsx b/components/factory/forms/MintForm.tsx index e1b125aa..02135835 100644 --- a/components/factory/forms/MintForm.tsx +++ b/components/factory/forms/MintForm.tsx @@ -27,6 +27,7 @@ export default function MintForm({ refetch, balance, totalSupply, + onMultiMintClick, }: Readonly<{ isAdmin: boolean; admin: string; @@ -35,12 +36,11 @@ export default function MintForm({ refetch: () => void; balance: string; totalSupply: string; + onMultiMintClick: () => void; }>) { const [amount, setAmount] = useState(''); const [recipient, setRecipient] = useState(address); - const [isModalOpen, setIsModalOpen] = useState(false); - const [payoutPairs, setPayoutPairs] = useState([{ address: '', amount: '' }]); const { setToastMessage } = useToast(); const { tx, isSigning, setIsSigning } = useTx(chainName); const { estimateFee } = useFeeEstimation(chainName); @@ -119,67 +119,6 @@ export default function MintForm({ } }; - const handleMultiMint = async () => { - if (payoutPairs.some(pair => !pair.address || !pair.amount || isNaN(Number(pair.amount)))) { - setToastMessage({ - type: 'alert-error', - title: 'Missing fields', - description: 'Please fill in all fields with valid values.', - bgColor: '#e74c3c', - }); - return; - } - setIsSigning(true); - try { - const payoutMsg = payout({ - authority: admin ?? '', - payoutPairs: payoutPairs.map(pair => ({ - address: pair.address, - coin: { - denom: denom.base, - amount: BigInt(parseFloat(pair.amount) * Math.pow(10, exponent)).toString(), - }, - })), - }); - const encodedMessage = Any.fromAmino({ - type: payoutMsg.typeUrl, - value: MsgPayout.encode(payoutMsg.value).finish(), - }); - const msg = submitProposal({ - groupPolicyAddress: admin ?? '', - messages: [encodedMessage], - metadata: '', - proposers: [address ?? ''], - title: `Manifest Module Control: Multi Mint MFX`, - summary: `This proposal includes multiple mint actions for MFX.`, - exec: 0, - }); - - const fee = await estimateFee(address ?? '', [msg]); - await tx([msg], { - fee, - onSuccess: () => { - setPayoutPairs([{ address: '', amount: '' }]); - setIsModalOpen(false); - refetch(); - }, - }); - } catch (error) { - console.error('Error during multi-minting:', error); - } finally { - setIsSigning(false); - } - }; - - const addPayoutPair = () => setPayoutPairs([...payoutPairs, { address: '', amount: '' }]); - const removePayoutPair = (index: number) => - setPayoutPairs(payoutPairs.filter((_, i) => i !== index)); - const updatePayoutPair = (index: number, field: 'address' | 'amount', value: string) => { - const newPairs = [...payoutPairs]; - newPairs[index][field] = value; - setPayoutPairs(newPairs); - }; - return (
@@ -334,24 +273,13 @@ export default function MintForm({ )} )} - - setIsModalOpen(false)} - payoutPairs={payoutPairs} - updatePayoutPair={updatePayoutPair} - addPayoutPair={addPayoutPair} - removePayoutPair={removePayoutPair} - handleMultiMint={handleMultiMint} - isSigning={isSigning} - /> )}
{isMFX && ( - -

- Mint{' '} - - {truncateString(denom.display ?? 'Denom', 20).toUpperCase()} - -

-
- {isLoading ? ( -
+ <> + +
+
+ +
+

+ Mint{' '} + + {truncateString(denom.display ?? 'Denom', 20).toUpperCase()} + +

+
+ {isLoading ? (
-
- ) : ( - - )} + ) : ( + + )} +
-
-
- -
- +
+ +
+ + + {/* Render MultiMintModal at the same level */} + { + const newPairs = [...payoutPairs]; + newPairs[index][field] = value; + setPayoutPairs(newPairs); + }} + addPayoutPair={() => setPayoutPairs([...payoutPairs, { address: '', amount: '' }])} + removePayoutPair={index => setPayoutPairs(payoutPairs.filter((_, i) => i !== index))} + handleMultiMint={async () => { + // ... handle multi mint logic ... + handleMultiMintClose(); + }} + isSigning={false} // Add your signing state here + /> + ); } diff --git a/components/factory/modals/denomInfo.tsx b/components/factory/modals/denomInfo.tsx index 10d54755..536b75b3 100644 --- a/components/factory/modals/denomInfo.tsx +++ b/components/factory/modals/denomInfo.tsx @@ -7,22 +7,14 @@ import { useRouter } from 'next/router'; export const DenomInfoModal: React.FC<{ denom: MetadataSDKType | null; modalId: string; -}> = ({ denom, modalId }) => { - const router = useRouter(); - - const handleClose = () => { - const { pathname, query } = router; - const { denom: _, ...restQuery } = query; - router.push({ pathname, query: restQuery }, undefined, { shallow: true }); - }; - + isOpen: boolean; + onClose: () => void; +}> = ({ denom, modalId, isOpen, onClose }) => { return ( - +
-
- + +

Denom Details

@@ -63,7 +55,7 @@ export const DenomInfoModal: React.FC<{
-
+
diff --git a/components/factory/modals/index.ts b/components/factory/modals/index.ts index 0e511c5e..1554bc63 100644 --- a/components/factory/modals/index.ts +++ b/components/factory/modals/index.ts @@ -1,2 +1,3 @@ export * from './denomInfo'; export * from './updateDenomMetadata'; +export * from './multiMfxMintModal'; diff --git a/components/factory/modals/multiMfxBurnModal.tsx b/components/factory/modals/multiMfxBurnModal.tsx index 92c341eb..6dec0aee 100644 --- a/components/factory/modals/multiMfxBurnModal.tsx +++ b/components/factory/modals/multiMfxBurnModal.tsx @@ -1,5 +1,10 @@ import React from 'react'; import { PiPlusCircle, PiMinusCircle } from 'react-icons/pi'; +import { Formik, Form, FieldArray, Field, FieldProps } from 'formik'; +import Yup from '@/utils/yupExtensions'; +import { NumberInput, TextInput } from '@/components/react/inputs'; +import { PiAddressBook } from 'react-icons/pi'; +import { PlusIcon, MinusIcon } from '@/components/icons'; interface BurnPair { address: string; @@ -17,6 +22,23 @@ interface MultiBurnModalProps { isSigning: boolean; } +const BurnPairSchema = Yup.object().shape({ + address: Yup.string().manifestAddress().required('Required'), + amount: Yup.number().positive('Amount must be positive').required('Required'), +}); + +const MultiBurnSchema = Yup.object().shape({ + burnPairs: Yup.array() + .of(BurnPairSchema) + .min(1, 'At least one burn pair is required') + .test('unique-address', 'Addresses must be unique', function (pairs) { + if (!pairs) return true; + const addresses = pairs.map(pair => pair.address); + const uniqueAddresses = new Set(addresses); + return uniqueAddresses.size === addresses.length; + }), +}); + export function MultiBurnModal({ isOpen, onClose, @@ -29,63 +51,150 @@ export function MultiBurnModal({ }: MultiBurnModalProps) { return ( -
-

Multi Burn MFX

-
-
- {burnPairs.map((pair, index) => ( -
-
- - updateBurnPair(index, 'address', e.target.value)} - /> -
-
- - updateBurnPair(index, 'amount', e.target.value)} - /> -
- -
- ))} -
-
- - + +

+ Multi Burn MFX +

+
+ + {({ values, isValid, setFieldValue }) => ( +
+
+
Burn Pairs
+ +
+ +
+ + {({ remove }) => ( +
+ {values.burnPairs.map((pair, index) => ( +
+ {index > 0 && ( +
#{index + 1}
+ )} +
+ + {({ field, meta }: FieldProps) => ( +
+ { + updateBurnPair(index, 'address', ''); + setFieldValue(`burnPairs.${index}.address`, ''); + }} + className="btn btn-primary btn-sm text-white absolute right-2 top-1/2 transform -translate-y-1/2" + > + + + } + /> + {meta.touched && meta.error && ( +
+ )} +
+ )} +
+
+
+ + {({ field, meta }: FieldProps) => ( +
+ + {meta.touched && meta.error && ( +
+ )} +
+ )} +
+
+ {index > 0 && ( + + )} +
+ ))} +
+ )} +
+
+ +
+ + +
+
+ )} +
diff --git a/components/factory/modals/multiMfxMintModal.tsx b/components/factory/modals/multiMfxMintModal.tsx index 7e4c608a..1c9db485 100644 --- a/components/factory/modals/multiMfxMintModal.tsx +++ b/components/factory/modals/multiMfxMintModal.tsx @@ -1,6 +1,10 @@ // MultiMintModal.tsx -import React from 'react'; -import { PiPlusCircle, PiMinusCircle } from 'react-icons/pi'; +import React, { useRef } from 'react'; +import { Formik, Form, FieldArray, Field, FieldProps } from 'formik'; +import Yup from '@/utils/yupExtensions'; +import { NumberInput, TextInput } from '@/components/react/inputs'; +import { PiAddressBook } from 'react-icons/pi'; +import { TrashIcon, PlusIcon, MinusIcon } from '@/components/icons'; interface PayoutPair { address: string; @@ -18,6 +22,23 @@ interface MultiMintModalProps { isSigning: boolean; } +const PayoutPairSchema = Yup.object().shape({ + address: Yup.string().manifestAddress().required('Required'), + amount: Yup.number().positive('Amount must be positive').required('Required'), +}); + +const MultiMintSchema = Yup.object().shape({ + payoutPairs: Yup.array() + .of(PayoutPairSchema) + .min(1, 'At least one payout pair is required') + .test('unique-address', 'Addresses must be unique', function (pairs) { + if (!pairs) return true; + const addresses = pairs.map(pair => pair.address); + const uniqueAddresses = new Set(addresses); + return uniqueAddresses.size === addresses.length; + }), +}); + export function MultiMintModal({ isOpen, onClose, @@ -30,63 +51,149 @@ export function MultiMintModal({ }: MultiMintModalProps) { return ( -
-

Multi Mint MFX

-
-
- {payoutPairs.map((pair, index) => ( -
-
- - updatePayoutPair(index, 'address', e.target.value)} - /> -
-
- - updatePayoutPair(index, 'amount', e.target.value)} - /> -
- -
- ))} -
-
- - + +

+ Multi Mint MFX +

+
+ + {({ values, isValid, setFieldValue }) => ( +
+
+
Payout Pairs
+ +
+ +
+ + {({ remove }) => ( +
+ {values.payoutPairs.map((pair, index) => ( +
+ {index > 0 && ( +
#{index + 1}
+ )} +
+ + {({ field, meta }: FieldProps) => ( +
+ { + updatePayoutPair(index, 'address', ''); + setFieldValue(`payoutPairs.${index}.address`, ''); + }} + className="btn btn-primary btn-sm text-white absolute right-2 top-1/2 transform -translate-y-1/2" + > + + + } + /> + {meta.touched && meta.error && ( +
+
+
+ )} +
+ )} +
+
+
+ + {({ field, meta }: FieldProps) => ( +
+ + {meta.touched && meta.error && ( +
+
+
+ )} +
+ )} +
+
+ {index > 0 && ( + + )} +
+ ))} +
+ )} +
+
+ +
+ + +
+
+ )} +