diff --git a/README.md b/README.md index f9effdc9..5cc0876e 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,12 @@ Install dependencies npm install ``` +Install chain descriptors + +```bash + npm run add-descriptors +``` + Start the app ```bash diff --git a/src/app/routes.tsx b/src/app/routes.tsx index bdd50ed3..8c739f25 100644 --- a/src/app/routes.tsx +++ b/src/app/routes.tsx @@ -2,7 +2,6 @@ import { lazy } from 'react'; import { LayoutBasic } from '@components/layouts/basic'; import { LayoutCodeEditor } from '@components/layouts/codeEditor'; -import { NotFound } from '@views/notFound'; const Home = lazy(() => import('../views/home')); const CodeEditor = lazy(() => import('../views/codeEditor')); @@ -12,8 +11,16 @@ const BlockDetails = lazy(() => import('../views/blockDetails')); const Explorer = lazy(() => import('../views/explorer')); const SignedExtrinsics = lazy(() => import('../views/signedExtrinsics')); const Forks = lazy(() => import('../views/forks')); +const Extrinsics = lazy(() => import('../views/extrinsics')); +const ChainState = lazy(() => import('../views/chainState')); +const Constants = lazy(() => import('../views/constants')); +const RuntimeCalls = lazy(() => import('../views/runtimeCalls')); const Onboarding = lazy(() => import('../views/onboarding')); const LatestBlocks = lazy(() => import('../views/latestBlocks')); +const Decoder = lazy(() => import('../views/decoder')); +const DecoderDynamic = lazy(() => import('../views/decoderDynamic')); +const RpcCalls = lazy(() => import('../views/rpcCalls')); +const NotFound = lazy(() => import('../views/notFound')); export const routes = () => ([ { @@ -39,6 +46,34 @@ export const routes = () => ([ path: 'login-callback', element: , }, + { + path: 'extrinsics', + element: , + }, + { + path: 'chain-state', + element: , + }, + { + path: 'constants', + element: , + }, + { + path: 'runtime-calls', + element: , + }, + { + path: 'rpc-calls', + element: , + }, + { + path: 'decoder', + element: , + }, + { + path: 'decoder-dynamic', + element: , + }, ], }, { diff --git a/src/components/callDocs.tsx b/src/components/callDocs.tsx new file mode 100644 index 00000000..53b3b46d --- /dev/null +++ b/src/components/callDocs.tsx @@ -0,0 +1,42 @@ +import { Icon } from '@components/icon'; +import { cn } from '@utils/helpers'; + +interface ICallDocs { + docs: string[]; + className?: string; +} + +export const CallDocs = ({ + docs, + className, +}: ICallDocs) => { + if (docs.length <= 0) { + return null; + } else { + return ( +
+ +
+ { + docs?.map((doc, i) => ( +

+ {doc} +

+ )) + } +
+
+ ); + } +}; diff --git a/src/components/chainState/index.ts b/src/components/chainState/index.ts new file mode 100644 index 00000000..ff7d2160 --- /dev/null +++ b/src/components/chainState/index.ts @@ -0,0 +1 @@ +export { default as InvocationStorageArgs } from './invocationStorageArgs'; diff --git a/src/components/chainState/invocationStorageArgs.tsx b/src/components/chainState/invocationStorageArgs.tsx new file mode 100644 index 00000000..0f2d62e7 --- /dev/null +++ b/src/components/chainState/invocationStorageArgs.tsx @@ -0,0 +1,37 @@ +import { NotImplemented } from '@components/invocationArgsMapper/notImplemented'; +import { useStoreChain } from '@stores'; + +import { InvocationMapper } from '../invocationArgsMapper/invocationMapper'; + +import type { InvocationStorageArgs as Type } from '@components/invocationArgsMapper/types'; +import type { TMetaDataStorageItem } from '@custom-types/papi'; +import type { MetadataLookup } from '@polkadot-api/metadata-builders'; + +const shouldSkipRendering = (storage: TMetaDataStorageItem, lookup: MetadataLookup | null): boolean => { + return storage.type.tag === 'plain' || !lookup; +}; + +const InvocationStorageArgs = ({ args, onChange }: Type) => { + const lookup = useStoreChain?.use?.lookup?.(); + if (!lookup) { + return null; + } + + try { + if (!shouldSkipRendering(args, lookup)) { + return ( + + ); + } else { + return null; + } + } catch (error) { + console.error(error); + return ; + } +}; + +export default InvocationStorageArgs; diff --git a/src/components/decoder/index.ts b/src/components/decoder/index.ts new file mode 100644 index 00000000..b7ba878f --- /dev/null +++ b/src/components/decoder/index.ts @@ -0,0 +1 @@ +export { default as InvocationDecoder } from './invocationDecoder'; diff --git a/src/components/decoder/invocationDecoder.tsx b/src/components/decoder/invocationDecoder.tsx new file mode 100644 index 00000000..d2f29709 --- /dev/null +++ b/src/components/decoder/invocationDecoder.tsx @@ -0,0 +1,52 @@ +/* eslint-disable react/jsx-no-bind */ +/* eslint-disable react-hooks/exhaustive-deps */ +import { useCallback } from 'react'; + +import { InvocationDecoderArgs } from '@components/decoder/invocationDecoderArgs'; +import { NotImplemented } from '@components/invocationArgsMapper/notImplemented'; + +import styles from '../invocationArgsMapper/styles.module.css'; + +import type { InvocationDecoder as Type } from '@components/invocationArgsMapper/types'; + +const InvocationDecoder = ({ fields, onChange }: Type) => { + const handleOnChange = useCallback((index: number, args: unknown) => { + onChange(index, args); + }, []); + + if (!fields) { + return null; + } else { + return ( +
+ { + fields.map((field, index) => { + const { name, type, description } = field; + if (!type) { + return ; + } else { + return ( +
+ + {name} + +
+
+ handleOnChange(index, args)} + placeholder={description} + /> +
+
+
+ ); + } + }) + } +
+ ); + } +}; + +export default InvocationDecoder; diff --git a/src/components/decoder/invocationDecoderArgs.tsx b/src/components/decoder/invocationDecoderArgs.tsx new file mode 100644 index 00000000..8a05ca62 --- /dev/null +++ b/src/components/decoder/invocationDecoderArgs.tsx @@ -0,0 +1,49 @@ +import { NotImplemented } from '@components/invocationArgsMapper/notImplemented'; +import { OrderBuilder } from '@components/metadataBuilders/orderBuilder'; +import { PrimitiveBuilder } from '@components/metadataBuilders/primitiveBuilder'; + +import type { IDecoderBuilderProps } from '@components/invocationArgsMapper/types'; +import type { InvocationDecoderArgs as Type } from '@constants/decoders/types'; + +const mapperCore: Record JSX.Element> = { + array: (props) => ( + + ), + string: (props) => ( + + ), + hex: (props) => ( + + ), +}; +export const InvocationDecoderArgs = (props: IDecoderBuilderProps) => { + if (!props) { + return null; + } else { + try { + const decoderType = props.decoder.type; + const InvocationComponent = mapperCore[decoderType] ?? NotImplemented; + + return ; + } catch (error) { + console.error(error); + return ; + } + } +}; diff --git a/src/components/decoderDynamic/index.ts b/src/components/decoderDynamic/index.ts new file mode 100644 index 00000000..ef3dd215 --- /dev/null +++ b/src/components/decoderDynamic/index.ts @@ -0,0 +1 @@ +export { default as InvocationDecoderDynamic } from './invocationDecoderDynamic'; diff --git a/src/components/decoderDynamic/invocationDecoderDynamic.tsx b/src/components/decoderDynamic/invocationDecoderDynamic.tsx new file mode 100644 index 00000000..3c21d7fc --- /dev/null +++ b/src/components/decoderDynamic/invocationDecoderDynamic.tsx @@ -0,0 +1,28 @@ +import { PrimitiveBuilder } from '@components/metadataBuilders/primitiveBuilder'; + +import styles from '../invocationArgsMapper/styles.module.css'; + +import type { InvocationDecoderDynamic as Type } from '@components/invocationArgsMapper/types'; + +const InvocationDecoderDynamic = ({ onChange }: Type) => { + return ( +
+
+ + SCALE-encoded value + +
+
+ +
+
+
+
+ ); +}; + +export default InvocationDecoderDynamic; diff --git a/src/components/invocationArgsMapper/index.tsx b/src/components/invocationArgsMapper/index.tsx new file mode 100644 index 00000000..86c9bf16 --- /dev/null +++ b/src/components/invocationArgsMapper/index.tsx @@ -0,0 +1,39 @@ +import { NotImplemented } from '@components/invocationArgsMapper/notImplemented'; +import { useStoreChain } from '@stores'; + +import { InvocationMapper } from './invocationMapper'; + +import type { InvocationArgsMapper as Type } from '@components/invocationArgsMapper/types'; + +export const InvocationArgsMapper = ({ invocationVar, onChange }: Type) => { + const lookup = useStoreChain?.use?.lookup?.(); + + try { + if (!lookup) { + return null; + } else { + if (!invocationVar?.type) { + return ; + } else { + if (invocationVar.type !== 'lookupEntry') { + return ( + + ); + } else { + return ( + + ); + } + } + } + } catch (error) { + console.error(error); + return ; + } +}; diff --git a/src/components/invocationArgsMapper/invocationMapper.tsx b/src/components/invocationArgsMapper/invocationMapper.tsx new file mode 100644 index 00000000..81080674 --- /dev/null +++ b/src/components/invocationArgsMapper/invocationMapper.tsx @@ -0,0 +1,121 @@ +import { NotImplemented } from '@components/invocationArgsMapper/notImplemented'; +import { AccountBuilder } from '@components/metadataBuilders/accountBuilder'; +import ArrayVarBuilder from '@components/metadataBuilders/arrayBuilder/arrayBuilder'; +import { BitstreamBuilder } from '@components/metadataBuilders/bitstreamBuilder'; +import CompactVarBuilder from '@components/metadataBuilders/compactBuilder/compactBuilder'; +import { ConditionalParamBuilder } from '@components/metadataBuilders/conditionalBuilder'; +import { EnumBuilder } from '@components/metadataBuilders/enumBuilder'; +import { OrderBuilder } from '@components/metadataBuilders/orderBuilder'; +import { PrimitiveBuilder } from '@components/metadataBuilders/primitiveBuilder'; +import { StructBuilder } from '@components/metadataBuilders/structBuilder'; +import { TupleBuilder } from '@components/metadataBuilders/tupleBuilder'; +import { VoidBuilder } from '@components/metadataBuilders/voidBuilder'; + +import type { InvocationMapperProps } from '@components/invocationArgsMapper/types'; +import type { + AccountId20, + AccountId32, + ArrayVar, + CompactVar, + EnumVar, + OptionVar, + PrimitiveVar, + SequenceVar, + StructVar, + TupleVar, +} from '@polkadot-api/metadata-builders'; + +const mapperCore: Record JSX.Element> = { + result: () => , + bitSequence: ({ onChange, placeholder }) => ( + + ), + compact: ({ onChange, invokationVar }) => ( + + ), + array: ({ onChange, invokationVar }) => ( + + ), + enum: ({ invokationVar, onChange }) => ( + + ), + struct: ({ onChange, invokationVar }) => ( + + ), + AccountId20: ({ invokationVar, onChange }) => ( + + ), + AccountId32: ({ invokationVar, onChange }) => ( + + ), + tuple: ({ invokationVar, onChange }) => ( + + ), + sequence: ({ onChange, invokationVar, placeholder }) => ( + + ), + void: ({ onChange }) => , + primitive: ({ onChange, invokationVar, placeholder }) => ( + + ), + option: ({ onChange, invokationVar }) => { + + return ( + + ); + }, +}; + +export const InvocationMapper = (props: InvocationMapperProps) => { + if (!props) { + return null; + } else { + try { + if (!props.invokationVar) { + return ; + } else { + const InvocationComponent = mapperCore[props?.invokationVar?.type] ?? NotImplemented; + return ; + } + } catch (error) { + console.error(error); + return ; + } + } +}; diff --git a/src/components/invocationArgsMapper/invocationRpcSelect.tsx b/src/components/invocationArgsMapper/invocationRpcSelect.tsx new file mode 100644 index 00000000..2812ef8c --- /dev/null +++ b/src/components/invocationArgsMapper/invocationRpcSelect.tsx @@ -0,0 +1,41 @@ +/* eslint-disable react-hooks/exhaustive-deps */ + +import { + useCallback, + useEffect, + useState, +} from 'react'; + +import { PDSelect } from '@components/pdSelect'; + +import type { InvocationRpcArg } from '@components/invocationArgsMapper/types'; + +export const InvocationRpcSelect = ({ onChange, rpc }: InvocationRpcArg) => { + const [ + selectValue, + setSelectValue, + ] = useState(rpc.options?.at(0)); + + const handleOnChange = useCallback((value: string) => { + onChange(value); + setSelectValue(value); + }, []); + + useEffect(() => { + onChange(selectValue); + }, []); + + const selectItems = rpc?.options?.map((opt) => ({ + label: opt, + value: opt, + key: `rpc-select-${opt}`, + })); + + return ( + + ); +}; diff --git a/src/components/invocationArgsMapper/notImplemented.tsx b/src/components/invocationArgsMapper/notImplemented.tsx new file mode 100644 index 00000000..844714f3 --- /dev/null +++ b/src/components/invocationArgsMapper/notImplemented.tsx @@ -0,0 +1,7 @@ +export const NotImplemented = () => { + return ( +
+ Not Implemented +
+ ); +}; diff --git a/src/components/invocationArgsMapper/styles.module.css b/src/components/invocationArgsMapper/styles.module.css new file mode 100644 index 00000000..ba4e337d --- /dev/null +++ b/src/components/invocationArgsMapper/styles.module.css @@ -0,0 +1,23 @@ +.invocationContainer { + @apply border-l border-dashed pl-4 pt-2; + @apply empty:hidden flex flex-col gap-4; + @apply border-dev-purple-300 dark:border-dev-purple-700; +} + +.invocationInputField { + @apply w-full border dark:border-dev-purple-700 border-dev-white-900 outline-none; + @apply w-full outline-none; + @apply p-3 bg-transparent; + @apply font-geist font-body1-regular; + + @apply read-only:opacity-60 read-only:cursor-not-allowed +} + +.invocationInputErrorState { + @apply transition-[border]; + @apply !border-dev-red-700 +} + +.invocationGroup { + @apply flex flex-col gap-2 +} diff --git a/src/components/invocationArgsMapper/types.ts b/src/components/invocationArgsMapper/types.ts new file mode 100644 index 00000000..391a9b93 --- /dev/null +++ b/src/components/invocationArgsMapper/types.ts @@ -0,0 +1,124 @@ +import type { InvocationDecoderArgs } from '@constants/decoders/types'; +import type { IRpcArg } from '@constants/rpcCalls/types'; +import type { + TMetaDataApiMethod, + TMetaDataCallBuilder, + TMetaDataPallet, + TMetaDataStorageItem, +} from '@custom-types/papi'; +import type { + AccountId20, + AccountId32, + ArrayVar, + CompactVar, + EnumVar, + OptionVar, + PrimitiveVar, + SequenceVar, + StructVar, + TupleVar, + Var, +} from '@polkadot-api/metadata-builders'; +import type { InjectedPolkadotAccount } from 'polkadot-api/dist/reexports/pjs-signer'; + +export interface InvocationOnChangeProps { + onChange: (args: unknown) => void; +} + +export interface InvocationOnChangeWithIndexProps { + onChange: (index: number, args: unknown) => void; +} + +export interface InvocationStorageArgs extends InvocationOnChangeProps { + args: TMetaDataStorageItem; +} + +export interface InvocationMapperProps extends InvocationOnChangeProps { + invokationVar: Var; + placeholder?: string; +} +export interface InvocationRpcArgs extends InvocationOnChangeWithIndexProps { + rpcs: IRpcArg[]; +} + +export interface InvocationRpcArg extends InvocationOnChangeProps { + rpc: IRpcArg; + placeholder?: string; + readOnly?: boolean; +} + +export interface InvocationRuntimeArgs extends InvocationOnChangeProps { + runtimeMethod: TMetaDataApiMethod; +} + +export interface InvocationArgsMapper extends InvocationOnChangeProps { + pallet: TMetaDataPallet; + name: string; + invocationVar: TMetaDataCallBuilder; +} + +export interface InvocationDecoder extends InvocationOnChangeWithIndexProps { + fields: InvocationDecoderArgs[]; +} + +export interface IDecoderBuilderProps extends InvocationOnChangeProps { + decoder: InvocationDecoderArgs; + placeholder?: string; + readOnly?: boolean; +} + +export interface InvocationDecoderDynamic extends InvocationOnChangeProps { } + +export interface IBitstreamBuilder extends InvocationOnChangeProps { + minLength: number; + placeholder?: string; + readOnly?: boolean; +} +export interface IArrayVarBuilder extends InvocationOnChangeProps { + data: ArrayVar; +} +export interface ICompactBuilder extends InvocationOnChangeProps { + compact: CompactVar; +} +export interface IStructBuilder extends InvocationOnChangeProps { + struct: StructVar; +} + +export interface StructArgs { + key: string; + value: unknown; +} + +export interface ICustomAccount extends InvocationOnChangeProps { + accountId: AccountId20 | AccountId32; +} + +export interface IAccountBuilder extends InvocationOnChangeProps { + accountId: AccountId20 | AccountId32; +} + +export interface IAccountSelectBuilder extends InvocationOnChangeProps { + accounts: InjectedPolkadotAccount[]; +} +export interface ITupleBuilder extends InvocationOnChangeProps { + tuple: TupleVar; +} + +export interface ISequenceBuilder extends InvocationOnChangeProps { + sequence: SequenceVar; + placeholder?: string; +} + +export interface IPrimitiveBuilder extends InvocationOnChangeProps { + primitive: PrimitiveVar; + placeholder?: string; + readOnly?: boolean; +} + +export interface IConditionalBuilder extends InvocationOnChangeProps { + condition: OptionVar; +} + +export interface IEnumBuilder extends InvocationOnChangeProps { + enum: EnumVar; +} diff --git a/src/components/metadataBuilders/accountBuilder/accountBuilder.tsx b/src/components/metadataBuilders/accountBuilder/accountBuilder.tsx new file mode 100644 index 00000000..08bba882 --- /dev/null +++ b/src/components/metadataBuilders/accountBuilder/accountBuilder.tsx @@ -0,0 +1,85 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { type InjectedPolkadotAccount } from 'polkadot-api/pjs-signer'; +import { + useCallback, + useEffect, + useState, +} from 'react'; + +import { PDSwitch } from '@components/pdSwitch'; +import { useStoreWallet } from 'src/stores/wallet'; + +import styles from '../../invocationArgsMapper/styles.module.css'; + +import { ManualAccountInput } from './manualAccountInput'; +import { WalletAccountSelector } from './walletAccountSelector'; + +import type { IAccountBuilder } from '@components/invocationArgsMapper/types'; + +export const AccountBuilder = ({ accountId, onChange }: IAccountBuilder) => { + const walletAccounts = useStoreWallet?.use?.accounts?.() ?? []; + const [ + isManualInput, + setIsManualInput, + ] = useState(false); + + // Reset to wallet selection mode when accounts change + useEffect(() => { + setIsManualInput(false); + if (walletAccounts.length > 0) { + onChange(walletAccounts[0].address); + } + }, [walletAccounts]); + + // Set default wallet account when switching back from manual mode + useEffect(() => { + if (!isManualInput && walletAccounts.length > 0) { + onChange(walletAccounts[0].address); + } + }, [ + isManualInput, + walletAccounts, + ]); + + const handleAccountSelect = useCallback((account: unknown) => { + const selectedAccount = account as InjectedPolkadotAccount; + if (selectedAccount?.address) { + onChange(selectedAccount.address); + } + }, []); + + const handleManualInputToggle = useCallback(() => { + setIsManualInput((prev) => !prev); + }, []); + + const renderAccountInput = () => { + if (isManualInput) { + return ( + + ); + } + + return ( + + ); + }; + + return ( +
+ +
+ {renderAccountInput()} +
+
+ ); +}; diff --git a/src/components/metadataBuilders/accountBuilder/index.ts b/src/components/metadataBuilders/accountBuilder/index.ts new file mode 100644 index 00000000..f1dc6d04 --- /dev/null +++ b/src/components/metadataBuilders/accountBuilder/index.ts @@ -0,0 +1,2 @@ +export { AccountBuilder } from './accountBuilder'; + diff --git a/src/components/metadataBuilders/accountBuilder/manualAccountInput.tsx b/src/components/metadataBuilders/accountBuilder/manualAccountInput.tsx new file mode 100644 index 00000000..9dabb517 --- /dev/null +++ b/src/components/metadataBuilders/accountBuilder/manualAccountInput.tsx @@ -0,0 +1,61 @@ +import { getSs58AddressInfo } from 'polkadot-api'; +import { + type ChangeEvent, + useCallback, + useEffect, + useState, +} from 'react'; + +import { cn } from '@utils/helpers'; + +import styles from '../../invocationArgsMapper/styles.module.css'; + +import type { + AccountId20, + AccountId32, +} from '@polkadot-api/metadata-builders'; + +interface ManualAccountInputProps { + accountId: AccountId20 | AccountId32; + onChange: (address: string) => void; +} + +export const ManualAccountInput = ({ + onChange, +}: ManualAccountInputProps) => { + const [ + address, + setAddress, + ] = useState(''); + const [ + isInvalid, + setIsInvalid, + ] = useState(false); + + useEffect(() => { + onChange(''); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const handleAddressChange = useCallback((e: ChangeEvent) => { + const newAddress = e.target.value; + setAddress(newAddress); + const { isValid } = getSs58AddressInfo(newAddress); + setIsInvalid(!isValid); + onChange(newAddress); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + + ); +}; diff --git a/src/components/metadataBuilders/accountBuilder/walletAccountSelector.tsx b/src/components/metadataBuilders/accountBuilder/walletAccountSelector.tsx new file mode 100644 index 00000000..ad171959 --- /dev/null +++ b/src/components/metadataBuilders/accountBuilder/walletAccountSelector.tsx @@ -0,0 +1,63 @@ +import { type InjectedPolkadotAccount } from 'polkadot-api/pjs-signer'; +import { + useCallback, + useEffect, + useMemo, + useState, +} from 'react'; + +import { PDSelect } from '@components/pdSelect'; + +interface WalletAccountSelectorProps { + accounts: InjectedPolkadotAccount[]; + onChange: (account: InjectedPolkadotAccount) => void; +} + +export const WalletAccountSelector = ({ accounts, onChange }: WalletAccountSelectorProps) => { + const [ + selectedAccount, + setSelectedAccount, + ] = useState( + accounts[0], + ); + + useEffect(() => { + if (!!!accounts.length) { + setSelectedAccount(undefined); + return; + } + + if (!selectedAccount) { + setSelectedAccount(accounts[0]); + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [accounts]); + + const handleAccountSelect = useCallback((address: string) => { + const account = accounts.find((acc) => acc.address === address); + if (account) { + setSelectedAccount(account); + onChange(account); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [accounts]); + + const accountOptions = useMemo(() => { + return accounts.map((account) => ({ + label: account.address, + value: account.address, + key: `wallet-account-${account.address}`, + })); + }, [accounts]); + + return ( + + ); +}; diff --git a/src/components/metadataBuilders/arrayBuilder/arrayBuilder.tsx b/src/components/metadataBuilders/arrayBuilder/arrayBuilder.tsx new file mode 100644 index 00000000..f5dac869 --- /dev/null +++ b/src/components/metadataBuilders/arrayBuilder/arrayBuilder.tsx @@ -0,0 +1,32 @@ +import { NotImplemented } from '@components/invocationArgsMapper/notImplemented'; +import { ArrayVarBuilderCore } from '@components/metadataBuilders/arrayBuilder/arrayBuilderCore'; +import { BitstreamBuilder } from '@components/metadataBuilders/bitstreamBuilder'; +import { varIsBinary } from '@utils/papi/helpers'; + +import type { IArrayVarBuilder } from '@components/invocationArgsMapper/types'; + +const ArrayVarBuilder = ({ data, onChange }: IArrayVarBuilder) => { + try { + if (!varIsBinary(data)) { + return ( + + ); + } else { + return ( + + ); + } + } catch (error) { + console.error(error); + return ; + } +}; + +export default ArrayVarBuilder; diff --git a/src/components/metadataBuilders/arrayBuilder/arrayBuilderCore.tsx b/src/components/metadataBuilders/arrayBuilder/arrayBuilderCore.tsx new file mode 100644 index 00000000..4359baad --- /dev/null +++ b/src/components/metadataBuilders/arrayBuilder/arrayBuilderCore.tsx @@ -0,0 +1,53 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +/* eslint-disable react/jsx-no-bind */ +import { + useCallback, + useEffect, + useState, +} from 'react'; + +import { InvocationMapper } from '@components/invocationArgsMapper/invocationMapper'; +import { buildArrayState } from '@utils/invocationMapper'; + +import styles from '../../invocationArgsMapper/styles.module.css'; + +import type { IArrayVarBuilder } from '@components/invocationArgsMapper/types'; + +export const ArrayVarBuilderCore = ({ data, onChange }: IArrayVarBuilder) => { + const [ + arrayProps, + setArrayProps, + ] = useState(buildArrayState(data.len || 0)); + + const handleUpdateVals = useCallback((vals: unknown[]) => { + onChange(vals.some((val) => !Boolean(val)) ? undefined : vals); + }, []); + + useEffect(() => { + handleUpdateVals(arrayProps); + }, [arrayProps]); + + const handleOnChange = useCallback((index: number, args: unknown) => { + setArrayProps((props) => { + const newArrayProps = [...props]; + newArrayProps[index] = args as typeof arrayProps[number]; + return newArrayProps; + }); + }, []); + + return ( +
+ { + arrayProps.map((arrProp, index) => { + return ( + handleOnChange(index, args)} + /> + ); + }) + } +
+ ); +}; diff --git a/src/components/metadataBuilders/arrayBuilder/index.ts b/src/components/metadataBuilders/arrayBuilder/index.ts new file mode 100644 index 00000000..ccc15a27 --- /dev/null +++ b/src/components/metadataBuilders/arrayBuilder/index.ts @@ -0,0 +1 @@ +export { default as ArrayBuilder } from './arrayBuilder'; diff --git a/src/components/metadataBuilders/bitstreamBuilder/bitstreamBuilder.tsx b/src/components/metadataBuilders/bitstreamBuilder/bitstreamBuilder.tsx new file mode 100644 index 00000000..1046ebd8 --- /dev/null +++ b/src/components/metadataBuilders/bitstreamBuilder/bitstreamBuilder.tsx @@ -0,0 +1,57 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { Binary } from 'polkadot-api'; +import { + useCallback, + useState, +} from 'react'; + +import { BitstreamInput } from '@components/metadataBuilders/bitstreamBuilder/bitstreamInput'; +import { PDFileUpload } from '@components/pdFileUpload'; +import { PDSwitch } from '@components/pdSwitch'; + +import styles from '../../invocationArgsMapper/styles.module.css'; + +import type { IBitstreamBuilder } from '@components/invocationArgsMapper/types'; +const emptyBin = Binary.fromText(''); +const BitstreamBuilder = ({ + onChange, + minLength, + placeholder, + readOnly, +}: IBitstreamBuilder) => { + const [ + isFile, + setIsFile, + ] = useState(false); + + const handleOnSwitch = useCallback(() => { + setIsFile((prev) => !prev); + onChange(emptyBin); + }, []); + + const shouldUploadFile = isFile && !readOnly; + + return ( +
+ + { + shouldUploadFile + ? + : ( + + ) + } +
+ ); +}; + +export default BitstreamBuilder; diff --git a/src/components/metadataBuilders/bitstreamBuilder/bitstreamInput.tsx b/src/components/metadataBuilders/bitstreamBuilder/bitstreamInput.tsx new file mode 100644 index 00000000..3f2d22f6 --- /dev/null +++ b/src/components/metadataBuilders/bitstreamBuilder/bitstreamInput.tsx @@ -0,0 +1,74 @@ +import { Binary } from 'polkadot-api'; +import { + type ChangeEvent, + useCallback, + useEffect, + useState, +} from 'react'; + +import { cn } from '@utils/helpers'; + +import styles from '../../invocationArgsMapper/styles.module.css'; + +import type { IBitstreamBuilder } from '@components/invocationArgsMapper/types'; + +export const BitstreamInput = ({ + onChange, + minLength, + placeholder, + readOnly, +}: IBitstreamBuilder) => { + const requiredHexLength = minLength * 2; + + const requiredBinaryLength = minLength; + const encodedValue = String().padEnd(requiredHexLength, '0'); + + const [ + value, + setValue, + ] = useState(requiredHexLength ? `0x${encodedValue}` : ''); + const [ + isError, + setIsError, + ] = useState(false); + + const handleOnChange = useCallback((e: ChangeEvent) => { + const text = e.target.value; + setValue(text); + }, []); + + useEffect(() => { + const isHex = value.startsWith('0x'); + + if (isHex) { + const _value = Binary.fromHex(value); + onChange(_value); + + const valueLength = _value.asBytes().length; + setIsError(minLength ? valueLength !== requiredBinaryLength : false); + } else { + const _value = Binary.fromHex(value); + onChange(_value); + + const valueLength = _value.asBytes().length; + setIsError(minLength ? valueLength !== requiredBinaryLength : false); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [value]); + + return ( + + ); +}; diff --git a/src/components/metadataBuilders/bitstreamBuilder/index.ts b/src/components/metadataBuilders/bitstreamBuilder/index.ts new file mode 100644 index 00000000..345318f3 --- /dev/null +++ b/src/components/metadataBuilders/bitstreamBuilder/index.ts @@ -0,0 +1 @@ +export { default as BitstreamBuilder } from './bitstreamBuilder'; diff --git a/src/components/metadataBuilders/compactBuilder/compactBuilder.tsx b/src/components/metadataBuilders/compactBuilder/compactBuilder.tsx new file mode 100644 index 00000000..c2d8ff66 --- /dev/null +++ b/src/components/metadataBuilders/compactBuilder/compactBuilder.tsx @@ -0,0 +1,42 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + type ChangeEvent, + useCallback, + useEffect, + useState, +} from 'react'; + +import { onWheelPreventDefault } from '@utils/callbacks'; +import { getCompactValue } from '@utils/invocationMapper'; + +import styles from '../../invocationArgsMapper/styles.module.css'; + +import type { ICompactBuilder } from '@components/invocationArgsMapper/types'; + +const CompactVarBuilder = ({ compact, onChange }: ICompactBuilder) => { + const [ + compactValue, + setCompactValue, + ] = useState('0'); + + const handleOnChange = useCallback((e: ChangeEvent) => { + setCompactValue(e.target.value); + }, []); + + useEffect(() => { + onChange(getCompactValue(compact.isBig || false, compactValue)); + }, [compactValue]); + + return ( + + ); +}; + +export default CompactVarBuilder; diff --git a/src/components/metadataBuilders/compactBuilder/index.ts b/src/components/metadataBuilders/compactBuilder/index.ts new file mode 100644 index 00000000..953c0bcc --- /dev/null +++ b/src/components/metadataBuilders/compactBuilder/index.ts @@ -0,0 +1 @@ +export { default as CompactBuilder } from './compactBuilder'; diff --git a/src/components/metadataBuilders/conditionalBuilder/conditionalBuilder.tsx b/src/components/metadataBuilders/conditionalBuilder/conditionalBuilder.tsx new file mode 100644 index 00000000..9e73602c --- /dev/null +++ b/src/components/metadataBuilders/conditionalBuilder/conditionalBuilder.tsx @@ -0,0 +1,65 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + useCallback, + useEffect, + useState, +} from 'react'; + +import { PDSwitch } from '@components/pdSwitch'; + +import { InvocationMapper } from '../../invocationArgsMapper/invocationMapper'; +import styles from '../../invocationArgsMapper/styles.module.css'; + +import type { IConditionalBuilder } from '@components/invocationArgsMapper/types'; + +const ConditionalParamBuilder = ({ condition, onChange }: IConditionalBuilder) => { + const [ + paramValue, + setParamValue, + ] = useState(undefined); + const [ + showParam, + setShowParam, + ] = useState(false); + + const handleOnChange = useCallback((args: unknown) => { + setParamValue(args as undefined); + }, []); + + const handleOnSwitch = useCallback(() => { + setShowParam((show) => !show); + }, []); + + useEffect(() => { + let value = undefined; + if (showParam) { + value = paramValue; + } + onChange(value); + }, [ + paramValue, + showParam, + ]); + + return ( +
+ + { + showParam + ? ( + + ) + : null + } +
+ ); +}; + +export default ConditionalParamBuilder; diff --git a/src/components/metadataBuilders/conditionalBuilder/index.ts b/src/components/metadataBuilders/conditionalBuilder/index.ts new file mode 100644 index 00000000..3e06e18d --- /dev/null +++ b/src/components/metadataBuilders/conditionalBuilder/index.ts @@ -0,0 +1 @@ +export { default as ConditionalParamBuilder } from './conditionalBuilder'; diff --git a/src/components/metadataBuilders/enumBuilder/enumBuilder.tsx b/src/components/metadataBuilders/enumBuilder/enumBuilder.tsx new file mode 100644 index 00000000..f8279e49 --- /dev/null +++ b/src/components/metadataBuilders/enumBuilder/enumBuilder.tsx @@ -0,0 +1,85 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + useCallback, + useEffect, + useMemo, + useState, +} from 'react'; + +import { PDSelect } from '@components/pdSelect'; + +import { InvocationMapper } from '../../invocationArgsMapper/invocationMapper'; +import styles from '../../invocationArgsMapper/styles.module.css'; + +import type { IEnumBuilder } from '@components/invocationArgsMapper/types'; + +const EnumBuilder = ({ onChange, ...props }: IEnumBuilder) => { + const enumPropsValue = props.enum; + const enumOptions = Object.keys(enumPropsValue.value); + + const selectItems = useMemo(() => { + return enumOptions.map((key, index) => ({ + label: key, + value: key, + key: `enum-select-${key}-${index}`, + })); + }, [enumOptions]); + + const [ + option, + setOption, + ] = useState(enumOptions.at(0)!); + + const handleSetValue = useCallback((args: unknown) => { + onChange({ type: option, value: args }); + }, [option]); + + const enumValue = enumPropsValue.value[option]; + const getEnumVariable = () => { + if (!enumValue) { + return undefined; + } else { + if (enumValue.type !== 'lookupEntry') { + return enumValue; + } else { + return enumValue.value; + } + } + }; + const enumVariable = getEnumVariable(); + + useEffect(() => { + if (enumVariable?.type === 'void') { + handleSetValue(undefined); + } + }, [ + option, + enumVariable, + handleSetValue, + ]); + + return ( +
+ + { + enumVariable + ? ( +
+ +
+ ) + : null + } +
+ ); +}; + +export default EnumBuilder; diff --git a/src/components/metadataBuilders/enumBuilder/index.ts b/src/components/metadataBuilders/enumBuilder/index.ts new file mode 100644 index 00000000..5fdd7f8a --- /dev/null +++ b/src/components/metadataBuilders/enumBuilder/index.ts @@ -0,0 +1 @@ +export { default as EnumBuilder } from './enumBuilder'; diff --git a/src/components/metadataBuilders/orderBuilder/index.ts b/src/components/metadataBuilders/orderBuilder/index.ts new file mode 100644 index 00000000..98d0d2a5 --- /dev/null +++ b/src/components/metadataBuilders/orderBuilder/index.ts @@ -0,0 +1 @@ +export { default as OrderBuilder } from './orderBuilder'; diff --git a/src/components/metadataBuilders/orderBuilder/orderBuilder.tsx b/src/components/metadataBuilders/orderBuilder/orderBuilder.tsx new file mode 100644 index 00000000..8546a215 --- /dev/null +++ b/src/components/metadataBuilders/orderBuilder/orderBuilder.tsx @@ -0,0 +1,32 @@ +import { BitstreamBuilder } from '@components/metadataBuilders/bitstreamBuilder'; +import { OrderBuilderCore } from '@components/metadataBuilders/orderBuilder/orderBuilderCore'; +import { varIsBinary } from '@utils/papi/helpers'; + +import styles from '../../invocationArgsMapper/styles.module.css'; + +import type { ISequenceBuilder } from '@components/invocationArgsMapper/types'; + +const OrderBuilder = ({ sequence, onChange, placeholder }: ISequenceBuilder) => { + if (!varIsBinary(sequence)) { + return ( + + ); + } else { + return ( +
+ +
+ ); + } +}; + +export default OrderBuilder; diff --git a/src/components/metadataBuilders/orderBuilder/orderBuilderCore.tsx b/src/components/metadataBuilders/orderBuilder/orderBuilderCore.tsx new file mode 100644 index 00000000..023fb24d --- /dev/null +++ b/src/components/metadataBuilders/orderBuilder/orderBuilderCore.tsx @@ -0,0 +1,136 @@ +/* eslint-disable react/jsx-no-bind */ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + useCallback, + useEffect, + useState, +} from 'react'; + +import { Icon } from '@components/icon'; +import { InvocationMapper } from '@components/invocationArgsMapper/invocationMapper'; +import { cn } from '@utils/helpers'; +import { buildSequenceState } from '@utils/invocationMapper'; + +import type { ISequenceBuilder } from '@components/invocationArgsMapper/types'; + +const STARTING_SEQUENCE_LENGTH = 1; + +export const OrderBuilderCore = ({ sequence, onChange, placeholder }: ISequenceBuilder) => { + const [ + sequenceLength, + setSequenceLength, + ] = useState(STARTING_SEQUENCE_LENGTH); + const [ + sequenceState, + setSequenceState, + ] = useState(buildSequenceState(sequenceLength)); + + useEffect(() => { + const res = sequenceState.map((p) => p.value); + onChange(res.includes(undefined) ? undefined : res); + }, [sequenceState]); + + const handleOnChange = useCallback((args: unknown, id: string) => { + setSequenceState((state) => { + const item = state.find((p) => p.id === id); + if (!item) { + return state; + } else { + const index = state.indexOf(item); + const newParams = [...state]; + newParams[index].value = args as undefined; + return newParams; + } + }); + }, []); + + const handleAddItem = useCallback(() => { + setSequenceLength((length) => length + 1); + setSequenceState((state) => ([ + ...state, + { id: crypto.randomUUID(), value: undefined }, + ])); + }, []); + + const handleRemoveItem = useCallback(() => { + setSequenceLength((length) => length - 1); + setSequenceState((params) => params.slice(0, -1)); + }, []); + + return ( +
+
0, + }, + )} + > + + + + +
+ { + sequenceState.map((state, index) => { + const nextType = sequence.value.type; + + return ( +
+ + {index} + : + +
+ handleOnChange(args, state.id)} + placeholder={placeholder} + /> +
+
+ ); + }) + } + +
+ ); +}; diff --git a/src/components/metadataBuilders/primitiveBuilder/index.ts b/src/components/metadataBuilders/primitiveBuilder/index.ts new file mode 100644 index 00000000..45ca267f --- /dev/null +++ b/src/components/metadataBuilders/primitiveBuilder/index.ts @@ -0,0 +1 @@ +export { default as PrimitiveBuilder } from './primitiveBuilder'; diff --git a/src/components/metadataBuilders/primitiveBuilder/primitiveBuilder.tsx b/src/components/metadataBuilders/primitiveBuilder/primitiveBuilder.tsx new file mode 100644 index 00000000..2ddf7759 --- /dev/null +++ b/src/components/metadataBuilders/primitiveBuilder/primitiveBuilder.tsx @@ -0,0 +1,121 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + type ChangeEvent, + useCallback, + useEffect, + useState, +} from 'react'; + +import { NotImplemented } from '@components/invocationArgsMapper/notImplemented'; +import { PDSwitch } from '@components/pdSwitch'; +import { onWheelPreventDefault } from '@utils/callbacks'; +import { handlePrimitiveInputChange } from '@utils/invocationMapper'; + +import styles from '../../invocationArgsMapper/styles.module.css'; + +import type { IPrimitiveBuilder } from '@components/invocationArgsMapper/types'; + +const PrimitiveBuilder = ({ + primitive, + onChange, + placeholder, + readOnly, +}: IPrimitiveBuilder) => { + const [ + primValue, + setPrimValue, + ] = useState(''); + + const handlePDSwitchChange = useCallback(() => { + setPrimValue((val) => Boolean(val) ? '' : 'true'); + }, []); + + const getNumericProps = () => ( + { + type: 'number', + inputMode: 'numeric' as const, + onWheelCapture: onWheelPreventDefault, + } + ); + + const getPrimitiveProps = () => ( + { + placeholder: placeholder || primitive.value, + value: primValue, + className: styles.invocationInputField, + onChange: (event: ChangeEvent) => setPrimValue(event.target.value), + readOnly: !!readOnly, + } + ); + + useEffect(() => { + onChange(handlePrimitiveInputChange(primitive, primValue)); + }, [primValue]); + + const primitiveRenderers: Record = { + i8: , + u8: , + i16: , + u16: , + i32: , + u32: , + i64: , + u64: , + i128: , + u128: , + i256: , + u256: , + str: , + bool: ( + + ), + char: ( + + ), + }; + return primitiveRenderers[primitive.value] || ; +}; + +export default PrimitiveBuilder; diff --git a/src/components/metadataBuilders/structBuilder/index.ts b/src/components/metadataBuilders/structBuilder/index.ts new file mode 100644 index 00000000..d1c52bc1 --- /dev/null +++ b/src/components/metadataBuilders/structBuilder/index.ts @@ -0,0 +1 @@ +export { default as StructBuilder } from './structBuilder'; diff --git a/src/components/metadataBuilders/structBuilder/structBuilder.tsx b/src/components/metadataBuilders/structBuilder/structBuilder.tsx new file mode 100644 index 00000000..cbd7ed4e --- /dev/null +++ b/src/components/metadataBuilders/structBuilder/structBuilder.tsx @@ -0,0 +1,71 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +/* eslint-disable react/jsx-no-bind */ +import { + useCallback, + useEffect, + useState, +} from 'react'; + +import { NotImplemented } from '@components/invocationArgsMapper/notImplemented'; +import { buildStructState } from '@utils/invocationMapper'; + +import { InvocationMapper } from '../../invocationArgsMapper/invocationMapper'; +import styles from '../../invocationArgsMapper/styles.module.css'; + +import type { + IStructBuilder, + StructArgs, +} from '@components/invocationArgsMapper/types'; + +const StructBuilder = ({ struct, onChange }: IStructBuilder) => { + const structEntries = Object.entries(struct.value); + + const [ + structState, + setStructState, + ] = useState(buildStructState(struct)); + + const handleOnChange = useCallback((key: StructArgs['key'], value: StructArgs['value']) => { + setStructState((structState) => { + const newArgs = { ...structState, [key]: value }; + return newArgs; + }); + }, []); + + useEffect(() => { + onChange(structState); + }, [structState]); + + try { + if (!structEntries) { + return null; + } else { + return structEntries.map((strEntry, index) => { + const [ + key, + value, + ] = strEntry; + + return ( +
+ + {key} + +
+ handleOnChange(key, args)} + /> +
+
+ ); + }); + } + } catch (error) { + console.error(error); + return ; + } +}; + +export default StructBuilder; diff --git a/src/components/metadataBuilders/tupleBuilder/index.ts b/src/components/metadataBuilders/tupleBuilder/index.ts new file mode 100644 index 00000000..d4251959 --- /dev/null +++ b/src/components/metadataBuilders/tupleBuilder/index.ts @@ -0,0 +1 @@ +export { default as TupleBuilder } from './tupleBuilder'; diff --git a/src/components/metadataBuilders/tupleBuilder/tupleBuilder.tsx b/src/components/metadataBuilders/tupleBuilder/tupleBuilder.tsx new file mode 100644 index 00000000..addd4941 --- /dev/null +++ b/src/components/metadataBuilders/tupleBuilder/tupleBuilder.tsx @@ -0,0 +1,27 @@ +import { BitstreamBuilder } from '@components/metadataBuilders/bitstreamBuilder'; +import { TupleBuilderCore } from '@components/metadataBuilders/tupleBuilder/tupleBuilderCore'; +import { varIsBinary } from '@utils/papi/helpers'; + +import type { ITupleBuilder } from '@components/invocationArgsMapper/types'; + +const TupleBuilder = ({ tuple, onChange }: ITupleBuilder) => { + if (!varIsBinary(tuple)) { + return ( + + ); + } else { + return ( +
+ +
+ ); + } +}; + +export default TupleBuilder; diff --git a/src/components/metadataBuilders/tupleBuilder/tupleBuilderCore.tsx b/src/components/metadataBuilders/tupleBuilder/tupleBuilderCore.tsx new file mode 100644 index 00000000..301cfd2c --- /dev/null +++ b/src/components/metadataBuilders/tupleBuilder/tupleBuilderCore.tsx @@ -0,0 +1,60 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +/* eslint-disable react/jsx-no-bind */ +import { + useCallback, + useEffect, + useState, +} from 'react'; + +import { InvocationMapper } from '@components/invocationArgsMapper/invocationMapper'; +import { NotImplemented } from '@components/invocationArgsMapper/notImplemented'; +import { buildArrayState } from '@utils/invocationMapper'; + +import styles from '../../invocationArgsMapper/styles.module.css'; + +import type { ITupleBuilder } from '@components/invocationArgsMapper/types'; + +export const TupleBuilderCore = ({ + tuple, + onChange, +}: ITupleBuilder) => { + const [ + state, + setState, + ] = useState(buildArrayState(tuple?.value?.length || 0)); + + const handleOnChange = useCallback((index: number, value: unknown) => { + setState((tuple) => { + const newParams = [...tuple]; + newParams[index] = value as typeof newParams[number]; + return newParams; + }); + }, []); + + useEffect(() => { + onChange(state); + }, [state]); + + try { + if (!tuple) { + return null; + } else { + return ( +
+ { + tuple?.value?.map((tupleVal, i) => ( + handleOnChange(i, value)} + /> + )) + } +
+ ); + } + } catch (error) { + console.error(error); + return ; + } +}; diff --git a/src/components/metadataBuilders/voidBuilder/index.ts b/src/components/metadataBuilders/voidBuilder/index.ts new file mode 100644 index 00000000..e159d10e --- /dev/null +++ b/src/components/metadataBuilders/voidBuilder/index.ts @@ -0,0 +1 @@ +export { default as VoidBuilder } from './voidBuilder'; diff --git a/src/components/metadataBuilders/voidBuilder/voidBuilder.tsx b/src/components/metadataBuilders/voidBuilder/voidBuilder.tsx new file mode 100644 index 00000000..32192572 --- /dev/null +++ b/src/components/metadataBuilders/voidBuilder/voidBuilder.tsx @@ -0,0 +1,19 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { useRef } from 'react'; + +import type { InvocationOnChangeProps } from '@components/invocationArgsMapper/types'; + +interface IVoidBuilder extends InvocationOnChangeProps { } + +const VoidBuilder = ({ onChange }: IVoidBuilder) => { + const hasCalledOnChange = useRef(false); + + if (!hasCalledOnChange.current) { + onChange(undefined); + hasCalledOnChange.current = true; + } + + return null; +}; + +export default VoidBuilder; diff --git a/src/components/modals/modalRequestExample/index.tsx b/src/components/modals/modalRequestExample/index.tsx deleted file mode 100644 index 7ea92530..00000000 --- a/src/components/modals/modalRequestExample/index.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { cn } from '@utils/helpers'; - -import { - type IModal, - Modal, -} from '../modal'; - -interface IModalGithubLogin extends Pick {} - -export const ModalRequestExample = ({ onClose }: IModalGithubLogin) => { - - return ( - -
Request Example
-
- -