├── .gitignore ├── .papi ├── descriptors │ ├── .gitignore │ └── package.json ├── metadata │ └── polkadot_people.scale └── polkadot-api.json ├── LICENSE ├── README.md ├── components.json ├── eslint.config.js ├── index.html ├── package.json ├── patches └── react18-json-view.patch ├── pnpm-lock.yaml ├── public ├── .well-known │ └── walletconnect.txt ├── papi_logo-dark.svg ├── papi_logo-light.svg └── vite.svg ├── src ├── App.tsx ├── ThemeProvider.tsx ├── chopsticks │ └── chopsticks.ts ├── codec-components │ ├── BinaryViewCodec │ │ ├── CEnum.tsx │ │ ├── CStruct.tsx │ │ ├── ListComponents.tsx │ │ ├── codec-components.tsx │ │ ├── components.ts │ │ └── index.ts │ ├── EditCodec │ │ ├── CAccountId.tsx │ │ ├── CBytes.tsx │ │ ├── CEnum.tsx │ │ ├── CEthAccount.tsx │ │ ├── CSequence.tsx │ │ ├── CStr.tsx │ │ ├── CStruct.tsx │ │ ├── CTuple.tsx │ │ ├── EditNumber.tsx │ │ ├── Tree │ │ │ ├── CEnum.tsx │ │ │ ├── CStruct.tsx │ │ │ ├── ListComponents.tsx │ │ │ ├── codec-components.tsx │ │ │ ├── components.ts │ │ │ └── index.ts │ │ ├── codec-components.tsx │ │ ├── components.ts │ │ └── index.tsx │ ├── LookupTypeEdit │ │ ├── BinaryDisplay.tsx │ │ ├── FocusPath.tsx │ │ ├── LookupTypeEdit.tsx │ │ ├── binaryDisplay.css │ │ └── index.ts │ ├── ViewCodec │ │ ├── CAccountId.tsx │ │ ├── CBytes.tsx │ │ ├── CEnum.tsx │ │ ├── CStruct.tsx │ │ ├── CopyBinary.tsx │ │ ├── ListComponents.tsx │ │ ├── TitleContext.tsx │ │ ├── codec-components.tsx │ │ ├── components.ts │ │ ├── index.tsx │ │ └── utils.ts │ └── common │ │ ├── ListItem.tsx │ │ ├── Markers.tsx │ │ ├── SubtreeFocus.ts │ │ ├── paths.state.ts │ │ └── scroll.ts ├── components │ ├── AccountIdDisplay.tsx │ ├── ActionButton.tsx │ ├── BinaryEditButton.tsx │ ├── BinaryInput.tsx │ ├── ButtonGroup.tsx │ ├── CircularProgress.tsx │ ├── CommandPopover.tsx │ ├── Copy.tsx │ ├── DocsRenderer.tsx │ ├── EthAccountDisplay.tsx │ ├── EthIdenticon.tsx │ ├── Expand.tsx │ ├── Icons.tsx │ ├── JsonDisplay.tsx │ ├── Loading.tsx │ ├── Modal.tsx │ ├── PolkadotIdenticon.tsx │ ├── Popover.tsx │ ├── Select.tsx │ ├── TextInputField.tsx │ ├── Toggle.tsx │ ├── icons │ │ ├── binary.svg │ │ ├── chopsticks_dark.svg │ │ ├── chopsticks_light.svg │ │ ├── enum.svg │ │ ├── focus.svg │ │ ├── switch_binary.svg │ │ └── walletConnect.svg │ ├── jsonDisplay.css │ ├── modal.css │ ├── ui │ │ ├── accordion.tsx │ │ ├── badge.tsx │ │ ├── button.tsx │ │ ├── command.tsx │ │ ├── dialog.tsx │ │ ├── label.tsx │ │ ├── popover.tsx │ │ ├── radio-group.tsx │ │ ├── scroll-area.tsx │ │ ├── select.tsx │ │ ├── sheet.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ └── tooltip.tsx │ ├── useHeightObserver.ts │ ├── useRefEffect.ts │ ├── useSynchroniseInput.ts │ └── withSuspense.tsx ├── hashParams.tsx ├── index.css ├── lib │ ├── contextState.ts │ ├── externalState.ts │ ├── groupBy.ts │ └── utils.ts ├── main.tsx ├── pages │ ├── Constants.tsx │ ├── Explorer │ │ ├── BlockPopover.tsx │ │ ├── BlockTable.tsx │ │ ├── BlockTime.tsx │ │ ├── Detail │ │ │ ├── BlockBody.tsx │ │ │ ├── BlockDetail.tsx │ │ │ ├── BlockEvents.tsx │ │ │ ├── BlockInfo.tsx │ │ │ ├── BlockState.tsx │ │ │ ├── BlockStorageDiff.tsx │ │ │ ├── Extrinsic.tsx │ │ │ └── index.ts │ │ ├── EpochTime.tsx │ │ ├── EventPopover.tsx │ │ ├── Events.tsx │ │ ├── Explorer.tsx │ │ ├── FinalizingTable.tsx │ │ ├── Summary.tsx │ │ ├── block.state.ts │ │ ├── blockTime.state.ts │ │ ├── events.state.ts │ │ └── index.ts │ ├── Extrinsics │ │ ├── EditMode.tsx │ │ ├── Extrinsics.tsx │ │ ├── JsonMode.tsx │ │ ├── SubmitTx │ │ │ ├── AccountProvider.tsx │ │ │ ├── ExtensionProvider.tsx │ │ │ ├── SubmitTx.tsx │ │ │ └── SubmitTxForm.tsx │ │ ├── index.ts │ │ └── jsonView.css │ ├── Header.tsx │ ├── Metadata │ │ ├── Editor.tsx │ │ ├── Extrinsic.tsx │ │ ├── Lookup.tsx │ │ ├── Metadata.tsx │ │ ├── Pallets.tsx │ │ ├── RuntimeApis.tsx │ │ ├── V15Fields.tsx │ │ └── index.ts │ ├── Network │ │ ├── Network.tsx │ │ └── index.ts │ ├── RpcCalls │ │ ├── RpcCallResults.tsx │ │ ├── RpcCalls.tsx │ │ ├── index.ts │ │ └── rpcCalls.state.ts │ ├── RuntimeCalls │ │ ├── RuntimeCallQuery.tsx │ │ ├── RuntimeCallResults.tsx │ │ ├── RuntimeCalls.tsx │ │ ├── index.ts │ │ └── runtimeCalls.state.ts │ ├── Storage │ │ ├── Storage.tsx │ │ ├── StorageDecode.tsx │ │ ├── StorageQuery.tsx │ │ ├── StorageSet.tsx │ │ ├── StorageSubscriptions.tsx │ │ ├── index.ts │ │ └── storage.state.ts │ └── Transactions │ │ ├── Transaction.tsx │ │ ├── Transactions.tsx │ │ ├── index.ts │ │ └── transactions.state.ts ├── smoldot.ts ├── state │ ├── chains │ │ ├── chain.state.ts │ │ ├── chainspecs │ │ │ ├── acala.ts │ │ │ ├── ajuna.ts │ │ │ ├── astar.ts │ │ │ ├── hydradx.ts │ │ │ ├── invarch.ts │ │ │ ├── kusama.ts │ │ │ ├── kusama_asset_hub.ts │ │ │ ├── kusama_coretime.ts │ │ │ ├── kusama_encointer.ts │ │ │ ├── kusama_people.ts │ │ │ ├── paseo.ts │ │ │ ├── paseo_asset_hub.ts │ │ │ ├── polkadot.ts │ │ │ ├── polkadot_asset_hub.ts │ │ │ ├── polkadot_bridge_hub.ts │ │ │ ├── polkadot_collectives.ts │ │ │ ├── polkadot_coretime.ts │ │ │ ├── polkadot_people.ts │ │ │ ├── westend.ts │ │ │ ├── westend_asset_hub.ts │ │ │ ├── westend_collectives.ts │ │ │ └── westend_people.ts │ │ ├── networks │ │ │ ├── index.ts │ │ │ ├── kusama.json │ │ │ ├── paseo.json │ │ │ ├── polkadot.json │ │ │ └── westend.json │ │ ├── smoldot.ts │ │ └── websocket.ts │ ├── extension-accounts.state.ts │ ├── identity.state.ts │ └── walletconnect.state.ts ├── utils │ ├── byteArray.ts │ ├── cn.ts │ ├── default.tsx │ ├── index.ts │ ├── localStorageSubject.ts │ ├── shape.ts │ ├── short-str.ts │ └── withLogs.ts └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json ├── vercel.json ├── vite.config.ts └── whitelist.ts /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | *.tsbuildinfo 10 | 11 | node_modules 12 | dist 13 | dist-ssr 14 | *.local 15 | 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | -------------------------------------------------------------------------------- /.papi/descriptors/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | !package.json -------------------------------------------------------------------------------- /.papi/descriptors/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0-autogenerated.1071370278631255394", 3 | "name": "@polkadot-api/descriptors", 4 | "files": [ 5 | "dist" 6 | ], 7 | "exports": { 8 | ".": { 9 | "types": "./dist/index.d.ts", 10 | "module": "./dist/index.mjs", 11 | "import": "./dist/index.mjs", 12 | "require": "./dist/index.js" 13 | }, 14 | "./package.json": "./package.json" 15 | }, 16 | "main": "./dist/index.js", 17 | "module": "./dist/index.mjs", 18 | "browser": "./dist/index.mjs", 19 | "types": "./dist/index.d.ts", 20 | "sideEffects": false, 21 | "peerDependencies": { 22 | "polkadot-api": ">=1.11.2" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.papi/metadata/polkadot_people.scale: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polkadot-api/papi-console/884c1727550dd7a73b24ccb30b75a0e4da13b90b/.papi/metadata/polkadot_people.scale -------------------------------------------------------------------------------- /.papi/polkadot-api.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 0, 3 | "descriptorPath": ".papi/descriptors", 4 | "entries": { 5 | "polkadot_people": { 6 | "chain": "polkadot_people", 7 | "metadata": ".papi/metadata/polkadot_people.scale", 8 | "genesis": "0x67fa177a097bfa18f77ea95ab56e9bcdfeb0e5b8a40e46298bb93e16b6fc5008", 9 | "codeHash": "0x5ad90a21b395a16a6d720983bb9fdd096577710beea33c7882bb61a7518dbb79" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # https://dev.papi.how 2 | 3 | Docs coming soon(ish) 4 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "src/index.css", 9 | "baseColor": "neutral", 10 | "cssVariables": false, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | } 20 | } -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from "@eslint/js" 2 | import globals from "globals" 3 | import reactHooks from "eslint-plugin-react-hooks" 4 | import reactRefresh from "eslint-plugin-react-refresh" 5 | import tseslint from "typescript-eslint" 6 | 7 | export default tseslint.config( 8 | { ignores: ["dist"] }, 9 | { 10 | extends: [js.configs.recommended, ...tseslint.configs.recommended], 11 | files: ["**/*.{ts,tsx}"], 12 | languageOptions: { 13 | ecmaVersion: 2020, 14 | globals: globals.browser, 15 | }, 16 | plugins: { 17 | "react-hooks": reactHooks, 18 | "react-refresh": reactRefresh, 19 | }, 20 | rules: { 21 | ...reactHooks.configs.recommended.rules, 22 | "react-refresh/only-export-components": [ 23 | "warn", 24 | { allowConstantExport: true }, 25 | ], 26 | "@typescript-eslint/no-explicit-any": "off", 27 | "@typescript-eslint/no-unnecessary-type-constraint": "off", 28 | "@typescript-eslint/no-empty-object-type": "off", 29 | "@typescript-eslint/no-unused-vars": [ 30 | "warn", 31 | { 32 | args: "all", 33 | argsIgnorePattern: "^_", 34 | caughtErrors: "all", 35 | caughtErrorsIgnorePattern: "^_", 36 | destructuredArrayIgnorePattern: "^_", 37 | varsIgnorePattern: "^_", 38 | ignoreRestSiblings: true, 39 | }, 40 | ], 41 | "react-refresh/only-export-components": "off", 42 | "prefer-const": "warn", 43 | }, 44 | }, 45 | ) 46 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | PAPI Console 9 | 10 | 11 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "papi-console", 3 | "private": true, 4 | "version": "0.0.4", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc -b && vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview", 11 | "postinstall": "papi --whitelist whitelist.ts" 12 | }, 13 | "prettier": { 14 | "semi": false 15 | }, 16 | "dependencies": { 17 | "@acala-network/chopsticks-core": "^1.1.0", 18 | "@noble/hashes": "^1.8.0", 19 | "@polkadot-api/descriptors": "file:.papi/descriptors", 20 | "@polkadot-api/json-rpc-provider-proxy": "^0.2.4", 21 | "@polkadot-api/metadata-builders": "^0.12.2", 22 | "@polkadot-api/observable-client": "^0.12.0", 23 | "@polkadot-api/react-builder": "0.2.9", 24 | "@polkadot-api/substrate-bindings": "^0.14.0", 25 | "@polkadot-api/substrate-client": "^0.4.1", 26 | "@polkadot-api/tx-utils": "^0.1.0", 27 | "@polkadot-api/utils": "^0.2.0", 28 | "@radix-ui/react-accordion": "^1.2.11", 29 | "@radix-ui/react-dialog": "^1.1.14", 30 | "@radix-ui/react-label": "^2.1.7", 31 | "@radix-ui/react-popover": "^1.1.14", 32 | "@radix-ui/react-radio-group": "^1.3.7", 33 | "@radix-ui/react-scroll-area": "^1.2.9", 34 | "@radix-ui/react-select": "^2.2.5", 35 | "@radix-ui/react-slot": "^1.2.3", 36 | "@radix-ui/react-tabs": "^1.1.12", 37 | "@radix-ui/react-toggle": "^1.1.9", 38 | "@radix-ui/react-toggle-group": "^1.1.10", 39 | "@radix-ui/react-tooltip": "^1.2.7", 40 | "@react-rxjs/core": "^0.10.8", 41 | "@react-rxjs/utils": "^0.9.7", 42 | "@walletconnect/modal": "^2.7.0", 43 | "@walletconnect/universal-provider": "^2.21.3", 44 | "@walletconnect/utils": "^2.21.3", 45 | "blo": "^2.0.0", 46 | "buffer": "^6.0.3", 47 | "class-variance-authority": "^0.7.1", 48 | "clsx": "^2.1.1", 49 | "cmdk": "1.1.1", 50 | "idb-keyval": "^6.2.2", 51 | "lucide-react": "^0.511.0", 52 | "polkadot-api": "^1.14.0", 53 | "react": "^19.1.0", 54 | "react-dom": "^19.1.0", 55 | "react-portal": "^4.3.0", 56 | "react-router-dom": "^7.6.2", 57 | "react-svg": "^16.3.0", 58 | "react-virtuoso": "^4.13.0", 59 | "react18-json-view": "^0.2.9", 60 | "rxjs": "^7.8.2", 61 | "save-as": "^0.1.8", 62 | "tailwind-merge": "^3.3.1", 63 | "tailwindcss-animate": "^1.0.7", 64 | "uuid": "^11.1.0" 65 | }, 66 | "devDependencies": { 67 | "@eslint/js": "^9.29.0", 68 | "@tailwindcss/vite": "^4.1.10", 69 | "@types/node": "^24.0.3", 70 | "@types/react": "^19.1.8", 71 | "@types/react-dom": "^19.1.6", 72 | "@types/react-portal": "^4.0.7", 73 | "@types/uuid": "^10.0.0", 74 | "@vitejs/plugin-react": "^4.5.2", 75 | "@walletconnect/types": "^2.21.3", 76 | "eslint": "^9.29.0", 77 | "eslint-plugin-react-hooks": "5.2.0", 78 | "eslint-plugin-react-refresh": "^0.4.20", 79 | "globals": "^16.2.0", 80 | "prettier": "^3.5.3", 81 | "tailwindcss": "^4.1.10", 82 | "typescript": "^5.8.3", 83 | "typescript-eslint": "^8.34.1", 84 | "vite": "^6.3.5" 85 | }, 86 | "packageManager": "pnpm@10.11.1+sha512.e519b9f7639869dc8d5c3c5dfef73b3f091094b0a006d7317353c72b124e80e1afd429732e28705ad6bfa1ee879c1fce46c128ccebd3192101f43dd67c667912", 87 | "pnpm": { 88 | "patchedDependencies": { 89 | "react18-json-view": "patches/react18-json-view.patch" 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /public/.well-known/walletconnect.txt: -------------------------------------------------------------------------------- 1 | 06b06ca3-8453-4275-82db-2fe72b0b3dbd=22cb06009b0c03a852f533b7733c6884df12547ad3c53782083a7a5d7a538e0a -------------------------------------------------------------------------------- /public/papi_logo-dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/papi_logo-light.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Navigate, Route, Routes } from "react-router-dom" 2 | import { Constants } from "./pages/Constants" 3 | import { Explorer } from "./pages/Explorer" 4 | import { Extrinsics } from "./pages/Extrinsics" 5 | import { Header } from "./pages/Header" 6 | import { Metadata } from "./pages/Metadata" 7 | import { RpcCalls } from "./pages/RpcCalls" 8 | import { RuntimeCalls } from "./pages/RuntimeCalls" 9 | import { Storage } from "./pages/Storage" 10 | import { Transactions } from "./pages/Transactions" 11 | 12 | export default function App() { 13 | return ( 14 |
15 |
16 |
17 |
18 | 19 | } /> 20 | } /> 21 | } /> 22 | } /> 23 | } /> 24 | } /> 25 | } /> 26 | } /> 27 | 28 |
29 |
30 | 31 |
32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /src/ThemeProvider.tsx: -------------------------------------------------------------------------------- 1 | import { state, useStateObservable } from "@react-rxjs/core" 2 | import { 3 | createContext, 4 | FC, 5 | PropsWithChildren, 6 | useContext, 7 | useLayoutEffect, 8 | } from "react" 9 | import { fromEvent, map } from "rxjs" 10 | 11 | const getCurrentMedia = (): "light" | "dark" => 12 | window.matchMedia 13 | ? window.matchMedia("(prefers-color-scheme: dark)").matches 14 | ? "dark" 15 | : "light" 16 | : "dark" 17 | 18 | const defaultTheme = getCurrentMedia() 19 | 20 | const ThemeContext = createContext<"light" | "dark">(defaultTheme) 21 | 22 | export const useTheme = () => useContext(ThemeContext) 23 | 24 | const theme$ = state( 25 | fromEvent( 26 | window.matchMedia("(prefers-color-scheme: dark)"), 27 | "change", 28 | ).pipe(map((evt) => (evt.matches ? "dark" : "light"))), 29 | defaultTheme, 30 | ) 31 | 32 | export const ThemeProvider: FC = ({ children }) => { 33 | const theme = useStateObservable(theme$) 34 | 35 | useLayoutEffect(() => { 36 | if (theme === "dark") { 37 | document.body.classList.add("dark") 38 | } else { 39 | document.body.classList.remove("dark") 40 | } 41 | }, [theme]) 42 | 43 | return {children} 44 | } 45 | -------------------------------------------------------------------------------- /src/codec-components/BinaryViewCodec/CEnum.tsx: -------------------------------------------------------------------------------- 1 | import { EditEnum, NOTIN } from "@polkadot-api/react-builder" 2 | import { u8 } from "@polkadot-api/substrate-bindings" 3 | import { useStateObservable } from "@react-rxjs/core" 4 | import { isActive$ } from "../common/paths.state" 5 | import { useSubtreeFocus } from "../common/SubtreeFocus" 6 | import { 7 | headerHighlight, 8 | highlight, 9 | MissingData, 10 | toConcatHex, 11 | } from "./codec-components" 12 | 13 | export const CEnum: EditEnum = ({ value, inner, path, shape }) => { 14 | const isActive = useStateObservable(isActive$(path.join("."))) 15 | const focus = useSubtreeFocus() 16 | const sub = focus.getNextPath(path) 17 | if (sub) { 18 | return inner 19 | } 20 | 21 | if (value === NOTIN) return 22 | 23 | const entry = shape.value[value.type] 24 | if (!entry) { 25 | console.log(shape.value, value.type) 26 | throw new Error("Type not found??") 27 | } 28 | return ( 29 | 30 | 31 | {toConcatHex(u8.enc(entry.idx))} 32 | 33 | {inner} 34 | 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /src/codec-components/BinaryViewCodec/CStruct.tsx: -------------------------------------------------------------------------------- 1 | import { EditStruct, NOTIN } from "@polkadot-api/react-builder" 2 | import { useStateObservable } from "@react-rxjs/core" 3 | import React from "react" 4 | import { isActive$ } from "../common/paths.state" 5 | import { useSubtreeFocus } from "../common/SubtreeFocus" 6 | import { highlight, MissingData } from "./codec-components" 7 | 8 | export const CStruct: EditStruct = ({ innerComponents, value, path }) => { 9 | const isActive = useStateObservable(isActive$(path.join("."))) 10 | const focus = useSubtreeFocus() 11 | const sub = focus.getNextPath(path) 12 | if (sub) { 13 | const field = Object.entries(innerComponents).find(([key]) => key === sub) 14 | return field?.[1] 15 | } 16 | 17 | if (value === NOTIN) { 18 | return 19 | } 20 | 21 | return ( 22 | 23 | {Object.entries(innerComponents).map(([key, jsx]) => ( 24 | {jsx} 25 | ))} 26 | 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /src/codec-components/BinaryViewCodec/ListComponents.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | EditArray, 3 | EditComplexCodecComponentProps, 4 | EditSequence, 5 | EditTuple, 6 | NOTIN, 7 | } from "@polkadot-api/react-builder" 8 | import { compact } from "@polkadot-api/substrate-bindings" 9 | import { useStateObservable } from "@react-rxjs/core" 10 | import React, { ReactNode } from "react" 11 | import { isActive$ } from "../common/paths.state" 12 | import { useSubtreeFocus } from "../common/SubtreeFocus" 13 | import { 14 | headerHighlight, 15 | highlight, 16 | MissingData, 17 | toConcatHex, 18 | } from "./codec-components" 19 | 20 | export const CSequence: EditSequence = (props) => 21 | 22 | export const CArray: EditArray = (props) => ( 23 | 24 | ) 25 | 26 | export const CTuple: EditTuple = (props) => ( 27 | 28 | ) 29 | 30 | export const ListDisplay: React.FC< 31 | EditComplexCodecComponentProps & { 32 | innerComponents: ReactNode[] 33 | fixedLength?: boolean 34 | } 35 | > = (props) => { 36 | const { innerComponents, fixedLength, value, path } = props 37 | const isActive = useStateObservable(isActive$(path.join("."))) 38 | const focus = useSubtreeFocus() 39 | const sub = focus.getNextPath(path) 40 | if (sub) { 41 | return innerComponents[Number(sub)] 42 | } 43 | 44 | if (value === NOTIN) { 45 | return 46 | } 47 | 48 | return ( 49 | 50 | {fixedLength ? null : ( 51 | 52 | {toConcatHex(compact.enc(value.length))} 53 | 54 | )} 55 | {innerComponents.map((jsx, idx) => ( 56 | {jsx} 57 | ))} 58 | 59 | ) 60 | } 61 | -------------------------------------------------------------------------------- /src/codec-components/BinaryViewCodec/codec-components.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | EditAccountId, 3 | EditBigNumber, 4 | EditBool, 5 | EditBytes, 6 | EditEthAccount, 7 | EditNumber, 8 | EditOption, 9 | EditPrimitiveComponentProps, 10 | EditResult, 11 | EditStr, 12 | EditVoid, 13 | NOTIN, 14 | } from "@polkadot-api/react-builder" 15 | import { u8 } from "@polkadot-api/substrate-bindings" 16 | import { toHex } from "@polkadot-api/utils" 17 | import { useStateObservable } from "@react-rxjs/core" 18 | import { FC } from "react" 19 | import { isActive$ } from "../common/paths.state" 20 | import { useSubtreeFocus } from "../common/SubtreeFocus" 21 | import { Circle } from "lucide-react" 22 | 23 | export const CVoid: EditVoid = () => null 24 | 25 | const CPrimitive: FC> = ({ 26 | encodedValue, 27 | path, 28 | }) => { 29 | const isActive = useStateObservable(isActive$(path.join("."))) 30 | return encodedValue ? ( 31 | 32 | 33 | {toConcatHex(encodedValue)} 34 | 35 | 36 | ) : ( 37 | 38 | ) 39 | } 40 | export const CBool: EditBool = CPrimitive 41 | export const CStr: EditStr = CPrimitive 42 | export const CEthAccount: EditEthAccount = CPrimitive 43 | export const CBigNumber: EditBigNumber = CPrimitive 44 | export const CNumber: EditNumber = CPrimitive 45 | export const CAccountId: EditAccountId = CPrimitive 46 | export const CBytes: EditBytes = CPrimitive 47 | 48 | export const COption: EditOption = ({ path, value, inner }) => { 49 | const isActive = useStateObservable(isActive$(path.join("."))) 50 | const focus = useSubtreeFocus() 51 | const sub = focus.getNextPath(path) 52 | if (sub) { 53 | return inner 54 | } 55 | if (value === NOTIN) return 56 | 57 | return ( 58 | 59 | 60 | {toConcatHex(u8.enc(value ? 1 : 0))} 61 | 62 | {inner} 63 | 64 | ) 65 | } 66 | 67 | export const CResult: EditResult = ({ value, inner, path }) => { 68 | const isActive = useStateObservable(isActive$(path.join("."))) 69 | const focus = useSubtreeFocus() 70 | const sub = focus.getNextPath(path) 71 | if (sub) return inner 72 | 73 | if (value === NOTIN) return 74 | 75 | return ( 76 | 77 | 78 | {toConcatHex(u8.enc(value.success ? 0 : 1))} 79 | 80 | {inner} 81 | 82 | ) 83 | } 84 | 85 | export const MissingData = () => ( 86 | 87 | [ 88 | 93 | ] 94 | 95 | ) 96 | export const toConcatHex = (value: Uint8Array) => toHex(value).slice(2) 97 | export const highlight = (isActive: boolean) => 98 | isActive ? "text-polkadot-400 mx-1" : "" 99 | export const headerHighlight = (isActive: boolean) => 100 | isActive ? "text-polkadot-500" : "" 101 | -------------------------------------------------------------------------------- /src/codec-components/BinaryViewCodec/components.ts: -------------------------------------------------------------------------------- 1 | export * from "./CEnum" 2 | export * from "./CStruct" 3 | export * from "./codec-components" 4 | export * from "./ListComponents" 5 | -------------------------------------------------------------------------------- /src/codec-components/BinaryViewCodec/index.ts: -------------------------------------------------------------------------------- 1 | import { getCodecComponent } from "@polkadot-api/react-builder" 2 | 3 | import * as treeComponents from "./components" 4 | 5 | export const BinaryViewCodec = getCodecComponent(treeComponents) 6 | -------------------------------------------------------------------------------- /src/codec-components/EditCodec/CBytes.tsx: -------------------------------------------------------------------------------- 1 | import { EditBytes } from "@polkadot-api/react-builder" 2 | import { BinaryInput } from "@/components/BinaryInput" 3 | 4 | export const CBytes: EditBytes = ({ value, onValueChanged, len }) => ( 5 | 6 | ) 7 | -------------------------------------------------------------------------------- /src/codec-components/EditCodec/CEnum.tsx: -------------------------------------------------------------------------------- 1 | import { SearchableSelect } from "@/components/Select" 2 | import { isEnumComplex, isEnumVoid } from "@/utils/shape" 3 | import { EditEnum, NOTIN } from "@polkadot-api/react-builder" 4 | import { Marker } from "../common/Markers" 5 | import { useSubtreeFocus } from "../common/SubtreeFocus" 6 | 7 | export const CEnum: EditEnum = ({ 8 | type, 9 | value, 10 | tags, 11 | inner, 12 | shape, 13 | onValueChanged, 14 | path, 15 | }) => { 16 | const focus = useSubtreeFocus() 17 | const sub = focus.getNextPath(path) 18 | if (sub) { 19 | return inner 20 | } 21 | 22 | const options = tags.map((t) => t.tag).sort() 23 | const getOptionType = (option: string) => { 24 | const innerType = shape.value[option] 25 | if (innerType.type !== "lookupEntry") return "" 26 | switch (innerType.value.type) { 27 | case "primitive": 28 | return ` (${innerType.value.value})` 29 | case "compact": 30 | return ` (${innerType.value.size})` 31 | case "AccountId20": 32 | case "AccountId32": 33 | return ` (${innerType.value.type})` 34 | } 35 | return "" 36 | } 37 | 38 | const isComplexShape = type !== "blank" && isEnumComplex(shape, value.type) 39 | return ( 40 |
41 | {value === NOTIN ? null : } 42 |
43 | { 45 | if (selected && options.includes(selected)) { 46 | const value = isEnumVoid(shape, selected) ? null : NOTIN 47 | onValueChanged({ type: selected, value }) 48 | } 49 | }} 50 | value={value === NOTIN ? "" : value.type} 51 | options={options.map((option) => ({ 52 | text: option + getOptionType(option), 53 | value: option, 54 | }))} 55 | /> 56 | {!isComplexShape && inner} 57 |
58 | {isComplexShape &&
{inner}
} 59 |
60 | ) 61 | } 62 | -------------------------------------------------------------------------------- /src/codec-components/EditCodec/CSequence.tsx: -------------------------------------------------------------------------------- 1 | import { EditSequence, NOTIN } from "@polkadot-api/react-builder" 2 | import { CirclePlus } from "lucide-react" 3 | import { twMerge as clsx } from "tailwind-merge" 4 | import { ListItem } from "../common/ListItem" 5 | import { useSubtreeFocus } from "../common/SubtreeFocus" 6 | 7 | export const CSequence: EditSequence = ({ 8 | innerComponents, 9 | value, 10 | onValueChanged, 11 | path, 12 | }) => { 13 | const focus = useSubtreeFocus() 14 | const sub = focus.getNextPath(path) 15 | if (sub) { 16 | return innerComponents[Number(sub)] 17 | } 18 | 19 | const addItem = () => { 20 | const curr = value !== NOTIN ? value.slice() : [] 21 | 22 | curr.push(NOTIN) 23 | onValueChanged([...curr]) 24 | } 25 | 26 | const removeItem = (idx: number) => { 27 | const curr = value !== NOTIN ? value.slice() : [] 28 | curr.splice(idx, 1) 29 | onValueChanged([...curr]) 30 | } 31 | 32 | return ( 33 |
34 |
    35 | {innerComponents.map((item, idx) => ( 36 | { 40 | removeItem(idx) 41 | }} 42 | path={[...path, String(idx)]} 43 | > 44 | {item} 45 | 46 | ))} 47 |
48 | 57 |
58 | ) 59 | } 60 | -------------------------------------------------------------------------------- /src/codec-components/EditCodec/CStr.tsx: -------------------------------------------------------------------------------- 1 | import { withDefault } from "@/utils/default" 2 | import { EditStr } from "@polkadot-api/react-builder" 3 | 4 | // TODO 5 | export const CStr: EditStr = ({ value, onValueChanged }) => { 6 | return ( 7 |
8 | { 12 | onValueChanged(evt.target.value) 13 | }} 14 | /> 15 |
16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /src/codec-components/EditCodec/CStruct.tsx: -------------------------------------------------------------------------------- 1 | import { ExpandBtn } from "@/components/Expand" 2 | import { getFinalType } from "@/utils/shape" 3 | import { EditStruct } from "@polkadot-api/react-builder" 4 | import { useStateObservable } from "@react-rxjs/core" 5 | import React, { useContext } from "react" 6 | import { twMerge as clsx, twMerge } from "tailwind-merge" 7 | import { Marker } from "../common/Markers" 8 | import { useSubtreeFocus } from "../common/SubtreeFocus" 9 | import { 10 | isActive$, 11 | isCollapsed$, 12 | PathsRoot, 13 | setHovered, 14 | toggleCollapsed, 15 | } from "../common/paths.state" 16 | 17 | const StructItem: React.FC<{ 18 | name: string 19 | children: React.ReactNode 20 | path: string[] 21 | type?: string 22 | }> = ({ name, children, path, type }) => { 23 | const pathsRootId = useContext(PathsRoot) 24 | const pathStr = path.join(".") 25 | const isActive = useStateObservable(isActive$(pathStr)) 26 | const isExpanded = !useStateObservable(isCollapsed$(pathStr)) 27 | 28 | return ( 29 |
  • setHovered(pathsRootId, { id: pathStr, hover: true })} 35 | onMouseLeave={() => 36 | setHovered(pathsRootId, { id: pathStr, hover: false }) 37 | } 38 | > 39 | 40 | toggleCollapsed(pathsRootId, pathStr)} 42 | className="cursor-pointer flex select-none items-center py-1 gap-1" 43 | > 44 | 45 | {name} 46 | {type && ( 47 |
    48 | ({type}) 49 |
    50 | )} 51 |
    52 |
    58 | {children} 59 |
    60 |
  • 61 | ) 62 | } 63 | 64 | export const CStruct: EditStruct = ({ innerComponents, path, shape }) => { 65 | const focus = useSubtreeFocus() 66 | const sub = focus.getNextPath(path) 67 | if (sub) { 68 | const field = Object.entries(innerComponents).find(([key]) => key === sub) 69 | return field?.[1] 70 | } 71 | 72 | return ( 73 |
    74 |
      75 | {Object.entries(innerComponents).map(([name, jsx]) => ( 76 | 82 | {jsx} 83 | 84 | ))} 85 |
    86 |
    87 | ) 88 | } 89 | -------------------------------------------------------------------------------- /src/codec-components/EditCodec/CTuple.tsx: -------------------------------------------------------------------------------- 1 | import { useSubtreeFocus } from "../common/SubtreeFocus" 2 | import { EditTuple } from "@polkadot-api/react-builder" 3 | 4 | export const CTuple: EditTuple = ({ innerComponents, path }) => { 5 | const focus = useSubtreeFocus() 6 | const sub = focus.getNextPath(path) 7 | if (sub) { 8 | return innerComponents[Number(sub)] 9 | } 10 | 11 | return ( 12 | <> 13 | Tuple 14 |
      15 | {innerComponents.map((jsx, idx) => ( 16 |
    • {jsx}
    • 17 | ))} 18 |
    19 | 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /src/codec-components/EditCodec/EditNumber.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/rules-of-hooks */ 2 | import { FC } from "react" 3 | import { 4 | EditBigNumber, 5 | EditBigNumberInterface, 6 | EditNumber, 7 | EditNumberInterface, 8 | NOTIN, 9 | } from "@polkadot-api/react-builder" 10 | import { TextInputField } from "./codec-components" 11 | import { useSynchronizeInput } from "@/components/useSynchroniseInput" 12 | 13 | const NumericEdit = 14 | ( 15 | parseValue: (value: string) => T | NOTIN, 16 | ): FC => 17 | ({ value, onValueChanged, numType }) => { 18 | const getValidation = (value: string) => { 19 | const sign = numType.substring(0, 1) 20 | const size = Number(numType.substring(1)) 21 | 22 | const maxValue = sign === "i" ? 2 ** size / 2 - 1 : 2 ** size - 1 23 | const minValue = sign === "i" ? -(2 ** size / 2) : 0 24 | 25 | const parsed = parseValue(value) 26 | if (parsed === NOTIN) return null 27 | if (parsed > maxValue) return "Too high. Max is " + maxValue 28 | if (parsed < minValue) return "Too low. Min is " + minValue 29 | return null 30 | } 31 | 32 | const [inputValue, setInputValue] = useSynchronizeInput( 33 | value as any, 34 | onValueChanged as any, 35 | (v) => (getValidation(v) ? NOTIN : parseValue(v)), 36 | ) 37 | 38 | return ( 39 | 46 | ) 47 | } 48 | 49 | export const CNumber: EditNumber = NumericEdit((value: string) => { 50 | const parsed = Number(value) 51 | return value.trim() === "" || !Number.isSafeInteger(parsed) ? NOTIN : parsed 52 | }) 53 | export const CBigNumber: EditBigNumber = NumericEdit((value: string) => { 54 | try { 55 | return value.trim() === "" ? NOTIN : BigInt(value) 56 | } catch (_) { 57 | return NOTIN 58 | } 59 | }) 60 | -------------------------------------------------------------------------------- /src/codec-components/EditCodec/Tree/CStruct.tsx: -------------------------------------------------------------------------------- 1 | import { lookupToType, TypeIcon, TypeIcons } from "@/components/Icons" 2 | import { isComplex } from "@/utils/shape" 3 | import { EditStruct } from "@polkadot-api/react-builder" 4 | import { useStateObservable } from "@react-rxjs/core" 5 | import { FC, PropsWithChildren, useState } from "react" 6 | import { twMerge } from "tailwind-merge" 7 | import { isCollapsedRoot$ } from "../../common/paths.state" 8 | import { scrollToMarker } from "../../common/scroll" 9 | import { useSubtreeFocus } from "../../common/SubtreeFocus" 10 | import { 11 | BinaryStatus, 12 | ChildrenProviders, 13 | ItemTitle, 14 | useReportBinaryStatus, 15 | } from "./codec-components" 16 | 17 | const StructField: FC< 18 | PropsWithChildren<{ 19 | label: string 20 | path: string 21 | icon: (typeof TypeIcons)[TypeIcon] 22 | onZoom?: () => void 23 | onNavigate?: () => void 24 | }> 25 | > = ({ label, children, onZoom, onNavigate, path, icon: Icon }) => { 26 | const [titleElement, setTitleElement] = useState(null) 27 | const [binaryStatus, setBinaryStatus] = useState() 28 | const isCollapsed = useStateObservable(isCollapsedRoot$(path)) 29 | 30 | return ( 31 |
    32 | 41 | {label} 42 | 43 |
    49 | 53 | {children} 54 | 55 |
    56 |
    57 | ) 58 | } 59 | 60 | export const CStruct: EditStruct = ({ 61 | innerComponents, 62 | shape, 63 | path, 64 | type, 65 | encodedValue, 66 | onValueChanged, 67 | decode, 68 | }) => { 69 | const focus = useSubtreeFocus() 70 | useReportBinaryStatus(type, encodedValue, onValueChanged, decode) 71 | 72 | const sub = focus.getNextPath(path) 73 | if (sub) { 74 | const field = Object.entries(innerComponents).find(([key]) => key === sub) 75 | return field?.[1] 76 | } 77 | 78 | return ( 79 |
    80 | {Object.entries(innerComponents).map(([key, jsx]) => { 81 | const innerShape = shape.value[key] 82 | const isComplexShape = isComplex(innerShape.type) 83 | const innerPath = [...path, key] 84 | 85 | return ( 86 | focus.setFocus(innerPath) 92 | : undefined 93 | } 94 | onNavigate={() => scrollToMarker(innerPath)} 95 | path={innerPath.join(".")} 96 | icon={ 97 | isComplexShape 98 | ? TypeIcons.object 99 | : TypeIcons[lookupToType[innerShape.type]] 100 | } 101 | > 102 | {jsx} 103 | 104 | ) 105 | })} 106 |
    107 | ) 108 | } 109 | -------------------------------------------------------------------------------- /src/codec-components/EditCodec/Tree/components.ts: -------------------------------------------------------------------------------- 1 | export * from "./CEnum" 2 | export * from "./CStruct" 3 | export * from "./codec-components" 4 | export * from "./ListComponents" 5 | -------------------------------------------------------------------------------- /src/codec-components/EditCodec/Tree/index.ts: -------------------------------------------------------------------------------- 1 | import { getCodecComponent } from "@polkadot-api/react-builder" 2 | 3 | import * as treeComponents from "./components" 4 | 5 | export const TreeCodec = getCodecComponent(treeComponents) 6 | -------------------------------------------------------------------------------- /src/codec-components/EditCodec/components.ts: -------------------------------------------------------------------------------- 1 | export * from "./CAccountId" 2 | export * from "./CEnum" 3 | export * from "./EditNumber" 4 | export * from "./CBytes" 5 | export * from "./CStruct" 6 | export * from "./CSequence" 7 | export * from "./CTuple" 8 | export * from "./CStr" 9 | export * from "./codec-components" 10 | export * from "./CEthAccount" 11 | -------------------------------------------------------------------------------- /src/codec-components/EditCodec/index.tsx: -------------------------------------------------------------------------------- 1 | import { getCodecComponent } from "@polkadot-api/react-builder" 2 | import * as editComponents from "./components" 3 | 4 | export const EditCodec = getCodecComponent(editComponents) 5 | -------------------------------------------------------------------------------- /src/codec-components/LookupTypeEdit/BinaryDisplay.tsx: -------------------------------------------------------------------------------- 1 | import { BinaryViewCodec } from "@/codec-components/BinaryViewCodec" 2 | import { BinaryEditButton } from "@/components/BinaryEditButton" 3 | import { CopyText } from "@/components/Copy" 4 | import { ExpandBtn } from "@/components/Expand" 5 | import { CodecComponentType, NOTIN } from "@polkadot-api/react-builder" 6 | import { Binary, HexString } from "@polkadot-api/substrate-bindings" 7 | import { toHex } from "@polkadot-api/utils" 8 | import { ComponentProps, FC, useState } from "react" 9 | import { twMerge } from "tailwind-merge" 10 | import { EditCodec } from "../EditCodec" 11 | import "./binaryDisplay.css" 12 | 13 | export const BinaryDisplay: FC< 14 | ComponentProps & { 15 | codec: { 16 | enc: (value: any | NOTIN) => Uint8Array 17 | dec: (value: Uint8Array | HexString) => any | NOTIN 18 | } 19 | className?: string 20 | } 21 | > = ({ codecType, metadata, value, onUpdate, codec, className }) => { 22 | const [wrap, setWrap] = useState(false) 23 | const encoded = (() => { 24 | if (value.type === CodecComponentType.Initial) { 25 | if (!value.value) return null 26 | return typeof value.value === "string" 27 | ? Binary.fromHex(value.value).asBytes() 28 | : value.value 29 | } 30 | if (value.value.empty || !value.value.encoded) return null 31 | return value.value.encoded 32 | })() 33 | const hex = encoded ? toHex(encoded) : null 34 | const isEmpty = 35 | (value.type === CodecComponentType.Initial && !value.value) || 36 | (value.type === CodecComponentType.Updated && value.value.empty) 37 | 38 | return ( 39 |
    40 |
    41 | 42 |
    49 | {isEmpty ? ( 50 |
    51 | Start by filling out the value, or enter a binary using the edit 52 | binary button at the end of this line. 53 |
    54 | ) : ( 55 | <> 56 | 0x 57 | 62 | 63 | )} 64 |
    65 |
    66 | setWrap((v) => !v)} 74 | /> 75 | { 78 | const encoded = codec.enc(decoded) 79 | onUpdate?.({ empty: false, decoded, encoded }) 80 | return true 81 | }} 82 | decode={codec.dec} 83 | iconProps={{ 84 | size: 24, 85 | }} 86 | /> 87 |
    88 |
    89 |
    90 | ) 91 | } 92 | -------------------------------------------------------------------------------- /src/codec-components/LookupTypeEdit/binaryDisplay.css: -------------------------------------------------------------------------------- 1 | .binary-display-codec span { 2 | transition: 0.2s margin; 3 | } 4 | -------------------------------------------------------------------------------- /src/codec-components/LookupTypeEdit/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./FocusPath" 2 | export * from "./LookupTypeEdit" 3 | export * from "./BinaryDisplay" 4 | -------------------------------------------------------------------------------- /src/codec-components/ViewCodec/CAccountId.tsx: -------------------------------------------------------------------------------- 1 | import { AccountIdDisplay } from "@/components/AccountIdDisplay" 2 | import { ViewAccountId } from "@polkadot-api/react-builder" 3 | import { useReportBinary } from "./CopyBinary" 4 | 5 | export const CAccountId: ViewAccountId = ({ value, encodedValue }) => { 6 | useReportBinary(encodedValue) 7 | 8 | return 9 | } 10 | -------------------------------------------------------------------------------- /src/codec-components/ViewCodec/CBytes.tsx: -------------------------------------------------------------------------------- 1 | import { getBytesFormat } from "@/components/BinaryInput" 2 | import { ViewBytes } from "@polkadot-api/react-builder" 3 | import { useReportBinary } from "./CopyBinary" 4 | import { SwitchBinary } from "@/components/Icons" 5 | import { useState } from "react" 6 | 7 | export const CBytes: ViewBytes = ({ value, encodedValue }) => { 8 | const [forceBinary, setForceBinary] = useState(false) 9 | 10 | useReportBinary(encodedValue) 11 | const format = getBytesFormat(value) 12 | 13 | return ( 14 |
    15 | {format.type === "text" ? ( 16 | 23 | ) : null} 24 | {forceBinary ? value.asHex() : format.value} 25 |
    26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /src/codec-components/ViewCodec/CEnum.tsx: -------------------------------------------------------------------------------- 1 | import { ViewEnum } from "@polkadot-api/react-builder" 2 | import { useStateObservable } from "@react-rxjs/core" 3 | import { useContext, useState } from "react" 4 | import { Portal } from "react-portal" 5 | import { twMerge } from "tailwind-merge" 6 | import { Marker } from "../common/Markers" 7 | import { isActive$, PathsRoot, setHovered } from "../common/paths.state" 8 | import { useSubtreeFocus } from "../common/SubtreeFocus" 9 | import { useAppendTitle } from "../EditCodec/Tree/CEnum" 10 | import { CopyBinary, useReportBinary } from "./CopyBinary" 11 | import { ChildProvider, TitleContext } from "./TitleContext" 12 | 13 | export const CEnum: ViewEnum = ({ value, inner, path, encodedValue }) => { 14 | const focus = useSubtreeFocus() 15 | const titleContainer = useContext(TitleContext) 16 | const titleElement = useAppendTitle(titleContainer, "") 17 | const [newElement, setNewElement] = useState(null) 18 | const pathStr = path.join(".") 19 | const isActive = useStateObservable(isActive$(pathStr)) 20 | const pathId = useContext(PathsRoot) 21 | useReportBinary(encodedValue) 22 | const sub = focus.getNextPath(path) 23 | if (sub) { 24 | return inner 25 | } 26 | 27 | if (titleContainer) { 28 | return ( 29 | <> 30 | {titleElement ? ( 31 | / {value.type} 32 | ) : null} 33 | {inner} 34 | 35 | ) 36 | } 37 | 38 | return ( 39 |
    setHovered(pathId, { id: pathStr, hover: true })} 42 | onMouseLeave={() => setHovered(pathId, { id: pathStr, hover: false })} 43 | > 44 | 45 |
    46 |
    47 | {value.type} 48 |
    49 | {isActive && } 50 |
    51 |
    52 | {inner} 53 |
    54 |
    55 | ) 56 | } 57 | -------------------------------------------------------------------------------- /src/codec-components/ViewCodec/CopyBinary.tsx: -------------------------------------------------------------------------------- 1 | import { CopyText } from "@/components/Copy" 2 | import { Binary } from "polkadot-api" 3 | import { 4 | createContext, 5 | FC, 6 | PropsWithChildren, 7 | useContext, 8 | useEffect, 9 | useState, 10 | } from "react" 11 | import { noop } from "rxjs" 12 | import { twMerge } from "tailwind-merge" 13 | 14 | const CopyBinaryContext = createContext<{ 15 | value: Uint8Array | null 16 | setValue: (value: Uint8Array) => void 17 | }>({ 18 | value: null, 19 | setValue: noop, 20 | }) 21 | 22 | export const CopyBinaryProvider: FC = ({ children }) => { 23 | const [value, setValue] = useState(null) 24 | 25 | return ( 26 | 32 | {children} 33 | 34 | ) 35 | } 36 | 37 | export const useReportBinary = (value: Uint8Array) => { 38 | const { setValue } = useContext(CopyBinaryContext) 39 | useEffect(() => setValue(value), [setValue, value]) 40 | } 41 | 42 | export const CopyChildBinary: FC<{ visible?: boolean }> = ({ visible }) => { 43 | const { value } = useContext(CopyBinaryContext) 44 | 45 | return ( 46 | 50 | ) 51 | } 52 | 53 | export const CopyBinary: FC<{ value: Uint8Array; visible?: boolean }> = ({ 54 | value, 55 | visible = true, 56 | }) => ( 57 | 62 | ) 63 | -------------------------------------------------------------------------------- /src/codec-components/ViewCodec/ListComponents.tsx: -------------------------------------------------------------------------------- 1 | import { ViewArray, ViewSequence, ViewTuple } from "@polkadot-api/react-builder" 2 | import { useStateObservable } from "@react-rxjs/core" 3 | import { FC, PropsWithChildren, ReactNode } from "react" 4 | import { ListItem } from "../common/ListItem" 5 | import { isActive$ } from "../common/paths.state" 6 | import { useSubtreeFocus } from "../common/SubtreeFocus" 7 | import { CopyChildBinary } from "./CopyBinary" 8 | import { ChildProvider } from "./TitleContext" 9 | import { 10 | ArrayVar, 11 | SequenceVar, 12 | TupleVar, 13 | Var, 14 | } from "@polkadot-api/metadata-builders" 15 | import { isComplexNested } from "./utils" 16 | 17 | const ListItemComponent: FC< 18 | PropsWithChildren<{ 19 | idx: number 20 | path: string[] 21 | field: Var 22 | value: unknown 23 | }> 24 | > = ({ idx, path, children, field, value }) => { 25 | const pathStr = path.join(".") 26 | const isActive = useStateObservable(isActive$(pathStr)) 27 | const isComplexShape = isComplexNested(field, value) 28 | 29 | return ( 30 | 31 | 36 | 37 | 38 | } 39 | inline={!isComplexShape} 40 | > 41 | {children} 42 | 43 | 44 | ) 45 | } 46 | 47 | const ListComponent: FC<{ 48 | innerComponents: ReactNode[] 49 | path: string[] 50 | shape: ArrayVar | TupleVar | SequenceVar 51 | value: unknown[] 52 | }> = ({ innerComponents, path, shape, value }) => { 53 | const focus = useSubtreeFocus() 54 | const sub = focus.getNextPath(path) 55 | if (sub) { 56 | return innerComponents[Number(sub)] 57 | } 58 | 59 | return ( 60 |
      61 | {innerComponents.length ? ( 62 | innerComponents.map((jsx, idx) => ( 63 | 70 | {jsx} 71 | 72 | )) 73 | ) : ( 74 | (Empty) 75 | )} 76 |
    77 | ) 78 | } 79 | 80 | export const CArray: ViewArray = (props) => 81 | export const CSequence: ViewSequence = (props) => 82 | export const CTuple: ViewTuple = (props) => 83 | -------------------------------------------------------------------------------- /src/codec-components/ViewCodec/TitleContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, FC, PropsWithChildren } from "react" 2 | import { CopyBinaryProvider } from "./CopyBinary" 3 | 4 | export const TitleContext = createContext(null) 5 | 6 | export const ChildProvider: FC< 7 | PropsWithChildren<{ titleElement: HTMLElement | null }> 8 | > = ({ titleElement, children }) => ( 9 | 10 | {children} 11 | 12 | ) 13 | -------------------------------------------------------------------------------- /src/codec-components/ViewCodec/codec-components.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ViewBigNumber, 3 | ViewBool, 4 | ViewEthAccount, 5 | ViewNumber, 6 | ViewOption, 7 | ViewResult, 8 | ViewStr, 9 | ViewVoid, 10 | } from "@polkadot-api/react-builder" 11 | 12 | export const CBool: ViewBool = ({ value }) => { 13 | return
    {value ? "Yes" : "No"}
    14 | } 15 | 16 | export const CVoid: ViewVoid = () => null 17 | 18 | export const CEthAccount: ViewEthAccount = ({ value }) => {value} 19 | 20 | export const COption: ViewOption = ({ value, inner }) => { 21 | const selected = value !== undefined 22 | return ( 23 |
    24 | {selected ? inner : None} 25 |
    26 | ) 27 | } 28 | 29 | export const CResult: ViewResult = ({ value, inner }) => { 30 | return ( 31 |
    32 |
    {value.success ? "OK" : "KO"}
    33 | {inner} 34 |
    35 | ) 36 | } 37 | 38 | export const CStr: ViewStr = ({ value }) =>
    {value}
    39 | export const CNumber: ViewNumber = ({ value }) =>
    {value}
    40 | export const CBigNumber: ViewBigNumber = ({ value }) => ( 41 |
    {String(value)}
    42 | ) 43 | -------------------------------------------------------------------------------- /src/codec-components/ViewCodec/components.ts: -------------------------------------------------------------------------------- 1 | export * from "./CAccountId" 2 | export * from "./CEnum" 3 | export * from "./CBytes" 4 | export * from "./CStruct" 5 | export * from "./ListComponents" 6 | export * from "./codec-components" 7 | -------------------------------------------------------------------------------- /src/codec-components/ViewCodec/index.tsx: -------------------------------------------------------------------------------- 1 | import { getCodecComponent } from "@polkadot-api/react-builder" 2 | import * as viewComponents from "./components" 3 | 4 | export const ViewCodec = getCodecComponent(viewComponents) 5 | -------------------------------------------------------------------------------- /src/codec-components/ViewCodec/utils.ts: -------------------------------------------------------------------------------- 1 | import { getEnumInnerVar, isComplex } from "@/utils/shape" 2 | import { Var } from "@polkadot-api/metadata-builders" 3 | 4 | export const isComplexNested = (field: Var, value: any): boolean => { 5 | if (!isComplex(field.type)) return false 6 | 7 | if (field.type === "enum") 8 | return isComplexNested(getEnumInnerVar(field, value.type), value.value) 9 | 10 | if (field.type === "option") 11 | return value == null ? false : isComplexNested(field.value, value) 12 | 13 | if (field.type === "sequence" && value.length === 0) { 14 | return false 15 | } 16 | 17 | return true 18 | } 19 | -------------------------------------------------------------------------------- /src/codec-components/common/ListItem.tsx: -------------------------------------------------------------------------------- 1 | import { ExpandBtn } from "@/components/Expand" 2 | import { useStateObservable } from "@react-rxjs/core" 3 | import { Dot, Trash2 } from "lucide-react" 4 | import { ReactNode, useContext } from "react" 5 | import { twMerge as clsx, twMerge } from "tailwind-merge" 6 | import { Marker } from "./Markers" 7 | import { 8 | isActive$, 9 | isCollapsed$, 10 | PathsRoot, 11 | setHovered, 12 | toggleCollapsed, 13 | } from "./paths.state" 14 | 15 | export const ListItem: React.FC<{ 16 | idx: number 17 | children: React.ReactNode 18 | path: string[] 19 | onDelete?: () => void 20 | actions?: ReactNode 21 | inline?: boolean 22 | }> = ({ idx, onDelete, children, path, actions, inline }) => { 23 | const pathsRootId = useContext(PathsRoot) 24 | const pathStr = path.join(".") 25 | const isActive = useStateObservable(isActive$(pathStr)) 26 | const isCollapsed = useStateObservable(isCollapsed$(pathStr)) 27 | 28 | const title = inline ? ( 29 |
    30 | 31 | 32 | 33 | Item {idx + 1}. 34 | 35 |
    {children}
    36 | {onDelete ? ( 37 | 43 | ) : null} 44 | {actions} 45 |
    46 | ) : ( 47 |
    48 | 49 | toggleCollapsed(pathsRootId, pathStr)} 52 | > 53 | 54 | Item {idx + 1}. 55 | 56 | {onDelete ? ( 57 | 63 | ) : null} 64 | {actions} 65 |
    66 | ) 67 | 68 | return ( 69 |
  • setHovered(pathsRootId, { id: pathStr, hover: true })} 72 | onMouseLeave={() => 73 | setHovered(pathsRootId, { id: pathStr, hover: false }) 74 | } 75 | > 76 | {title} 77 | {inline ? null : ( 78 |
    84 | {children} 85 |
    86 | )} 87 |
  • 88 | ) 89 | } 90 | -------------------------------------------------------------------------------- /src/codec-components/common/SubtreeFocus.ts: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from "react" 2 | import { noop } from "rxjs" 3 | 4 | const SubtreeFocusContext = createContext<{ 5 | callback: (path: string[]) => void 6 | path: null | string[] 7 | } | null>(null) 8 | 9 | export const SubtreeFocus = SubtreeFocusContext.Provider 10 | 11 | export const useSubtreeFocus = () => { 12 | const ctx = useContext(SubtreeFocusContext) 13 | if (!ctx) { 14 | return { 15 | setFocus: noop, 16 | getNextPath: () => null, 17 | } 18 | } 19 | 20 | return { 21 | setFocus: ctx.callback, 22 | getNextPath: (path: string[]) => { 23 | if (!ctx.path || path.length >= ctx.path.length) return null 24 | return ctx.path[path.length] 25 | }, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/codec-components/common/paths.state.ts: -------------------------------------------------------------------------------- 1 | import { createContextState } from "@/lib/contextState" 2 | import { createKeyedSignal } from "@react-rxjs/utils" 3 | import { createContext, useContext } from "react" 4 | import { map, scan } from "rxjs" 5 | 6 | export const PathsRoot = createContext("") 7 | 8 | const pathsState = createContextState(() => useContext(PathsRoot)) 9 | 10 | export const [collapsedToggle$, toggleCollapsed] = createKeyedSignal< 11 | string, 12 | string 13 | >() 14 | 15 | const collapsedPaths$ = pathsState( 16 | (id) => 17 | collapsedToggle$(id).pipe( 18 | scan((acc, v) => { 19 | if (acc.has(v)) acc.delete(v) 20 | else acc.add(v) 21 | return acc 22 | }, new Set()), 23 | ), 24 | new Set(), 25 | ) 26 | 27 | export const isCollapsed$ = pathsState( 28 | (path: string, id: string) => 29 | collapsedPaths$(id).pipe(map((v) => v.has(path))), 30 | false, 31 | ) 32 | 33 | /** 34 | * Returns true if it's a collapsed root. 35 | * Same as `isCollapsed`, but returns `false` if a parent path is also collapsed. 36 | */ 37 | export const isCollapsedRoot$ = pathsState( 38 | (path: string, id: string) => 39 | collapsedPaths$(id).pipe( 40 | map((collapsedPaths) => { 41 | if (!collapsedPaths.has(path)) return false 42 | 43 | return !Array.from(collapsedPaths).some( 44 | (otherPath) => otherPath !== path && path.startsWith(otherPath), 45 | ) 46 | }), 47 | ), 48 | false, 49 | ) 50 | 51 | export const [hoverChange$, setHovered] = createKeyedSignal< 52 | string, 53 | { 54 | id: string 55 | hover: boolean 56 | } 57 | >() 58 | 59 | const hoverPaths$ = pathsState( 60 | (id: string) => 61 | hoverChange$(id).pipe( 62 | scan((acc, v) => { 63 | if (v.hover) acc.add(v.id) 64 | else acc.delete(v.id) 65 | return acc 66 | }, new Set()), 67 | ), 68 | new Set(), 69 | ) 70 | 71 | export const isActive$ = pathsState( 72 | (path: string, id: string) => 73 | hoverPaths$(id).pipe( 74 | map((hoverPaths) => { 75 | if (!hoverPaths.has(path)) return false 76 | 77 | // Here it's the opposite of `isCollapsedRoot`: We don't want to highlight the root if a child is being highlighted 78 | return !Array.from(hoverPaths).some( 79 | (otherPath) => otherPath !== path && otherPath.startsWith(path), 80 | ) 81 | }), 82 | ), 83 | false, 84 | ) 85 | -------------------------------------------------------------------------------- /src/codec-components/common/scroll.ts: -------------------------------------------------------------------------------- 1 | export function synchronizeScroll( 2 | scrollingElement: HTMLElement, 3 | targetElement: HTMLElement, 4 | ) { 5 | const onScroll = async () => { 6 | const buffer = targetElement.offsetHeight / 3 7 | const listScrollPos = 8 | Math.max(0, scrollingElement.scrollTop - buffer) / 9 | (scrollingElement.scrollHeight - scrollingElement.offsetHeight) 10 | 11 | targetElement.scrollTop = 12 | listScrollPos * 13 | (targetElement.scrollHeight - targetElement.offsetHeight + buffer / 2) 14 | } 15 | 16 | scrollingElement.addEventListener("scroll", onScroll) 17 | // Used as scroll target for markers... 18 | // Otherwise smooth scroll with `element.scrollIntoView` gets interrupted. 19 | // This should be passed probably by context, but at this scale it works. 20 | ;(window as any).scrollingElement = scrollingElement 21 | return () => { 22 | scrollingElement.removeEventListener("scroll", onScroll) 23 | ;(window as any).scrollingElement = null 24 | } 25 | } 26 | 27 | export const scrollToMarker = (id: string[]) => { 28 | const element = document.getElementById("marker-" + id.join(".")) 29 | if (!element) return 30 | 31 | const scrollingElement: HTMLElement = (window as any).scrollingElement 32 | if (scrollingElement) { 33 | const elementRect = element.getBoundingClientRect() 34 | const scrollRect = scrollingElement.getBoundingClientRect() 35 | 36 | scrollingElement.scrollBy({ 37 | top: elementRect.top - scrollRect.top - 50, 38 | behavior: "smooth", 39 | }) 40 | } else { 41 | element.scrollIntoView({ behavior: "instant" }) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/components/AccountIdDisplay.tsx: -------------------------------------------------------------------------------- 1 | import { PolkadotIdenticon } from "@/components/PolkadotIdenticon" 2 | import { accountDetail$, getPublicKey } from "@/state/extension-accounts.state" 3 | import { identity$, isVerified } from "@/state/identity.state" 4 | import { useStateObservable } from "@react-rxjs/core" 5 | import { CheckCircle } from "lucide-react" 6 | import { SS58String } from "polkadot-api" 7 | import { FC } from "react" 8 | import { twMerge } from "tailwind-merge" 9 | 10 | export const AccountIdDisplay: FC<{ 11 | value: SS58String 12 | className?: string 13 | }> = ({ value, className }) => { 14 | const details = useStateObservable(accountDetail$(value)) 15 | const identity = useStateObservable(identity$(value)) 16 | 17 | const name = identity?.displayName ?? details?.name 18 | 19 | return ( 20 |
    21 | 26 |
    27 | {name && ( 28 | 29 | {name} 30 | {isVerified(identity) && ( 31 | 35 | )} 36 | 37 | )} 38 | 39 | {value} 40 | 41 |
    42 |
    43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /src/components/ActionButton.tsx: -------------------------------------------------------------------------------- 1 | import { ButtonHTMLAttributes, forwardRef } from "react" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export const ActionButton = forwardRef< 5 | HTMLButtonElement, 6 | ButtonHTMLAttributes 7 | >((props, ref) => ( 8 | 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /src/components/DocsRenderer.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export const DocsRenderer: FC<{ docs: string[]; className?: string }> = ({ 5 | docs, 6 | className, 7 | }) => { 8 | if (!docs.length) return null 9 | if (docs.length === 1) { 10 | return ( 11 |
    12 |

    {docs[0]}

    13 |
    14 | ) 15 | } 16 | 17 | const normalizedDocs = 18 | docs.length > 1 ? ["/*", ...docs.map((v) => ` *${v}`), " */"] : docs 19 | return ( 20 |
    1 && "text-xs font-mono whitespace-pre", 24 | className, 25 | )} 26 | > 27 | {normalizedDocs.map((d, i) => ( 28 |

    {d}

    29 | ))} 30 |
    31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /src/components/EthAccountDisplay.tsx: -------------------------------------------------------------------------------- 1 | import { accountDetail$ } from "@/state/extension-accounts.state" 2 | import { useStateObservable } from "@react-rxjs/core" 3 | import { HexString } from "polkadot-api" 4 | import { FC } from "react" 5 | import { twMerge } from "tailwind-merge" 6 | import { EthIdenticon } from "./EthIdenticon" 7 | 8 | export const EthAccountDisplay: FC<{ 9 | value: HexString 10 | className?: string 11 | }> = ({ value, className }) => { 12 | const { name } = useStateObservable(accountDetail$(value)) ?? {} 13 | return ( 14 |
    15 | 16 |
    17 | {name && {name}} 18 | 19 | {value} 20 | 21 |
    22 |
    23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /src/components/EthIdenticon.tsx: -------------------------------------------------------------------------------- 1 | import { HexString } from "polkadot-api" 2 | import { FC } from "react" 3 | import { blo } from "blo" 4 | 5 | export const EthIdenticon: FC<{ address: HexString; size?: number }> = ({ 6 | address, 7 | size, 8 | }) => { 9 | return 10 | } 11 | -------------------------------------------------------------------------------- /src/components/Expand.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentProps, FC } from "react" 2 | import { ChevronRight } from "lucide-react" 3 | import { twMerge } from "tailwind-merge" 4 | 5 | export const ExpandBtn: FC< 6 | { 7 | expanded: boolean 8 | direction?: "horizontal" | "vertical" 9 | } & ComponentProps 10 | > = ({ expanded, direction = "horizontal", ...props }) => ( 11 | 24 | ) 25 | -------------------------------------------------------------------------------- /src/components/Icons.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from "@/ThemeProvider" 2 | import { LookupEntry } from "@polkadot-api/metadata-builders" 3 | import { 4 | Ban, 5 | Binary, 6 | Braces, 7 | CircleHelp, 8 | Copy, 9 | Hash, 10 | List, 11 | LoaderCircle, 12 | LucideProps, 13 | User, 14 | } from "lucide-react" 15 | import { FC, useEffect, useRef } from "react" 16 | import { Props, ReactSVG } from "react-svg" 17 | import { twMerge } from "tailwind-merge" 18 | import binarySvg from "./icons/binary.svg" 19 | import chopsticksLogoDark from "./icons/chopsticks_dark.svg" 20 | import chopsticksLogoLight from "./icons/chopsticks_light.svg" 21 | import enumSvg from "./icons/enum.svg" 22 | import focusSvg from "./icons/focus.svg" 23 | import switchBinarySvg from "./icons/switch_binary.svg" 24 | import walletConnectSvg from "./icons/walletConnect.svg" 25 | 26 | type CustomIconProps = Omit & { size?: number } 27 | const CustomIcon: FC< 28 | CustomIconProps & { 29 | url: string 30 | } 31 | > = ({ size = 16, url, ...props }) => { 32 | const ref = useRef(null) 33 | 34 | useEffect(() => { 35 | if (!ref.current) return 36 | ref.current.setAttribute("width", String(size)) 37 | ref.current.setAttribute("height", String(size)) 38 | }, [size]) 39 | 40 | return ( 41 | { 45 | ref.current = svg 46 | svg.setAttribute("width", String(size)) 47 | svg.setAttribute("height", String(size)) 48 | }} 49 | /> 50 | ) 51 | } 52 | 53 | const customIcon = (url: string) => (props: CustomIconProps) => ( 54 | 55 | ) 56 | 57 | const themeIcon = (light: string, dark: string) => (props: CustomIconProps) => { 58 | const theme = useTheme() 59 | return 60 | } 61 | 62 | export const Focus = customIcon(focusSvg) 63 | export const Enum = customIcon(enumSvg) 64 | export const BinaryEdit = customIcon(binarySvg) 65 | export const WalletConnect = customIcon(walletConnectSvg) 66 | export const SwitchBinary = customIcon(switchBinarySvg) 67 | export const Chopsticks = themeIcon(chopsticksLogoLight, chopsticksLogoDark) 68 | 69 | export const Spinner = (props: LucideProps) => ( 70 | 74 | ) 75 | 76 | export const TypeIcons = { 77 | list: List, 78 | enum: Enum, 79 | primitive: Hash, 80 | binary: Binary, 81 | account: User, 82 | object: Braces, 83 | maybe: CircleHelp, 84 | void: Ban, 85 | } 86 | export type TypeIcon = keyof typeof TypeIcons 87 | 88 | export const lookupToType: Record = { 89 | primitive: "primitive", 90 | void: "void", 91 | compact: "primitive", 92 | bitSequence: "binary", 93 | AccountId32: "account", 94 | AccountId20: "account", 95 | tuple: "list", 96 | struct: "object", 97 | sequence: "list", 98 | array: "list", 99 | option: "maybe", 100 | result: "maybe", 101 | enum: "enum", 102 | } 103 | 104 | export const CopyBinaryIcon = ({ size = 16, ...props }: CustomIconProps) => ( 105 | 113 | 114 | 123 | 0x 124 | 125 | 126 | ) 127 | -------------------------------------------------------------------------------- /src/components/JsonDisplay.tsx: -------------------------------------------------------------------------------- 1 | import { bytesToString } from "@/components/BinaryInput" 2 | import { Binary } from "@polkadot-api/substrate-bindings" 3 | import { FC } from "react" 4 | import ReactJson from "react18-json-view" 5 | import "react18-json-view/src/dark.css" 6 | import "react18-json-view/src/style.css" 7 | import "./jsonDisplay.css" 8 | import { useTheme } from "@/ThemeProvider" 9 | 10 | export const JsonDisplay: FC<{ 11 | src: unknown 12 | collapsed?: boolean | number 13 | }> = ({ src, ...props }) => { 14 | const theme = useTheme() 15 | 16 | return ( 17 | (v instanceof Binary ? bytesToString(v) : v)} 22 | customizeCopy={(v) => 23 | JSON.stringify( 24 | v, 25 | (_, v) => 26 | typeof v === "bigint" 27 | ? `${v}n` 28 | : v instanceof Binary 29 | ? bytesToString(v) 30 | : v, 31 | 2, 32 | ) 33 | } 34 | {...props} 35 | /> 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /src/components/Loading.tsx: -------------------------------------------------------------------------------- 1 | import { FC, PropsWithChildren, useEffect, useState } from "react" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export const Loading: FC = ({ children }) => { 5 | const [visible, setVisible] = useState(false) 6 | 7 | useEffect(() => { 8 | const token = requestAnimationFrame(() => setVisible(true)) 9 | return () => cancelAnimationFrame(token) 10 | }, []) 11 | 12 | return ( 13 |
    19 | {children} 20 |
    21 | ) 22 | } 23 | 24 | export const LoadingMetadata: FC = () => ( 25 | Waiting for metadata… 26 | ) 27 | export const LoadingBlocks: FC = () => Waiting for blocks… 28 | -------------------------------------------------------------------------------- /src/components/Popover.tsx: -------------------------------------------------------------------------------- 1 | import { Trigger, Arrow, Content, Portal, Root } from "@radix-ui/react-popover" 2 | import { FC, PropsWithChildren, ReactNode } from "react" 3 | 4 | export const Popover: FC< 5 | PropsWithChildren<{ content: ReactNode; open?: boolean }> 6 | > = ({ children, content, open }) => ( 7 | 8 | {children} 9 | 10 | 11 | {content} 12 | 13 | 14 | 15 | 16 | ) 17 | -------------------------------------------------------------------------------- /src/components/TextInputField.tsx: -------------------------------------------------------------------------------- 1 | import { FC, ReactNode, useState } from "react" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export const TextInputField: FC<{ 5 | className?: string 6 | error?: string | boolean | null 7 | softError?: string | boolean | null 8 | warn?: string | boolean | null 9 | placeholder?: string 10 | value: string 11 | onChange: (value: string) => void 12 | children?: (inputElement: ReactNode) => ReactNode 13 | }> = ({ 14 | className, 15 | error, 16 | softError, 17 | warn, 18 | value, 19 | placeholder, 20 | onChange, 21 | children = (v) => v, 22 | }) => { 23 | const [hasBlurred, blur] = useState(false) 24 | 25 | return ( 26 |
    27 | {children( 28 | onChange(evt.target.value)} 38 | onFocus={() => blur(false)} 39 | onBlur={() => blur(true)} 40 | />, 41 | )} 42 | {(hasBlurred || error) && 43 | (typeof (error || softError) === "string" ? ( 44 | {error || softError} 45 | ) : typeof warn === "string" ? ( 46 | {warn} 47 | ) : null)} 48 |
    49 | ) 50 | } 51 | -------------------------------------------------------------------------------- /src/components/Toggle.tsx: -------------------------------------------------------------------------------- 1 | import * as Toggle from "@radix-ui/react-toggle" 2 | 3 | const SliderToggle: React.FC<{ 4 | isToggled: boolean 5 | toggle: () => void 6 | id?: string 7 | }> = ({ isToggled, toggle, id }) => { 8 | return ( 9 | toggle()} 13 | className={ 14 | "relative w-8 h-5 rounded-full p-0.5 transition-colors border border-foreground/20" + 15 | (isToggled ? " bg-primary" : " bg-primary/20") 16 | } 17 | aria-label="Toggle" 18 | > 19 | 23 | 24 | ) 25 | } 26 | 27 | export default SliderToggle 28 | -------------------------------------------------------------------------------- /src/components/icons/binary.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/components/icons/chopsticks_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 15 | 34 | 40 | 47 | 57 | 67 | 69 | 77 | 81 | 85 | 86 | 94 | 98 | 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /src/components/icons/chopsticks_light.svg: -------------------------------------------------------------------------------- 1 | 2 | 15 | 34 | 40 | 47 | 56 | 65 | 67 | 75 | 79 | 83 | 84 | 92 | 96 | 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /src/components/icons/enum.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/components/icons/focus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/components/icons/switch_binary.svg: -------------------------------------------------------------------------------- 1 | 11 | 12 | 0x 13 | 14 | 15 | 16 | Aa 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/components/icons/walletConnect.svg: -------------------------------------------------------------------------------- 1 | 2 | 9 | 13 | 17 | 18 | -------------------------------------------------------------------------------- /src/components/jsonDisplay.css: -------------------------------------------------------------------------------- 1 | .json-view .json-view--copy { 2 | opacity: 0.4; 3 | } 4 | .json-view .json-view--copy:hover { 5 | opacity: 1; 6 | } 7 | .json-view .jv-button { 8 | padding: 0 0.3rem; 9 | } 10 | -------------------------------------------------------------------------------- /src/components/modal.css: -------------------------------------------------------------------------------- 1 | /* standarize native modal */ 2 | .modal { 3 | max-width: calc(100% - 6px - 2em); 4 | max-height: calc(100% - 6px - 2em); 5 | } 6 | 7 | dialog.modal[open] { 8 | opacity: 1; 9 | transform: translateY(0); 10 | } 11 | 12 | dialog.modal { 13 | opacity: 0; 14 | transform: translateY(30px); 15 | transition: 16 | opacity 0.3s ease-out, 17 | transform 0.3s ease-out, 18 | overlay 0.3s ease-out allow-discrete, 19 | display 0.3s ease-out allow-discrete; 20 | } 21 | 22 | @starting-style { 23 | dialog.modal[open] { 24 | opacity: 0; 25 | transform: translateY(30px); 26 | } 27 | } 28 | 29 | /* Transition the :backdrop when the dialog modal is promoted to the top layer */ 30 | dialog.modal::backdrop { 31 | opacity: 0; 32 | transition: 33 | display 0.1s allow-discrete, 34 | overlay 0.1s allow-discrete, 35 | opacity 0.1s; 36 | } 37 | 38 | dialog.modal[open]::backdrop { 39 | opacity: 1; 40 | } 41 | 42 | /* This starting-style rule cannot be nested inside the above selector 43 | because the nesting selector cannot represent pseudo-elements. */ 44 | @starting-style { 45 | dialog.modal[open]::backdrop { 46 | opacity: 0; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/components/ui/accordion.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as AccordionPrimitive from "@radix-ui/react-accordion" 3 | import { ChevronDown } from "lucide-react" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const Accordion = AccordionPrimitive.Root 8 | 9 | const AccordionItem = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ className, ...props }, ref) => ( 13 | 18 | )) 19 | AccordionItem.displayName = "AccordionItem" 20 | 21 | const AccordionTrigger = React.forwardRef< 22 | React.ElementRef, 23 | React.ComponentPropsWithoutRef 24 | >(({ className, children, ...props }, ref) => ( 25 | 26 | svg]:rotate-180", 30 | className, 31 | )} 32 | {...props} 33 | > 34 | {children} 35 | 36 | 37 | 38 | )) 39 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName 40 | 41 | const AccordionContent = React.forwardRef< 42 | React.ElementRef, 43 | React.ComponentPropsWithoutRef 44 | >(({ className, children, ...props }, ref) => ( 45 | 50 |
    {children}
    51 |
    52 | )) 53 | 54 | AccordionContent.displayName = AccordionPrimitive.Content.displayName 55 | 56 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } 57 | -------------------------------------------------------------------------------- /src/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const badgeVariants = cva( 8 | "inline-flex items-center justify-center rounded-md border border-neutral-200 px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-neutral-950 focus-visible:ring-neutral-950/50 focus-visible:ring-[3px] aria-invalid:ring-red-500/20 dark:aria-invalid:ring-red-500/40 aria-invalid:border-red-500 transition-[color,box-shadow] overflow-hidden dark:border-neutral-800 dark:focus-visible:border-neutral-300 dark:focus-visible:ring-neutral-300/50 dark:aria-invalid:ring-red-900/20 dark:dark:aria-invalid:ring-red-900/40 dark:aria-invalid:border-red-900", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "border-transparent bg-neutral-900 text-neutral-50 [a&]:hover:bg-neutral-900/90 dark:bg-neutral-50 dark:text-neutral-900 dark:[a&]:hover:bg-neutral-50/90", 14 | secondary: 15 | "border-transparent bg-neutral-100 text-neutral-900 [a&]:hover:bg-neutral-100/90 dark:bg-neutral-800 dark:text-neutral-50 dark:[a&]:hover:bg-neutral-800/90", 16 | destructive: 17 | "border-transparent bg-red-500 text-white [a&]:hover:bg-red-500/90 focus-visible:ring-red-500/20 dark:focus-visible:ring-red-500/40 dark:bg-red-500/60 dark:bg-red-900 dark:[a&]:hover:bg-red-900/90 dark:focus-visible:ring-red-900/20 dark:dark:focus-visible:ring-red-900/40 dark:dark:bg-red-900/60", 18 | outline: 19 | "text-neutral-950 [a&]:hover:bg-neutral-100 [a&]:hover:text-neutral-900 dark:text-neutral-50 dark:[a&]:hover:bg-neutral-800 dark:[a&]:hover:text-neutral-50", 20 | }, 21 | }, 22 | defaultVariants: { 23 | variant: "default", 24 | }, 25 | } 26 | ) 27 | 28 | function Badge({ 29 | className, 30 | variant, 31 | asChild = false, 32 | ...props 33 | }: React.ComponentProps<"span"> & 34 | VariantProps & { asChild?: boolean }) { 35 | const Comp = asChild ? Slot : "span" 36 | 37 | return ( 38 | 43 | ) 44 | } 45 | 46 | export { Badge, badgeVariants } 47 | -------------------------------------------------------------------------------- /src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | }, 34 | ) 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean 40 | forceSvgSize?: boolean 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ( 45 | { 46 | className, 47 | variant, 48 | size, 49 | asChild = false, 50 | forceSvgSize = true, 51 | ...props 52 | }, 53 | ref, 54 | ) => { 55 | const Comp = asChild ? Slot : "button" 56 | return ( 57 | 66 | ) 67 | }, 68 | ) 69 | Button.displayName = "Button" 70 | 71 | export { Button, buttonVariants } 72 | -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as LabelPrimitive from "@radix-ui/react-label" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/utils" 6 | 7 | const labelVariants = cva( 8 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", 9 | ) 10 | 11 | const Label = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef & 14 | VariantProps 15 | >(({ className, ...props }, ref) => ( 16 | 21 | )) 22 | Label.displayName = LabelPrimitive.Root.displayName 23 | 24 | export { Label } 25 | -------------------------------------------------------------------------------- /src/components/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as PopoverPrimitive from "@radix-ui/react-popover" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const Popover = PopoverPrimitive.Root 7 | 8 | const PopoverTrigger = PopoverPrimitive.Trigger 9 | 10 | const PopoverContent = React.forwardRef< 11 | React.ElementRef, 12 | React.ComponentPropsWithoutRef 13 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 14 | 15 | 25 | 26 | )) 27 | PopoverContent.displayName = PopoverPrimitive.Content.displayName 28 | 29 | export { Popover, PopoverTrigger, PopoverContent } 30 | -------------------------------------------------------------------------------- /src/components/ui/radio-group.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as RadioGroupPrimitive from "@radix-ui/react-radio-group" 3 | import { Circle } from "lucide-react" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const RadioGroup = React.forwardRef< 8 | React.ElementRef, 9 | React.ComponentPropsWithoutRef 10 | >(({ className, ...props }, ref) => { 11 | return ( 12 | 17 | ) 18 | }) 19 | RadioGroup.displayName = RadioGroupPrimitive.Root.displayName 20 | 21 | const RadioGroupItem = React.forwardRef< 22 | React.ElementRef, 23 | React.ComponentPropsWithoutRef 24 | >(({ className, ...props }, ref) => { 25 | return ( 26 | 34 | 35 | 36 | 37 | 38 | ) 39 | }) 40 | RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName 41 | 42 | export { RadioGroup, RadioGroupItem } 43 | -------------------------------------------------------------------------------- /src/components/ui/scroll-area.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const ScrollArea = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, children, ...props }, ref) => ( 10 | 15 | 16 | {children} 17 | 18 | 19 | 20 | 21 | )) 22 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName 23 | 24 | const ScrollBar = React.forwardRef< 25 | React.ElementRef, 26 | React.ComponentPropsWithoutRef 27 | >(({ className, orientation = "vertical", ...props }, ref) => ( 28 | 41 | 42 | 43 | )) 44 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName 45 | 46 | export { ScrollArea, ScrollBar } 47 | -------------------------------------------------------------------------------- /src/components/ui/tabs.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/utils" 4 | 5 | export type DivProps = React.DetailedHTMLProps< 6 | React.HTMLAttributes, 7 | HTMLDivElement 8 | > 9 | 10 | export const TabsList: React.FC = ({ className, children }) => ( 11 |
    17 | {children} 18 |
    19 | ) 20 | 21 | export const TabsTrigger: React.FC = ({ 22 | className, 23 | children, 24 | active, 25 | ...props 26 | }) => ( 27 |
    36 | {children} 37 |
    38 | ) 39 | -------------------------------------------------------------------------------- /src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { 6 | return ( 7 |