Skip to content

Commit

Permalink
feat: tranfer token ownership (#152)
Browse files Browse the repository at this point in the history
---------

Signed-off-by: Félix C. Morency <[email protected]>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
  • Loading branch information
fmorency and coderabbitai[bot] authored Dec 12, 2024
1 parent 88e8e7b commit 0b5f1a9
Show file tree
Hide file tree
Showing 9 changed files with 348 additions and 14 deletions.
Binary file modified bun.lockb
Binary file not shown.
58 changes: 53 additions & 5 deletions components/factory/components/MyDenoms.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@ import { useState, useEffect, useMemo } from 'react';
import { useRouter } from 'next/router';
import { DenomImage } from './DenomImage';
import Link from 'next/link';
import { truncateString } from '@/utils';
import { SearchIcon, MintIcon, BurnIcon } from '@/components/icons';
import { truncateString, ExtendedMetadataSDKType, shiftDigits, formatTokenDisplay } from '@/utils';
import { SearchIcon, MintIcon, BurnIcon, TransferIcon } from '@/components/icons';
import { DenomInfoModal } from '@/components/factory/modals/denomInfo';
import MintModal from '@/components/factory/modals/MintModal';
import BurnModal from '@/components/factory/modals/BurnModal';
import { UpdateDenomMetadataModal } from '@/components/factory/modals/updateDenomMetadata';
import { PiInfo } from 'react-icons/pi';
import { ExtendedMetadataSDKType, shiftDigits, formatTokenDisplay } from '@/utils';
import { usePoaGetAdmin } from '@/hooks';
import useIsMobile from '@/hooks/useIsMobile';
import TransferModal from '@/components/factory/modals/TransferModal';

export default function MyDenoms({
denoms,
Expand All @@ -27,14 +27,15 @@ export default function MyDenoms({
const [searchQuery, setSearchQuery] = useState('');
const [currentPage, setCurrentPage] = useState(1);
const [openUpdateDenomMetadataModal, setOpenUpdateDenomMetadataModal] = useState(false);
const [openTransferDenomModal, setOpenTransferDenomModal] = useState(false);
const isMobile = useIsMobile();

const pageSize = isMobile ? 5 : 8;

const router = useRouter();
const [selectedDenom, setSelectedDenom] = useState<ExtendedMetadataSDKType | null>(null);
const [modalType, setModalType] = useState<
'mint' | 'burn' | 'multimint' | 'multiburn' | 'update' | 'info' | null
'mint' | 'burn' | 'multimint' | 'multiburn' | 'update' | 'info' | 'transfer' | null
>(null);

const filteredDenoms = useMemo(() => {
Expand Down Expand Up @@ -70,12 +71,18 @@ export default function MyDenoms({
action === 'multimint' ||
action === 'multiburn' ||
action === 'update' ||
action === 'transfer' ||
action === 'info'
) {
setModalType(action as 'mint' | 'burn' | 'multimint' | 'multiburn' | 'update' | 'info');
setModalType(
action as 'mint' | 'burn' | 'multimint' | 'multiburn' | 'update' | 'info' | 'transfer'
);
if (action === 'update') {
setOpenUpdateDenomMetadataModal(true);
}
if (action === 'transfer') {
setOpenTransferDenomModal(true);
}
} else {
setModalType('info');
}
Expand All @@ -90,12 +97,14 @@ export default function MyDenoms({
setSelectedDenom(null);
setModalType(null);
setOpenUpdateDenomMetadataModal(false);
setOpenTransferDenomModal(false);
router.push('/factory', undefined, { shallow: true });
};

const handleUpdateModalClose = () => {
setSelectedDenom(null);
setOpenUpdateDenomMetadataModal(false);
setOpenTransferDenomModal(false);
setModalType(null);
router.push('/factory', undefined, { shallow: true });
};
Expand All @@ -109,6 +118,15 @@ export default function MyDenoms({
router.push(`/factory?denom=${denom.base}&action=update`, undefined, { shallow: true });
};

const handleTransferModal = (denom: ExtendedMetadataSDKType, e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
setSelectedDenom(denom);
setModalType('transfer');
setOpenTransferDenomModal(true);
router.push(`/factory?denom=${denom.base}&action=transfer`, undefined, { shallow: true });
};

const handleSwitchToMultiMint = () => {
setModalType('multimint');
router.push(`/factory?denom=${selectedDenom?.base}&action=multimint`, undefined, {
Expand Down Expand Up @@ -246,6 +264,7 @@ export default function MyDenoms({
shallow: true,
});
}}
onTransfer={e => handleTransferModal(denom, e)}
onUpdate={e => handleUpdateModal(denom, e)}
/>
))}
Expand Down Expand Up @@ -374,6 +393,25 @@ export default function MyDenoms({
}
}}
/>
<TransferModal
modalId="transfer-denom-modal"
openTransferDenomModal={openTransferDenomModal}
setOpenTransferDenomModal={open => {
if (!open) {
handleCloseModal();
} else {
setOpenTransferDenomModal(true);
}
}}
onSuccess={() => {
refetchDenoms();
handleUpdateModalClose();
}}
denom={selectedDenom}
address={address}
isOpen={modalType === 'transfer'}
onClose={handleCloseModal}
/>
</div>
);
}
Expand All @@ -383,12 +421,14 @@ function TokenRow({
onSelectDenom,
onMint,
onBurn,
onTransfer,
onUpdate,
}: {
denom: ExtendedMetadataSDKType;
onSelectDenom: () => void;
onMint: (e: React.MouseEvent) => void;
onBurn: (e: React.MouseEvent) => void;
onTransfer: (e: React.MouseEvent) => void;
onUpdate: (e: React.MouseEvent) => void;
}) {
// Add safety checks for the values
Expand Down Expand Up @@ -447,6 +487,14 @@ function TokenRow({
<BurnIcon className="w-7 h-7 text-current" />
</button>

<button
disabled={denom.base.includes('umfx')}
className="btn btn-md bg-base-300 text-primary btn-square group-hover:bg-secondary hover:outline hover:outline-primary hover:outline-1 outline-none"
onClick={onTransfer}
>
<TransferIcon className="w-7 h-7 text-current" />
</button>

<button
disabled={denom.base.includes('umfx')}
className="btn btn-md bg-base-300 text-primary btn-square group-hover:bg-secondary hover:outline hover:outline-primary hover:outline-1 outline-none"
Expand Down
224 changes: 224 additions & 0 deletions components/factory/modals/TransferModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
import { useEffect } from 'react';
import { ExtendedMetadataSDKType, truncateString } from '@/utils';
import { useDenomAuthorityMetadata, useFeeEstimation, useTx } from '@/hooks';
import { chainName } from '@/config';
import { osmosis } from '@liftedinit/manifestjs';
import { createPortal } from 'react-dom';
import Yup from '@/utils/yupExtensions';
import { Form, Formik, FormikValues } from 'formik';
import { TextInput } from '@/components';
import { useToast } from '@/contexts';

const TokenOwnershipSchema = Yup.object().shape({
newAdmin: Yup.string().required('New admin address is required').manifestAddress(),
});

export default function TransferModal({
openTransferDenomModal,
setOpenTransferDenomModal,
denom,
address,
modalId,
isOpen,
onClose,
onSuccess,
}: {
openTransferDenomModal: boolean;
setOpenTransferDenomModal: (open: boolean) => void;
denom: ExtendedMetadataSDKType | null;
address: string;
modalId: string;
isOpen: boolean;
onClose: () => void;
onSuccess: () => void;
}) {
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape' && isOpen) {
onClose();
}
};

document.addEventListener('keydown', handleEscape);
return () => document.removeEventListener('keydown', handleEscape);
}, [isOpen]);
const { setToastMessage } = useToast();

const handleCloseModal = (formikReset?: () => void) => {
setOpenTransferDenomModal(false);
formikReset?.();
};

const { denomAuthority, isDenomAuthorityLoading } = useDenomAuthorityMetadata(denom?.base ?? '');
const formData = {
denom: denom?.base ?? '',
currentAdmin: denomAuthority,
newAdmin: '',
};

const { tx, isSigning, setIsSigning } = useTx(chainName);
const { estimateFee } = useFeeEstimation(chainName);
const { changeAdmin } = osmosis.tokenfactory.v1beta1.MessageComposer.withTypeUrl;

const handleTransfer = async (values: FormikValues, resetForm: () => void) => {
setIsSigning(true);
try {
const msg = changeAdmin({
sender: address,
denom: denom?.base ?? '',
newAdmin: values.newAdmin,
});

const fee = await estimateFee(address, [msg]);

await tx([msg], {
fee,
onSuccess: () => {
onSuccess();
handleCloseModal(resetForm);
},
});
} catch (error) {
console.error('Error during transaction setup:', error);
let errorMessage = 'An unknown error occurred while transferring ownership.';

if (error instanceof Error) {
if (error.message.includes('unauthorized account')) {
errorMessage = 'Unauthorized account. Please check your account and try again.';
}
}

setToastMessage({
type: 'alert-error',
title: 'Error transferring ownership',
description: errorMessage,
bgColor: '#e74c3c',
});
throw error;
} finally {
setIsSigning(false);
}
};

const modalContent = (
<dialog
id={modalId}
className={`modal ${openTransferDenomModal ? 'modal-open' : ''}`}
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
zIndex: 9999,
backgroundColor: 'transparent',
padding: 0,
margin: 0,
height: '100vh',
width: '100vw',
display: openTransferDenomModal ? 'flex' : 'none',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Formik
initialValues={formData}
validationSchema={TokenOwnershipSchema}
onSubmit={(values, { resetForm }) => handleTransfer(values, resetForm)}
validateOnChange={true}
validateOnBlur={true}
>
{({ isValid, dirty, values, handleChange, handleSubmit, resetForm }) => (
<div className="modal-box max-w-4xl mx-auto p-6 bg-[#F4F4FF] dark:bg-[#1D192D] rounded-[24px] shadow-lg relative">
<form method="dialog">
<button
type="button"
className="btn btn-sm btn-circle btn-ghost absolute right-4 top-4 text-[#00000099] dark:text-[#FFFFFF99] hover:bg-[#0000000A] dark:hover:bg-[#FFFFFF1A]"
onClick={() => handleCloseModal(() => resetForm())}
>
</button>
</form>
<h3 className="text-xl font-semibold text-[#161616] dark:text-white mb-6">
Update administrator for{' '}
<span className="font-light text-primary">
{denom?.display?.startsWith('factory')
? denom?.display?.split('/').pop()?.toUpperCase()
: truncateString(denom?.display ?? 'DENOM', 12)}
</span>
</h3>
<div className="divider divider-horizontal -mt-4 -mb-0"></div>
{isDenomAuthorityLoading ? (
<div className="skeleton h-[17rem] max-h-72 w-full"></div>
) : (
<>
<Form className="py-4 space-y-6">
<div className="grid gap-6 sm:grid-cols-2"></div>
<TextInput
label="SUBDENOM"
name="subdenom"
value={denom?.base}
title={denom?.base}
disabled={true}
helperText="This field cannot be modified"
/>
<TextInput
name="currentAdmin"
label="Current Admin"
value={denomAuthority?.admin ?? 'No admin available'}
disabled={true}
helperText="This field cannot be modified"
/>
<TextInput name="newAdmin" label="New Admin" onChange={handleChange} />
</Form>
<div className="mt-4 flex flex-row justify-center gap-2 w-full">
<button
type="button"
className="btn w-1/2 focus:outline-none dark:bg-[#FFFFFF0F] bg-[#0000000A] dark:text-white text-black"
onClick={() => handleCloseModal(() => resetForm())}
>
Cancel
</button>
<button
type="submit"
className="btn w-1/2 btn-gradient text-white"
onClick={() => handleSubmit()}
disabled={isSigning || !isValid || !dirty}
>
{isSigning ? (
<span className="loading loading-dots"></span>
) : (
'Transfer Ownership'
)}
</button>
</div>
</>
)}
</div>
)}
</Formik>
<form
method="dialog"
className="modal-backdrop"
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
zIndex: -1,
backgroundColor: 'rgba(0, 0, 0, 0.3)',
}}
>
<button onClick={() => handleCloseModal()}>close</button>
</form>
</dialog>
);

// Only render if we're in the browser
if (typeof document !== 'undefined') {
return createPortal(modalContent, document.body);
}

return null;
}
1 change: 1 addition & 0 deletions components/factory/modals/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './denomInfo';
export * from './updateDenomMetadata';
export * from '../../admins/modals/multiMfxMintModal';
export * from '../../admins/modals/multiMfxBurnModal';
export * from './TransferModal';
6 changes: 6 additions & 0 deletions helpers/formReducer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -377,3 +377,9 @@ export const proposalFormDataReducer = (
throw new Error('Unknown action type');
}
};

export type TransferTokenFormData = {
denom: string;
currentAdmin: string | undefined;
newAdmin: string;
};
Loading

0 comments on commit 0b5f1a9

Please sign in to comment.