diff --git a/src/assets/networks/coretime/index.ts b/src/assets/networks/coretime/index.ts new file mode 100644 index 00000000..6b832342 --- /dev/null +++ b/src/assets/networks/coretime/index.ts @@ -0,0 +1,4 @@ +import KusamaCoretime from './kusama.png'; +import RococoCoretime from './rococo.png'; + +export { KusamaCoretime, RococoCoretime }; diff --git a/src/assets/networks/coretime/kusama.png b/src/assets/networks/coretime/kusama.png new file mode 100644 index 00000000..ce8a3ef7 Binary files /dev/null and b/src/assets/networks/coretime/kusama.png differ diff --git a/src/assets/networks/coretime.png b/src/assets/networks/coretime/rococo.png similarity index 100% rename from src/assets/networks/coretime.png rename to src/assets/networks/coretime/rococo.png diff --git a/src/assets/networks/index.tsx b/src/assets/networks/index.tsx new file mode 100644 index 00000000..8a8035a8 --- /dev/null +++ b/src/assets/networks/index.tsx @@ -0,0 +1,5 @@ +import RegionX from './regionx.png'; + +export * from './coretime'; +export * from './relay'; +export { RegionX }; diff --git a/src/assets/networks/relay/index.ts b/src/assets/networks/relay/index.ts new file mode 100644 index 00000000..c5d0bf51 --- /dev/null +++ b/src/assets/networks/relay/index.ts @@ -0,0 +1,4 @@ +import Kusama from './kusama.png'; +import Rococo from './rococo.png'; + +export { Kusama, Rococo }; diff --git a/src/assets/networks/kusama.png b/src/assets/networks/relay/kusama.png similarity index 100% rename from src/assets/networks/kusama.png rename to src/assets/networks/relay/kusama.png diff --git a/src/assets/networks/rococo.png b/src/assets/networks/relay/rococo.png similarity index 100% rename from src/assets/networks/rococo.png rename to src/assets/networks/relay/rococo.png diff --git a/src/components/Elements/NetworkSelect/index.tsx b/src/components/Elements/NetworkSelect/index.tsx index afc3b201..1cbbf169 100644 --- a/src/components/Elements/NetworkSelect/index.tsx +++ b/src/components/Elements/NetworkSelect/index.tsx @@ -1,9 +1,12 @@ import { FormControl, InputLabel, MenuItem, Select } from '@mui/material'; import { useRouter } from 'next/router'; +import { useNetwork } from '@/contexts/network'; +import { NetworkType } from '@/models'; + const RelaySelect = () => { const router = useRouter(); - const { network } = router.query; + const { network } = useNetwork(); const handleChange = (e: any) => { router.push( @@ -16,19 +19,21 @@ const RelaySelect = () => { ); }; - return ( + return network !== NetworkType.NONE ? ( Network + ) : ( + <> ); }; diff --git a/src/components/Elements/Selectors/ChainSelector/index.tsx b/src/components/Elements/Selectors/ChainSelector/index.tsx index 7ddcf621..ced01401 100644 --- a/src/components/Elements/Selectors/ChainSelector/index.tsx +++ b/src/components/Elements/Selectors/ChainSelector/index.tsx @@ -1,5 +1,6 @@ import { Box, + CircularProgress, FormControl, InputLabel, MenuItem, @@ -8,32 +9,61 @@ import { } from '@mui/material'; import Image from 'next/image'; -import CoretimeIcon from '@/assets/networks/coretime.png'; -import RegionXIcon from '@/assets/networks/regionx.png'; -// import KusamaIcon from '@/assets/networks/kusama.png'; -import RococoIcon from '@/assets/networks/rococo.png'; -import { ChainType } from '@/models'; +import { useCoretimeApi, useRelayApi } from '@/contexts/apis'; +import { ChainType, NetworkType } from '@/models'; interface ChainSelectorProps { chain: ChainType; setChain: (_: ChainType) => void; } +import { + Kusama, + KusamaCoretime, + RegionX, + Rococo, + RococoCoretime, +} from '@/assets/networks'; +import { ApiState } from '@/contexts/apis/types'; +import { useNetwork } from '@/contexts/network'; + +const coretimeIcons = { + [NetworkType.NONE]: RococoCoretime, + [NetworkType.KUSAMA]: KusamaCoretime, + [NetworkType.ROCOCO]: RococoCoretime, +}; + +const relayIcons = { + [NetworkType.NONE]: Rococo, + [NetworkType.KUSAMA]: Kusama, + [NetworkType.ROCOCO]: Rococo, +}; + export const ChainSelector = ({ chain, setChain }: ChainSelectorProps) => { + const { network } = useNetwork(); + const { + state: { name: coretimeChain, apiState: coretimeState }, + } = useCoretimeApi(); + const { + state: { name: relayChain, apiState: relayState }, + } = useRelayApi(); + const menuItems = [ { - icon: RococoIcon, - label: 'Relay Chain', + icon: relayIcons[network], + label: relayChain, value: ChainType.RELAY, + loading: coretimeState !== ApiState.READY, }, { - icon: CoretimeIcon, - label: 'Coretime Chain', + icon: coretimeIcons[network], + label: coretimeChain, value: ChainType.CORETIME, + loading: relayState !== ApiState.READY, }, { - icon: RegionXIcon, + icon: RegionX, label: 'RegionX Chain', value: ChainType.REGIONX, }, @@ -48,17 +78,21 @@ export const ChainSelector = ({ chain, setChain }: ChainSelectorProps) => { label='Origin' onChange={(e) => setChain(e.target.value as ChainType)} > - {menuItems.map(({ icon, label, value }, index) => ( + {menuItems.map(({ icon, label, value, loading }, index) => ( - + icon - - {label} - + {loading ? ( + + ) : ( + + {label} + + )} ))} diff --git a/src/contexts/apis/CoretimeApi/index.tsx b/src/contexts/apis/CoretimeApi/index.tsx index 7289ddf3..d19b14dd 100644 --- a/src/contexts/apis/CoretimeApi/index.tsx +++ b/src/contexts/apis/CoretimeApi/index.tsx @@ -1,8 +1,9 @@ -import { useRouter } from 'next/router'; import React, { useContext, useEffect, useReducer } from 'react'; import { ApiState } from '@/contexts/apis/types'; +import { useNetwork } from '@/contexts/network'; import { useToast } from '@/contexts/toast'; +import { NetworkType } from '@/models'; import { connect, disconnect, initialState, reducer } from '../common'; import { WS_KUSAMA_CORETIME_CHAIN, WS_ROCOCO_CORETIME_CHAIN } from '../consts'; @@ -36,8 +37,7 @@ const CoretimeApiContextProvider = (props: any) => { const [state, dispatch] = useReducer(reducer, initialState); const { toastError, toastSuccess } = useToast(); - const router = useRouter(); - const { network } = router.query; + const { network } = useNetwork(); useEffect(() => { state.apiError && toastError(`Failed to connect to Coretime chain`); @@ -45,28 +45,25 @@ const CoretimeApiContextProvider = (props: any) => { useEffect(() => { state.apiState === ApiState.READY && - toastSuccess('Successfully connected to the Coretime chain'); - }, [state.apiState, toastSuccess]); + state.name && + toastSuccess(`Successfully connected to ${state.name}`); + }, [state.apiState, state.name, toastSuccess]); const getUrl = (network: any): string => { - if (!network || network == 'rococo') { - return WS_ROCOCO_CORETIME_CHAIN; - } else if (network == 'kusama') { - return WS_KUSAMA_CORETIME_CHAIN; - } else { - /* eslint-disable no-console */ - console.error(`Network: ${network} not recognized`); - // Default to rococo. - return WS_ROCOCO_CORETIME_CHAIN; - } + return network === NetworkType.ROCOCO + ? WS_ROCOCO_CORETIME_CHAIN + : WS_KUSAMA_CORETIME_CHAIN; }; const disconnectCoretime = () => disconnect(state); useEffect(() => { - if (state.socket == getUrl(network)) return; - const updateNetwork = network != '' && state.socket != getUrl(network); - connect(state, getUrl(network), dispatch, updateNetwork, types); + if (network === NetworkType.NONE || state.socket == getUrl(network)) return; + const updateNetwork = state.socket != getUrl(network); + if (updateNetwork) { + disconnectCoretime(); + connect(state, getUrl(network), dispatch, updateNetwork, types); + } }, [network, state]); return ( diff --git a/src/contexts/apis/RelayApi/index.tsx b/src/contexts/apis/RelayApi/index.tsx index 7fbb2c4a..360eae43 100644 --- a/src/contexts/apis/RelayApi/index.tsx +++ b/src/contexts/apis/RelayApi/index.tsx @@ -1,11 +1,11 @@ -import { useRouter } from 'next/router'; import React, { useContext, useEffect, useReducer, useState } from 'react'; import { parseHNString } from '@/utils/functions'; import { ApiState } from '@/contexts/apis/types'; +import { useNetwork } from '@/contexts/network'; import { useToast } from '@/contexts/toast'; -import { ParaId } from '@/models'; +import { NetworkType, ParaId } from '@/models'; import { connect, disconnect, initialState, reducer } from '../common'; import { WS_KUSAMA_RELAY_CHAIN, WS_ROCOCO_RELAY_CHAIN } from '../consts'; @@ -25,8 +25,7 @@ const RelayApiContextProvider = (props: any) => { const { toastError, toastSuccess } = useToast(); const [paraIds, setParaIds] = useState([]); - const router = useRouter(); - const { network } = router.query; + const { network } = useNetwork(); useEffect(() => { state.apiError && toastError(`Failed to connect to relay chain`); @@ -34,28 +33,25 @@ const RelayApiContextProvider = (props: any) => { useEffect(() => { state.apiState === ApiState.READY && - toastSuccess('Successfully connected to the relay chain'); - }, [state.apiState, toastSuccess]); + state.name && + toastSuccess(`Successfully connected to ${state.name}`); + }, [state.apiState, state.name, toastSuccess]); const disconnectRelay = () => disconnect(state); const getUrl = (network: any): string => { - if (!network || network == 'rococo') { - return WS_ROCOCO_RELAY_CHAIN; - } else if (network == 'kusama') { - return WS_KUSAMA_RELAY_CHAIN; - } else { - /* eslint-disable no-console */ - console.error(`Network: ${network} not recognized`); - // Default to rococo. - return WS_ROCOCO_RELAY_CHAIN; - } + return network === NetworkType.ROCOCO + ? WS_ROCOCO_RELAY_CHAIN + : WS_KUSAMA_RELAY_CHAIN; }; useEffect(() => { - if (state.socket == getUrl(network)) return; - const updateNetwork = network != '' && state.socket != getUrl(network); - connect(state, getUrl(network), dispatch, updateNetwork); + if (network === NetworkType.NONE || state.socket == getUrl(network)) return; + const updateNetwork = state.socket != getUrl(network); + if (updateNetwork) { + disconnectRelay(); + connect(state, getUrl(network), dispatch, updateNetwork); + } }, [network, state]); useEffect(() => { diff --git a/src/contexts/apis/common.ts b/src/contexts/apis/common.ts index ad96b28f..22cfa972 100644 --- a/src/contexts/apis/common.ts +++ b/src/contexts/apis/common.ts @@ -14,6 +14,7 @@ export type State = { apiError: any; apiState: ApiState; symbol: string; + name: string; }; export const initialState: State = { @@ -24,6 +25,7 @@ export const initialState: State = { apiError: null, apiState: ApiState.DISCONNECTED, symbol: '', + name: '', }; /// @@ -49,9 +51,16 @@ export const reducer = (state: any, action: any) => { case 'CONNECT_ERROR': return { ...state, apiState: ApiState.ERROR, apiError: action.payload }; case 'DISCONNECTED': - return { ...state, apiState: ApiState.DISCONNECTED }; + return { + ...state, + apiState: ApiState.DISCONNECTED, + symbol: '', + name: '', + }; case 'SET_SYMBOL': return { ...state, symbol: action.payload }; + case 'SET_NAME': + return { ...state, name: action.payload }; default: throw new Error(`Unknown type: ${action.type}`); } @@ -79,25 +88,31 @@ export const connect = ( _api.on('connected', () => { dispatch({ type: 'CONNECT', payload: _api, socket }); // `ready` event is not emitted upon reconnection and is checked explicitly here. - _api.isReady.then(() => { - dispatch({ type: 'CONNECT_SUCCESS' }); - const chainInfo = _api.registry.getChainProperties(); - if (chainInfo?.tokenSymbol.isSome) { - const [symbol] = chainInfo.tokenSymbol.toHuman() as [string]; - dispatch({ - type: 'SET_SYMBOL', - payload: symbol, - }); - } + _api.isReady.then(() => dispatch({ type: 'CONNECT_SUCCESS' })); + }); + _api.on('ready', async () => { + dispatch({ type: 'CONNECT_SUCCESS' }); + const chainInfo = _api.registry.getChainProperties(); + if (chainInfo?.tokenSymbol.isSome) { + const [symbol] = chainInfo.tokenSymbol.toHuman() as [string]; + dispatch({ + type: 'SET_SYMBOL', + payload: symbol, + }); + } + + const name = await _api.rpc.system.chain(); + dispatch({ + type: 'SET_NAME', + payload: name, }); }); - _api.on('ready', () => dispatch({ type: 'CONNECT_SUCCESS' })); _api.on('error', (err) => dispatch({ type: 'CONNECT_ERROR', payload: err })); _api.on('disconnected', () => dispatch({ type: 'DISCONNECTED' })); }; export const disconnect = (state: any) => { const { api, apiState } = state; - if (apiState === ApiState.DISCONNECTED) return; + if (!api || apiState === ApiState.DISCONNECTED) return; api.disconnect(); }; diff --git a/src/contexts/network/index.tsx b/src/contexts/network/index.tsx new file mode 100644 index 00000000..90e72f9f --- /dev/null +++ b/src/contexts/network/index.tsx @@ -0,0 +1,61 @@ +import { useRouter } from 'next/router'; +import React, { createContext, useContext, useEffect, useState } from 'react'; + +import { NetworkType } from '@/models'; + +interface NetworkData { + network: NetworkType; + setNetwork: (_network: NetworkType) => void; +} + +const defaultNetworkData: NetworkData = { + network: NetworkType.ROCOCO, + setNetwork: (_network: NetworkType) => { + /** */ + }, +}; + +const NetworkDataContext = createContext(defaultNetworkData); + +interface Props { + children: React.ReactNode; +} + +const NetworkProvider = ({ children }: Props) => { + const [activeNetwork, setActiveNetwork] = useState(NetworkType.NONE); + + const router = useRouter(); + const { network } = router.query; + + useEffect(() => { + if (!router.isReady) return; + if (network === 'rococo') setActiveNetwork(NetworkType.ROCOCO); + else if (network === 'kusama') setActiveNetwork(NetworkType.KUSAMA); + else { + // invalid network param. redirect to the default chain: rococo + router.push( + { + pathname: router.pathname, + query: { + ...router.query, + network: 'rococo', + }, + }, + undefined, + { shallow: false } + ); + } + }, [network, router.isReady]); + + const setNetwork = (network: NetworkType) => setActiveNetwork(network); + + return ( + + {children} + + ); +}; + +const useNetwork = () => useContext(NetworkDataContext); + +export { NetworkProvider, useNetwork }; diff --git a/src/models/types.ts b/src/models/types.ts index 73a11532..05118c28 100644 --- a/src/models/types.ts +++ b/src/models/types.ts @@ -36,6 +36,15 @@ export enum ChainType { REGIONX = 3, } +export enum NetworkType { + // eslint-disable-next-line no-unused-vars + NONE = 'none', + // eslint-disable-next-line no-unused-vars + ROCOCO = 'rococo', + // eslint-disable-next-line no-unused-vars + KUSAMA = 'kusama', +} + export type Sender = { address: string; signer: Signer; diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index c9a20c51..8eb733b7 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -25,6 +25,7 @@ import { } from '@/contexts/apis/consts'; import { ContextDataProvider } from '@/contexts/common'; import { MarketProvider } from '@/contexts/market'; +import { NetworkProvider } from '@/contexts/network'; import { RegionDataProvider } from '@/contexts/regions'; import { SaleInfoProvider } from '@/contexts/sales'; import { TaskDataProvider } from '@/contexts/tasks'; @@ -69,32 +70,34 @@ export default function MyApp(props: MyAppProps) { - - - - - - - - - {getLayout()} - - - - - - - - + + + + + + + + + + {getLayout()} + + + + + + + + + diff --git a/src/pages/transfer.tsx b/src/pages/transfer.tsx index 85d40431..91c34cd0 100644 --- a/src/pages/transfer.tsx +++ b/src/pages/transfer.tsx @@ -1,7 +1,6 @@ import ArrowDownward from '@mui/icons-material/ArrowDownwardOutlined'; import { LoadingButton } from '@mui/lab'; import { - Alert, Box, Button, DialogActions, @@ -65,12 +64,10 @@ const TransferPage = () => { const [working, setWorking] = useState(false); const [newOwner, setNewOwner] = useState(''); - const [originChain, setOriginChain] = useState(ChainType.NONE); + const [originChain, setOriginChain] = useState(ChainType.CORETIME); const [destinationChain, setDestinationChain] = useState( - ChainType.NONE + ChainType.CORETIME ); - const [statusLabel, _setStatusLabel] = useState(''); - const [selectedRegion, setSelectedRegion] = useState( null ); @@ -308,11 +305,6 @@ const TransferPage = () => { /> )} - {statusLabel && ( - - {statusLabel} - - )}