diff --git a/.env.example b/.env.example index b6eeb41..fd90eb7 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,3 @@ CONTRACT_IDENTITY="AddressOfIdentityContract" -CONTRACT_ADDRESS_BOOK="AddressOfAddressBookContract" \ No newline at end of file +CONTRACT_ADDRESS_BOOK="AddressOfAddressBookContract" +RELAY_CHAIN="kusama_or_polkadot" \ No newline at end of file diff --git a/next.config.js b/next.config.js index 41d7c89..e0948aa 100644 --- a/next.config.js +++ b/next.config.js @@ -7,6 +7,7 @@ const nextConfig = { env: { CONTRACT_IDENTITY: process.env.CONTRACT_IDENTITY, CONTRACT_ADDRESS_BOOK: process.env.CONTRACT_ADDRESS_BOOK, + RELAY_CHAIN: process.env.RELAY_CHAIN, }, async redirects() { return [ diff --git a/src/consts/index.ts b/src/consts/index.ts new file mode 100644 index 0000000..2b58f71 --- /dev/null +++ b/src/consts/index.ts @@ -0,0 +1,7 @@ +type RELAY_CHAIN_OPTION = 'polkadot' | 'kusama'; +const RELAY_CHAIN_ENDPOINTS = { + polkadot: "wss://polkadot.api.onfinality.io/public-ws", + kusama: "wss://kusama.api.onfinality.io/public-ws" +}; +export const RELAY_CHAIN = (process.env.RELAY_CHAIN || 'polkadot') as RELAY_CHAIN_OPTION; +export const RELAY_CHAIN_ENDPOINT = RELAY_CHAIN_ENDPOINTS[RELAY_CHAIN]; \ No newline at end of file diff --git a/src/contexts/RelayApi/index.tsx b/src/contexts/RelayApi/index.tsx new file mode 100644 index 0000000..366765b --- /dev/null +++ b/src/contexts/RelayApi/index.tsx @@ -0,0 +1,105 @@ +import { ApiPromise, WsProvider } from '@polkadot/api'; +import jsonrpc from '@polkadot/types/interfaces/jsonrpc'; +import { DefinitionRpcExt } from '@polkadot/types/types'; +import React, { useContext, useEffect, useReducer } from 'react'; + +import { RELAY_CHAIN_ENDPOINT } from '@/consts'; + +import { useToast } from '../Toast'; + +/// +// Initial state for `useReducer` + +type State = { + socket: string; + jsonrpc: Record>; + api: any; + apiError: any; + apiState: any; +}; + +const initialState: State = { + // These are the states + socket: RELAY_CHAIN_ENDPOINT, + jsonrpc: { ...jsonrpc }, + api: null, + apiError: null, + apiState: null, +}; + +/// +// Reducer function for `useReducer` + +const reducer = (state: any, action: any) => { + switch (action.type) { + case 'CONNECT_INIT': + return { ...state, apiState: 'CONNECT_INIT' }; + case 'CONNECT': + return { ...state, api: action.payload, apiState: 'CONNECTING' }; + case 'CONNECT_SUCCESS': + return { ...state, apiState: 'READY' }; + case 'CONNECT_ERROR': + return { ...state, apiState: 'ERROR', apiError: action.payload }; + default: + throw new Error(`Unknown type: ${action.type}`); + } +}; + +/// +// Connecting to the Substrate node + +const connect = (state: any, dispatch: any) => { + const { apiState, socket, jsonrpc } = state; + // We only want this function to be performed once + if (apiState) return; + + dispatch({ type: 'CONNECT_INIT' }); + + const provider = new WsProvider(socket); + const _api = new ApiPromise({ provider, rpc: jsonrpc }); + + // Set listeners for disconnection and reconnection event. + _api.on('connected', () => { + dispatch({ type: 'CONNECT', payload: _api }); + // `ready` event is not emitted upon reconnection and is checked explicitly here. + _api.isReady.then(() => dispatch({ type: 'CONNECT_SUCCESS' })); + }); + _api.on('ready', () => dispatch({ type: 'CONNECT_SUCCESS' })); + _api.on('error', (err) => dispatch({ type: 'CONNECT_ERROR', payload: err })); +}; + +const defaultValue = { + state: initialState, +}; + +const RelayApiContext = React.createContext(defaultValue); + +const RelayApiContextProvider = (props: any) => { + const [state, dispatch] = useReducer(reducer, initialState); + const { toastError, toastSuccess } = useToast(); + + useEffect(() => { + state.apiError && + toastError( + `Failed to connect to relay chain: error = ${state.apiError.toString()}` + ); + }, [state.apiError]); + + useEffect(() => { + state.apiState === 'READY' && + toastSuccess('Successfully connected to relay chain'); + }, [state.apiState]); + + useEffect(() => { + connect(state, dispatch); + }, [process.env.RELAY_CHAIN_ENDPOINT]); + + return ( + + {props.children} + + ); +}; +const useRelayApi = () => useContext(RelayApiContext); + +export { RelayApiContextProvider, useRelayApi }; diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index afe611b..35d40a1 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -14,6 +14,7 @@ import theme from '@/utils/muiTheme'; import { Layout } from '@/components/Layout'; +import { RelayApiContextProvider } from '@/contexts/RelayApi'; import { ToastProvider } from '@/contexts/Toast'; import { IdentityContractProvider } from '@/contracts'; import { AddressBookContractProvider } from '@/contracts/addressbook/context'; @@ -42,19 +43,21 @@ export default function MyApp(props: MyAppProps) { {/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */} - - - - - {getLayout()} - - - - + + + + + + {getLayout()} + + + + + diff --git a/src/pages/identity.tsx b/src/pages/identity.tsx index fe7aa4d..660266c 100644 --- a/src/pages/identity.tsx +++ b/src/pages/identity.tsx @@ -9,8 +9,8 @@ import { Grid, Typography, } from '@mui/material'; +import styles from '@styles/pages/identity.module.scss'; import { useState } from 'react'; -import '@styles/pages/identity.module.scss'; import { CreateIdentity, RemoveIdentity } from '@/components/Buttons'; import { AddressCard } from '@/components/Cards'; @@ -35,7 +35,7 @@ const IdentityPage = () => { return ( <> - + My Identity diff --git a/src/pages/transfer.tsx b/src/pages/transfer.tsx index c23eded..e980b65 100644 --- a/src/pages/transfer.tsx +++ b/src/pages/transfer.tsx @@ -1,39 +1,97 @@ -import { Box, FormLabel, MenuItem, TextField } from '@mui/material'; -import '@styles/pages/transfer.module.scss'; +import { + Backdrop, + Box, + CircularProgress, + FormControl, + FormLabel, + MenuItem, + TextField, +} from '@mui/material'; +import styles from '@styles/pages/transfer.module.scss'; +import { useCallback, useEffect, useState } from 'react'; +import { useRelayApi } from '@/contexts/RelayApi'; +import { useToast } from '@/contexts/Toast'; import { useIdentity } from '@/contracts'; const TransferPage = () => { const { networks } = useIdentity(); + const [sourceChainId, setSourceChainId] = useState(); + const [destChainId, setDestChainId] = useState(); + const { + state: { api: relayApi }, + } = useRelayApi(); + const { toastSuccess, toastError } = useToast(); + const [loadingAssets, setLoadingAssets] = useState(false); + + const loadAssets = useCallback(async () => { + if (sourceChainId === undefined || destChainId === undefined) return; + setLoadingAssets(true); + + if (sourceChainId !== destChainId) { + const hrmp = await relayApi.query.hrmp.hrmpChannels({ + sender: sourceChainId, + recipient: destChainId, + }); + + if (hrmp.isEmpty) { + toastError( + "There's no HRMP channel open between the source and destination chain" + ); + } + } + + setLoadingAssets(false); + }, [sourceChainId, destChainId]); + + useEffect(() => { + loadAssets(); + }, [sourceChainId, destChainId]); return ( - - List of networks - setNetworkId(Number(e.target.value))} + + + + Source chain + setSourceChainId(Number(e.target.value))} + > + {Object.values(networks).map((network, index) => ( + + {network.name} + + ))} + + + + Destination chain + setDestChainId(Number(e.target.value))} + > + {Object.values(networks).map((network, index) => ( + + {network.name} + + ))} + + + + theme.zIndex.drawer + 1 }} + open={loadingAssets} > - {Object.entries(networks).map(([id, network], index) => ( - - {network.name} - - ))} - + + ); }; diff --git a/styles/global.scss b/styles/global.scss index da77b61..9c8947b 100644 --- a/styles/global.scss +++ b/styles/global.scss @@ -54,7 +54,7 @@ .MuiFormLabel-root { font-size: 16px; - font-weight: 600; + // font-weight: 600; line-height: 1.6; color: #181336; } @@ -70,7 +70,7 @@ .form-item { display: flex; flex-direction: column; - gap: 4px; + gap: 8px; } .modal-buttons { diff --git a/styles/pages/identity.module.scss b/styles/pages/identity.module.scss index 87027bc..423eae5 100644 --- a/styles/pages/identity.module.scss +++ b/styles/pages/identity.module.scss @@ -1,4 +1,4 @@ -.identity-container { +.identityContainer { display: flex; align-items: center; justify-content: space-between; diff --git a/styles/pages/transfer.module.scss b/styles/pages/transfer.module.scss index e69de29..5f99598 100644 --- a/styles/pages/transfer.module.scss +++ b/styles/pages/transfer.module.scss @@ -0,0 +1,9 @@ +.transferContainer { + display: flex; + flex-direction: column; + background: white; + margin-left: auto; + margin-right: auto; + min-width: 500px; + padding: 24px 16px; +}