Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/components/cores/CoreItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<CardProps> = ({
Expand All @@ -29,6 +31,8 @@ const CoreItem: React.FC<CardProps> = ({
begin,
duration,
constants,
assignedTask,
assignedTaskName,
}) => {
const pathname = usePathname()

Expand Down Expand Up @@ -78,6 +82,14 @@ const CoreItem: React.FC<CardProps> = ({
End Relay Block: {(begin + duration) * constants.timeslicePeriod}
</p>
</div>
{assignedTask !== null && assignedTask !== undefined && (
<div className="flex flex-row text-gray-12 p-1 ">
<p className="px-2">
Assigned to: {assignedTask}
{assignedTaskName ? ` (${assignedTaskName})` : ''}
</p>
</div>
)}
</div>
</div>
</Link>
Expand Down
11 changes: 11 additions & 0 deletions src/components/cores/CoreItemSectionDisplay.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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 (
<>
Expand All @@ -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)}
/>
) : (
<CoreItem
Expand Down
76 changes: 73 additions & 3 deletions src/components/extrinsics/broker/AssignModal.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import PrimaryButton from '@/components/button/PrimaryButton'
import Modal from '@/components/modal/Modal'
import { network_list } from '@/config/network'
import { useOwnedParaIds } from '@/hooks/useOwnedParaIds'
import { RegionIdProps } from '@/types/broker'
import { getChainFromPath } from '@/utils/common/chainPath'
import { truncateHash } from '@/utils/truncateHash'
import { encodeAddress } from '@polkadot/util-crypto'
import { TxButtonProps, useInkathon, useTxButton } from '@poppyseed/lastic-sdk'
import { usePathname } from 'next/navigation'
import { FC, useState } from 'react'

interface AssignModalProps {
Expand All @@ -16,6 +20,23 @@ const AssignModal: FC<AssignModalProps> = ({ 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
Expand Down Expand Up @@ -44,23 +65,72 @@ const AssignModal: FC<AssignModalProps> = ({ isOpen, onClose, regionId }) => {
<Modal isOpen={isOpen} onClose={onClose} title={`Assign Core Nb: ${regionId.core} To Para ID`}>
<div className="flex flex-col p-4">
<div className="flex flex-col mb-4">
<p className="text-lg font-semibold mb-2">Assing Core Nb: {regionId.core}</p>
<p className="text-lg font-semibold mb-2">Assign Core Nb: {regionId.core}</p>
<p className="text-lg mb-2">
Account:{' '}
{activeAccount
? truncateHash(encodeAddress(activeAccount.address, activeChain?.ss58Prefix || 42), 8)
: 'error'}
</p>
<label htmlFor="task" className="text-lg font-semibold mb-2">
To para ID:
To Para ID:
</label>
<div className="mb-4 rounded-md border border-gray-6 dark:border-gray-17 p-3">
<div className="flex flex-col gap-2">
<label htmlFor="para-search" className="text-sm font-semibold">
Your ParaIds
</label>
<input
id="para-search"
className="text-md border rounded-md p-2 focus:ring-blue-500 focus:border-blue-500"
type="text"
value={paraSearch}
onChange={(e) => setParaSearch(e.target.value)}
placeholder="Search by ParaId, name, or lifecycle"
/>
{loadingOwnedParaIds ? (
<p className="text-sm text-gray-15">Loading your ParaIds...</p>
) : filteredOwnedParaIds.length > 0 ? (
<div className="flex max-h-40 flex-col gap-2 overflow-y-auto">
{filteredOwnedParaIds.map(({ hasCode, lifecycle, name, paraId }) => (
<button
key={paraId}
type="button"
onClick={() => setTask(paraId)}
className={`rounded-md border px-3 py-2 text-left text-sm hover:bg-pink-50 hover:dark:bg-gray-22 ${
task === paraId ? 'border-pink-400 bg-pink-50 dark:bg-gray-22' : ''
}`}
>
<span className="font-semibold">
{paraId}
{name ? ` - ${name}` : ''}
</span>
<span className="block text-xs text-gray-14">
{[lifecycle, hasCode ? 'code uploaded' : 'no code']
.filter(Boolean)
.join(' · ')}
</span>
</button>
))}
</div>
) : (
<p className="text-sm text-gray-15">
No owned ParaIds found. You can still enter a ParaId manually.
</p>
)}
</div>
</div>
<input
id="task"
className="text-lg border rounded-md p-2 mb-4 focus:ring-blue-500 focus:border-blue-500"
type="number"
value={task}
onChange={(e) => setTask(parseInt(e.target.value, 10))}
onChange={(e) => {
const parsedTask = parseInt(e.target.value, 10)
setTask(Number.isNaN(parsedTask) ? 0 : parsedTask)
}}
/>
{knownTaskName && <p className="text-sm text-gray-15 mb-4">Selected: {knownTaskName}</p>}
<label htmlFor="finality" className="text-lg font-semibold mb-2">
Finality:
</label>
Expand Down
117 changes: 117 additions & 0 deletions src/hooks/useOwnedParaIds.tsx
Original file line number Diff line number Diff line change
@@ -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<OwnedParaId[]>([])
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 }
}