diff --git a/packages/apps-routing/src/index.ts b/packages/apps-routing/src/index.ts index a7ec36b84f..2c15d79168 100644 --- a/packages/apps-routing/src/index.ts +++ b/packages/apps-routing/src/index.ts @@ -24,6 +24,7 @@ import nfts from './nfts'; import parachains from './parachains'; import poll from './poll'; import rpc from './rpc'; +import runtime from './runtime'; import settings from './settings'; import signing from './signing'; import society from './society'; @@ -63,6 +64,7 @@ export default function create (t: TFunction): Routes { storage(t), extrinsics(t), rpc(t), + runtime(t), signing(t), sudo(t), files(t), diff --git a/packages/apps-routing/src/runtime.ts b/packages/apps-routing/src/runtime.ts new file mode 100644 index 0000000000..15203eb073 --- /dev/null +++ b/packages/apps-routing/src/runtime.ts @@ -0,0 +1,20 @@ +// Copyright 2017-2022 @polkadot/apps-routing authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { TFunction } from 'i18next'; +import type { Route } from './types'; + +import Component from '@polkadot/app-runtime'; + +export default function create (t: TFunction): Route { + return { + Component, + display: { + needsApi: [] + }, + group: 'developer', + icon: 'arrows-to-circle', + name: 'runtime', + text: t('nav.runtime', 'Runtime calls', { ns: 'apps-routing' }) + }; +} diff --git a/packages/page-rpc/README.md b/packages/page-rpc/README.md index 204a833808..0c1a76e9fa 100644 --- a/packages/page-rpc/README.md +++ b/packages/page-rpc/README.md @@ -1 +1 @@ -# @polkadot/app-toolbox +# @polkadot/app-rpc diff --git a/packages/page-rpc/src/Rpc/Results.tsx b/packages/page-rpc/src/Rpc/Results.tsx index 28656acc01..e28e28b319 100644 --- a/packages/page-rpc/src/Rpc/Results.tsx +++ b/packages/page-rpc/src/Rpc/Results.tsx @@ -24,11 +24,11 @@ function Results ({ queue = [] }: Props): React.ReactElement | null { return (
- {filtered.map(({ error, id, result, rpc: { method, section } }): React.ReactNode => ( + {filtered.map(({ error, id, result, rpc: { method, section, type } }): React.ReactNode => ( { ]); return ( -
+
| null { + if (!results.length) { + return null; + } + + return ( +
+ {results.map(({ def: { method, section, type }, error, id, result }): React.ReactNode => ( + {valueToText('', result)} + } + /> + ))} +
+ ); +} + +export default React.memo(Results); diff --git a/packages/page-runtime/src/Runtime/Selection.tsx b/packages/page-runtime/src/Runtime/Selection.tsx new file mode 100644 index 0000000000..db9f484a19 --- /dev/null +++ b/packages/page-runtime/src/Runtime/Selection.tsx @@ -0,0 +1,100 @@ +// Copyright 2017-2022 @polkadot/app-runtime authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { ParamDef, RawParam } from '@polkadot/react-params/types'; +import type { DefinitionCallNamed } from '@polkadot/types/types'; + +import React, { useCallback, useMemo, useState } from 'react'; + +import { Button, InputCalls } from '@polkadot/react-components'; +import Params from '@polkadot/react-params'; +import { getTypeDef } from '@polkadot/types/create'; + +import { useTranslation } from '../translate'; + +interface Props { + onSubmit: (call: DefinitionCallNamed, values: RawParam[]) => void; +} + +interface State { + isValid: boolean; + method: DefinitionCallNamed | null; + values: RawParam[]; +} + +function Selection ({ onSubmit }: Props): React.ReactElement { + const { t } = useTranslation(); + const [{ isValid, method, values }, setState] = useState({ + isValid: false, + method: null, + values: [] + }); + + const params = useMemo( + () => method + ? method.params.map(({ name, type }): ParamDef => ({ + name, + type: getTypeDef(type) + })) + : [], + [method] + ); + + const _nextState = useCallback( + (newState: Partial) => setState((prevState: State): State => { + const { method = prevState.method, values = prevState.values } = newState; + const isValid = values.reduce((isValid, value) => isValid && value.isValid === true, !!method && method.params.length <= values.length); + + return { + isValid, + method, + values + }; + }), + [] + ); + + const _onChangeMethod = useCallback( + (method: DefinitionCallNamed) => _nextState({ method, values: [] }), + [_nextState] + ); + + const _onChangeValues = useCallback( + (values: RawParam[]) => _nextState({ values }), + [_nextState] + ); + + const _onSubmit = useCallback( + (): void => { + method && onSubmit(method, values); + }, + [onSubmit, method, values] + ); + + return ( +
+ ('The actual runtime function to make a call to.')} + label={t('call the selected endpoint')} + onChange={_onChangeMethod} + /> + {method && ( + + )} + +
+ ); +} + +export default React.memo(Selection); diff --git a/packages/page-runtime/src/Runtime/index.tsx b/packages/page-runtime/src/Runtime/index.tsx new file mode 100644 index 0000000000..846e22795f --- /dev/null +++ b/packages/page-runtime/src/Runtime/index.tsx @@ -0,0 +1,46 @@ +// Copyright 2017-2022 @polkadot/app-runtime authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { RawParam } from '@polkadot/react-params/types'; +import type { DefinitionCallNamed } from '@polkadot/types/types'; +import type { Result } from './types'; + +import React, { useCallback, useState } from 'react'; + +import { useApi } from '@polkadot/react-hooks'; + +import Results from './Results'; +import Selection from './Selection'; + +let id = 0; + +function RuntimeApp (): React.ReactElement { + const { api } = useApi(); + const [results, setResults] = useState([]); + + const addResult = useCallback( + (result: Result) => setResults((prev) => [result].concat(...prev)), + [] + ); + + const onSubmit = useCallback( + (def: DefinitionCallNamed, values: RawParam[]): void => { + api.call[def.section][def.method](...values.map(({ value }) => value)) + .then((result) => addResult({ def, id: ++id, result })) + .catch((e): void => { + addResult({ def, error: e as Error, id: ++id }); + console.error(e); + }); + }, + [api, addResult] + ); + + return ( + <> + + + + ); +} + +export default React.memo(RuntimeApp); diff --git a/packages/page-runtime/src/Runtime/types.ts b/packages/page-runtime/src/Runtime/types.ts new file mode 100644 index 0000000000..8a41c7fce2 --- /dev/null +++ b/packages/page-runtime/src/Runtime/types.ts @@ -0,0 +1,11 @@ +// Copyright 2017-2022 @polkadot/app-runtime authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { Codec, DefinitionCallNamed } from '@polkadot/types/types'; + +export interface Result { + id: number; + error?: Error; + def: DefinitionCallNamed; + result?: Codec; +} diff --git a/packages/page-runtime/src/index.tsx b/packages/page-runtime/src/index.tsx new file mode 100644 index 0000000000..3177127f38 --- /dev/null +++ b/packages/page-runtime/src/index.tsx @@ -0,0 +1,38 @@ +// Copyright 2017-2022 @polkadot/app-runtime authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { AppProps as Props } from '@polkadot/react-components/types'; + +import React, { useRef } from 'react'; +import { Route, Switch } from 'react-router'; + +import { Tabs } from '@polkadot/react-components'; + +import Runtime from './Runtime'; +import { useTranslation } from './translate'; + +function RuntimeApp ({ basePath }: Props): React.ReactElement { + const { t } = useTranslation(); + + const itemsRef = useRef([ + { + isRoot: true, + name: 'runtime', + text: t('Calls') + } + ]); + + return ( +
+ + + + +
+ ); +} + +export default React.memo(RuntimeApp); diff --git a/packages/page-runtime/src/translate.ts b/packages/page-runtime/src/translate.ts new file mode 100644 index 0000000000..a63f5afa59 --- /dev/null +++ b/packages/page-runtime/src/translate.ts @@ -0,0 +1,10 @@ +// Copyright 2017-2022 @polkadot/app-runtime authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { UseTranslationResponse } from 'react-i18next'; + +import { useTranslation as useTranslationBase } from 'react-i18next'; + +export function useTranslation (): UseTranslationResponse<'app-runtime', undefined> { + return useTranslationBase('app-runtime'); +} diff --git a/packages/page-runtime/tsconfig.build.json b/packages/page-runtime/tsconfig.build.json new file mode 100644 index 0000000000..da24d9f225 --- /dev/null +++ b/packages/page-runtime/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "baseUrl": "..", + "outDir": "./build", + "rootDir": "./src" + }, + "references": [] +} diff --git a/packages/react-components/src/InputCalls/SelectMethod.tsx b/packages/react-components/src/InputCalls/SelectMethod.tsx new file mode 100644 index 0000000000..e4cd67049a --- /dev/null +++ b/packages/react-components/src/InputCalls/SelectMethod.tsx @@ -0,0 +1,43 @@ +// Copyright 2017-2022 @polkadot/react-components authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { DefinitionCallNamed } from '@polkadot/types/types'; +import type { DropdownOption } from '../util/types'; + +import React, { useCallback } from 'react'; + +import Dropdown from '../Dropdown'; + +interface Props { + className?: string; + defs: Record>; + isError?: boolean; + onChange: (value: DefinitionCallNamed) => void; + options: DropdownOption[]; + value: DefinitionCallNamed; +} + +function SelectMethod ({ className = '', defs, isError, onChange, options, value }: Props): React.ReactElement | null { + const _transform = useCallback( + (method: string) => defs[value.section][method], + [defs, value] + ); + + if (!options.length) { + return null; + } + + return ( + + ); +} + +export default React.memo(SelectMethod); diff --git a/packages/react-components/src/InputCalls/SelectSection.tsx b/packages/react-components/src/InputCalls/SelectSection.tsx new file mode 100644 index 0000000000..928de09f43 --- /dev/null +++ b/packages/react-components/src/InputCalls/SelectSection.tsx @@ -0,0 +1,34 @@ +// Copyright 2017-2022 @polkadot/react-components authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { DefinitionCallNamed } from '@polkadot/types/types'; +import type { DropdownOptions } from '../util/types'; + +import React from 'react'; + +import Dropdown from '../Dropdown'; + +interface Props { + className?: string; + defaultValue?: string; + isError?: boolean; + onChange: (value: string) => void; + options: DropdownOptions; + value: DefinitionCallNamed; +} + +function SelectSection ({ className = '', defaultValue, isError, onChange, options, value }: Props): React.ReactElement { + return ( + + ); +} + +export default React.memo(SelectSection); diff --git a/packages/react-components/src/InputCalls/index.tsx b/packages/react-components/src/InputCalls/index.tsx new file mode 100644 index 0000000000..b67f050d80 --- /dev/null +++ b/packages/react-components/src/InputCalls/index.tsx @@ -0,0 +1,86 @@ +// Copyright 2017-2022 @polkadot/react-components authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +// TODO: We have a lot shared between this and InputExtrinsic & InputStorage + +import type { DefinitionCallNamed } from '@polkadot/types/types'; +import type { DropdownOptions } from '../util/types'; + +import React, { useCallback, useEffect, useState } from 'react'; + +import LinkedWrapper from '../InputExtrinsic/LinkedWrapper'; +import methodOptions from './options/method'; +import sectionOptions from './options/section'; +import SelectMethod from './SelectMethod'; +import SelectSection from './SelectSection'; +import useRuntime from './useRuntime'; + +interface Props { + className?: string; + help?: React.ReactNode; + label: React.ReactNode; + onChange?: (value: DefinitionCallNamed) => void; + withLabel?: boolean; +} + +function InputCalls ({ className, help, label, onChange, withLabel }: Props): React.ReactElement | null { + const [defs, defaultValue] = useRuntime(); + const [optionsSection] = useState(() => sectionOptions(defs)); + const [optionsMethod, setOptionsMethod] = useState(() => methodOptions(defs, defaultValue && defaultValue.section)); + const [value, setValue] = useState(() => defaultValue); + + useEffect((): void => { + value && onChange && onChange(value); + }, [onChange, value]); + + const _onMethodChange = useCallback( + (newValue: DefinitionCallNamed): void => { + if (value !== newValue) { + // set via callback since the method is a function itself + setValue(() => newValue); + } + }, + [value] + ); + + const _onSectionChange = useCallback( + (newSection: string): void => { + if (value && newSection !== value.section) { + const optionsMethod = methodOptions(defs, newSection); + + setOptionsMethod(optionsMethod); + _onMethodChange(defs[newSection][optionsMethod[0].value]); + } + }, + [_onMethodChange, defs, value] + ); + + if (!value) { + return null; + } + + return ( + + + + + ); +} + +export default React.memo(InputCalls); diff --git a/packages/react-components/src/InputCalls/options/method.tsx b/packages/react-components/src/InputCalls/options/method.tsx new file mode 100644 index 0000000000..f6d2d04251 --- /dev/null +++ b/packages/react-components/src/InputCalls/options/method.tsx @@ -0,0 +1,47 @@ +// Copyright 2017-2022 @polkadot/react-components authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { DefinitionCallNamed } from '@polkadot/types/types'; +import type { DropdownOption, DropdownOptions } from '../../util/types'; + +import React from 'react'; + +export default function createOptions (runtime: Record>, sectionName: string | null): DropdownOptions { + if (!sectionName) { + return []; + } + + const section = runtime[sectionName]; + + if (!section || Object.keys(runtime[sectionName]).length === 0) { + return []; + } + + return Object + .keys(runtime[sectionName]) + .sort() + .map((m) => section[m]) + .map(({ description, method, params }): DropdownOption => { + const inputs = params.map(({ name }) => name).join(', '); + + return { + className: 'ui--DropdownLinked-Item', + key: `${sectionName}_${method}`, + text: [ +
+ {method}({inputs}) +
, +
+ {description || method} +
+ ], + value: method + }; + }); +} diff --git a/packages/react-components/src/InputCalls/options/section.ts b/packages/react-components/src/InputCalls/options/section.ts new file mode 100644 index 0000000000..c434096cb0 --- /dev/null +++ b/packages/react-components/src/InputCalls/options/section.ts @@ -0,0 +1,16 @@ +// Copyright 2017-2022 @polkadot/react-components authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { DefinitionCallNamed } from '@polkadot/types/types'; +import type { DropdownOptions } from '../../util/types'; + +export default function createOptions (runtime: Record>): DropdownOptions { + return Object + .keys(runtime) + .sort() + .filter((s) => Object.keys(runtime[s]).length !== 0) + .map((value): { text: string; value: string } => ({ + text: value, + value + })); +} diff --git a/packages/react-components/src/InputCalls/useRuntime.ts b/packages/react-components/src/InputCalls/useRuntime.ts new file mode 100644 index 0000000000..ebd59ab8f1 --- /dev/null +++ b/packages/react-components/src/InputCalls/useRuntime.ts @@ -0,0 +1,55 @@ +// Copyright 2017-2022 @polkadot/react-components authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { ApiPromise } from '@polkadot/api'; +import type { DefinitionCallNamed } from '@polkadot/types/types'; + +import { useMemo } from 'react'; + +import { createNamedHook, useApi } from '@polkadot/react-hooks'; + +function getEntries (obj: Record): [string, T][] { + return Object + .entries(obj) + .sort(([a], [b]) => a.localeCompare(b)); +} + +function getAllCalls (api: ApiPromise): [Record>, DefinitionCallNamed | null] { + const result: Record> = {}; + let defValue: DefinitionCallNamed | null = null; + const sections = getEntries(api.call); + + for (let i = 0; i < sections.length; i++) { + const [section, methodsObj] = sections[i]; + const methods = getEntries(methodsObj); + + for (let j = 0; j < methods.length; j++) { + const [method, { meta }] = methods[j] as unknown as [string, { meta: DefinitionCallNamed }]; + + if (meta) { + if (!result[section]) { + result[section] = {}; + + if (defValue === null) { + defValue = meta; + } + } + + result[section][method] = meta; + } + } + } + + return [result, defValue]; +} + +function useRuntimeImpl (): [Record>, DefinitionCallNamed | null] { + const { api } = useApi(); + + return useMemo( + () => getAllCalls(api), + [api] + ); +} + +export default createNamedHook('useRuntime', useRuntimeImpl); diff --git a/packages/react-components/src/InputRpc/index.tsx b/packages/react-components/src/InputRpc/index.tsx index 6140ccef4e..e5340efbaa 100644 --- a/packages/react-components/src/InputRpc/index.tsx +++ b/packages/react-components/src/InputRpc/index.tsx @@ -21,7 +21,6 @@ interface Props { className?: string; defaultValue: DefinitionRpcExt; help?: React.ReactNode; - isError?: boolean; label: React.ReactNode; onChange?: (value: DefinitionRpcExt) => void; withLabel?: boolean; diff --git a/packages/react-components/src/index.tsx b/packages/react-components/src/index.tsx index 38c4bfd0f4..89b4ac1ccd 100644 --- a/packages/react-components/src/index.tsx +++ b/packages/react-components/src/index.tsx @@ -54,6 +54,7 @@ export { default as InputAddress } from './InputAddress'; export { default as InputAddressMulti } from './InputAddressMulti'; export { default as InputAddressSimple } from './InputAddressSimple'; export { default as InputBalance } from './InputBalance'; +export { default as InputCalls } from './InputCalls'; export { default as InputConsts } from './InputConsts'; export { default as InputExtrinsic } from './InputExtrinsic'; export { default as InputFile } from './InputFile'; diff --git a/tsconfig.base.json b/tsconfig.base.json index eb3e04e1a3..5bd3f741a4 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -52,6 +52,8 @@ "@polkadot/app-poll/*": ["page-poll/src/*"], "@polkadot/app-rpc": ["page-rpc/src"], "@polkadot/app-rpc/*": ["page-rpc/src/*"], + "@polkadot/app-runtime": ["page-runtime/src"], + "@polkadot/app-runtime/*": ["page-runtime/src/*"], "@polkadot/app-settings": ["page-settings/src"], "@polkadot/app-settings/*": ["page-settings/src/*"], "@polkadot/app-signing": ["page-signing/src"], diff --git a/yarn.lock b/yarn.lock index c28d357c8d..39e6a0e092 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2930,6 +2930,15 @@ __metadata: languageName: unknown linkType: soft +"@polkadot/app-runtime@workspace:packages/page-runtime": + version: 0.0.0-use.local + resolution: "@polkadot/app-runtime@workspace:packages/page-runtime" + dependencies: + "@babel/runtime": ^7.18.6 + "@polkadot/react-components": ^0.116.2-73-x + languageName: unknown + linkType: soft + "@polkadot/app-settings@workspace:packages/page-settings": version: 0.0.0-use.local resolution: "@polkadot/app-settings@workspace:packages/page-settings"