import { EditCodec } from "@/codec-components/EditCodec" import { ActionButton } from "@/components/ActionButton" import { BinaryEditButton } from "@/components/BinaryEditButton" import { MetadataEntryInput } from "@/components/MetadataEntryInput" import SliderToggle from "@/components/Toggle" import { useNavigate } from "@/hashParams" import { CodecComponentType, CodecComponentValue, NOTIN, } from "@polkadot-api/react-builder" import { Input } from "@polkahub/ui-components" import { state, useStateObservable, withDefault } from "@react-rxjs/core" import { createSignal, mergeWithKey } from "@react-rxjs/utils" import { Binary, Enum } from "polkadot-api" import { fromHex } from "polkadot-api/utils" import { ChangeEvent, FC } from "react" import { combineLatest, distinctUntilChanged, filter, firstValueFrom, map, merge, scan, startWith, switchMap, take, withLatestFrom, } from "rxjs" import { twMerge } from "tailwind-merge" import { selectedBlock$ } from "./BlockPicker" import { decodeKeyArgs, decodeKeyTarget } from "./decodeKey" import { addStorageSubscription, storageEntryState } from "./storage.state" export const StorageQuery: FC = () => { const isReady = useStateObservable(isReady$) const navigate = useNavigate() const submit = async () => { const [entry, keyValues, keysEnabled, { hash }, selectedKey] = await firstValueFrom( combineLatest([ storageEntryState.selectedEntry$, keyValues$, argsEnabled$, selectedBlock$, selectedKey$, ]), ) if (selectedKey?.type === "raw") { const id = await addStorageSubscription({ blockHash: hash ?? null, pallet: entry!.pallet, item: entry!.entry, value: Enum("rawQuery", selectedKey.key), }) navigate(`/storage/${id}`) } else { const args = keyValues.slice(0, keysEnabled) const id = await addStorageSubscription({ blockHash: hash ?? null, pallet: entry!.pallet, item: entry!.entry, value: Enum("query", args), }) navigate(`/storage/${id}`) } } return (
Query
) } const keys$ = storageEntryState.selectedEntry$.pipeState( filter((e) => !!e), map((entry) => entry.key), withDefault([] as number[]), distinctUntilChanged((a, b) => a.join(",") === b.join(",")), ) const hashers$ = storageEntryState.selectedEntry$.pipeState( filter((e) => !!e), map((entry) => entry.hashers), withDefault([] as string[]), ) const [toggleKey$, toggleKey] = createSignal() export const [changeKeysEnabled$, setKeysEnabled] = createSignal() const argsEnabled$ = keys$.pipeState( switchMap((k) => mergeWithKey({ toggle: toggleKey$, set: changeKeysEnabled$, }).pipe( /* acc=2 [X,X, , ] 0 1 2 3 toggle 0 => acc=0 toggle 1 => acc=1 toggle 2 => acc=3 toggle 3 => acc=4 */ scan( (acc, evt) => evt.type === "set" || acc > evt.payload ? evt.payload : evt.payload + 1, k.length, ), startWith(k.length), ), ), withDefault(0), ) export const [keyValueChange$, setKeyValue] = createSignal<{ idx: number value: unknown | NOTIN }>() export const keyValues$ = keys$.pipeState( switchMap((keys) => { const values: unknown[] = keys.map(() => NOTIN) return keyValueChange$.pipe( scan((acc, change) => { const newValue = [...acc] newValue[change.idx] = change.value return newValue }, values), startWith(values), ) }), withDefault([] as unknown[]), ) export const StorageKeysInput: FC<{ disableToggle?: boolean }> = ({ disableToggle }) => { const keys = useStateObservable(keys$) const hashers = useStateObservable(hashers$) const argsEnabled = useStateObservable(argsEnabled$) return (
    {keys.map((type, idx) => (
  1. {disableToggle ? null : ( idx} toggle={() => toggleKey(idx)} /> )}
  2. ))}
) } const builderState$ = state( selectedBlock$.pipe( map(({ ctx }) => ({ ...ctx.dynamicBuilder, lookup: ctx.lookup, })), ), null, ) const keysCodec$ = combineLatest([keys$, builderState$]).pipe( map(([keys, builder]) => keys.map((type) => builder?.buildDefinition(type))), ) const argInputValue$ = state( (idx: number) => keyValues$.pipe( withLatestFrom(keysCodec$), map(([v, codecs], i): CodecComponentValue => { if (i === 0) { try { return { type: CodecComponentType.Initial, value: codecs[idx]?.enc(v[idx]), } } catch { return { type: CodecComponentType.Initial, } } } return { type: CodecComponentType.Updated, value: v[idx] === NOTIN ? { empty: true, } : { empty: false, decoded: v[idx], }, } }), ), { type: CodecComponentType.Initial, } satisfies CodecComponentValue, ) const StorageArgInput: FC<{ idx: number type: number disabled: boolean hasher: string }> = ({ idx, type, disabled, hasher }) => { const builder = useStateObservable(builderState$) const value = useStateObservable(argInputValue$(idx)) if (!builder) return null const codec = builder.buildDefinition(type) const getBinValue = () => { try { return ( (value.type === CodecComponentType.Initial ? value.value : value.value.empty ? null : (value.value.encoded ?? codec.enc(value.value.decoded))) ?? null ) } catch (ex) { console.error(ex) } } const binaryValue = getBinValue() const getTypeName = () => { const lookupEntry = builder.lookup(type) switch (lookupEntry.type) { case "primitive": return lookupEntry.value case "compact": return lookupEntry.size case "enum": return "Enum" case "array": if ( lookupEntry.value.type === "primitive" && lookupEntry.value.value === "u8" ) { return "Binary" } return null case "bitSequence": case "AccountId20": case "AccountId32": return lookupEntry.type default: return null } } return (
{getTypeName()} ({hasher})
setKeyValue({ idx, value })} decode={codec.dec} />
setKeyValue({ idx, value: value.empty ? NOTIN : value.decoded }) } />
) } const keyCodec$ = state( combineLatest([selectedBlock$, storageEntryState.selectedEntry$]).pipe( map(([{ ctx }, selectedEntry]) => selectedEntry ? ctx.dynamicBuilder.buildStorage( selectedEntry.pallet, selectedEntry.entry, ).keys : null, ), ), ) export const encodedKey$ = state( combineLatest([keyCodec$, keyValues$, argsEnabled$]).pipe( map(([codec, keyValues, keysEnabled]) => { const args = keyValues.slice(0, keysEnabled) if ( keyValues.length < keysEnabled || !args.every((v) => v !== NOTIN) || !codec ) { return null } try { return codec.enc(...args) } catch (_) { return null } }), ), null, ) export const [selectedKeyChange$, setSelectedKey] = createSignal<{ type: "fromValue" | "raw" key: string }>() const selectedKey$ = state( merge( encodedKey$.pipe( distinctUntilChanged(), map((key) => (key ? { type: "fromValue", key } : null)), ), selectedKeyChange$, ), ) const isReady$ = state( selectedKey$.pipe(map((v) => v && v.key.length > 2)), false, ) const [keyInputChange$, onKeyInputChange] = createSignal>() const [keyInputBlur$, onKeyInputBlur] = createSignal() const keyInput$ = state( merge( keyInputChange$.pipe( map((v) => v.currentTarget.value), withLatestFrom(selectedBlock$.pipe(map((v) => v.ctx))), map(([value, ctx]) => { const error = value.length > 3 && (() => { try { return !decodeKeyTarget(ctx, Binary.fromHex(value)) } catch { return true } })() return { value, error } }), ), merge( selectedKey$, keyInputBlur$.pipe(switchMap(() => selectedKey$.pipe(take(1)))), ).pipe(map((value) => ({ value: value?.key ?? "", error: false }))), ), { value: "", error: false, }, ) export const KeyInput: FC = () => { const keyInput = useStateObservable(keyInput$) const builder = useStateObservable(builderState$) const selectedEntry = useStateObservable(storageEntryState.selectedEntry$) const keysEnabled = useStateObservable(argsEnabled$) if (!builder || !selectedEntry) return null return (
{ onKeyInputBlur() try { const ctx = { dynamicBuilder: builder, lookup: builder.lookup, } const target = decodeKeyTarget(ctx, Binary.fromHex(keyInput.value)) if (!target) return let newKeysEnabled = keysEnabled if ( target.pallet.name !== selectedEntry.pallet || target.item.name !== selectedEntry.entry ) { storageEntryState.selectEntry({ group: target.pallet.name, item: target.item.name, }) newKeysEnabled = target.item.type.tag === "plain" ? 0 : target.item.type.value.hashers.length } const args = decodeKeyArgs(ctx, target) if (!args) { setKeysEnabled(0) setSelectedKey({ type: "raw", key: keyInput.value, }) return } if (args.length !== newKeysEnabled) { if (args.length > newKeysEnabled) { toggleKey(args.length - 1) } else { toggleKey(args.length) } } args.forEach((value, idx) => setKeyValue({ idx, value, }), ) } catch { return } }} />
) }