Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: token factory group controls #158

Closed
wants to merge 12 commits into from
Binary file modified bun.lockb
Binary file not shown.
492 changes: 321 additions & 171 deletions components/factory/components/MyDenoms.tsx

Large diffs are not rendered by default.

402 changes: 161 additions & 241 deletions components/factory/forms/BurnForm.tsx

Large diffs are not rendered by default.

304 changes: 155 additions & 149 deletions components/factory/forms/MintForm.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import { useFeeEstimation, useTx } from '@/hooks';
import { osmosis } from '@liftedinit/manifestjs';
import { osmosis, cosmos } from '@liftedinit/manifestjs';

import { parseNumberToBigInt, shiftDigits, ExtendedMetadataSDKType, truncateString } from '@/utils';
import { MdContacts } from 'react-icons/md';
Expand All @@ -10,32 +10,33 @@ import Yup from '@/utils/yupExtensions';
import { NumberInput, TextInput } from '@/components/react/inputs';
import { TailwindModal } from '@/components/react/modal';
import env from '@/config/env';
import { Any } from '@liftedinit/manifestjs/dist/codegen/google/protobuf/any';
import { MsgMint } from '@liftedinit/manifestjs/dist/codegen/osmosis/tokenfactory/v1beta1/tx';
import { useGroupAddressStore } from '@/stores/groupAddressStore';

export default function MintForm({
isAdmin,
denom,
address,
refetch,
balance,
totalSupply,
onMultiMintClick,
isGroup,
}: Readonly<{
isAdmin: boolean;
denom: ExtendedMetadataSDKType;
address: string;
refetch: () => void;
balance: string;
totalSupply: string;
onMultiMintClick: () => void;
isGroup: boolean;
}>) {
const [amount, setAmount] = useState('');
const [recipient, setRecipient] = useState(address);
const [isContactsOpen, setIsContactsOpen] = useState(false);

const { selectedAddress } = useGroupAddressStore();
const { tx, isSigning, setIsSigning } = useTx(env.chain);
const { estimateFee } = useFeeEstimation(env.chain);
const { mint } = osmosis.tokenfactory.v1beta1.MessageComposer.withTypeUrl;

const { submitProposal } = cosmos.group.v1.MessageComposer.withTypeUrl;
const exponent =
denom?.denom_units?.find((unit: { denom: string }) => unit.denom === denom.display)?.exponent ||
0;
Expand Down Expand Up @@ -64,8 +65,32 @@ export default function MintForm({
mintToAddress: recipient,
});

const fee = await estimateFee(address ?? '', [msg]);
await tx([msg], {
const mintProp = submitProposal({
groupPolicyAddress: selectedAddress ?? '',
messages: [
Any.fromPartial({
typeUrl: MsgMint.typeUrl,
value: MsgMint.encode(
mint({
amount: {
amount: amountInBaseUnits,
denom: denom.base,
},
sender: selectedAddress ?? '',
mintToAddress: recipient,
}).value
).finish(),
}),
],
metadata: '',
proposers: [address],
title: `Mint Tokens`,
summary: `This proposal will mint ${amount} ${denom.display.split('/').pop()} to ${recipient}`,
exec: 0,
});

Comment on lines +68 to +91
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Ensure 'selectedAddress' is defined before use

The selectedAddress is used with a fallback to an empty string when constructing the proposal message. If selectedAddress is undefined or empty, it may lead to invalid transactions. Please ensure that selectedAddress is correctly defined when isGroup is true to prevent potential errors.

const fee = await estimateFee(address ?? '', [isGroup ? mintProp : msg]);
await tx([isGroup ? mintProp : msg], {
fee,
onSuccess: () => {
setAmount('');
Expand All @@ -82,157 +107,138 @@ export default function MintForm({
return (
<div className="animate-fadeIn text-sm z-10">
<div className="rounded-lg">
{isMFX && !isAdmin ? (
<div className="w-full p-2 justify-center items-center my-auto leading-tight text-xl flex flex-col font-medium text-pretty">
<span>You must be a member of the admin group to mint MFX.</span>
<div className="grid grid-cols-2 gap-6">
<div>
<p className="text-sm font-light text-gray-500 dark:text-gray-400 mb-2">NAME</p>
<div className="bg-base-300 p-4 rounded-md">
<p className="font-semibold text-md truncate text-black dark:text-white">
{denom.name}
</p>
</div>
</div>
) : (
<>
<div className="grid grid-cols-2 gap-6">
<div>
<p className="text-sm font-light text-gray-500 dark:text-gray-400 mb-2">NAME</p>
<div className="bg-base-300 p-4 rounded-md">
<p className="font-semibold text-md truncate text-black dark:text-white">
{denom.name}
</p>
</div>
</div>

<div>
<p className="text-sm font-light text-gray-500 truncate dark:text-gray-400 mb-2">
CIRCULATING SUPPLY
</p>
<div className="bg-base-300 p-4 rounded-md">
<p className="font-semibold text-md truncate text-black dark:text-white">
{Number(shiftDigits(totalSupply, -exponent)).toLocaleString(undefined, {
maximumFractionDigits: exponent,
})}{' '}
</p>
</div>
</div>
<div>
<p className="text-sm font-light text-gray-500 truncate dark:text-gray-400 mb-2">
CIRCULATING SUPPLY
</p>
<div className="bg-base-300 p-4 rounded-md">
<p className="font-semibold text-md truncate text-black dark:text-white">
{Number(shiftDigits(totalSupply, -exponent)).toLocaleString(undefined, {
maximumFractionDigits: exponent,
})}{' '}
</p>
</div>
{!denom.base.includes('umfx') && (
<Formik
initialValues={{ amount: '', recipient: address }}
validationSchema={MintSchema}
onSubmit={values => {
setAmount(values.amount);
setRecipient(values.recipient);
handleMint();
}}
validateOnChange={true}
validateOnBlur={true}
>
{({ isValid, dirty, setFieldValue, errors, touched }) => (
<Form>
<div className="flex space-x-4 mt-8">
<div className="flex-grow relative">
<NumberInput
showError={false}
label="AMOUNT"
name="amount"
placeholder="Enter amount"
value={amount}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setAmount(e.target.value);
setFieldValue('amount', e.target.value);
}}
className={`input input-bordered w-full ${
touched.amount && errors.amount ? 'input-error' : ''
}`}
/>
{touched.amount && errors.amount && (
<div
className="tooltip tooltip-bottom tooltip-open tooltip-error bottom-0 absolute left-1/2 transform -translate-x-1/2 translate-y-full mt-1 z-50 text-white text-xs"
data-tip={errors.amount}
>
<div className="w-0 h-0"></div>
</div>
)}
</div>
<div className="flex-grow relative">
<TextInput
showError={false}
label="RECIPIENT"
name="recipient"
placeholder="Recipient address"
value={recipient}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setRecipient(e.target.value);
setFieldValue('recipient', e.target.value);
}}
className={`input input-bordered w-full transition-none ${
touched.recipient && errors.recipient ? 'input-error' : ''
}`}
rightElement={
<button
type="button"
aria-label="contacts-btn"
onClick={() => setIsContactsOpen(true)}
className="btn btn-primary btn-sm text-white"
>
<MdContacts className="w-5 h-5" />
</button>
}
/>
{touched.recipient && errors.recipient && (
<div
className="tooltip tooltip-bottom tooltip-open tooltip-error bottom-0 absolute left-1/2 transform -translate-x-1/2 translate-y-full mt-1 z-50 text-white text-xs"
data-tip={errors.recipient}
>
<div className="w-0 h-0"></div>
</div>
)}
</div>
</div>
{!denom.base.includes('umfx') && (
<Formik
initialValues={{ amount: '', recipient: address }}
validationSchema={MintSchema}
onSubmit={values => {
setAmount(values.amount);
setRecipient(values.recipient);
handleMint();
}}
validateOnChange={true}
validateOnBlur={true}
>
{({ isValid, dirty, setFieldValue, errors, touched }) => (
<Form>
<div className="flex space-x-4 mt-8">
<div className="flex-grow relative">
<NumberInput
showError={false}
label="AMOUNT"
name="amount"
placeholder="Enter amount"
value={amount}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setAmount(e.target.value);
setFieldValue('amount', e.target.value);
}}
className={`input input-bordered w-full ${
touched.amount && errors.amount ? 'input-error' : ''
}`}
/>
{touched.amount && errors.amount && (
<div
className="tooltip tooltip-bottom tooltip-open tooltip-error bottom-0 absolute left-1/2 transform -translate-x-1/2 translate-y-full mt-1 z-50 text-white text-xs"
data-tip={errors.amount}
>
<div className="w-0 h-0"></div>
</div>
</div>
<div className="flex justify-end mt-6">
{!denom.base.includes('umfx') && (
)}
</div>
<div className="flex-grow relative">
<TextInput
showError={false}
label="RECIPIENT"
name="recipient"
placeholder="Recipient address"
value={recipient}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setRecipient(e.target.value);
setFieldValue('recipient', e.target.value);
}}
className={`input input-bordered w-full transition-none ${
touched.recipient && errors.recipient ? 'input-error' : ''
}`}
rightElement={
<button
type="submit"
aria-label={`mint-btn-${denom.display}`}
className="btn btn-gradient btn-md flex-grow text-white"
disabled={isSigning || !isValid || !dirty}
type="button"
aria-label="contacts-btn"
onClick={() => setIsContactsOpen(true)}
className="btn btn-primary btn-sm text-white"
>
{isSigning ? (
<span className="loading loading-dots loading-xs"></span>
) : (
`Mint ${
denom.display.startsWith('factory')
? denom.display.split('/').pop()?.toUpperCase()
: truncateString(denom.display, 12)
}`
)}
<MdContacts className="w-5 h-5" />
</button>
)}
</div>
<TailwindModal
isOpen={isContactsOpen}
setOpen={setIsContactsOpen}
showContacts={true}
currentAddress={address}
onSelect={(selectedAddress: string) => {
setRecipient(selectedAddress);
setFieldValue('recipient', selectedAddress);
}}
}
/>
</Form>
)}
</Formik>
{touched.recipient && errors.recipient && (
<div
className="tooltip tooltip-bottom tooltip-open tooltip-error bottom-0 absolute left-1/2 transform -translate-x-1/2 translate-y-full mt-1 z-50 text-white text-xs"
data-tip={errors.recipient}
>
<div className="w-0 h-0"></div>
</div>
)}
</div>
</div>
<div className="flex justify-end mt-6">
{!denom.base.includes('umfx') && (
<button
type="submit"
aria-label={`mint-btn-${denom.display}`}
className="btn btn-gradient btn-md flex-grow text-white"
disabled={isSigning || !isValid || !dirty}
>
{isSigning ? (
<span className="loading loading-dots loading-xs"></span>
) : (
`Mint ${
denom.display.startsWith('factory')
? denom.display.split('/').pop()?.toUpperCase()
: truncateString(denom.display, 12)
}`
)}
</button>
)}
</div>
<TailwindModal
isOpen={isContactsOpen}
setOpen={setIsContactsOpen}
showContacts={true}
currentAddress={address}
onSelect={(selectedAddress: string) => {
setRecipient(selectedAddress);
setFieldValue('recipient', selectedAddress);
}}
/>
</Form>
)}
</>
</Formik>
)}
</div>
{isMFX && (
<button
type="button"
onClick={onMultiMintClick}
className="btn btn-gradient btn-md flex-grow w-full text-white mt-6"
aria-label="multi-mint-button"
disabled={!isAdmin}
>
Multi Mint
</button>
)}
</div>
);
}
6 changes: 3 additions & 3 deletions components/factory/modals/BurnModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export default function BurnModal({
isOpen,
onClose,
onSwitchToMultiBurn,
isGroup,
}: {
denom: ExtendedMetadataSDKType | null;
address: string;
Expand All @@ -22,6 +23,7 @@ export default function BurnModal({
isOpen: boolean;
onClose: () => void;
onSwitchToMultiBurn: () => void;
isGroup: boolean;
}) {
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
Expand Down Expand Up @@ -88,14 +90,12 @@ export default function BurnModal({
<div className="skeleton h-[17rem] max-h-72 w-full"></div>
) : (
<BurnForm
isAdmin={isAdmin ?? false}
admin={poaAdmin ?? ''}
balance={balance}
totalSupply={totalSupply}
refetch={refetch}
address={address}
denom={denom}
onMultiBurnClick={onSwitchToMultiBurn}
isGroup={isGroup}
/>
)}
</div>
Expand Down
Loading
Loading