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
-
-
-
-
-
-
-
- );
-};
diff --git a/src/components/papiQuery/queryButton.tsx b/src/components/papiQuery/queryButton.tsx
new file mode 100644
index 00000000..2da19d10
--- /dev/null
+++ b/src/components/papiQuery/queryButton.tsx
@@ -0,0 +1,38 @@
+import { cn } from '@utils/helpers';
+
+import type { ReactNode } from 'react';
+
+interface IQueryButton {
+ children: ReactNode;
+ onClick?: () => void;
+ disabled?: boolean;
+ className?: string;
+}
+
+export const QueryButton = ({
+ children,
+ className,
+ disabled,
+ onClick,
+}: IQueryButton) => {
+
+ return (
+
+ );
+};
diff --git a/src/components/papiQuery/queryFormContainer.tsx b/src/components/papiQuery/queryFormContainer.tsx
new file mode 100644
index 00000000..371a06b8
--- /dev/null
+++ b/src/components/papiQuery/queryFormContainer.tsx
@@ -0,0 +1,13 @@
+import type { ReactNode } from 'react';
+
+interface IQueryFormContainer {
+ children: ReactNode;
+}
+
+export const QueryFormContainer = ({ children }: IQueryFormContainer) => {
+ return (
+
+ {children}
+
+ );
+};
diff --git a/src/components/papiQuery/queryResult.tsx b/src/components/papiQuery/queryResult.tsx
new file mode 100644
index 00000000..c2018638
--- /dev/null
+++ b/src/components/papiQuery/queryResult.tsx
@@ -0,0 +1,119 @@
+import {
+ useEffect,
+ useState,
+} from 'react';
+import 'react18-json-view/src/dark.css';
+import 'react18-json-view/src/style.css';
+import JsonView from 'react18-json-view';
+
+import { Icon } from '@components/icon';
+import { Loader } from '@components/loader';
+import { useStoreUI } from '@stores';
+import {
+ cn,
+ sleep,
+} from '@utils/helpers';
+import { unwrapApiResult } from '@utils/papi/helpers';
+
+interface IQueryResult {
+ title: string;
+ path: string;
+ result?: unknown;
+ isLoading?: boolean;
+ onRemove?: () => void;
+}
+
+export const QueryResult = ({
+ title,
+ path,
+ result,
+ isLoading,
+ onRemove,
+}: IQueryResult) => {
+
+ const [
+ resultIsLoading,
+ setResultIsLoading,
+ ] = useState(true);
+
+ const theme = useStoreUI?.use?.theme?.();
+
+ useEffect(() => {
+ void (async () => {
+ // used to prevent a flickering feel when the result loads too quickly
+ await sleep(500);
+ setResultIsLoading(!!isLoading);
+ })();
+ }, [isLoading]);
+
+ return (
+
+
+
+
+
+ Path:
+ {' '}
+ {' '}
+
+ {path}
+
+
+
+
+ {
+ resultIsLoading
+ ?
+ : (
+ <>
+ Result
+
+ >
+ )
+ }
+
+
+
+ );
+};
diff --git a/src/components/papiQuery/queryResultContainer.tsx b/src/components/papiQuery/queryResultContainer.tsx
new file mode 100644
index 00000000..41acbb85
--- /dev/null
+++ b/src/components/papiQuery/queryResultContainer.tsx
@@ -0,0 +1,14 @@
+import type { ReactNode } from 'react';
+
+interface IQueryResultContainer {
+ children: ReactNode;
+}
+
+export const QueryResultContainer = ({ children }: IQueryResultContainer) => {
+ return (
+
+ );
+};
diff --git a/src/components/papiQuery/queryViewContainer.tsx b/src/components/papiQuery/queryViewContainer.tsx
new file mode 100644
index 00000000..c564c4bc
--- /dev/null
+++ b/src/components/papiQuery/queryViewContainer.tsx
@@ -0,0 +1,20 @@
+import { cn } from '@utils/helpers';
+
+import type { ReactNode } from 'react';
+
+interface IQueryViewContainer {
+ children: ReactNode;
+}
+
+export const QueryViewContainer = ({ children }: IQueryViewContainer) => {
+ return (
+
+ {children}
+
+ );
+};
diff --git a/src/components/pdFileUpload/index.tsx b/src/components/pdFileUpload/index.tsx
index a0039b5a..18118fc4 100644
--- a/src/components/pdFileUpload/index.tsx
+++ b/src/components/pdFileUpload/index.tsx
@@ -12,9 +12,9 @@ import {
formatNumber,
} from '@utils/helpers';
-interface IPDFileUpload {
- onChange: (args: unknown) => void;
-}
+import type { InvocationOnChangeProps } from '@components/invocationArgsMapper/types';
+
+interface IPDFileUpload extends InvocationOnChangeProps {}
export const PDFileUpload = ({ onChange }: IPDFileUpload) => {
const refInput = useRef(null);
@@ -33,12 +33,11 @@ export const PDFileUpload = ({ onChange }: IPDFileUpload) => {
const val = Binary.fromBytes(new Uint8Array(buffer));
onChange(val);
}
-
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
- const handleDrop = async (e: DragEvent) => {
+ const handleFileDrop = async (e: DragEvent) => {
e.preventDefault();
const file = e.dataTransfer?.files.item(0);
@@ -51,20 +50,20 @@ export const PDFileUpload = ({ onChange }: IPDFileUpload) => {
}
};
- const handleDragOver = (e: DragEvent) => {
+ const handleFileDragOver = (e: DragEvent) => {
e.preventDefault();
};
const element = refInput?.current;
if (refInput) {
- element?.addEventListener('drop', handleDrop);
- element?.addEventListener('dragover', handleDragOver);
+ element?.addEventListener('drop', handleFileDrop);
+ element?.addEventListener('dragover', handleFileDragOver);
}
return () => {
- element?.removeEventListener('drop', handleDrop);
- element?.removeEventListener('dragover', handleDragOver);
+ element?.removeEventListener('drop', handleFileDrop);
+ element?.removeEventListener('dragover', handleFileDragOver);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
diff --git a/src/components/rpcCalls/index.ts b/src/components/rpcCalls/index.ts
new file mode 100644
index 00000000..0a62c373
--- /dev/null
+++ b/src/components/rpcCalls/index.ts
@@ -0,0 +1 @@
+export { default as InvocationRpcArgs } from './invocationRpcArgs';
diff --git a/src/components/rpcCalls/invocationRpcArg.tsx b/src/components/rpcCalls/invocationRpcArg.tsx
new file mode 100644
index 00000000..e53e9f2a
--- /dev/null
+++ b/src/components/rpcCalls/invocationRpcArg.tsx
@@ -0,0 +1,70 @@
+import { InvocationRpcSelect } from '@components/invocationArgsMapper/invocationRpcSelect';
+import { NotImplemented } from '@components/invocationArgsMapper/notImplemented';
+import { OrderBuilder } from '@components/metadataBuilders/orderBuilder';
+import { PrimitiveBuilder } from '@components/metadataBuilders/primitiveBuilder';
+
+import type { InvocationRpcArg as Type } from '@components/invocationArgsMapper/types';
+import type { TRpcCall } from '@constants/rpcCalls/types';
+
+const mapperCore: Record JSX.Element> = {
+ string: (props) => (
+
+ ),
+ boolean: (props) => (
+
+ ),
+ select: (props) => ,
+ hex: (props) => (
+
+ ),
+ array: (props) => (
+
+ ),
+ number: (props) => (
+
+ ),
+};
+
+export const InvocationRpcArg = (props: Type) => {
+ const { rpc } = props;
+ const type = rpc.type;
+
+ if (!type) {
+ return null;
+ }
+
+ try {
+ const mapType = mapperCore[type];
+ if (!mapType) {
+ return ;
+ } else {
+ const InvocationComponent = mapperCore[type] ?? NotImplemented;
+ return ;
+ }
+ } catch (error) {
+ console.error(error);
+ return ;
+ }
+};
diff --git a/src/components/rpcCalls/invocationRpcArgs.tsx b/src/components/rpcCalls/invocationRpcArgs.tsx
new file mode 100644
index 00000000..3a22d45a
--- /dev/null
+++ b/src/components/rpcCalls/invocationRpcArgs.tsx
@@ -0,0 +1,56 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+/* eslint-disable react/jsx-no-bind */
+
+import { useCallback } from 'react';
+
+import { InvocationRpcArg } from '@components/rpcCalls/invocationRpcArg';
+
+import styles from '../invocationArgsMapper/styles.module.css';
+
+import type { InvocationRpcArgs as Type } from '@components/invocationArgsMapper/types';
+
+const InvocationRpcArgs = ({ rpcs, onChange }: Type) => {
+ const handleOnChange = useCallback((index: number, args: unknown) => {
+ onChange(index, args);
+ }, []);
+
+ return (
+
+ {
+ rpcs.map((rpc, index) => {
+ const isOptional = rpc.optional;
+ const {
+ description,
+ readOnly,
+ name,
+ type,
+ } = rpc;
+
+ return (
+
+
+ {
+ isOptional
+ ? `Optional<${name}>`
+ : name
+ }
+
+
+
+ handleOnChange(index, args)}
+ placeholder={description}
+ readOnly={readOnly}
+ rpc={rpc}
+ />
+
+
+
+ );
+ })
+ }
+
+ );
+};
+
+export default InvocationRpcArgs;
diff --git a/src/components/runtimeCalls/index.ts b/src/components/runtimeCalls/index.ts
new file mode 100644
index 00000000..2556d5c1
--- /dev/null
+++ b/src/components/runtimeCalls/index.ts
@@ -0,0 +1 @@
+export { default as InvocationRuntimeArgs } from './invocationRuntimeArgs';
diff --git a/src/components/runtimeCalls/invocationRuntimeArgs.tsx b/src/components/runtimeCalls/invocationRuntimeArgs.tsx
new file mode 100644
index 00000000..41718bb9
--- /dev/null
+++ b/src/components/runtimeCalls/invocationRuntimeArgs.tsx
@@ -0,0 +1,79 @@
+/* eslint-disable react/jsx-no-bind */
+/* eslint-disable react-hooks/exhaustive-deps */
+import {
+ useCallback,
+ useEffect,
+ useState,
+} from 'react';
+
+import { InvocationMapper } from '@components/invocationArgsMapper/invocationMapper';
+import { useStoreChain } from '@stores';
+import { cn } from '@utils/helpers';
+import { initRuntimeParams } from '@utils/invocationMapper';
+
+import styles from '../invocationArgsMapper/styles.module.css';
+
+import type { InvocationRuntimeArgs as Type } from '@components/invocationArgsMapper/types';
+
+const InvocationRuntimeArgs = ({
+ runtimeMethod,
+ onChange,
+}: Type) => {
+ const lookup = useStoreChain?.use?.lookup?.();
+ const { inputs } = runtimeMethod;
+
+ const [
+ runtimeParams,
+ setRuntimeParams,
+ ] = useState(initRuntimeParams(inputs));
+
+ useEffect(() => {
+ onChange(runtimeParams);
+ }, []);
+
+ const handleOnChange = useCallback((key: string, value: unknown) => {
+ setRuntimeParams((params) => {
+ const newParams = Object.assign(
+ { ...params },
+ { [key]: value },
+ );
+
+ onChange(newParams);
+ return newParams;
+ });
+ }, []);
+
+ if (!Boolean(inputs.length) || !lookup) {
+ return null;
+ } else {
+ return (
+
+ {
+ inputs.map((arg) => {
+ const runtimeLookup = lookup(arg.type);
+ return (
+
+
+ {arg.name}
+
+
+ handleOnChange(arg.name, args)}
+ />
+
+
+ );
+ })
+ }
+
+ );
+ }
+
+};
+
+export default InvocationRuntimeArgs;
diff --git a/src/constants/decoders/types.ts b/src/constants/decoders/types.ts
index a2d963af..ed43f7a4 100644
--- a/src/constants/decoders/types.ts
+++ b/src/constants/decoders/types.ts
@@ -1,13 +1,13 @@
export interface IDecoders {
[key: string]: {
- params: IDecoderParam[];
+ params: InvocationDecoderArgs[];
};
}
type TDecoderProp = 'string' | 'hex' | 'array';
type TArrayItem = 'string' | 'hex';
-export interface IDecoderParam {
+export interface InvocationDecoderArgs {
name: string;
type: TDecoderProp;
diff --git a/src/constants/links.ts b/src/constants/links.ts
index ac19f92b..92bf3e75 100644
--- a/src/constants/links.ts
+++ b/src/constants/links.ts
@@ -18,4 +18,22 @@ export const HOME_LINKS = [
title: 'Forks',
description: 'Keep tabs on finalized blocks and forks as they happen, ensuring you stay informed about network developments!',
},
+ {
+ to: '/chain-state',
+ iconName: 'icon-chain',
+ title: 'Chain State',
+ description: 'Access Chain constants and storage values directly from the live nodes of Polkadot, Kusama, and their system chains.',
+ },
+ {
+ to: '/rpc-calls',
+ iconName: 'icon-rpcCalls',
+ title: 'RPC Calls',
+ description: 'Initiate RPC Calls on selected endpoints to interact seamlessly with the blockchain network.',
+ },
+ {
+ to: '/runtime-calls',
+ iconName: 'icon-runtimeCalls',
+ title: 'Runtime Calls',
+ description: 'Effortlessly retrieve information from existing APIs and their methods, including account nonce, epoch, and more within the ecosystem.',
+ },
];
diff --git a/src/constants/navigation.ts b/src/constants/navigation.ts
index 8db32cc0..fc6d847f 100644
--- a/src/constants/navigation.ts
+++ b/src/constants/navigation.ts
@@ -22,4 +22,47 @@ export const NAVIGATION_ITEMS: TNavItem[] = [
},
type: 'link',
},
+ {
+ linkProps: {
+ title: 'Developer',
+ items: [
+ {
+ title: 'Chain State',
+ to: '/chain-state',
+ icon: 'icon-chain',
+ },
+ {
+ title: 'Constants',
+ to: '/constants',
+ icon: 'icon-chart',
+ },
+ {
+ title: 'RPC Calls',
+ to: '/rpc-calls',
+ icon: 'icon-rpcCalls',
+ },
+ {
+ title: 'Runtime Calls',
+ to: '/runtime-calls',
+ icon: 'icon-runtimeCalls',
+ },
+ {
+ title: 'Extrinsics',
+ to: '/extrinsics',
+ icon: 'icon-zipFile',
+ },
+ {
+ title: 'Decoder',
+ to: '/decoder',
+ icon: 'icon-database',
+ },
+ {
+ title: 'Decoder Dynamic',
+ to: '/decoder-dynamic',
+ icon: 'icon-databaseRepeat',
+ },
+ ],
+ },
+ type: 'dropdown',
+ },
];
diff --git a/src/constants/rpcCalls/types.ts b/src/constants/rpcCalls/types.ts
index 4913e7ae..3aa628da 100644
--- a/src/constants/rpcCalls/types.ts
+++ b/src/constants/rpcCalls/types.ts
@@ -2,7 +2,7 @@ import type { MetadataPrimitives } from '@polkadot-api/metadata-builders';
export interface IRpcCalls {
[key: string]: {
- params: IRpcCallParam[];
+ params: IRpcArg[];
docs?: string[];
link?: string;
};
@@ -11,7 +11,7 @@ export interface IRpcCalls {
export type TRpcCall = 'boolean' | 'string' | 'hex' | 'select' | 'array' | 'number';
export type TArrayItem = 'string';
-export interface IRpcCallParam {
+export interface IRpcArg {
name: string;
type: TRpcCall;
diff --git a/src/types/papi.ts b/src/types/papi.ts
index 818086ff..1accbc79 100644
--- a/src/types/papi.ts
+++ b/src/types/papi.ts
@@ -7,7 +7,7 @@ import type {
} from '@polkadot-api/metadata-builders';
import type { V15 } from '@polkadot-api/substrate-bindings';
-export type TMetaDataCallParam = {
+export type TMetaDataCallBuilder = {
type: 'lookupEntry';
value: LookupEntry;
} | Var;
diff --git a/src/utils/codec/index.ts b/src/utils/codec/index.ts
index 3dc2d258..4936b21e 100644
--- a/src/utils/codec/index.ts
+++ b/src/utils/codec/index.ts
@@ -21,10 +21,10 @@ export const decodeExtrinsic = (extrinsic: string): IBlockExtrinsic | undefined
}
};
export const babeDigestCodec = ScaleEnum({
- authority_index: u32,
- one: u32,
- two: u32,
- three: u32,
+ a: u32,
+ b: u32,
+ c: u32,
+ d: u32,
});
export const blockHeaderCodec = blockHeader;
diff --git a/src/utils/invocationMapper.ts b/src/utils/invocationMapper.ts
new file mode 100644
index 00000000..6ba7de94
--- /dev/null
+++ b/src/utils/invocationMapper.ts
@@ -0,0 +1,63 @@
+import type { StructArgs } from '@components/invocationArgsMapper/types';
+import type { TMetaDataApiMethod } from '@custom-types/papi';
+import type {
+ PrimitiveVar,
+ StructVar,
+} from '@polkadot-api/metadata-builders';
+
+export const initRuntimeParams = (inputs: TMetaDataApiMethod['inputs']) => {
+ return inputs.reduce((acc: { [key: string]: unknown }, curr): { [key: string]: unknown } => {
+ acc[curr.name] = undefined;
+ return acc;
+ }, {});
+};
+
+export const buildArrayState = (
+ length: number,
+ initialValue?: T,
+): T[] => {
+ if (length < 0) return [];
+ if (length === 0) return [];
+
+ return Array.from({ length }, () => initialValue as T);
+};
+
+export const buildSequenceState = (length: number) => {
+ return Array.from({ length }).map(() => ({ id: crypto.randomUUID(), value: undefined }));
+};
+
+export const buildStructState = (struct: StructVar) => {
+ return Object
+ .keys(struct.value)
+ .reduce((acc: { [key: StructArgs['key']]: StructArgs['value'] }, key) => {
+ acc[key] = undefined;
+ return acc;
+ }, {});
+};
+
+export const handlePrimitiveInputChange = (v: PrimitiveVar, value: string) => {
+ const primitiveHandlers: Record boolean | string | number | bigint | undefined> = {
+ bool: (value) => Boolean(value),
+ char: (value) => value,
+ str: (value) => value,
+ u8: (value) => Number(value),
+ i8: (value) => Number(value),
+ u16: (value) => Number(value),
+ i16: (value) => Number(value),
+ u32: (value) => Number(value),
+ i32: (value) => Number(value),
+ u64: (value) => BigInt(Number(value).toFixed(0)),
+ i64: (value) => BigInt(Number(value).toFixed(0)),
+ u128: (value) => BigInt(Number(value).toFixed(0)),
+ i128: (value) => BigInt(Number(value).toFixed(0)),
+ u256: (value) => BigInt(Number(value).toFixed(0)),
+ i256: (value) => BigInt(Number(value).toFixed(0)),
+ };
+
+ const handler = primitiveHandlers[v.value];
+ return handler ? handler(value) : undefined;
+};
+
+export const getCompactValue = (isBig: boolean, value: string) => {
+ return isBig ? BigInt(Number(value).toFixed(0)) : Number(value);
+};
diff --git a/src/utils/papi/helpers.ts b/src/utils/papi/helpers.ts
index 5d379bfb..39d0cf76 100644
--- a/src/utils/papi/helpers.ts
+++ b/src/utils/papi/helpers.ts
@@ -1,3 +1,10 @@
+import {
+ Binary,
+ FixedSizeBinary,
+} from 'polkadot-api';
+
+import type { Var } from '@polkadot-api/metadata-builders';
+
type AssertFunction = (condition: unknown, message: string) => asserts condition;
export const assert: AssertFunction = (condition, message) => {
if (!condition) {
@@ -10,3 +17,55 @@ export const checkIfCompatable = (isCompatable: boolean, message: string) => {
throw new Error(message);
}
};
+
+export const varIsBinary = (variabel: Var) => {
+ switch (variabel.type) {
+ case 'tuple':
+ return variabel.value.every((lookupEntry) =>
+ lookupEntry.type === 'primitive' && lookupEntry.value === 'u8');
+ case 'sequence':
+ case 'array':
+ return variabel.value.type === 'primitive' && variabel.value.value === 'u8';
+ default:
+ return false;
+ }
+};
+
+export const unwrapApiResult = (data: unknown): unknown => {
+ // for rpc calls
+ if (data && typeof data === 'object' && 'asHex' in data) {
+ return (data as Binary).asHex();
+ }
+
+ if (data instanceof Binary) {
+ return data.asHex();
+ }
+
+ if (data instanceof FixedSizeBinary) {
+ return data.asHex();
+ }
+
+ // AuthorityDiscoveryApi_authorities
+ if (typeof data !== 'object') {
+ return data;
+ }
+
+ if (data === null) {
+ return data;
+ }
+
+ if (Array.isArray(data)) {
+
+ return data.map(unwrapApiResult);
+ }
+
+ return Object.fromEntries(
+ Object.entries(data).map(([
+ key,
+ value,
+ ]) => [
+ key,
+ unwrapApiResult(value),
+ ] as const),
+ );
+};
diff --git a/src/utils/rpc/getBlockDetails.ts b/src/utils/rpc/getBlockDetails.ts
index 351f37ae..10467c81 100644
--- a/src/utils/rpc/getBlockDetails.ts
+++ b/src/utils/rpc/getBlockDetails.ts
@@ -8,6 +8,8 @@ import {
} from '@utils/codec';
import { formatPrettyNumberString } from '@utils/helpers';
import {
+ getIdentity,
+ getSuperIdentity,
getSystemDigestData,
getSystemEvents,
getValidators,
@@ -18,8 +20,8 @@ import type { IMappedBlockExtrinsic } from '@custom-types/block';
import type { BlockDetails } from '@custom-types/rawClientReturnTypes';
import type { RuntimeVersion } from '@polkadot/types/interfaces';
import type {
+ FixedSizeBinary,
HexString,
- SS58String,
} from 'polkadot-api';
import type { useDynamicBuilder } from 'src/hooks/useDynamicBuilder';
@@ -48,16 +50,46 @@ const getBlockValidator = async ({
authorIndex = babeDigestCodec.dec(digestData).value;
}
- let authors: SS58String[] = [];
+ let authors: string[] = [];
if (isRelayChain) {
authors = await getValidators(api, blockHash)
.catch();
}
- const address = isRelayChain && authors[authorIndex];
+ const address = isRelayChain ? authors[authorIndex] : '';
+
+ let identity;
+
+ identity = await getIdentity(peopleApi, address)
+ .catch();
+ if (identity) {
+ identity = (identity?.[0]?.info?.display?.value as FixedSizeBinary<2>)?.asText?.();
+ }
+
+ const superIdentity = await getSuperIdentity(peopleApi, address)
+ .catch();
+
+ if (superIdentity?.[0] && !identity) {
+ identity = await getIdentity(peopleApi, superIdentity[0])
+ .catch();
+
+ if (identity) {
+ const identityVal = (identity?.[0]?.info?.display?.value as FixedSizeBinary<2>)?.asText?.();
+ const superIdentityVal = (superIdentity?.[1]?.value as FixedSizeBinary<2>)?.asText?.();
+
+ if (identityVal) {
+ if (superIdentityVal) {
+ identity = `${identityVal}/${superIdentityVal}`;
+ } else {
+ identity = identityVal;
+ }
+ }
+ }
+ }
return {
+ name: identity?.toString?.(),
address,
identity: '',
};
@@ -150,7 +182,6 @@ export const getBlockDetailsWithPAPI = async ({
runtime,
identity: identity.status === 'fulfilled' ? identity.value : undefined,
...(blockHeader.status === 'fulfilled' ? blockHeader.value : {}),
- // ...blockHeader,
},
body: {
extrinsics,
@@ -207,7 +238,7 @@ export const getBlockDetailsWithRawClient = async ({
try {
events = storageCodec.dec(storageValue) as Awaited>;
} catch (error) {
- console.log('Could not decode storage value');
+ console.log('Could not decode storage value!');
}
// Initialize timestamp variable
diff --git a/src/views/blockDetails/blockBody/index.tsx b/src/views/blockDetails/blockBody/index.tsx
index 5300c7b3..ffa9e2d6 100644
--- a/src/views/blockDetails/blockBody/index.tsx
+++ b/src/views/blockDetails/blockBody/index.tsx
@@ -8,6 +8,7 @@ import {
import { Icon } from '@components/icon';
import { ModalJSONViewer } from '@components/modals/modalJSONViewer';
+import { PDScrollArea } from '@components/pdScrollArea';
import { Tabs } from '@components/tabs';
import { ToolTip } from '@components/tooltTip';
import { cn } from '@utils/helpers';
@@ -85,7 +86,7 @@ export const BlockBody = (props: BlockBodyProps) => {
]);
return (
-
+
{
-
-
-
-
-
-
-
-
-
-
-
- Extrinsic ID |
- Height |
- Time |
- Result |
- Action |
- |
-
-
-
- {
- visibleExtrinsics.map((extrinsic: IMappedBlockExtrinsic, extrinsicIndex: number) => {
- const extrinsicTimestamp = extrinsic.timestamp || blockTimestamp;
- const timeAgo = extrinsicTimestamp && formatDistanceToNowStrict(
- new Date(extrinsicTimestamp),
- { addSuffix: true },
- );
- return (
+
+
+
+
+
+
+
+
+
+
+
+
+ Extrinsic ID |
+ Height |
+ Time |
+ Result |
+ Action |
+ |
+
+
+
+ {
+ visibleExtrinsics.map((extrinsic: IMappedBlockExtrinsic, extrinsicIndex: number) => {
+ const extrinsicTimestamp = extrinsic.timestamp || blockTimestamp;
+ const timeAgo = extrinsicTimestamp && formatDistanceToNowStrict(
+ new Date(extrinsicTimestamp),
+ { addSuffix: true },
+ );
+ return (
+
+ {extrinsic.id} |
+ {blockNumber} |
+ {timeAgo} |
+
+
+ |
+
+ {extrinsic.extrinsicData.method.section}
+ {' '}
+ (
+ {extrinsic.extrinsicData.method.method}
+ )
+ |
+
+
+
+
+
+
+ |
+
+ );
+ })
+ }
+
+
+
+ {
+ bodyData.extrinsics.length > 3 && (
+
+
+
+ )
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+ Event ID |
+ Action |
+ Type |
+ |
+
+
+
+ {
+ visibleEvents.map((event: IMappedBlockEvent, eventIndex: number) => (
- {extrinsic.id} |
- {blockNumber} |
- {timeAgo} |
-
+ {blockNumber}
+ -
+ {eventIndex}
|
- {extrinsic.extrinsicData.method.section}
+ {event.event.type}
{' '}
(
- {extrinsic.extrinsicData.method.method}
+ {event.event.value.type}
)
|
+ {event.phase.type} |
@@ -169,89 +252,11 @@ export const BlockBody = (props: BlockBodyProps) => {
|
- );
- })
- }
-
-
- {
- bodyData.extrinsics.length > 3 && (
-
-
-
- )
- }
-
-
-
-
-
-
-
-
-
-
-
- Event ID |
- Action |
- Type |
- |
-
-
-
- {
- visibleEvents.map((event: IMappedBlockEvent, eventIndex: number) => (
-
-
- {blockNumber}
- -
- {eventIndex}
- |
-
- {event.event.type}
- {' '}
- (
- {event.event.value.type}
- )
- |
- {event.phase.type} |
-
-
-
-
-
-
- |
-
- ))
- }
-
-
+ ))
+ }
+
+
+
{bodyData.events.length > 3 && (
)}
-
+
-
+
);
};
diff --git a/src/views/blockDetails/index.tsx b/src/views/blockDetails/index.tsx
index 7743734c..f561a40a 100644
--- a/src/views/blockDetails/index.tsx
+++ b/src/views/blockDetails/index.tsx
@@ -6,6 +6,7 @@ import {
import { useParams } from 'react-router-dom';
import { Icon } from '@components/icon';
+import { Loader } from '@components/loader';
import { PageHeader } from '@components/pageHeader';
import { PDLink } from '@components/pdLink';
import { useStoreChain } from '@stores';
@@ -85,7 +86,7 @@ const BlockDetails = () => {
]);
if (!blockData) {
- return 'Loading...';
+ return ;
}
return (
diff --git a/src/views/chainState/index.tsx b/src/views/chainState/index.tsx
new file mode 100644
index 00000000..8b6bcf40
--- /dev/null
+++ b/src/views/chainState/index.tsx
@@ -0,0 +1,338 @@
+import {
+ useCallback,
+ useEffect,
+ useMemo,
+ useState,
+} from 'react';
+
+import { CallDocs } from '@components/callDocs';
+import { InvocationStorageArgs } from '@components/chainState';
+import { Loader } from '@components/loader';
+import { QueryButton } from '@components/papiQuery/queryButton';
+import { QueryFormContainer } from '@components/papiQuery/queryFormContainer';
+import { QueryResult } from '@components/papiQuery/queryResult';
+import { QueryResultContainer } from '@components/papiQuery/queryResultContainer';
+import { QueryViewContainer } from '@components/papiQuery/queryViewContainer';
+import { PDSelect } from '@components/pdSelect';
+import { useStoreChain } from '@stores';
+import { useDynamicBuilder } from 'src/hooks/useDynamicBuilder';
+
+import type { TRelayApi } from '@custom-types/chain';
+
+interface ISubscription {
+ unsubscribe: () => void;
+ id?: string;
+}
+
+const ChainState = () => {
+ const dynamicBuilder = useDynamicBuilder();
+
+ const metadata = useStoreChain?.use?.metadata?.();
+ const chain = useStoreChain?.use?.chain?.();
+
+ const palletsWithStorage = useMemo(() => metadata?.pallets?.filter((p) => p.storage)?.sort((a, b) => a.name.localeCompare(b.name)), [metadata]);
+ const palletSelectItems = useMemo(() => palletsWithStorage?.map((pallet) => ({
+ label: pallet.name,
+ value: pallet.name,
+ key: `chainState-pallet-${pallet.name}`,
+ })), [palletsWithStorage]);
+
+ const [
+ palletSelected,
+ setPalletSelected,
+ ] = useState(palletsWithStorage?.at(0));
+
+ const storageCalls = useMemo(() => palletSelected?.storage?.items?.sort((a, b) => a.name.localeCompare(b.name)), [palletSelected]);
+ const storageCallItems = useMemo(() => {
+ if (palletSelected) {
+ return storageCalls?.map((item) => ({
+ label: item.name,
+ value: item.name,
+ key: `chainState-call-${item.name}`,
+ }));
+ }
+ return undefined;
+ }, [
+ palletSelected,
+ storageCalls,
+ ]);
+
+ const [
+ storageSelected,
+ setStorageSelected,
+ ] = useState(palletSelected?.storage?.items?.at?.(0));
+
+ const [
+ callArgs,
+ setCallArgs,
+ ] = useState(undefined);
+
+ const [
+ encodedStorageKey,
+ setEncodedStorageKey,
+ ] = useState('');
+
+ const [
+ queries,
+ setQueries,
+ ] = useState<{ pallet: string; storage: string; id: string; args: unknown }[]>([]);
+
+ const [
+ subscriptions,
+ setSubscriptions,
+ ] = useState([]);
+
+ useEffect(() => {
+ setQueries([]);
+ setCallArgs(undefined);
+ setPalletSelected(undefined);
+ subscriptions.forEach((sub) => {
+ sub?.unsubscribe?.();
+ });
+
+ return () => {
+ setQueries([]);
+ subscriptions.forEach((sub) => {
+ sub?.unsubscribe?.();
+ });
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [chain.id]);
+
+ useEffect(() => {
+ if (palletsWithStorage) {
+ const defaultPalletSelected = palletsWithStorage[0];
+ setPalletSelected(defaultPalletSelected);
+ setStorageSelected(defaultPalletSelected?.storage?.items?.sort((a, b) => a.name.localeCompare(b.name)).at?.(0));
+ }
+ }, [palletsWithStorage]);
+
+ useEffect(() => {
+ if (palletSelected?.name && storageSelected?.name && dynamicBuilder) {
+ try {
+ const storageCodec = dynamicBuilder.buildStorage(palletSelected.name, storageSelected.name);
+ const filteredCallArgs = [callArgs].filter((arg) => Boolean(arg));
+ const encodedKey = storageCodec.enc(...filteredCallArgs);
+
+ setEncodedStorageKey(encodedKey);
+ } catch (error) {
+ setEncodedStorageKey('');
+ console.log(error);
+
+ }
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [
+ palletSelected,
+ storageSelected,
+ callArgs,
+ ]);
+
+ const handlePalletSelect = useCallback((selectedPalletName: string) => {
+ if (palletsWithStorage) {
+ const selectedPallet = palletsWithStorage.find((pallet) => pallet.name === selectedPalletName);
+
+ if (selectedPallet) {
+ setPalletSelected(selectedPallet);
+ setStorageSelected(selectedPallet.storage?.items?.sort((a, b) => a.name.localeCompare(b.name)).at(0));
+ setCallArgs(undefined);
+ }
+ }
+ }, [palletsWithStorage]);
+
+ const handleStorageSelect = useCallback((selectedCallName: string) => {
+ if (palletSelected) {
+ const selectedStorage = palletSelected.storage?.items.find((item) => item.name === selectedCallName);
+ setStorageSelected(selectedStorage);
+ setCallArgs(undefined);
+ }
+ }, [palletSelected]);
+
+ const handleQuerySubmit = useCallback(() => {
+ if (palletSelected?.name && storageSelected?.name && dynamicBuilder) {
+ setQueries((queries) => {
+ const newQueries = [...queries];
+ newQueries.unshift({
+ pallet: palletSelected.name,
+ storage: storageSelected.name,
+ id: crypto.randomUUID(),
+ args: callArgs,
+ });
+ return newQueries;
+ });
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [
+ palletSelected,
+ storageSelected,
+ callArgs,
+ ]);
+
+ const handleSubscribe = useCallback((subscription: ISubscription) => {
+ setSubscriptions((subs) => ([
+ ...subs,
+ subscription,
+ ]));
+ }, []);
+
+ const handleUnsubscribe = useCallback((id: string) => {
+ setQueries((queries) => queries.filter((query) => query.id !== id));
+
+ const subscriptionToCancel = subscriptions.find((sub) => sub.id === id);
+ if (subscriptionToCancel) {
+ subscriptionToCancel.unsubscribe();
+ setSubscriptions((subs) => subs.filter((sub) => sub.id !== id));
+ }
+ }, [subscriptions]);
+
+ if (!palletSelected) {
+ return ;
+ }
+
+ return (
+
+
+
+
+
+ {
+ storageCallItems && (
+
+ )
+ }
+
+
+ {
+ storageSelected && (
+
+ )
+ }
+
+
+ Subscribe to
+ {' '}
+ {palletSelected?.name}
+ /
+ {storageSelected?.name}
+
+
+ {
+ (encodedStorageKey) && (
+
+ Encoded Storage Key:
+
+ {' '}
+ {encodedStorageKey}
+
+ )
+ }
+
+ d) || []} />
+
+
+
+ {
+ queries.map((query) => (
+
+ ))
+ }
+
+
+ );
+};
+
+export default ChainState;
+
+const Query = (
+ {
+ querie,
+ onSubscribe,
+ onUnsubscribe,
+ }: {
+ querie: { pallet: string; storage: string; id: string; args: unknown };
+ onSubscribe: (subscription: ISubscription) => void;
+ onUnsubscribe: (id: string) => void;
+ }) => {
+ const api = useStoreChain?.use?.api?.() as TRelayApi;
+
+ const [
+ result,
+ setResult,
+ ] = useState();
+ const [
+ isLoading,
+ setIsLoading,
+ ] = useState(true);
+
+ useEffect(() => {
+ const catchError = (err: Error) => {
+ setIsLoading(false);
+ setResult(err?.message || 'Unexpected Error');
+ };
+
+ if (api) {
+ try {
+ // @TODO: fix types
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const subscription = (api.query as any)[querie.pallet][querie.storage]
+ .watchValue(querie.args || 'finalized')
+ .subscribe((res: unknown) => {
+ setResult(res);
+ setIsLoading(false);
+ });
+
+ subscription.id = querie.id;
+ onSubscribe(subscription);
+
+ } catch (error) {
+ catchError(error as Error);
+ }
+ }
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [
+ querie,
+ api,
+ ]);
+
+ const handleUnsubscribe = useCallback(() => {
+ onUnsubscribe(querie.id);
+ }, [
+ querie,
+ onUnsubscribe,
+ ]);
+
+ return (
+
+ );
+};
diff --git a/src/views/constants/index.tsx b/src/views/constants/index.tsx
new file mode 100644
index 00000000..f487b646
--- /dev/null
+++ b/src/views/constants/index.tsx
@@ -0,0 +1,233 @@
+import {
+ useCallback,
+ useEffect,
+ useMemo,
+ useState,
+} from 'react';
+
+import { CallDocs } from '@components/callDocs';
+import { Loader } from '@components/loader';
+import { QueryButton } from '@components/papiQuery/queryButton';
+import { QueryFormContainer } from '@components/papiQuery/queryFormContainer';
+import { QueryResult } from '@components/papiQuery/queryResult';
+import { QueryResultContainer } from '@components/papiQuery/queryResultContainer';
+import { QueryViewContainer } from '@components/papiQuery/queryViewContainer';
+import { PDSelect } from '@components/pdSelect';
+import { useStoreChain } from '@stores';
+
+import type { TRelayApi } from '@custom-types/chain';
+
+const Constants = () => {
+ const metadata = useStoreChain?.use?.metadata?.();
+ const chain = useStoreChain?.use?.chain?.();
+
+ const palletsWithConstants = useMemo(() => metadata?.pallets?.filter((p) => p.constants.length > 0)?.sort((a, b) => a.name.localeCompare(b.name)), [metadata]);
+ const palletSelectItems = useMemo(() => palletsWithConstants?.map((pallet) => ({
+ label: pallet.name,
+ value: pallet.name,
+ key: `pallet-select-${pallet.name}`,
+ })), [palletsWithConstants]);
+
+ const [
+ palletSelected,
+ setPalletSelected,
+ ] = useState(palletsWithConstants?.at(0));
+
+ const constants = useMemo(() => palletSelected?.constants?.sort((a, b) => a.name.localeCompare(b.name)), [palletSelected]);
+ const constantItems = useMemo(() => {
+ if (palletSelected) {
+ return constants?.map((item) => ({
+ label: item.name,
+ value: item.name,
+ key: `constant-select-${item.name}`,
+ }));
+ }
+ return undefined;
+ }, [
+ constants,
+ palletSelected,
+ ]);
+
+ const [
+ constantSelected,
+ setConstantSelected,
+ ] = useState(palletSelected?.constants?.at?.(0));
+ const [
+ queries,
+ setQueries,
+ ] = useState<{ pallet: string; storage: string; id: string }[]>([]);
+
+ useEffect(() => {
+ setQueries([]);
+ setPalletSelected(undefined);
+ }, [chain.id]);
+
+ useEffect(() => {
+ if (palletsWithConstants) {
+ const defaultPalletSelected = palletsWithConstants[0];
+ setPalletSelected(defaultPalletSelected);
+ setConstantSelected(defaultPalletSelected?.constants?.sort((a, b) => a.name.localeCompare(b.name)).at?.(0));
+ }
+ }, [palletsWithConstants]);
+
+ const handlePalletSelect = useCallback((selectedPalletName: string) => {
+ if (palletsWithConstants) {
+ const selectedPallet = palletsWithConstants.find((pallet) => pallet.name === selectedPalletName);
+
+ if (selectedPallet) {
+ setPalletSelected(selectedPallet);
+ setConstantSelected(selectedPallet.constants?.sort((a, b) => a.name.localeCompare(b.name)).at(0));
+ }
+ }
+ }, [palletsWithConstants]);
+
+ const handleConstantSelect = useCallback((constantSelectedName: string) => {
+ if (palletSelected) {
+ const selectedStorage = palletSelected.constants.find((constant) => constant.name === constantSelectedName);
+ setConstantSelected(selectedStorage);
+ }
+ }, [palletSelected]);
+
+ const handleQuerySubmit = useCallback(() => {
+ if (palletSelected?.name && constantSelected?.name) {
+ setQueries((queries) => {
+ const newQueries = [...queries];
+ newQueries.unshift({
+ pallet: palletSelected.name,
+ storage: constantSelected.name,
+ id: crypto.randomUUID(),
+ });
+ return newQueries;
+ });
+ }
+ }, [
+ palletSelected,
+ constantSelected,
+ ]);
+
+ const handleUnsubscribe = useCallback((id: string) => {
+ setQueries((queries) => queries.filter((query) => query.id !== id));
+ }, []);
+
+ if (!palletSelected) {
+ return ;
+ }
+
+ return (
+
+
+
+
+ {
+ constantItems && (
+
+ )
+ }
+
+
+ d) || []} />
+
+
+ Query
+ {' '}
+ {palletSelected?.name}
+ /
+ {constantSelected?.name}
+
+
+
+
+ {
+ queries.map((query) => (
+
+ ))
+ }
+
+
+ );
+};
+
+export default Constants;
+
+const Query = (
+ {
+ querie,
+ onUnsubscribe,
+ }: {
+ querie: { pallet: string; storage: string; id: string };
+ onUnsubscribe: (id: string) => void;
+ }) => {
+ const api = useStoreChain?.use?.api?.() as TRelayApi;
+
+ const [
+ result,
+ setResult,
+ ] = useState();
+ const [
+ isLoading,
+ setIsLoading,
+ ] = useState(true);
+
+ useEffect(() => {
+ const catchError = (err: Error) => {
+ setIsLoading(false);
+ setResult(err?.message || 'Unexpected Error');
+ };
+
+ if (api) {
+ try {
+ // @TODO: fix types
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (api.constants as any)[querie.pallet][querie.storage]?.()
+ .then((res: unknown) => {
+ setResult(res);
+ setIsLoading(false);
+ })
+ .catch(catchError);
+
+ } catch (error) {
+ catchError(error as Error);
+ }
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [
+ querie,
+ api,
+ ]);
+
+ const handleUnsubscribe = useCallback(() => {
+ onUnsubscribe(querie.id);
+ }, [
+ querie,
+ onUnsubscribe,
+ ]);
+
+ return (
+
+ );
+};
diff --git a/src/views/decoder/index.tsx b/src/views/decoder/index.tsx
new file mode 100644
index 00000000..9d64bb54
--- /dev/null
+++ b/src/views/decoder/index.tsx
@@ -0,0 +1,222 @@
+import {
+ useCallback,
+ useEffect,
+ useState,
+} from 'react';
+
+import { InvocationDecoder } from '@components/decoder';
+import { Loader } from '@components/loader';
+import { QueryButton } from '@components/papiQuery/queryButton';
+import { QueryFormContainer } from '@components/papiQuery/queryFormContainer';
+import { QueryResult } from '@components/papiQuery/queryResult';
+import { QueryResultContainer } from '@components/papiQuery/queryResultContainer';
+import { QueryViewContainer } from '@components/papiQuery/queryViewContainer';
+import { PDSelect } from '@components/pdSelect';
+import { decoders } from '@constants/decoders';
+import { useStoreChain } from '@stores';
+import {
+ blockHeaderCodec,
+ decodeExtrinsic,
+ metadataCodec,
+} from '@utils/codec';
+
+const decoderSelectItems = Object.keys(decoders).map((decoder) => ({
+ key: `decoder-${decoder}`,
+ label: decoder,
+ value: decoder,
+}));
+
+interface IDecoderField {
+ key: string;
+ value: unknown;
+}
+
+const Decoder = () => {
+ const chain = useStoreChain?.use?.chain?.();
+ const metadata = useStoreChain?.use?.metadata?.();
+
+ const [
+ decoder,
+ setDecoder,
+ ] = useState(decoderSelectItems.at(0)!.value);
+ const [
+ queries,
+ setQueries,
+ ] = useState([]);
+ const [
+ decoderFields,
+ setDecoderFields,
+ ] = useState([]);
+
+ useEffect(() => {
+ if (decoder) {
+ setDecoderFields(decoders[decoder]
+ .params
+ .map((param) => ({
+ key: param.name,
+ value: undefined,
+ })),
+ );
+ }
+ }, [decoder]);
+
+ // RESET STATES ON CHAIN CHANGE
+ useEffect(() => {
+ setQueries([]);
+ }, [chain.id]);
+
+ const handleOnDecoderChange = useCallback((decoderSelected: string) => {
+ setDecoder(decoderSelected);
+ }, []);
+
+ const handleOnChange = useCallback((index: number, args: unknown) => {
+ setDecoderFields((params) => {
+ if (!params || params.length === 0) {
+ return [];
+ } else {
+ const newParams = [...params];
+ newParams[index].value = args;
+ return newParams;
+ }
+ });
+ }, []);
+
+ const handleDecode = useCallback(() => {
+ setQueries((queries) => {
+ const newQueries = [...queries];
+ newQueries.unshift({
+ decoder,
+ args: decoderFields,
+ id: crypto.randomUUID(),
+ });
+ return newQueries;
+ });
+
+ }, [
+ decoderFields,
+ decoder,
+ ]);
+
+ const handleOnUnsubscribe = useCallback((id: string) => {
+ setQueries((queries) => queries.filter((query) => query.id !== id));
+ }, []);
+
+ if (!metadata) {
+ return ;
+ }
+
+ return (
+
+
+
+
+ {
+ decoder
+ && decoders?.[decoder]?.params?.length > 0 && (
+
+ )
+ }
+
+
+ Decode
+ {' '}
+ {decoder}
+
+
+
+
+ {
+ queries.map((query) => (
+
+ ))
+ }
+
+
+ );
+};
+
+export default Decoder;
+
+interface IQuery {
+ decoder: string;
+ id: string;
+ args: IDecoderField[];
+}
+
+const Query = (
+ {
+ querie,
+ onUnsubscribe,
+ }: {
+ querie: IQuery;
+ onUnsubscribe: (id: string) => void;
+ }) => {
+ const [
+ result,
+ setResult,
+ ] = useState();
+
+ useEffect(() => {
+ const catchError = (err?: Error) => {
+ setResult(err?.message || 'Unexpected Error');
+ };
+
+ try {
+ switch (querie.decoder) {
+ case 'blockHeader':
+ setResult(blockHeaderCodec.dec(querie.args.at(0)?.value as string));
+ break;
+
+ case 'extrinsics':
+ setResult((querie.args.at(0)?.value as string[])
+ ?.map((extrinsic) => decodeExtrinsic(extrinsic)));
+ break;
+
+ case 'metadata':
+ setResult(metadataCodec.dec(querie.args.at(0)?.value as string));
+ break;
+
+ default:
+ catchError();
+ break;
+ }
+
+ } catch (error) {
+ catchError(error as Error);
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [querie]);
+
+ const handleUnsubscribe = useCallback(() => {
+ onUnsubscribe(querie.id);
+ }, [
+ querie,
+ onUnsubscribe,
+ ]);
+
+ return (
+
+ );
+};
diff --git a/src/views/decoderDynamic/index.tsx b/src/views/decoderDynamic/index.tsx
new file mode 100644
index 00000000..3902cec9
--- /dev/null
+++ b/src/views/decoderDynamic/index.tsx
@@ -0,0 +1,355 @@
+import {
+ useCallback,
+ useEffect,
+ useMemo,
+ useState,
+} from 'react';
+
+import { InvocationDecoderDynamic } from '@components/decoderDynamic';
+import { Loader } from '@components/loader';
+import { QueryButton } from '@components/papiQuery/queryButton';
+import { QueryFormContainer } from '@components/papiQuery/queryFormContainer';
+import { QueryResult } from '@components/papiQuery/queryResult';
+import { QueryResultContainer } from '@components/papiQuery/queryResultContainer';
+import { QueryViewContainer } from '@components/papiQuery/queryViewContainer';
+import { PDSelect } from '@components/pdSelect';
+import { useStoreChain } from '@stores';
+import { useDynamicBuilder } from 'src/hooks/useDynamicBuilder';
+
+import type { EnumVar } from '@polkadot-api/metadata-builders';
+
+const decoderTypes = [
+ 'Storage',
+ 'Runtime',
+ 'Extrinsic',
+];
+
+const decoderTypeSelectItems = decoderTypes.map((decoder) => ({
+ key: `decoder-${decoder}`,
+ label: decoder,
+ value: decoder,
+}));
+
+interface IQuery {
+ type: string;
+ pallet: string;
+ method: string;
+ id: string;
+ args: string;
+}
+
+const DecoderDynamic = () => {
+ const chain = useStoreChain?.use?.chain?.();
+ const metadata = useStoreChain?.use?.metadata?.();
+ const lookup = useStoreChain?.use?.lookup?.();
+ const dynamicBuilder = useDynamicBuilder();
+
+ const [
+ queries,
+ setQueries,
+ ] = useState([]);
+ const [
+ callParams,
+ setCallParams,
+ ] = useState('');
+ const [
+ decoderType,
+ setDecoderType,
+ ] = useState(decoderTypes[0]);
+ const [
+ pallet,
+ setPallet,
+ ] = useState('');
+ const [
+ method,
+ setMethod,
+ ] = useState('');
+
+ const palletSelectItems = useMemo(() => {
+ if (!metadata) return [];
+
+ switch (decoderType) {
+ case 'Storage': {
+ const storageItems = metadata?.pallets
+ ?.filter((p) => p.storage)
+ ?.sort((a, b) => a.name.localeCompare(b.name))
+ ?.map((pallet) => ({
+ label: pallet.name,
+ value: pallet.name,
+ key: `chainState-pallet-${pallet.name}`,
+ })) || [];
+
+ setPallet(storageItems?.[0]?.value);
+ return storageItems;
+ }
+ case 'Runtime': {
+ const runtimeItems = metadata?.apis
+ ?.sort((a, b) => a.name.localeCompare(b.name))
+ ?.map((api) => ({
+ label: api.name,
+ value: api.name,
+ key: `api-select-${api.name}`,
+ })) || [];
+
+ setPallet(runtimeItems?.[0]?.value);
+ return runtimeItems;
+ }
+ case 'Extrinsic': {
+ const callItems = metadata?.pallets
+ ?.filter((p) => p.calls)
+ ?.sort((a, b) => a.name.localeCompare(b.name))
+ ?.map((pallet) => ({
+ label: pallet.name,
+ value: pallet.name,
+ key: `extrinsic-pallet-${pallet.name}`,
+ })) || [];
+
+ setPallet(callItems?.[0]?.value);
+ return callItems;
+ }
+ default:
+ return [];
+ }
+
+ }, [
+ decoderType,
+ metadata,
+ ]);
+
+ const methodSelectItems = useMemo(() => {
+ if (!metadata || !lookup) return [];
+
+ switch (decoderType) {
+ case 'Storage': {
+ const storageMethodItems = metadata?.pallets
+ ?.find((_pallet) => _pallet.name === pallet)
+ ?.storage
+ ?.items
+ ?.map((item) => ({
+ label: item.name,
+ value: item.name,
+ key: `chainState-call-${item.name}`,
+ })) || [];
+
+ setMethod(storageMethodItems?.[0]?.value);
+ return storageMethodItems;
+ }
+ case 'Runtime': {
+ const runtimeMethodItems = metadata?.apis
+ ?.find((api) => api.name === pallet)
+ ?.methods
+ ?.map((item) => ({
+ label: item.name,
+ value: item.name,
+ key: `chainState-call-${item.name}`,
+ })) || [];
+
+ setMethod(runtimeMethodItems?.[0]?.value);
+ return runtimeMethodItems;
+ }
+ case 'Extrinsic': {
+ const extrinsicMethodCalls = metadata?.pallets
+ ?.find((_pallet) => _pallet.name === pallet)
+ ?.calls;
+
+ if (typeof extrinsicMethodCalls === 'undefined') return [];
+ const extrinsicMethodLookup = lookup(extrinsicMethodCalls) as EnumVar;
+
+ const extrinsicMethodItems = Object.entries(extrinsicMethodLookup?.value || {})
+ ?.sort((a, b) => a[0].localeCompare(b[0]))
+ ?.map((item) => ({
+ label: item?.[0],
+ value: item?.[0],
+ key: `chainState-call-${item?.[0]}`,
+ })) || [];
+
+ setMethod(extrinsicMethodItems[0].value);
+ return extrinsicMethodItems;
+ }
+ default:
+ return [];
+ }
+
+ }, [
+ pallet,
+ decoderType,
+ metadata,
+ lookup,
+ ]);
+
+ // RESET STATES ON CHAIN CHANGE
+ useEffect(() => {
+ setQueries([]);
+ }, [chain.id]);
+
+ const handleDecoderTypeSelect = useCallback((decoderSelected: string) => {
+ setDecoderType(decoderSelected);
+ }, []);
+ const handlePalletSelect = useCallback((decoderSelected: string) => {
+ setPallet(decoderSelected);
+ }, []);
+ const handleMethodSelect = useCallback((decoderSelected: string) => {
+ setMethod(decoderSelected);
+ }, []);
+
+ const handleParamChange = useCallback((args: unknown) => {
+ setCallParams(args as string);
+ }, []);
+
+ const handleDecode = useCallback(() => {
+ setQueries((queries) => {
+ const newQueries = [...queries];
+ newQueries.unshift({
+ type: decoderType,
+ pallet,
+ method,
+ args: callParams,
+ id: crypto.randomUUID(),
+ });
+ return newQueries;
+ });
+
+ }, [
+ callParams,
+ decoderType,
+ pallet,
+ method,
+ ]);
+
+ const handleStorageUnsubscribe = useCallback((id: string) => {
+ setQueries((queries) => queries.filter((query) => query.id !== id));
+ }, []);
+
+ if (!dynamicBuilder) {
+ return ;
+ }
+
+ return (
+
+
+
+
+
+
+
+ Decode
+ {' '}
+ {decoderType}
+ /
+ {pallet}
+ /
+ {method}
+
+
+
+
+ {
+ queries.map((query) => (
+
+ ))
+ }
+
+
+ );
+};
+
+export default DecoderDynamic;
+
+const Query = (
+ {
+ querie,
+ onUnsubscribe,
+ }: {
+ querie: IQuery;
+ onUnsubscribe: (id: string) => void;
+ }) => {
+ const dynamicBuilder = useDynamicBuilder();
+
+ const [
+ result,
+ setResult,
+ ] = useState();
+
+ useEffect(() => {
+ const catchError = (err?: Error) => {
+ setResult(err?.message || 'Unexpected Error');
+ };
+
+ try {
+ switch (querie.type) {
+ case 'Storage':
+ setResult(dynamicBuilder?.buildStorage(querie.pallet, querie.method)
+ .dec(querie.args));
+ break;
+
+ case 'Runtime':
+ setResult(dynamicBuilder?.buildRuntimeCall(querie.pallet, querie.method)
+ .value
+ .dec(querie.args));
+ break;
+
+ case 'Extrinsic':
+ setResult(dynamicBuilder?.buildCall(querie.pallet, querie.method)
+ .codec
+ .dec(`0x${querie.args.slice(6)}`)); /* Remove the pallet/method location hex value from the args */
+ break;
+
+ default:
+ catchError();
+ break;
+ }
+
+ } catch (error) {
+ catchError(error as Error);
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [querie]);
+
+ const handleUnsubscribe = useCallback(() => {
+ onUnsubscribe(querie.id);
+ }, [
+ querie,
+ onUnsubscribe,
+ ]);
+
+ return (
+
+ );
+};
diff --git a/src/views/extrinsics/index.tsx b/src/views/extrinsics/index.tsx
new file mode 100644
index 00000000..f26cab03
--- /dev/null
+++ b/src/views/extrinsics/index.tsx
@@ -0,0 +1,374 @@
+import { type EnumVar } from '@polkadot-api/metadata-builders';
+import { mergeUint8 } from '@polkadot-api/utils';
+import { Binary } from 'polkadot-api';
+import {
+ useCallback,
+ useEffect,
+ useMemo,
+ useState,
+} from 'react';
+
+import { InvocationArgsMapper } from '@components/invocationArgsMapper';
+import { Loader } from '@components/loader';
+import { WalletAccountSelector } from '@components/metadataBuilders/accountBuilder/walletAccountSelector';
+import { QueryButton } from '@components/papiQuery/queryButton';
+import { QueryFormContainer } from '@components/papiQuery/queryFormContainer';
+import { QueryResult } from '@components/papiQuery/queryResult';
+import { QueryResultContainer } from '@components/papiQuery/queryResultContainer';
+import { QueryViewContainer } from '@components/papiQuery/queryViewContainer';
+import { PDSelect } from '@components/pdSelect';
+import { useStoreChain } from '@stores';
+import { useDynamicBuilder } from 'src/hooks/useDynamicBuilder';
+import { useViewBuilder } from 'src/hooks/useViewBuilder';
+import { useStoreWallet } from 'src/stores/wallet';
+
+import type { InvocationArgsMapper as InvocationArgsMapperProps } from '@components/invocationArgsMapper/types';
+import type { TRelayApi } from '@custom-types/chain';
+import type {
+ InjectedPolkadotAccount,
+ PolkadotSigner,
+} from 'polkadot-api/dist/reexports/pjs-signer';
+
+const Extrinsics = () => {
+ const dynamicBuilder = useDynamicBuilder();
+ const viewBuilder = useViewBuilder();
+
+ const metadata = useStoreChain?.use?.metadata?.();
+ const lookup = useStoreChain?.use?.lookup?.();
+ const chain = useStoreChain?.use?.chain?.();
+
+ const accounts = useStoreWallet?.use?.accounts?.();
+
+ const [
+ signer,
+ setSigner,
+ ] = useState(accounts.at(0)?.polkadotSigner);
+
+ // apply / reset signer on wallet connect / disconnect
+ useEffect(() => {
+ setSigner(accounts?.at(0)?.polkadotSigner);
+ }, [accounts]);
+
+ const palletsWithCalls = useMemo(() => metadata?.pallets?.filter((p) => p.calls)?.sort((a, b) => a.name.localeCompare(b.name)), [metadata]);
+ const palletSelectItems = useMemo(() => palletsWithCalls?.map((pallet) => ({
+ label: pallet.name,
+ value: pallet.name,
+ key: `extrinsic-pallet-${pallet.name}`,
+ })) || [], [palletsWithCalls]);
+
+ const [
+ palletSelected,
+ setPalledSelected,
+ ] = useState(palletsWithCalls?.[0]);
+
+ const [
+ queries,
+ setQueries,
+ ] = useState<{ pallet: string; storage: string; id: string; args: unknown }[]>([]);
+ const [
+ callArgs,
+ setCallArgs,
+ ] = useState();
+
+ const [
+ calls,
+ setCalls,
+ ] = useState[]>([]);
+ const [
+ callSelected,
+ setCallSelected,
+ ] = useState(calls.at(0));
+
+ useEffect(() => {
+ setQueries([]);
+ setCallArgs({});
+ setPalledSelected(undefined);
+ }, [chain.id]);
+
+ useEffect(() => {
+ if (palletsWithCalls && lookup) {
+ setPalledSelected(palletsWithCalls[0]);
+ const palletCalls = lookup(palletsWithCalls[0].calls!) as EnumVar;
+ const calls = Object.entries(palletCalls?.value || {})
+ .sort((a, b) => a[0].localeCompare(b[0]))
+ .map(([
+ name,
+ invocationVar,
+ ]) => ({
+ name,
+ invocationVar,
+ }));
+
+ setCalls(calls);
+ setCallSelected(calls.at(0));
+ }
+ }, [
+ palletsWithCalls,
+ lookup,
+ ]);
+
+ const callSelectItems = useMemo(() => calls?.map((call) => ({
+ label: call.name,
+ value: call.name,
+ key: `extrinsic-call-${call.name}`,
+ })) || [], [calls]);
+
+ const [
+ encodedCall,
+ setEncodedCall,
+ ] = useState(Binary.fromHex('0x'));
+
+ const decodedCall = useMemo(() => {
+ if (viewBuilder && encodedCall) {
+ try {
+ return viewBuilder.callDecoder(encodedCall.asHex());
+ } catch {
+ return undefined;
+ }
+ }
+ return undefined;
+ }, [
+ encodedCall,
+ viewBuilder,
+ ]);
+
+ useEffect(() => {
+ if (dynamicBuilder && palletSelected?.name && callSelected?.name) {
+ try {
+ const callCodec = dynamicBuilder.buildCall(palletSelected.name, callSelected.name);
+ const locationBytes = new Uint8Array(callCodec.location);
+ const encodedCall = Binary.fromBytes(
+ mergeUint8(
+ locationBytes,
+ callCodec.codec.enc(callArgs),
+ ),
+ );
+
+ setEncodedCall(encodedCall);
+ } catch (err) {
+ setEncodedCall(Binary.fromHex('0x'));
+ console.log(err);
+ }
+ }
+
+ }, [
+ palletSelected,
+ callSelected,
+ callArgs,
+ dynamicBuilder,
+ ]);
+
+ const submitTx = useCallback(async () => {
+ if (palletSelected?.name && callSelected?.name) {
+ setQueries((queries) => {
+ const newQueries = [...queries];
+ newQueries.unshift({
+ pallet: palletSelected.name,
+ storage: callSelected.name,
+ id: crypto.randomUUID(),
+ args: callArgs,
+ });
+ return newQueries;
+ });
+ }
+ }, [
+ callArgs,
+ palletSelected,
+ callSelected,
+ ]);
+
+ const handlePalletSelect = useCallback((palletSelectedName: string) => {
+ if (palletsWithCalls && lookup) {
+ const palletSelected = palletsWithCalls.find((pallet) => pallet.name === palletSelectedName);
+
+ if (palletSelected) {
+ setPalledSelected(palletSelected);
+
+ const palletCalls = lookup(palletSelected.calls!) as EnumVar;
+ const calls = Object.entries(palletCalls?.value || {})
+ .sort((a, b) => a[0].localeCompare(b[0]))
+ .map(([
+ name,
+ invocationVar,
+ ]) => ({
+ name,
+ invocationVar,
+ }));
+
+ setCalls(calls);
+ setCallSelected(calls.at(0));
+ setCallArgs({});
+ }
+ }
+ }, [
+ palletsWithCalls,
+ lookup,
+ ]);
+
+ const handleCallSelect = useCallback((callSelectedName: string) => {
+ const selectedCall = calls.find((call) => call.name === callSelectedName);
+
+ setCallSelected(selectedCall);
+ setCallArgs({});
+ }, [calls]);
+
+ const handleUnsubscribe = useCallback((id: string) => {
+ setQueries((queries) => queries.filter((query) => query.id !== id));
+ }, []);
+
+ const handleAccountSelect = useCallback((accountSelected: unknown) => {
+ setSigner((accountSelected as InjectedPolkadotAccount).polkadotSigner);
+ }, []);
+
+ if (!palletSelected) {
+ return ;
+ }
+
+ return (
+
+
+
+
+ {
+ (callSelectItems.length > 0) && (
+
+ )
+ }
+
+
+ Signer
+
+
+ {
+ (palletSelected && callSelected) && (
+
+
+
+ )
+ }
+
+ Sign and Submit
+ {' '}
+ {palletSelected?.name}
+ /
+ {callSelected?.name}
+
+ {
+ (encodedCall && decodedCall) && (
+
+ Encoded Call:
+ {' '}
+
+ {' '}
+ {encodedCall.asHex()}
+
+ )
+ }
+
+
+ {
+ signer && queries.map((query) => (
+
+ ))
+ }
+
+
+ );
+};
+
+export default Extrinsics;
+
+const Query = ({
+ querie,
+ onUnsubscribe,
+ signer,
+}: {
+ querie: { pallet: string; storage: string; id: string; args: unknown };
+ onUnsubscribe: (id: string) => void;
+ signer: PolkadotSigner;
+}) => {
+ const api = useStoreChain?.use?.api?.() as TRelayApi;
+ const [
+ result,
+ setResult,
+ ] = useState();
+ const [
+ isLoading,
+ setIsLoading,
+ ] = useState(true);
+
+ useEffect(() => {
+ const catchError = (err: Error) => {
+ setIsLoading(false);
+ setResult(err?.message || 'Unexpected Error');
+ };
+
+ if (api) {
+ try {
+ // @TODO: fix types
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const res = (api.tx as any)[querie.pallet][querie.storage](querie.args);
+
+ res.signAndSubmit(signer).then((res: unknown) => {
+ setResult(res);
+ setIsLoading(false);
+ })
+ .catch(catchError);
+
+ } catch (error) {
+ catchError(error as Error);
+ }
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [
+ querie,
+ api,
+ ]);
+
+ const handleUnsubscribe = useCallback(() => {
+ onUnsubscribe(querie.id);
+ }, [
+ querie,
+ onUnsubscribe,
+ ]);
+
+ return (
+
+ );
+};
diff --git a/src/views/notFound/index.tsx b/src/views/notFound/index.tsx
index 8a8d0618..7e374029 100644
--- a/src/views/notFound/index.tsx
+++ b/src/views/notFound/index.tsx
@@ -1,6 +1,6 @@
import { Suspense } from 'react';
-export const NotFound = () => {
+const NotFound = () => {
return (
>}>
@@ -11,3 +11,4 @@ export const NotFound = () => {
};
NotFound.displayName = 'NotFound';
+export default NotFound;
diff --git a/src/views/onboarding/components/defaultExamples/index.tsx b/src/views/onboarding/components/defaultExamples/index.tsx
index bfe70aa5..af2e14b6 100644
--- a/src/views/onboarding/components/defaultExamples/index.tsx
+++ b/src/views/onboarding/components/defaultExamples/index.tsx
@@ -3,18 +3,14 @@ import {
useState,
} from 'react';
+import { Icon } from '@components/icon';
import { PDLink } from '@components/pdLink';
import { PDScrollArea } from '@components/pdScrollArea';
import { snippets } from '@constants/snippets';
import { cn } from '@utils/helpers';
import { Search } from '@views/onboarding/components/search';
-interface DefaultExamplesProps {
- toggleVisibility: () => void;
-}
-
-export const DefaultExamples = (props: DefaultExamplesProps) => {
- const { toggleVisibility } = props;
+export const DefaultExamples = () => {
const [
filteredSnippets,
setFilteredSnippets,
@@ -29,19 +25,16 @@ export const DefaultExamples = (props: DefaultExamplesProps) => {
}, []);
return (
-
+ <>
-
+
{filteredSnippets.map((snippet, index) => (
-
{
)}
>
{snippet.name}
+
))}
-
-
+ >
);
};
diff --git a/src/views/onboarding/components/githubExamples/index.tsx b/src/views/onboarding/components/githubExamples/index.tsx
index 36164ed9..77a19375 100644
--- a/src/views/onboarding/components/githubExamples/index.tsx
+++ b/src/views/onboarding/components/githubExamples/index.tsx
@@ -1,10 +1,83 @@
-import { ExampleNotFound } from '../../../../components/exampleNotFound';
+import {
+ useCallback,
+ useEffect,
+ useState,
+} from 'react';
+
+import { ExampleNotFound } from '@components/exampleNotFound';
+import { Icon } from '@components/icon';
+import { Loader } from '@components/loader';
+import { PDLink } from '@components/pdLink';
+import { PDScrollArea } from '@components/pdScrollArea';
+import { cn } from '@utils/helpers';
+import { Search } from '@views/onboarding/components/search';
+import { useStoreCustomExamples } from 'src/stores/examples';
export const GithubExamples = () => {
+ const customExamples = useStoreCustomExamples.use.examples();
+ const { getExamples } = useStoreCustomExamples.use.actions();
+ const { isGettingExamples } = useStoreCustomExamples.use.loading();
+
+ const [
+ filteredSnippets,
+ setFilteredSnippets,
+ ] = useState(customExamples);
+
+ const handleSearch = useCallback((e: React.ChangeEvent
) => {
+ const query = e.target.value;
+ const filtered = customExamples.filter((snippet) =>
+ snippet.name.toLowerCase().includes(query.toLowerCase()),
+ );
+ setFilteredSnippets(filtered);
+ }, [customExamples]);
+
+ useEffect(() => {
+ setFilteredSnippets(customExamples);
+ }, [
+ customExamples,
+ getExamples,
+ ]);
+
+ useEffect(() => {
+ getExamples();
+ }, [getExamples]);
+
+ if (isGettingExamples) {
+ return ;
+ }
+
+ if (!customExamples.length) {
+ return ;
+ }
+
return (
-
- {/* TO DO LIST GITHUB EXAMPLES */}
-
-
+ <>
+
+
+
+ {filteredSnippets.map((snippet, index) => (
+ -
+
+ {snippet.name}
+
+
+
+ ))}
+
+
+ >
);
};
diff --git a/src/views/onboarding/index.tsx b/src/views/onboarding/index.tsx
index 1211d850..26a2b0d0 100644
--- a/src/views/onboarding/index.tsx
+++ b/src/views/onboarding/index.tsx
@@ -1,11 +1,9 @@
-import { useToggleVisibility } from '@pivanov/use-toggle-visibility';
import {
useEffect,
useRef,
useState,
} from 'react';
-import { ModalRequestExample } from '@components/modals/modalRequestExample';
import { PDLink } from '@components/pdLink';
import { Tabs } from '@components/tabs';
import { cn } from '@utils/helpers';
@@ -13,11 +11,6 @@ import { DefaultExamples } from '@views/onboarding/components/defaultExamples';
import { GithubExamples } from '@views/onboarding/components/githubExamples';
const Onboarding = () => {
- const [
- RequestExampleModal,
- toggleVisibility,
- ] = useToggleVisibility(ModalRequestExample);
-
const refContainer = useRef(null);
const [
initialTab,
@@ -29,7 +22,7 @@ const Onboarding = () => {
}, []);
return (
-
+
{
Skip
@@ -58,20 +53,13 @@ const Onboarding = () => {
tabsClassName="mb-10 p-1"
unmountOnHide={false}
>
-
-
+
+
-
);
};
diff --git a/src/views/rpcCalls/index.tsx b/src/views/rpcCalls/index.tsx
new file mode 100644
index 00000000..39b52953
--- /dev/null
+++ b/src/views/rpcCalls/index.tsx
@@ -0,0 +1,492 @@
+import React, {
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from 'react';
+
+import { CallDocs } from '@components/callDocs';
+import { Loader } from '@components/loader';
+import { QueryButton } from '@components/papiQuery/queryButton';
+import { QueryFormContainer } from '@components/papiQuery/queryFormContainer';
+import { QueryResult } from '@components/papiQuery/queryResult';
+import { QueryResultContainer } from '@components/papiQuery/queryResultContainer';
+import { QueryViewContainer } from '@components/papiQuery/queryViewContainer';
+import {
+ type IPDSelectItem,
+ PDSelect,
+} from '@components/pdSelect';
+import { InvocationRpcArgs } from '@components/rpcCalls';
+import {
+ newRpcCalls,
+ oldRpcCalls,
+} from '@constants/rpcCalls';
+import { useStoreChain } from '@stores';
+import {
+ blockHeaderCodec,
+ decodeExtrinsic,
+} from '@utils/codec';
+import { unwrapApiResult } from '@utils/papi/helpers';
+import {
+ mapRpcCallsToSelectMethodItems,
+ mapRpcCallsToSelectPalletItems,
+} from '@utils/rpc/rpc-calls';
+import { useDynamicBuilder } from 'src/hooks/useDynamicBuilder';
+
+interface ICallParam {
+ name: string;
+ value: unknown;
+}
+interface IQuery {
+ pallet: string;
+ method: string;
+ id: string;
+ args: ICallParam[];
+}
+
+const newRpcKeys = Object.keys(newRpcCalls);
+const RpcCalls = () => {
+
+ const chain = useStoreChain?.use?.chain?.();
+ const rawClient = useStoreChain?.use?.rawClient?.();
+ const rawClientSubscription = useStoreChain?.use?.rawClientSubscription?.();
+
+ const allRpcCalls = useMemo(() => ({
+ ...newRpcCalls,
+ ...oldRpcCalls,
+ }), []);
+
+ const refUncleanedSubscriptions = useRef
([]);
+
+ useEffect(() => {
+ return () => {
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ refUncleanedSubscriptions?.current?.forEach((sub) => {
+ rawClient?.request('chainHead_v1_unfollow', [sub])
+ .then(() => {
+ console.log('follow subscription cleaned', sub);
+ })
+ .catch(console.log);
+ });
+ };
+ }, [rawClient]);
+
+ useEffect(() => {
+ setQueries([]);
+ }, [chain.id]);
+
+ const [
+ methodSelectItems,
+ setMethodSelectItems,
+ ] = useState([]);
+ const [
+ methodSelected,
+ setMethodSelected,
+ ] = useState(methodSelectItems.at(0)?.value);
+
+ const palletSelectItems = useMemo(() => {
+ const newRpcItems = mapRpcCallsToSelectPalletItems(newRpcCalls);
+ const oldRpcItems = mapRpcCallsToSelectPalletItems(oldRpcCalls);
+
+ const palletItems = [
+ newRpcItems,
+ oldRpcItems,
+ ];
+
+ const methodItems = mapRpcCallsToSelectMethodItems({
+ rpcCalls: newRpcCalls,
+ ifPalletEquals: palletItems.at(0)?.at(0)?.value,
+ });
+
+ setMethodSelectItems(methodItems);
+ setMethodSelected(methodItems.at(0)?.value);
+
+ return palletItems;
+ }, []);
+
+ const [
+ palletSelected,
+ setPalletSelected,
+ ] = useState(palletSelectItems.at(0)?.at(0)?.value);
+
+ const [
+ callParams,
+ setCallParams,
+ ] = useState([]);
+ const [
+ queries,
+ setQueries,
+ ] = useState([]);
+
+ const rpcCall = useMemo(() => {
+ const call = `${palletSelected}_${methodSelected}`;
+
+ if (allRpcCalls[call]) {
+ setCallParams(allRpcCalls[call].params?.map((param) => ({ name: param.name, value: undefined })));
+ return allRpcCalls[call];
+ }
+
+ return undefined;
+ }, [
+ palletSelected,
+ methodSelected,
+ allRpcCalls,
+ ]);
+
+ const handlePalletSelect = useCallback((selectedPalletName: string) => {
+ setPalletSelected(selectedPalletName);
+
+ setMethodSelectItems(() => {
+ let methods: IPDSelectItem[] = [];
+ if (newRpcKeys.some((val) => val.split('_').at(0) === selectedPalletName)) {
+ methods = mapRpcCallsToSelectMethodItems({
+ rpcCalls: newRpcCalls,
+ ifPalletEquals: selectedPalletName,
+ });
+
+ } else {
+ methods = mapRpcCallsToSelectMethodItems({
+ rpcCalls: oldRpcCalls,
+ ifPalletEquals: selectedPalletName,
+ });
+
+ }
+
+ setMethodSelected(methods.at(0)?.value);
+ return methods;
+ });
+ }, []);
+
+ const handleMathodSelect = useCallback((selectedMethodName: string) => {
+ setMethodSelected(selectedMethodName);
+ }, []);
+
+ const handleOnChange = useCallback((index: number, args: unknown) => {
+ setCallParams((params) => {
+ if (!params || params.length === 0) {
+ return [];
+ } else {
+ const newParams = [...params];
+ newParams[index].value = args;
+ return newParams;
+ }
+ });
+ }, []);
+
+ const handleUnsubscribe = useCallback((id: string) => {
+ setQueries((queries) => queries.filter((query) => query.id !== id));
+ }, []);
+
+ const handleSubmit = useCallback(() => {
+ if (palletSelected && methodSelected) {
+ setQueries((queries) => {
+ const newQueries = [...queries];
+ newQueries.unshift({
+ pallet: palletSelected,
+ method: methodSelected,
+ args: callParams,
+ id: crypto.randomUUID(),
+ });
+ return newQueries;
+ });
+ }
+
+ }, [
+ callParams,
+ palletSelected,
+ methodSelected,
+ ]);
+
+ if (!rawClient || !rawClientSubscription) {
+ return ;
+ }
+
+ return (
+
+
+
+
+ {
+ rpcCall
+ && rpcCall?.params?.length > 0 && (
+
+ )
+ }
+
+
+ Submit Rpc Call
+
+
+
+
+
+
+ {
+ queries.map((query) => (
+
+ ))
+ }
+
+
+ );
+
+};
+
+const Query = ({
+ querie,
+ onUnsubscribe,
+ unCleanedSubscriptions,
+}: {
+ querie: IQuery;
+ onUnsubscribe: (id: string) => void;
+ unCleanedSubscriptions: React.MutableRefObject;
+}) => {
+
+ const rawClient = useStoreChain?.use?.rawClient?.();
+ const rawClientSubscription = useStoreChain?.use?.rawClientSubscription?.();
+ const dynamicBuilder = useDynamicBuilder();
+
+ const [
+ result,
+ setResult,
+ ] = useState();
+ const [
+ isLoading,
+ setIsLoading,
+ ] = useState(true);
+
+ const refFollowSubscription = useRef('');
+ const call = `${querie.pallet}_${querie.method}`;
+
+ useEffect(() => {
+
+ const catchError = (err: Error) => {
+ setIsLoading(false);
+ setResult(err?.message || 'Unexpected Error');
+ };
+
+ const runRawQuery = (onSucess?: (res: unknown) => void) => {
+ const args = querie?.args ? querie.args.map((arg) => arg.value).filter((arg) => arg !== undefined) : [];
+
+ rawClient?.request(call, args)
+ .then((...res) => {
+ setResult(res.at(0));
+ setIsLoading(false);
+ onSucess?.(res);
+ })
+ .catch(catchError);
+ };
+ if (rawClient) {
+ try {
+ switch (call) {
+ // CHAIN HEAD
+ case 'chainHead_v1_follow':
+ runRawQuery((res) => {
+ unCleanedSubscriptions.current.push(res as string);
+ refFollowSubscription.current = res as string;
+ });
+ break;
+ case 'chainHead_v1_unfollow':
+ runRawQuery(() => {
+ unCleanedSubscriptions.current = unCleanedSubscriptions.current.filter((sub) => sub !== querie.args.at(0)?.value as string);
+ });
+ break;
+ case 'chainHead_v1_header':
+ rawClientSubscription?.header(querie?.args?.at(1)?.value as string)
+ .then((res) => {
+ setResult({
+ raw: res,
+ decoded: blockHeaderCodec.dec(res),
+ });
+ setIsLoading(false);
+ })
+ .catch(catchError);
+ break;
+ case 'chainHead_v1_body':
+ rawClientSubscription?.body(querie?.args?.at(1)?.value as string)
+ .then((res) => {
+ setResult({
+ raw: res,
+ decoded: res.map((extrinsic) => decodeExtrinsic(extrinsic)),
+ });
+ setIsLoading(false);
+ })
+ .catch(catchError);
+ break;
+ case 'chainHead_v1_storage':
+ rawClientSubscription?.storage(
+ querie.args.at(1)?.value as string,
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ querie.args.at(2)?.value as any,
+ querie.args.at(3)?.value as string,
+ null,
+ )
+ .then((res) => {
+ setResult(res);
+ setIsLoading(false);
+ })
+ .catch(catchError);
+ break;
+ case 'chainHead_v1_call':
+ rawClientSubscription?.call(
+ querie.args.at(1)?.value as string,
+ querie.args.at(2)?.value as string,
+ querie.args.at(3)?.value as string,
+ )
+ .then((res) => {
+ if (dynamicBuilder) {
+ const args = (querie.args.at(2)?.value as string)?.split('_');
+
+ const decodedRes = dynamicBuilder.buildRuntimeCall(
+ args[0], args.slice(1).join('_'),
+ ).value
+ .dec(res);
+
+ setResult({
+ raw: res,
+ decoded: unwrapApiResult(decodedRes),
+ });
+ } else {
+ setResult(res);
+ }
+
+ setIsLoading(false);
+ })
+ .catch(catchError);
+ break;
+ case 'chainHead_v1_unpin':
+ rawClientSubscription
+ ?.unpin((querie?.args?.at(1)?.value as string[]))
+ .then((res) => {
+ setResult(res);
+ setIsLoading(false);
+ })
+ .catch(catchError);
+ break;
+ // OLD RPCS
+ case 'system_dryRun':
+ case 'grandpa_proveFinality':
+ case 'chain_getHeader':
+ case 'chain_getBlock':
+ case 'chain_getBlockHash':
+ case 'state_getPairs':
+ case 'state_getKeysPaged':
+ case 'state_getStorage':
+ case 'state_getStorageHash':
+ case 'state_getStorageSize':
+ case 'state_getMetadata':
+ case 'state_getRuntimeVersion':
+ case 'state_queryStorage':
+ case 'state_getReadProof':
+ case 'childstate_getStorage':
+ case 'childstate_getStorageHash':
+ case 'childstate_getStorageSize':
+ case 'payment_queryInfo':
+ rawClient?.request(call, querie.args.map((arg) => (arg.value || arg.value === 0) ? arg.value : null))
+ .then((res) => {
+ setResult(res);
+ setIsLoading(false);
+ })
+ .catch(catchError);
+ break;
+
+ case 'childstate_getKeys':
+ rawClient?.request(
+ call,
+ [
+ querie.args.at(0)?.value,
+ querie.args.at(1)?.value || '',
+ querie.args.at(2)?.value || null,
+ ],
+ )
+ .then((res) => {
+ setResult(res);
+ setIsLoading(false);
+ })
+ .catch(catchError);
+ break;
+
+ case 'author_removeExtrinsic':
+ rawClient?.request(call, querie.args.at(0)?.value as string[])
+ .then((res) => {
+ setResult(res);
+ setIsLoading(false);
+ })
+ .catch(catchError);
+ break;
+
+ default:
+ runRawQuery();
+ break;
+ }
+ } catch (error) {
+ catchError(error as Error);
+ }
+ }
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [
+ querie,
+ rawClient,
+ ]);
+
+ const handleUnsubscribe = useCallback(() => {
+ onUnsubscribe(querie.id);
+
+ if (call === 'chainHead_v1_follow' && refFollowSubscription.current) {
+ rawClient?.request('chainHead_v1_unfollow', [refFollowSubscription.current])
+ .then(() => {
+ console.log('follow subscription cleaned', refFollowSubscription.current);
+ })
+ .catch(console.log);
+ }
+ }, [
+ querie,
+ onUnsubscribe,
+ rawClient,
+ call,
+ ]);
+
+ return (
+
+ );
+};
+
+export default RpcCalls;
diff --git a/src/views/runtimeCalls/index.tsx b/src/views/runtimeCalls/index.tsx
new file mode 100644
index 00000000..87b730dc
--- /dev/null
+++ b/src/views/runtimeCalls/index.tsx
@@ -0,0 +1,288 @@
+import { Binary } from 'polkadot-api';
+import {
+ useCallback,
+ useEffect,
+ useMemo,
+ useState,
+} from 'react';
+
+import { CallDocs } from '@components/callDocs';
+import { Loader } from '@components/loader';
+import { QueryButton } from '@components/papiQuery/queryButton';
+import { QueryFormContainer } from '@components/papiQuery/queryFormContainer';
+import { QueryResult } from '@components/papiQuery/queryResult';
+import { QueryResultContainer } from '@components/papiQuery/queryResultContainer';
+import { QueryViewContainer } from '@components/papiQuery/queryViewContainer';
+import { PDSelect } from '@components/pdSelect';
+import { InvocationRuntimeArgs } from '@components/runtimeCalls';
+import { useStoreChain } from '@stores';
+import { useDynamicBuilder } from 'src/hooks/useDynamicBuilder';
+
+import type { TRelayApi } from '@custom-types/chain';
+
+const RuntimeCalls = () => {
+ const dynamicBuilder = useDynamicBuilder();
+
+ const metadata = useStoreChain?.use?.metadata?.();
+ const chain = useStoreChain?.use?.chain?.();
+
+ const apis = useMemo(() => metadata?.apis?.sort((a, b) => a.name.localeCompare(b.name)), [metadata]);
+ const apiItems = useMemo(() => apis?.map((api) => ({
+ label: api.name,
+ value: api.name,
+ key: `api-select-${api.name}`,
+ })), [apis]);
+
+ const [
+ apiSelected,
+ setApiSelected,
+ ] = useState(apis?.at(0));
+
+ const apiMethods = useMemo(() => apiSelected?.methods?.sort((a, b) => a.name.localeCompare(b.name)) || [], [apiSelected]);
+ const methodItems = useMemo(() => apiMethods?.map((item) => ({
+ label: item.name,
+ value: item.name,
+ key: `method-select-${item.name}`,
+ })), [apiMethods]);
+
+ const [
+ methodSelected,
+ setMethodSelected,
+ ] = useState(apiSelected?.methods.at(0));
+ const [
+ encodedCall,
+ setEncodedCall,
+ ] = useState(Binary.fromHex('0x'));
+ const [
+ runtimeMethodArgs,
+ setRuntimeMethodArgs,
+ ] = useState(undefined);
+ const [
+ queries,
+ setQueries,
+ ] = useState<{ pallet: string; storage: string; id: string; args: unknown }[]>([]);
+
+ useEffect(() => {
+ setQueries([]);
+ setApiSelected(undefined);
+ setRuntimeMethodArgs(undefined);
+ }, [chain.id]);
+
+ useEffect(() => {
+ if (apis) {
+ const defaultApiSelected = apis[0];
+ setApiSelected(defaultApiSelected);
+ setMethodSelected(defaultApiSelected?.methods?.sort((a, b) => a.name.localeCompare(b.name)).at?.(0));
+ }
+ }, [apis]);
+
+ const handlePalletSelect = useCallback((palletSelectedName: string) => {
+ if (apis) {
+ const selectedMethod = apis.find((api) => api.name === palletSelectedName);
+
+ if (selectedMethod) {
+ setApiSelected(selectedMethod);
+ setMethodSelected(selectedMethod.methods?.sort((a, b) => a.name.localeCompare(b.name)).at(0));
+ setRuntimeMethodArgs(undefined);
+ }
+ }
+ }, [apis]);
+
+ const handleCallSelect = useCallback((callSelectedName: string) => {
+ if (apiSelected) {
+ const selectedStorage = apiSelected.methods.find((item) => item.name === callSelectedName);
+ setMethodSelected(selectedStorage);
+ setRuntimeMethodArgs(undefined);
+ }
+ }, [apiSelected]);
+
+ const handleQuerySubmit = useCallback(() => {
+ if (apiSelected?.name && methodSelected?.name) {
+ setQueries((queries) => {
+ const newQueries = [...queries];
+ newQueries.unshift({
+ pallet: apiSelected.name,
+ storage: methodSelected.name,
+ id: crypto.randomUUID(),
+ args: runtimeMethodArgs,
+ });
+ return newQueries;
+ });
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [
+ apiSelected,
+ methodSelected,
+ runtimeMethodArgs,
+ ]);
+
+ useEffect(() => {
+ if (dynamicBuilder && apiSelected?.name && methodSelected?.name) {
+ try {
+ const callCodec = dynamicBuilder?.buildRuntimeCall(
+ apiSelected.name,
+ methodSelected.name,
+ )
+ .args
+ .enc(Object.values(runtimeMethodArgs || {}));
+
+ const _encodedCall = Binary.fromBytes(callCodec);
+ setEncodedCall(_encodedCall);
+
+ } catch (err) {
+ setEncodedCall(Binary.fromHex('0x'));
+ console.log(err);
+ }
+ }
+
+ }, [
+ apiSelected,
+ methodSelected,
+ runtimeMethodArgs,
+ dynamicBuilder,
+ ]);
+
+ const handleUnsubscribe = useCallback((id: string) => {
+ setQueries((queries) => queries.filter((query) => query.id !== id));
+ }, []);
+
+ if (!apiSelected) {
+ return ;
+ }
+
+ return (
+
+
+
+
+
+ {
+ methodItems && (
+
+ )
+ }
+
+
+ {
+ methodSelected && (
+
+ )
+ }
+
+ d) || []} />
+
+
+ Call
+ {' '}
+ {apiSelected?.name}
+ /
+ {methodSelected?.name}
+
+
+ {
+ encodedCall && (
+
+ Encoded Call:
+ {' '}
+
+ {' '}
+ {encodedCall.asHex()}
+
+ )
+ }
+
+
+
+ {
+ queries.map((query) => (
+
+ ))
+ }
+
+
+ );
+};
+
+export default RuntimeCalls;
+
+const Query = ({
+ querie,
+ onUnsubscribe,
+}: {
+ querie: { pallet: string; storage: string; id: string; args: unknown };
+ onUnsubscribe: (id: string) => void;
+}) => {
+ const api = useStoreChain?.use?.api?.() as TRelayApi;
+ const [
+ result,
+ setResult,
+ ] = useState();
+ const [
+ isLoading,
+ setIsLoading,
+ ] = useState(true);
+
+ useEffect(() => {
+ const catchError = (err: Error) => {
+ setIsLoading(false);
+ setResult(err?.message || 'Unexpected Error');
+ };
+
+ if (api) {
+ try {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (api.apis as any)[querie.pallet][querie.storage](...Object.values(querie.args as object))
+ .then((res: unknown) => {
+ setResult(res);
+ setIsLoading(false);
+ })
+ .catch(catchError);
+
+ } catch (error) {
+ catchError(error as Error);
+ }
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [
+ querie,
+ api,
+ ]);
+
+ const handleUnsubscribe = useCallback(() => {
+ onUnsubscribe(querie.id);
+ }, [
+ querie,
+ onUnsubscribe,
+ ]);
+
+ return (
+
+ );
+};
diff --git a/tailwind.config.js b/tailwind.config.js
index 5e87bd3d..4b196c49 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -1,6 +1,6 @@
/** @type {import('tailwindcss').Config} */
-const defaultTheme = require('tailwindcss/defaultTheme')
+import defaultTheme from 'tailwindcss/defaultTheme';
export default {
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],