diff --git a/bun.lockb b/bun.lockb index f882fe4b..840b8dde 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/components/admins/components/chainUpgrader.tsx b/components/admins/components/chainUpgrader.tsx new file mode 100644 index 00000000..57dcc07a --- /dev/null +++ b/components/admins/components/chainUpgrader.tsx @@ -0,0 +1,76 @@ +import { UpgradeModal, CancelUpgradeModal } from '@/components/admins/modals'; +import { useCurrentPlan } from '@/hooks/useQueries'; +import { PlanSDKType } from '@liftedinit/manifestjs/dist/codegen/cosmos/upgrade/v1beta1/upgrade'; +import { useState } from 'react'; + +export const ChainUpgrader = ({ admin, address }: { admin: string; address: string }) => { + const [isOpen, setIsOpen] = useState(false); + const [isCancelOpen, setIsCancelOpen] = useState(false); + const { plan, isPlanLoading, refetchPlan } = useCurrentPlan(); + + if (isPlanLoading) { + return ( +
+
+
+
+
+
+
+
+
+
+
+
+
+ ); + } + + return ( +
+
+

+ {plan ? 'Upgrade in progress' : 'No upgrade in progress'} +

+
+
+

Submit Chain Upgrade Proposal

+

+ Submit a proposal to instantiate a chain upgrade. +

+
+ +
+ + +
+ setIsOpen(false)} + refetchPlan={refetchPlan} + /> + setIsCancelOpen(false)} + refetchPlan={refetchPlan} + /> +
+ ); +}; diff --git a/components/admins/components/index.tsx b/components/admins/components/index.tsx index bb4c3844..4b418158 100644 --- a/components/admins/components/index.tsx +++ b/components/admins/components/index.tsx @@ -1 +1,3 @@ export * from './validatorList'; +export * from './stakeHolderPayout'; +export * from './chainUpgrader'; diff --git a/components/admins/components/stakeHolderPayout.tsx b/components/admins/components/stakeHolderPayout.tsx new file mode 100644 index 00000000..67d6c1af --- /dev/null +++ b/components/admins/components/stakeHolderPayout.tsx @@ -0,0 +1,50 @@ +import { MultiMintModal, MultiBurnModal } from '@/components/factory/modals'; +import { MFX_TOKEN_DATA } from '@/utils/constants'; +import { useState } from 'react'; + +interface StakeHolderPayoutProps { + admin: string; + address: string; +} + +export const StakeHolderPayout = ({ admin, address }: StakeHolderPayoutProps) => { + const [isOpenMint, setIsOpenMint] = useState(false); + const [isOpenBurn, setIsOpenBurn] = useState(false); + + return ( +
+
+

Stake Holder Payout

+

+ Burn MFX or mint MFX to pay out stake holders. +

+
+ +
+ + +
+ setIsOpenMint(false)} + admin={admin} + address={address} + denom={MFX_TOKEN_DATA} + /> + setIsOpenBurn(false)} + admin={admin} + address={address} + denom={MFX_TOKEN_DATA} + /> +
+ ); +}; diff --git a/components/admins/components/validatorList.tsx b/components/admins/components/validatorList.tsx index 3639aee2..cac4c7a9 100644 --- a/components/admins/components/validatorList.tsx +++ b/components/admins/components/validatorList.tsx @@ -6,6 +6,8 @@ import ProfileAvatar from '@/utils/identicon'; import Image from 'next/image'; import { TruncatedAddressWithCopy } from '@/components/react/addressCopy'; import { SearchIcon, TrashIcon } from '@/components/icons'; +import useIsMobile from '@/hooks/useIsMobile'; + export interface ExtendedValidatorSDKType extends ValidatorSDKType { consensus_power?: bigint; logo_url?: string; @@ -46,6 +48,11 @@ export default function ValidatorList({ })) : []; + const [currentPage, setCurrentPage] = useState(1); + const isMobile = useIsMobile(); + + const pageSize = isMobile ? 4 : 5; + const filteredValidators = useMemo(() => { const validators = active ? activeValidators : pendingValidators; return validators.filter(validator => @@ -53,6 +60,12 @@ export default function ValidatorList({ ); }, [active, activeValidators, pendingValidators, searchTerm]); + const totalPages = Math.ceil(filteredValidators.length / pageSize); + const paginatedValidators = filteredValidators.slice( + (currentPage - 1) * pageSize, + currentPage * pageSize + ); + const handleRemove = (validator: ExtendedValidatorSDKType) => { setValidatorToRemove(validator); setOpenWarningModal(true); @@ -68,7 +81,7 @@ export default function ValidatorList({ }; return ( -
+
@@ -124,10 +137,10 @@ export default function ValidatorList({ Moniker - + Address - + Consensus Power @@ -140,20 +153,20 @@ export default function ValidatorList({ .fill(0) .map((_, index) => ( - +
-
-
+
+
- -
+ +
- -
+ +
- -
+ +
))} @@ -186,7 +199,7 @@ export default function ValidatorList({ - {filteredValidators.map(validator => ( + {paginatedValidators.map(validator => ( + + {totalPages > 1 && ( +
e.stopPropagation()} + role="navigation" + aria-label="Pagination" + > + + + {[...Array(totalPages)].map((_, index) => { + const pageNum = index + 1; + if ( + pageNum === 1 || + pageNum === totalPages || + (pageNum >= currentPage - 1 && pageNum <= currentPage + 1) + ) { + return ( + + ); + } else if (pageNum === currentPage - 2 || pageNum === currentPage + 2) { + return ( + + ); + } + return null; + })} + + +
+ )}
); } diff --git a/components/admins/modals/cancelUpgradeModal.tsx b/components/admins/modals/cancelUpgradeModal.tsx new file mode 100644 index 00000000..b3f93bbe --- /dev/null +++ b/components/admins/modals/cancelUpgradeModal.tsx @@ -0,0 +1,186 @@ +import React, { useEffect } from 'react'; +import { createPortal } from 'react-dom'; +import { cosmos } from '@liftedinit/manifestjs'; +import { useFeeEstimation, useTx } from '@/hooks'; +import { Any } from '@liftedinit/manifestjs/dist/codegen/google/protobuf/any'; +import { chainName } from '@/config'; +import { MsgCancelUpgrade } from '@liftedinit/manifestjs/dist/codegen/cosmos/upgrade/v1beta1/tx'; +import { PlanSDKType } from '@liftedinit/manifestjs/dist/codegen/cosmos/upgrade/v1beta1/upgrade'; + +interface BaseModalProps { + isOpen: boolean; + onClose: () => void; + admin: string; + address: string | null; + plan: PlanSDKType; + refetchPlan: () => void; +} + +function InfoItem({ label, value }: { label: string; value?: string | number | bigint }) { + return ( +
+ {label} +
+ {value?.toString() || 'N/A'} +
+
+ ); +} + +export function CancelUpgradeModal({ + isOpen, + onClose, + admin, + address, + plan, + refetchPlan, +}: BaseModalProps) { + const { cancelUpgrade } = cosmos.upgrade.v1beta1.MessageComposer.withTypeUrl; + const { submitProposal } = cosmos.group.v1.MessageComposer.withTypeUrl; + const { tx, isSigning, setIsSigning } = useTx(chainName); + const { estimateFee } = useFeeEstimation(chainName); + + useEffect(() => { + const handleEscape = (e: KeyboardEvent) => { + if (e.key === 'Escape' && isOpen) { + onClose(); + } + }; + + document.addEventListener('keydown', handleEscape); + return () => document.removeEventListener('keydown', handleEscape); + }, [isOpen, onClose]); + + const handleCancelUpgrade = async () => { + setIsSigning(true); + try { + const msgUpgrade = cancelUpgrade({ + authority: admin, + }); + + const anyMessage = Any.fromPartial({ + typeUrl: msgUpgrade.typeUrl, + value: MsgCancelUpgrade.encode(msgUpgrade.value).finish(), + }); + + const groupProposalMsg = submitProposal({ + groupPolicyAddress: admin, + messages: [anyMessage], + metadata: '', + proposers: [address ?? ''], + title: `Cancel Upgrade`, + summary: `This proposal will cancel the upgrade`, + exec: 0, + }); + + const fee = await estimateFee(address ?? '', [groupProposalMsg]); + await tx([groupProposalMsg], { + fee, + onSuccess: () => { + setIsSigning(false); + onClose(); + refetchPlan(); + }, + }); + } catch (error) { + console.error('Error canceling upgrade:', error); + } finally { + setIsSigning(false); + } + }; + + const modalContent = ( + +
+
+

Cancel Upgrade

+
+ +
+
+ + {plan && ( +
+

+ Current Upgrade Plan +

+
+ + + + +
+
+ )} + +
+ + +
+
+
+ +
+
+ ); + + if (typeof document !== 'undefined') { + return createPortal(modalContent, document.body); + } + + return null; +} diff --git a/components/admins/modals/index.tsx b/components/admins/modals/index.tsx index 89a638a7..6ebbb987 100644 --- a/components/admins/modals/index.tsx +++ b/components/admins/modals/index.tsx @@ -1,2 +1,4 @@ export * from './validatorModal'; export * from './warningModal'; +export * from './upgradeModal'; +export * from './cancelUpgradeModal'; diff --git a/components/factory/modals/multiMfxBurnModal.tsx b/components/admins/modals/multiMfxBurnModal.tsx similarity index 72% rename from components/factory/modals/multiMfxBurnModal.tsx rename to components/admins/modals/multiMfxBurnModal.tsx index 8f3da005..18bbcd1c 100644 --- a/components/factory/modals/multiMfxBurnModal.tsx +++ b/components/admins/modals/multiMfxBurnModal.tsx @@ -1,4 +1,5 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; +import { createPortal } from 'react-dom'; import { Formik, Form, FieldArray, Field, FieldProps } from 'formik'; import Yup from '@/utils/yupExtensions'; @@ -8,10 +9,12 @@ import { PlusIcon, MinusIcon } from '@/components/icons'; import { MdContacts } from 'react-icons/md'; import { useTx, useFeeEstimation } from '@/hooks'; import { chainName } from '@/config'; -import { cosmos, osmosis, liftedinit } from '@liftedinit/manifestjs'; +import { cosmos, liftedinit } from '@liftedinit/manifestjs'; import { Any } from '@liftedinit/manifestjs/dist/codegen/google/protobuf/any'; -import { ExtendedMetadataSDKType, parseNumberToBigInt } from '@/utils'; +import { parseNumberToBigInt } from '@/utils'; +import { MetadataSDKType } from '@liftedinit/manifestjs/dist/codegen/cosmos/bank/v1beta1/bank'; +import { TailwindModal } from '@/components/react'; interface BurnPair { address: string; @@ -23,9 +26,7 @@ interface MultiBurnModalProps { onClose: () => void; admin: string; address: string; - denom: ExtendedMetadataSDKType | null; - exponent: number; - refetch: () => void; + denom: MetadataSDKType | null; } const BurnPairSchema = Yup.object().shape({ @@ -45,20 +46,25 @@ const MultiBurnSchema = Yup.object().shape({ }), }); -export function MultiBurnModal({ - isOpen, - onClose, - admin, - address, - denom, - exponent, - refetch, -}: MultiBurnModalProps) { - const [burnPairs, setBurnPairs] = useState([{ address: '', amount: '' }]); +export function MultiBurnModal({ isOpen, onClose, admin, address, denom }: MultiBurnModalProps) { + const [burnPairs, setBurnPairs] = useState([{ address: admin, amount: '' }]); const { tx, isSigning, setIsSigning } = useTx(chainName); const { estimateFee } = useFeeEstimation(chainName); const { burnHeldBalance } = liftedinit.manifest.v1.MessageComposer.withTypeUrl; const { submitProposal } = cosmos.group.v1.MessageComposer.withTypeUrl; + const [isContactsOpen, setIsContactsOpen] = useState(false); + const [selectedIndex, setSelectedIndex] = useState(null); + + useEffect(() => { + const handleEscape = (e: KeyboardEvent) => { + if (e.key === 'Escape' && isOpen) { + onClose(); + } + }; + + document.addEventListener('keydown', handleEscape); + return () => document.removeEventListener('keydown', handleEscape); + }, [isOpen, onClose]); const updateBurnPair = (index: number, field: 'address' | 'amount', value: string) => { const newPairs = [...burnPairs]; @@ -67,7 +73,7 @@ export function MultiBurnModal({ }; const addBurnPair = () => { - setBurnPairs([...burnPairs, { address: '', amount: '' }]); + setBurnPairs([...burnPairs, { address: admin, amount: '' }]); }; const removeBurnPair = (index: number) => { @@ -83,7 +89,7 @@ export function MultiBurnModal({ burnCoins: [ { denom: denom?.base ?? '', - amount: parseNumberToBigInt(pair.amount, exponent).toString(), + amount: parseNumberToBigInt(pair.amount, denom?.denom_units?.[1].exponent).toString(), }, ], }) @@ -101,7 +107,7 @@ export function MultiBurnModal({ messages: encodedMessages, metadata: '', proposers: [address], - title: `Manifest Module Control: Multi Burn MFX`, + title: `Burn MFX`, summary: `This proposal includes a multi-burn action for MFX.`, exec: 0, }); @@ -110,7 +116,6 @@ export function MultiBurnModal({ await tx([msg], { fee, onSuccess: () => { - refetch(); onClose(); }, }); @@ -121,13 +126,26 @@ export function MultiBurnModal({ } }; - return ( + const modalContent = (
@@ -138,8 +156,8 @@ export function MultiBurnModal({ ✕
-

+ Burn MFX

Burn Pairs
-
@@ -188,22 +192,12 @@ export function MultiBurnModal({ showError={false} label="Address" {...field} + value={admin} + disabled={true} placeholder="manifest1..." className={`input input-bordered w-full ${ meta.touched && meta.error ? 'input-error' : '' }`} - rightElement={ - - } /> {meta.touched && meta.error && (
-
-
+ { + if (selectedIndex !== null) { + updateBurnPair(selectedIndex, 'address', selectedAddress); + setFieldValue(`burnPairs.${selectedIndex}.address`, selectedAddress); + } + setIsContactsOpen(false); + setSelectedIndex(null); + }} + /> )}
-
- + +
); + + // Only render if we're in the browser + if (typeof document !== 'undefined') { + return createPortal(modalContent, document.body); + } + + return null; } diff --git a/components/factory/modals/multiMfxMintModal.tsx b/components/admins/modals/multiMfxMintModal.tsx similarity index 84% rename from components/factory/modals/multiMfxMintModal.tsx rename to components/admins/modals/multiMfxMintModal.tsx index 62733d3e..d396f26a 100644 --- a/components/factory/modals/multiMfxMintModal.tsx +++ b/components/admins/modals/multiMfxMintModal.tsx @@ -1,8 +1,9 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { Formik, Form, FieldArray, Field, FieldProps } from 'formik'; import Yup from '@/utils/yupExtensions'; import { NumberInput, TextInput } from '@/components/react/inputs'; import { TailwindModal } from '@/components/react'; +import { createPortal } from 'react-dom'; import { MdContacts } from 'react-icons/md'; import { PlusIcon, MinusIcon } from '@/components/icons'; @@ -11,7 +12,8 @@ import { chainName } from '@/config'; import { cosmos, liftedinit } from '@liftedinit/manifestjs'; import { Any } from '@liftedinit/manifestjs/dist/codegen/google/protobuf/any'; import { MsgPayout } from '@liftedinit/manifestjs/dist/codegen/liftedinit/manifest/v1/tx'; -import { ExtendedMetadataSDKType, parseNumberToBigInt } from '@/utils'; +import { parseNumberToBigInt, shiftDigits } from '@/utils'; +import { MetadataSDKType } from '@liftedinit/manifestjs/dist/codegen/cosmos/bank/v1beta1/bank'; //TODO: find max mint amount from team for mfx. Find tx size limit for max payout pairs interface PayoutPair { address: string; @@ -23,9 +25,7 @@ interface MultiMintModalProps { onClose: () => void; admin: string; address: string; - denom: ExtendedMetadataSDKType | null; - exponent: number; - refetch: () => void; + denom: MetadataSDKType | null; } const PayoutPairSchema = Yup.object().shape({ @@ -46,15 +46,7 @@ const MultiMintSchema = Yup.object().shape({ }), }); -export function MultiMintModal({ - isOpen, - onClose, - admin, - address, - denom, - exponent, - refetch, -}: MultiMintModalProps) { +export function MultiMintModal({ isOpen, onClose, admin, address, denom }: MultiMintModalProps) { const [payoutPairs, setPayoutPairs] = useState([{ address: '', amount: '' }]); const { tx, isSigning, setIsSigning } = useTx(chainName); const { estimateFee } = useFeeEstimation(chainName); @@ -63,6 +55,17 @@ export function MultiMintModal({ const [isContactsOpen, setIsContactsOpen] = useState(false); const [selectedIndex, setSelectedIndex] = useState(null); + useEffect(() => { + const handleEscape = (e: KeyboardEvent) => { + if (e.key === 'Escape' && isOpen) { + onClose(); + } + }; + + document.addEventListener('keydown', handleEscape); + return () => document.removeEventListener('keydown', handleEscape); + }, [isOpen]); + const updatePayoutPair = (index: number, field: 'address' | 'amount', value: string) => { const newPairs = [...payoutPairs]; newPairs[index] = { ...newPairs[index], [field]: value }; @@ -79,7 +82,9 @@ export function MultiMintModal({ const handleMultiMint = async (values: { payoutPairs: PayoutPair[] }) => { setIsSigning(true); + try { + const exponent = denom?.denom_units?.[1]?.exponent ?? 6; const payoutMsg = payout({ authority: admin, payoutPairs: values.payoutPairs.map(pair => ({ @@ -101,7 +106,7 @@ export function MultiMintModal({ messages: [encodedMessage], metadata: '', proposers: [address], - title: `Manifest Module Control: Multi Mint MFX`, + title: `Multi Mint MFX`, summary: `This proposal includes a multi-mint action for MFX.`, exec: 0, }); @@ -110,7 +115,6 @@ export function MultiMintModal({ await tx([msg], { fee, onSuccess: () => { - refetch(); onClose(); }, }); @@ -121,12 +125,26 @@ export function MultiMintModal({ } }; - return ( + const modalContent = (
@@ -172,7 +190,7 @@ export function MultiMintModal({ {values.payoutPairs.map((pair, index) => (
{index > 0 && (
#{index + 1}
@@ -196,7 +214,7 @@ export function MultiMintModal({ setSelectedIndex(index); setIsContactsOpen(true); }} - className="btn btn-primary btn-sm text-white absolute right-2 top-1/2 transform -translate-y-1/2" + className="btn btn-primary btn-sm text-white" > @@ -255,13 +273,17 @@ export function MultiMintModal({
-
-
- - + +
); + + // Only render if we're in the browser + if (typeof document !== 'undefined') { + return createPortal(modalContent, document.body); + } + + return null; } diff --git a/components/admins/modals/upgradeModal.tsx b/components/admins/modals/upgradeModal.tsx new file mode 100644 index 00000000..8cb0f569 --- /dev/null +++ b/components/admins/modals/upgradeModal.tsx @@ -0,0 +1,340 @@ +import React, { useEffect, useState, useMemo } from 'react'; +import { createPortal } from 'react-dom'; +import { cosmos } from '@liftedinit/manifestjs'; +import { useTx, useFeeEstimation, useGitHubReleases, GitHubRelease } from '@/hooks'; +import { chainName } from '@/config'; +import { Any } from '@liftedinit/manifestjs/dist/codegen/google/protobuf/any'; +import { MsgSoftwareUpgrade } from '@liftedinit/manifestjs/dist/codegen/cosmos/upgrade/v1beta1/tx'; +import { Formik, Form } from 'formik'; +import Yup from '@/utils/yupExtensions'; +import { TextInput } from '@/components/react/inputs'; +import { PiCaretDownBold } from 'react-icons/pi'; +import { SearchIcon } from '@/components/icons'; + +interface BaseModalProps { + isOpen: boolean; + onClose: () => void; + admin: string; + address: string; + refetchPlan: () => void; +} + +interface UpgradeInfo { + name: string; + upgradeable: boolean; + commitHash: string; +} + +const parseReleaseBody = (body: string): UpgradeInfo | null => { + try { + const nameMatch = body.match(/\*\*Upgrade Handler Name\*\*:\s*`([^`]+)`/); + const upgradeableMatch = body.match(/\*\*Upgradeable\*\*:\s*`([^`]+)`/); + const commitHashMatch = body.match(/\*\*Commit Hash\*\*:\s*`([^`]+)`/); + + if (!nameMatch || !upgradeableMatch || !commitHashMatch) { + return null; + } + + return { + name: nameMatch[1], + upgradeable: upgradeableMatch[1].toLowerCase() === 'true', + commitHash: commitHashMatch[1], + }; + } catch (error) { + console.error('Error parsing release body:', error); + return null; + } +}; + +const UpgradeSchema = Yup.object().shape({ + height: Yup.number().required('Height is required').integer('Must be a valid number'), +}); + +export function UpgradeModal({ isOpen, onClose, admin, address, refetchPlan }: BaseModalProps) { + const [searchTerm, setSearchTerm] = useState(''); + const { releases, isReleasesLoading } = useGitHubReleases(); + + // Filter releases that are upgradeable + const upgradeableReleases = useMemo(() => { + const allReleases = [...(releases || [])]; + return allReleases + .map(release => ({ + ...release, + upgradeInfo: parseReleaseBody(release.body), + })) + .filter(release => release.upgradeInfo?.upgradeable); + }, [releases]); + + const filteredReleases = upgradeableReleases.filter(release => + release.tag_name.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + useEffect(() => { + const handleEscape = (e: KeyboardEvent) => { + if (e.key === 'Escape' && isOpen) { + onClose(); + } + }; + + document.addEventListener('keydown', handleEscape); + return () => document.removeEventListener('keydown', handleEscape); + }, [isOpen, onClose]); + + const { softwareUpgrade } = cosmos.upgrade.v1beta1.MessageComposer.withTypeUrl; + const { submitProposal } = cosmos.group.v1.MessageComposer.withTypeUrl; + const { tx, isSigning, setIsSigning } = useTx(chainName); + const { estimateFee } = useFeeEstimation(chainName); + + const handleUpgrade = async (values: { name: string; height: string; info: string }) => { + setIsSigning(true); + const msgUpgrade = softwareUpgrade({ + plan: { + name: values.name, + height: BigInt(values.height), + time: new Date(0), + info: values.info, + }, + authority: admin, + }); + + const anyMessage = Any.fromPartial({ + typeUrl: msgUpgrade.typeUrl, + value: MsgSoftwareUpgrade.encode(msgUpgrade.value).finish(), + }); + + const groupProposalMsg = submitProposal({ + groupPolicyAddress: admin, + messages: [anyMessage], + metadata: '', + proposers: [address ?? ''], + title: `Upgrade the chain`, + summary: `This proposal will upgrade the chain`, + exec: 0, + }); + + const fee = await estimateFee(address ?? '', [groupProposalMsg]); + await tx([groupProposalMsg], { + fee, + onSuccess: () => { + setIsSigning(false); + refetchPlan(); + }, + }); + setIsSigning(false); + }; + + const initialValues = { + name: '', + height: '', + info: '', + selectedVersion: null as (GitHubRelease & { upgradeInfo?: UpgradeInfo | null }) | null, + }; + + const modalContent = ( + + { + handleUpgrade({ + name: values.selectedVersion?.upgradeInfo?.name || '', + height: values.height, + info: values.selectedVersion?.upgradeInfo?.commitHash || '', + }); + }} + validateOnChange={true} + validateOnBlur={true} + > + {({ isValid, dirty, values, handleChange, handleSubmit, setFieldValue, resetForm }) => ( +
+
+ +
+

+ Chain Upgrade +

+ +
+
+
+ +
+
+ +
    +
  • +
    + setSearchTerm(e.target.value)} + style={{ boxShadow: 'none', borderRadius: '8px' }} + /> + +
    +
  • + {isReleasesLoading ? ( +
  • + Loading versions... +
  • + ) : ( + filteredReleases?.map(release => ( +
  • { + setFieldValue('selectedVersion', release); + setFieldValue('name', release.upgradeInfo?.name || ''); + setFieldValue('info', release.upgradeInfo?.commitHash || ''); + // Get the dropdown element and remove focus + const dropdown = (e.target as HTMLElement).closest('.dropdown'); + if (dropdown) { + (dropdown as HTMLElement).removeAttribute('open'); + (dropdown.querySelector('label') as HTMLElement)?.focus(); + (dropdown.querySelector('label') as HTMLElement)?.blur(); + } + }} + onKeyDown={e => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + setFieldValue('selectedVersion', release); + setFieldValue('name', release.upgradeInfo?.name || ''); + setFieldValue('info', release.upgradeInfo?.commitHash || ''); + // Get the dropdown element and remove focus + const dropdown = (e.target as HTMLElement).closest('.dropdown'); + if (dropdown) { + (dropdown as HTMLElement).removeAttribute('open'); + (dropdown.querySelector('label') as HTMLElement)?.focus(); + (dropdown.querySelector('label') as HTMLElement)?.blur(); + } + } + }} + className="hover:bg-[#E0E0FF33] dark:hover:bg-[#FFFFFF0F] cursor-pointer rounded-lg" + > + + {release.tag_name} + +
  • + )) + )} +
+
+
+
+ + + +
+ +
+ + +
+
+
+ )} +
+
+ +
+
+ ); + + // Only render if we're in the browser + if (typeof document !== 'undefined') { + return createPortal(modalContent, document.body); + } + + return null; +} diff --git a/components/admins/modals/warningModal.tsx b/components/admins/modals/warningModal.tsx index 3cc71e05..a6907fd5 100644 --- a/components/admins/modals/warningModal.tsx +++ b/components/admins/modals/warningModal.tsx @@ -96,6 +96,10 @@ export function WarningModal({ id={modalId} className={`modal ${openWarningModal ? 'modal-open' : ''}`} onClose={handleClose} + role="dialog" + aria-modal="true" + aria-labelledby="modal-title" + aria-describedby="modal-description" style={{ position: 'fixed', top: 0, diff --git a/components/bank/components/historyBox.tsx b/components/bank/components/historyBox.tsx index c13b75e9..a21caac6 100644 --- a/components/bank/components/historyBox.tsx +++ b/components/bank/components/historyBox.tsx @@ -230,25 +230,28 @@ export function HistoryBox({ {isLoading ? (
-
+
{[...Array(skeletonGroupCount)].map((_, groupIndex) => (
-
+
{[...Array(skeletonTxCount)].map((_, txIndex) => (
-
-
+
+
+
+
+
-
+
))}
@@ -311,16 +314,19 @@ export function HistoryBox({ const metadata = metadatas?.metadatas.find( m => m.base === amt.denom ); + const display = metadata?.display ?? metadata?.symbol ?? ''; return metadata?.display.startsWith('factory') ? metadata?.display?.split('/').pop()?.toUpperCase() - : truncateString( - metadata?.display ?? metadata?.symbol ?? '', - 10 - ).toUpperCase(); + : display.length > 4 + ? display.slice(0, 4).toUpperCase() + '...' + : display.toUpperCase(); })}

-
e.stopPropagation()}> +
e.stopPropagation()} + > {tx.data.from_address.startsWith('manifest1') ? (
-

+

{getTransactionPlusMinus(tx, address)} + {tx.data.amount .map(amt => { const metadata = metadatas?.metadatas.find( diff --git a/components/bank/components/tokenList.tsx b/components/bank/components/tokenList.tsx index 57341884..99c03c40 100644 --- a/components/bank/components/tokenList.tsx +++ b/components/bank/components/tokenList.tsx @@ -134,16 +134,18 @@ export function TokenList({ {[...Array(pageSize)].map((_, i) => (

-
-
-
+
+
+
-
+
+
+
diff --git a/components/bank/forms/sendForm.tsx b/components/bank/forms/sendForm.tsx index 9ede0c3e..ab8e4857 100644 --- a/components/bank/forms/sendForm.tsx +++ b/components/bank/forms/sendForm.tsx @@ -106,17 +106,9 @@ export default function SendForm({ }) => { setIsSending(true); try { - if (!address) { - throw new Error('Wallet not connected'); - } - const exponent = values.selectedToken.metadata?.denom_units[1]?.exponent ?? 6; const amountInBaseUnits = parseNumberToBigInt(values.amount, exponent).toString(); - if (isGroup && !admin) { - throw new Error('Admin address not provided for group transaction'); - } - const msg = isGroup ? submitProposal({ groupPolicyAddress: admin!, diff --git a/components/factory/components/MyDenoms.tsx b/components/factory/components/MyDenoms.tsx index 0adc8820..7a394b77 100644 --- a/components/factory/components/MyDenoms.tsx +++ b/components/factory/components/MyDenoms.tsx @@ -9,9 +9,7 @@ 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 } from '@/utils'; -import { MultiMintModal } from '@/components/factory/modals'; -import { MultiBurnModal } from '../modals/multiMfxBurnModal'; +import { ExtendedMetadataSDKType, shiftDigits, formatTokenDisplay } from '@/utils'; import { usePoaGetAdmin } from '@/hooks'; import useIsMobile from '@/hooks/useIsMobile'; @@ -31,7 +29,7 @@ export default function MyDenoms({ const [openUpdateDenomMetadataModal, setOpenUpdateDenomMetadataModal] = useState(false); const isMobile = useIsMobile(); - const pageSize = isMobile ? 6 : 8; + const pageSize = isMobile ? 5 : 8; const router = useRouter(); const [selectedDenom, setSelectedDenom] = useState(null); @@ -169,31 +167,37 @@ export default function MyDenoms({ {isLoading - ? Array(isMobile ? 6 : 8) + ? Array(isMobile ? 5 : 8) .fill(0) .map((_, index) => (
-
+
+
+
+
- +
@@ -370,24 +374,6 @@ export default function MyDenoms({ } }} /> - -
); } @@ -408,7 +394,6 @@ function TokenRow({ // Add safety checks for the values const exponent = denom?.denom_units?.[1]?.exponent ?? 0; const totalSupply = denom?.totalSupply ?? '0'; - const balance = denom?.balance ?? '0'; // Format numbers safely const formatAmount = (amount: string) => { @@ -430,11 +415,7 @@ function TokenRow({
- - {denom.display.startsWith('factory') - ? denom.display.split('/').pop()?.toUpperCase() - : truncateString(denom.display, 12)} - + {formatTokenDisplay(denom.display)}
@@ -443,9 +424,7 @@ function TokenRow({
{formatAmount(totalSupply)} - - {truncateString(denom?.display ?? 'No ticker provided', 10).toUpperCase()} - + {formatTokenDisplay(denom.display)}
{ refetchProposals: jest.fn(), }), })); - renderWithChainProvider(); + renderWithChainProvider(); expect(screen.getByRole('status')).toBeInTheDocument(); }); @@ -94,7 +94,7 @@ describe('ProposalsForPolicy Component', () => { refetchProposals: jest.fn(), }), })); - renderWithChainProvider(); + renderWithChainProvider(); expect(screen.getByText('Error loading proposals')).toBeInTheDocument(); }); @@ -107,7 +107,7 @@ describe('ProposalsForPolicy Component', () => { refetchProposals: jest.fn(), }), })); - renderWithChainProvider(); + renderWithChainProvider(); expect(screen.getByText('No proposal was found.')).toBeInTheDocument(); }); @@ -120,7 +120,7 @@ describe('ProposalsForPolicy Component', () => { refetchProposals: jest.fn(), }), })); - renderWithChainProvider(); + renderWithChainProvider(); expect(screen.getByText('Proposals')).toBeInTheDocument(); const proposals = mockProposals['test_policy_address']; proposals.forEach(proposal => { diff --git a/components/groups/components/myGroups.tsx b/components/groups/components/myGroups.tsx index 56ee0291..1fdd1c51 100644 --- a/components/groups/components/myGroups.tsx +++ b/components/groups/components/myGroups.tsx @@ -44,7 +44,7 @@ export function YourGroups({ const [currentPage, setCurrentPage] = useState(1); const isMobile = useIsMobile(); - const pageSize = isMobile ? 6 : 8; + const pageSize = isMobile ? 4 : 8; const [selectedGroup, setSelectedGroup] = useState<{ policyAddress: string; @@ -244,7 +244,7 @@ export function YourGroups({
- +
@@ -253,7 +253,7 @@ export function YourGroups({
- +