diff --git a/README.md b/README.md index 1596546..23008ce 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ The UI currently supports: - Parachain Id reservation - Parachain code registration -### `/paras/renewal` +### `/renew` - Core renewal ### `/marketplace` diff --git a/src/components/Layout/Sidebar/index.tsx b/src/components/Layout/Sidebar/index.tsx index 2f43bbc..ba07db8 100644 --- a/src/components/Layout/Sidebar/index.tsx +++ b/src/components/Layout/Sidebar/index.tsx @@ -110,7 +110,7 @@ export const Sidebar = () => { parachains: [ { label: 'Renew', - route: '/paras/renewal', + route: '/renew', enabled: true, icon: , }, diff --git a/src/components/Paras/ParaDisplay/index.tsx b/src/components/Paras/ParaDisplay/index.tsx index 77c0e4d..ea7db8f 100644 --- a/src/components/Paras/ParaDisplay/index.tsx +++ b/src/components/Paras/ParaDisplay/index.tsx @@ -9,8 +9,10 @@ import Unknown from '../../../assets/unknown.svg'; interface ParaDisplayProps { paraId: number; network: NetworkType; + core?: number; } -export const ParaDisplay = ({ paraId, network }: ParaDisplayProps) => { + +export const ParaDisplay = ({ paraId, network, core }: ParaDisplayProps) => { const data = chainData[network][paraId]; if (data === undefined) @@ -18,6 +20,7 @@ export const ParaDisplay = ({ paraId, network }: ParaDisplayProps) => { Parachain #{paraId} + {core &&

| Core {core}

}
); @@ -32,6 +35,7 @@ export const ParaDisplay = ({ paraId, network }: ParaDisplayProps) => { )} {name}

#{paraId}

+ {core &&

| Core {core}

} ); }; diff --git a/src/components/Renew/action.tsx b/src/components/Renew/action.tsx new file mode 100644 index 0000000..ad472ab --- /dev/null +++ b/src/components/Renew/action.tsx @@ -0,0 +1,70 @@ +import { Stack } from '@mui/material'; +import { useState } from 'react'; + +import { RenewableParachain } from '@/hooks'; +import { useSubmitExtrinsic } from '@/hooks/submitExtrinsic'; + +import { ProgressButton } from '@/components'; + +import { useAccounts } from '@/contexts/account'; +import { useCoretimeApi } from '@/contexts/apis'; +import { useToast } from '@/contexts/toast'; + +interface RenewActionProps { + parachain: RenewableParachain; + enabled: boolean; +} + +export const RenewAction = ({ parachain, enabled }: RenewActionProps) => { + const [working, setWorking] = useState(false); + + const { + state: { activeAccount, activeSigner }, + } = useAccounts(); + const { + state: { api: coretimeApi, isApiReady: isCoretimeReady, decimals, symbol }, + } = useCoretimeApi(); + + const { toastError, toastInfo, toastSuccess } = useToast(); + const { submitExtrinsicWithFeeInfo } = useSubmitExtrinsic(); + + const handleRenew = () => { + if (!activeAccount || !coretimeApi || !isCoretimeReady || !activeSigner) return; + + const { core } = parachain; + + const txRenewal = coretimeApi.tx.broker.renew(core); + submitExtrinsicWithFeeInfo(symbol, decimals, txRenewal, activeAccount.address, activeSigner, { + ready: () => { + setWorking(true); + toastInfo('Transaction was initiated'); + }, + inBlock: () => toastInfo('In Block'), + finalized: () => setWorking(false), + success: () => { + toastSuccess('Successfully renewed the selected parachain'); + }, + fail: () => { + toastError(`Failed to renew the selected parachain`); + }, + error: (e) => { + toastError(`Failed to renew the selected parachain ${e}`); + setWorking(false); + }, + }); + }; + + return ( + <> + + + + + ); +}; diff --git a/src/components/Renew/info.tsx b/src/components/Renew/info.tsx new file mode 100644 index 0000000..5554520 --- /dev/null +++ b/src/components/Renew/info.tsx @@ -0,0 +1,193 @@ +import { Box, Stack, Tooltip, Typography } from '@mui/material'; +import { humanizer } from 'humanize-duration'; +import { Dispatch, SetStateAction, useEffect, useState } from 'react'; + +import { RenewableParachain } from '@/hooks'; +import { getBalanceString, timesliceToTimestamp } from '@/utils/functions'; +import theme from '@/utils/muiTheme'; + +import { Banner } from '@/components'; + +import { useCoretimeApi, useRelayApi } from '@/contexts/apis'; +import { useSaleInfo } from '@/contexts/sales'; +import { ContextStatus } from '@/models'; + +interface RenewableParaInfoProps { + parachain: RenewableParachain; + setRenewalEnabled: Dispatch>; +} + +export const RenewableParaInfo = ({ parachain, setRenewalEnabled }: RenewableParaInfoProps) => { + const [expiryTimestamp, setExpiryTimestamp] = useState(0); + + const { saleInfo, saleStatus, status: saleInfoStatus, phase } = useSaleInfo(); + + const { + state: { api: relayApi, isApiReady: isRelayReady }, + } = useRelayApi(); + const { + state: { api: coretimeApi, isApiReady: isCoretimeReady }, + timeslicePeriod, + } = useCoretimeApi(); + + const [loading, setLoading] = useState(false); + + useEffect(() => { + const getExpiry = async () => { + setLoading(true); + if ( + !coretimeApi || + !isCoretimeReady || + !relayApi || + !isRelayReady || + saleInfoStatus !== ContextStatus.LOADED + ) + return; + + const now = await timesliceToTimestamp( + relayApi, + saleStatus.lastCommittedTimeslice, + timeslicePeriod + ); + const expiry = await timesliceToTimestamp(relayApi, parachain.when, timeslicePeriod); + + if (expiry - now < 0) { + setExpiryTimestamp(phase.endpoints.fixed.end - now); + } else { + setExpiryTimestamp(expiry - now); + } + + setLoading(false); + }; + + getExpiry(); + }, [ + parachain, + coretimeApi, + isCoretimeReady, + relayApi, + isRelayReady, + timeslicePeriod, + saleInfoStatus, + saleStatus, + phase, + ]); + + useEffect(() => { + // if expiry is before the next region begin it should be possible to renew. + setRenewalEnabled(parachain.when <= saleInfo.regionBegin); + }, [saleInfo.regionBegin, parachain.when]); + + return ( + <> + + + {parachain.when > saleInfo.regionBegin ? ( + + ) : ( + <> + {/* If all cores are sold warn the user: */} + {saleInfo.coresSold === saleInfo.coresOffered && ( + + )} + {/* If not all cores are sold inform the user to renew: */} + {saleInfo.coresSold < saleInfo.coresOffered && ( + + )} + + )} + + + ); +}; + +interface ParachainInfoProps { + parachain: RenewableParachain; + expiryTimestamp: number; + expiryLoading: boolean; +} + +const ParachainInfo = ({ parachain, expiryTimestamp, expiryLoading }: ParachainInfoProps) => { + const { + state: { decimals, symbol }, + } = useCoretimeApi(); + + const formatDuration = humanizer({ units: ['w', 'd', 'h'], round: true }); + + return ( + <> + + + + + + + ); +}; + +interface PropertyProps { + property: string; + value: string; + tooltip?: string; +} + +const Property = ({ property, value, tooltip }: PropertyProps) => { + return ( + + + {property} + + + {tooltip && ( + + + ⓘ + + + )} + + {value} + + + + ); +}; diff --git a/src/components/Renew/renewPage.tsx b/src/components/Renew/renewPage.tsx new file mode 100644 index 0000000..d5023b1 --- /dev/null +++ b/src/components/Renew/renewPage.tsx @@ -0,0 +1,72 @@ +import { Backdrop, Box, CircularProgress, Paper, Typography, useTheme } from '@mui/material'; +import { useState } from 'react'; + +import { useRenewableParachains } from '@/hooks/renewableParas'; + +import { Balance } from '@/components'; + +import { ContextStatus } from '@/models'; + +import { RenewAction } from './action'; +import { RenewableParaInfo } from './info'; +import { SelectParachain } from './select'; + +const Renewal = () => { + const theme = useTheme(); + + const [activeIdx, setActiveIdx] = useState(0); + const [renewalEnabled, setRenewalEnabled] = useState(true); + const { status, parachains } = useRenewableParachains(); + + return status !== ContextStatus.LOADED ? ( + + + + ) : parachains.length === 0 ? ( + There are no renewable parachains. + ) : ( + <> + + + + Renew a parachain + + + Renew a parachain + + + + + + + + + + + + + + ); +}; + +export default Renewal; diff --git a/src/components/Renew/select.tsx b/src/components/Renew/select.tsx new file mode 100644 index 0000000..d819352 --- /dev/null +++ b/src/components/Renew/select.tsx @@ -0,0 +1,46 @@ +import { FormControl, InputLabel, MenuItem, Select, Stack, Typography } from '@mui/material'; + +import { RenewableParachain } from '@/hooks'; +import theme from '@/utils/muiTheme'; + +import { ParaDisplay } from '@/components'; + +import { useNetwork } from '@/contexts/network'; + +interface SelectParachainProps { + parachains: RenewableParachain[]; + activeIdx: number; + setActiveIdx: (_: number) => void; +} + +export const SelectParachain = ({ parachains, activeIdx, setActiveIdx }: SelectParachainProps) => { + const { network } = useNetwork(); + + return ( + + + Select a parachain to renew + + + Parachain + + + + ); +}; diff --git a/src/hooks/renewableParas.ts b/src/hooks/renewableParas.ts index b4ae5b8..71f3958 100644 --- a/src/hooks/renewableParas.ts +++ b/src/hooks/renewableParas.ts @@ -7,11 +7,12 @@ import { ApiState } from '@/contexts/apis/types'; import { useNetwork } from '@/contexts/network'; import { ContextStatus, NetworkType } from '@/models'; -type RenewableParachain = { +export type RenewableParachain = { core: number; paraId: number; price: number; mask: string; + // The point in time that the renewable workload on `core` ends and a fresh renewal may begin. when: number; }; diff --git a/src/pages/paras/index.tsx b/src/pages/paras/index.tsx index 6c3e825..22310ca 100644 --- a/src/pages/paras/index.tsx +++ b/src/pages/paras/index.tsx @@ -69,7 +69,7 @@ const ParachainManagement = () => { // Renew coretime with the given para id const onRenew = (paraId: number) => { router.push({ - pathname: 'paras/renewal', + pathname: 'renew', query: { network, paraId }, }); }; diff --git a/src/pages/paras/renewal.tsx b/src/pages/paras/renewal.tsx deleted file mode 100644 index fa4e48a..0000000 --- a/src/pages/paras/renewal.tsx +++ /dev/null @@ -1,295 +0,0 @@ -import { - Backdrop, - Box, - CircularProgress, - FormControl, - InputLabel, - MenuItem, - Paper, - Select, - Stack, - Tooltip, - Typography, - useTheme, -} from '@mui/material'; -import { humanizer } from 'humanize-duration'; -import { useRouter } from 'next/router'; -import { useEffect, useState } from 'react'; - -import { useRenewableParachains } from '@/hooks/renewableParas'; -import { useSubmitExtrinsic } from '@/hooks/submitExtrinsic'; -import { getBalanceString, timesliceToTimestamp } from '@/utils/functions'; - -import { Balance, Banner, ParaDisplay, ProgressButton } from '@/components'; - -import { useAccounts } from '@/contexts/account'; -import { useCoretimeApi, useRelayApi } from '@/contexts/apis'; -import { useNetwork } from '@/contexts/network'; -import { useSaleInfo } from '@/contexts/sales'; -import { useToast } from '@/contexts/toast'; -import { ContextStatus } from '@/models'; - -const Renewal = () => { - const router = useRouter(); - const theme = useTheme(); - - const { - state: { activeAccount, activeSigner }, - } = useAccounts(); - const { status, parachains } = useRenewableParachains(); - const { saleInfo, saleStatus, status: saleInfoStatus, phase } = useSaleInfo(); - - const { - state: { api: relayApi, isApiReady: isRelayReady }, - } = useRelayApi(); - const { - state: { api: coretimeApi, isApiReady: isCoretimeReady, decimals, symbol }, - timeslicePeriod, - } = useCoretimeApi(); - - const { toastError, toastInfo, toastSuccess } = useToast(); - const { network } = useNetwork(); - const { submitExtrinsicWithFeeInfo } = useSubmitExtrinsic(); - - const [loading, setLoading] = useState(false); - const [activeIdx, setActiveIdx] = useState(0); - const [working, setWorking] = useState(false); - const [expiryTimestamp, setExpiryTimestamp] = useState(0); - - const formatDuration = humanizer({ units: ['w', 'd', 'h'], round: true }); - - const handleRenew = () => { - if (!activeAccount || !coretimeApi || !isCoretimeReady || !activeSigner) return; - - const { core } = parachains[activeIdx]; - - const txRenewal = coretimeApi.tx.broker.renew(core); - submitExtrinsicWithFeeInfo(symbol, decimals, txRenewal, activeAccount.address, activeSigner, { - ready: () => { - setWorking(true); - toastInfo('Transaction was initiated'); - }, - inBlock: () => toastInfo('In Block'), - finalized: () => setWorking(false), - success: () => { - toastSuccess('Successfully renewed the selected parachain'); - }, - fail: () => { - toastError(`Failed to renew the selected parachain`); - }, - error: (e) => { - toastError(`Failed to renew the selected parachain ${e}`); - setWorking(false); - }, - }); - }; - - useEffect(() => { - const getExpiry = async () => { - setLoading(true); - if ( - !coretimeApi || - !isCoretimeReady || - !relayApi || - !isRelayReady || - !parachains[activeIdx] || - saleInfoStatus !== ContextStatus.LOADED - ) - return; - - const now = await timesliceToTimestamp( - relayApi, - saleStatus.lastCommittedTimeslice, - timeslicePeriod - ); - const expiry = await timesliceToTimestamp( - relayApi, - parachains[activeIdx].when, - timeslicePeriod - ); - - if (expiry - now < 0) { - setExpiryTimestamp(phase.endpoints.fixed.end - now); - } else { - setExpiryTimestamp(expiry - now); - } - - setLoading(false); - }; - - getExpiry(); - }, [ - coretimeApi, - isCoretimeReady, - relayApi, - isRelayReady, - activeIdx, - parachains, - timeslicePeriod, - saleInfoStatus, - saleStatus, - phase, - ]); - - useEffect(() => { - if (!router.isReady || status !== ContextStatus.LOADED || parachains.length === 0) return; - const { query } = router; - if (query['paraId'] === undefined) return; - const paraId = parseInt(query['paraId'] as string); - const index = parachains.findIndex((para) => para.paraId == paraId); - if (index === -1) { - toastError(`No renewable parachain found with ID = ${paraId}`); - return; - } - setActiveIdx(index); - }, [router, parachains, status, parachains.length, toastError]); - - return status !== ContextStatus.LOADED ? ( - - - - ) : parachains.length === 0 ? ( - There are no renewable parachains. - ) : ( - <> - - - - Renew a parachain - - - Renew a parachain - - - - - - - - - - Select a parachain to renew - - - Parachain - - - - - - - - - {saleInfo.coresSold === saleInfo.coresOffered && ( - - - - )} - {/* If not all cores are sold inform the user: */} - {saleInfo.coresSold < saleInfo.coresOffered && ( - - - - )} - - - - - - - ); -}; - -interface PropertyProps { - property: string; - value: any; - tooltip?: string; -} - -export const Property = ({ property, value, tooltip }: PropertyProps) => { - return ( - - - {property} - - - {tooltip && ( - - - ⓘ - - - )} - - {value} - - - - ); -}; - -export default Renewal; diff --git a/src/pages/renew.tsx b/src/pages/renew.tsx new file mode 100644 index 0000000..82e9bd1 --- /dev/null +++ b/src/pages/renew.tsx @@ -0,0 +1,2 @@ +import RenewPage from '@/components/Renew/renewPage'; +export default RenewPage;