diff --git a/src/components/cores/CoreItem.tsx b/src/components/cores/CoreItem.tsx index abb8b96..8ed0adc 100644 --- a/src/components/cores/CoreItem.tsx +++ b/src/components/cores/CoreItem.tsx @@ -17,6 +17,8 @@ interface CardProps { begin: number | null duration: number | null constants: BrokerConstantsType | null + assignedTask?: string | number | null + assignedTaskName?: string | null } const CoreItem: React.FC = ({ @@ -29,6 +31,8 @@ const CoreItem: React.FC = ({ begin, duration, constants, + assignedTask, + assignedTaskName, }) => { const pathname = usePathname() @@ -78,6 +82,14 @@ const CoreItem: React.FC = ({ End Relay Block: {(begin + duration) * constants.timeslicePeriod}

+ {assignedTask !== null && assignedTask !== undefined && ( +
+

+ Assigned to: {assignedTask} + {assignedTaskName ? ` (${assignedTaskName})` : ''} +

+
+ )} diff --git a/src/components/cores/CoreItemSectionDisplay.tsx b/src/components/cores/CoreItemSectionDisplay.tsx index d7de86a..1511e74 100644 --- a/src/components/cores/CoreItemSectionDisplay.tsx +++ b/src/components/cores/CoreItemSectionDisplay.tsx @@ -1,8 +1,11 @@ import CoreItem from '@/components/cores/CoreItem' +import { network_list } from '@/config/network' import { Region } from '@/types' import { parseNativeTokenToHuman } from '@/utils' +import { getChainFromPath } from '@/utils/common/chainPath' import { BrokerConstantsType, parseHNString } from '@poppyseed/lastic-sdk' import { CoreOwnerEvent } from '@poppyseed/squid-sdk' +import { usePathname } from 'next/navigation' import { useState } from 'react' interface SectionProps { @@ -27,9 +30,15 @@ export default function SectionDisplay({ tokenSymbol, }: SectionProps) { const [currentPage, setCurrentPage] = useState(1) + const pathname = usePathname() + const network = getChainFromPath(pathname) const itemsPerPage = 6 const handleNextPage = () => setCurrentPage(currentPage + 1) const handlePrevPage = () => setCurrentPage(currentPage - 1) + const getParaName = (task?: string | number | null) => + task !== null && task !== undefined + ? network_list[network]?.paraId?.[task.toString()]?.name + : null return ( <> @@ -56,6 +65,8 @@ export default function SectionDisplay({ begin={region.regionId.begin ?? null} duration={region.duration ?? null} constants={constants} + assignedTask={region.task ?? null} + assignedTaskName={getParaName(region.task)} /> ) : ( = ({ isOpen, onClose, regionId }) => { const { api, activeSigner, activeAccount, activeChain, addToast } = useInkathon() const [task, setTask] = useState(0) const [finality, setFinality] = useState('Provisional') + const [paraSearch, setParaSearch] = useState('') + const pathname = usePathname() + const network = getChainFromPath(pathname) + const { ownedParaIds, loading: loadingOwnedParaIds } = useOwnedParaIds() + + const knownTaskName = Number.isFinite(task) + ? network_list[network]?.paraId?.[task.toString()]?.name + : null + const filteredOwnedParaIds = ownedParaIds.filter(({ lifecycle, name, paraId }) => { + const search = paraSearch.trim().toLowerCase() + if (!search) return true + return ( + paraId.toString().includes(search) || + name?.toLowerCase().includes(search) || + lifecycle?.toLowerCase().includes(search) + ) + }) const txButtonProps: TxButtonProps = { api, // api is guaranteed to be defined here @@ -44,7 +65,7 @@ const AssignModal: FC = ({ isOpen, onClose, regionId }) => {
-

Assing Core Nb: {regionId.core}

+

Assign Core Nb: {regionId.core}

Account:{' '} {activeAccount @@ -52,15 +73,64 @@ const AssignModal: FC = ({ isOpen, onClose, regionId }) => { : 'error'}

+
+
+ + setParaSearch(e.target.value)} + placeholder="Search by ParaId, name, or lifecycle" + /> + {loadingOwnedParaIds ? ( +

Loading your ParaIds...

+ ) : filteredOwnedParaIds.length > 0 ? ( +
+ {filteredOwnedParaIds.map(({ hasCode, lifecycle, name, paraId }) => ( + + ))} +
+ ) : ( +

+ No owned ParaIds found. You can still enter a ParaId manually. +

+ )} +
+
setTask(parseInt(e.target.value, 10))} + onChange={(e) => { + const parsedTask = parseInt(e.target.value, 10) + setTask(Number.isNaN(parsedTask) ? 0 : parsedTask) + }} /> + {knownTaskName &&

Selected: {knownTaskName}

} diff --git a/src/hooks/useOwnedParaIds.tsx b/src/hooks/useOwnedParaIds.tsx new file mode 100644 index 0000000..998735f --- /dev/null +++ b/src/hooks/useOwnedParaIds.tsx @@ -0,0 +1,117 @@ +import { network_list } from '@/config/network' +import { getChainFromPath } from '@/utils/common/chainPath' +import { parseFormattedNumber } from '@/utils/helperFunc' +import { encodeAddress } from '@polkadot/util-crypto' +import { useInkathon } from '@poppyseed/lastic-sdk' +import { usePathname } from 'next/navigation' +import { useEffect, useState } from 'react' + +export type OwnedParaId = { + paraId: number + manager: string + hasCode: boolean + lifecycle: string | null + name?: string +} + +type OptionalChainValue = { + isSome?: boolean + unwrap?: () => { toString: () => string } +} + +const getAddressVariants = (address: string, ss58Prefix: number) => { + const addresses = new Set([address]) + try { + addresses.add(encodeAddress(address, ss58Prefix)) + } catch { + return addresses + } + return addresses +} + +export const useOwnedParaIds = () => { + const { activeAccount, activeChain, activeRelayChain, relayApi } = useInkathon() + const pathname = usePathname() + const network = getChainFromPath(pathname) + const [ownedParaIds, setOwnedParaIds] = useState([]) + const [loading, setLoading] = useState(false) + + useEffect(() => { + let mounted = true + + const fetchOwnedParaIds = async () => { + if (!relayApi?.query?.registrar?.paras || !activeAccount) { + setOwnedParaIds([]) + return + } + + setLoading(true) + try { + const relaySs58Prefix = activeRelayChain?.ss58Prefix || activeChain?.ss58Prefix || 42 + const activeAddresses = getAddressVariants(activeAccount.address, relaySs58Prefix) + const entries = await relayApi.query.registrar.paras.entries() + + const owned = entries + .map( + ([ + { + args: [paraId], + }, + optInfo, + ]: any) => { + if (optInfo.isNone) return null + const paraInfo = optInfo.unwrap() + const manager = paraInfo.manager.toString() + const paraIdNumber = parseFormattedNumber(paraId.toString()) + + if (!activeAddresses.has(manager)) return null + + return { + paraId: paraIdNumber, + manager, + } + }, + ) + .filter((entry): entry is { paraId: number; manager: string } => Boolean(entry)) + + const ids = owned.map(({ paraId }) => paraId) + const [codeHashes, lifecycles] = await Promise.all([ + relayApi.query.paras?.currentCodeHash?.multi + ? relayApi.query.paras.currentCodeHash.multi(ids) + : Promise.resolve([]), + relayApi.query.paras?.paraLifecycles?.multi + ? relayApi.query.paras.paraLifecycles.multi(ids) + : Promise.resolve([]), + ]) + + const withChainInfo = owned.map(({ paraId, manager }, index) => { + const codeHash = codeHashes[index] as OptionalChainValue | undefined + const lifecycle = lifecycles[index] as OptionalChainValue | undefined + + return { + paraId, + manager, + hasCode: Boolean(codeHash?.isSome), + lifecycle: lifecycle?.isSome ? lifecycle.unwrap?.().toString() || null : null, + name: network_list[network]?.paraId?.[paraId.toString()]?.name, + } + }) + + if (mounted) setOwnedParaIds(withChainInfo) + } catch (error) { + console.error('Failed to fetch owned ParaIds:', error) + if (mounted) setOwnedParaIds([]) + } finally { + if (mounted) setLoading(false) + } + } + + fetchOwnedParaIds() + + return () => { + mounted = false + } + }, [activeAccount, activeChain, activeRelayChain, network, relayApi]) + + return { ownedParaIds, loading } +}