feat: partial key storage getEntries (#182)
This commit is contained in:
@@ -25,11 +25,12 @@ import {
|
||||
scan,
|
||||
startWith,
|
||||
switchMap,
|
||||
take,
|
||||
withLatestFrom,
|
||||
} from "rxjs"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import { selectedBlock$ } from "./BlockPicker"
|
||||
import { decodeKey } from "./decodeKey"
|
||||
import { decodeKeyArgs, decodeKeyTarget } from "./decodeKey"
|
||||
import { addStorageSubscription, storageEntryState } from "./storage.state"
|
||||
|
||||
export const StorageQuery: FC = () => {
|
||||
@@ -37,23 +38,35 @@ export const StorageQuery: FC = () => {
|
||||
const navigate = useNavigate()
|
||||
|
||||
const submit = async () => {
|
||||
const [entry, keyValues, keysEnabled, { hash }] = await firstValueFrom(
|
||||
combineLatest([
|
||||
storageEntryState.selectedEntry$,
|
||||
keyValues$,
|
||||
argsEnabled$,
|
||||
selectedBlock$,
|
||||
]),
|
||||
)
|
||||
const args = keyValues.slice(0, keysEnabled)
|
||||
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}`)
|
||||
const id = await addStorageSubscription({
|
||||
blockHash: hash ?? null,
|
||||
pallet: entry!.pallet,
|
||||
item: entry!.entry,
|
||||
value: Enum("query", args),
|
||||
})
|
||||
navigate(`/storage/${id}`)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -136,17 +149,6 @@ export const keyValues$ = keys$.pipeState(
|
||||
withDefault([] as unknown[]),
|
||||
)
|
||||
|
||||
const isReady$ = state(
|
||||
combineLatest([keyValues$, argsEnabled$]).pipe(
|
||||
map(
|
||||
([keyValues, keysEnabled]) =>
|
||||
keyValues.length >= keysEnabled &&
|
||||
keyValues.slice(0, keysEnabled).every((v) => v !== NOTIN),
|
||||
),
|
||||
),
|
||||
false,
|
||||
)
|
||||
|
||||
export const StorageKeysInput: FC<{
|
||||
disableToggle?: boolean
|
||||
}> = ({ disableToggle }) => {
|
||||
@@ -348,6 +350,26 @@ export const encodedKey$ = state(
|
||||
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<ChangeEvent<HTMLInputElement>>()
|
||||
const [keyInputBlur$, onKeyInputBlur] = createSignal<void>()
|
||||
@@ -361,7 +383,7 @@ const keyInput$ = state(
|
||||
value.length > 3 &&
|
||||
(() => {
|
||||
try {
|
||||
return !decodeKey(ctx, Binary.fromHex(value))
|
||||
return !decodeKeyTarget(ctx, Binary.fromHex(value))
|
||||
} catch {
|
||||
return true
|
||||
}
|
||||
@@ -370,9 +392,10 @@ const keyInput$ = state(
|
||||
return { value, error }
|
||||
}),
|
||||
),
|
||||
merge(encodedKey$, keyInputBlur$.pipe(switchMap(() => encodedKey$))).pipe(
|
||||
map((value) => ({ value: value ?? "", error: false })),
|
||||
),
|
||||
merge(
|
||||
selectedKey$,
|
||||
keyInputBlur$.pipe(switchMap(() => selectedKey$.pipe(take(1)))),
|
||||
).pipe(map((value) => ({ value: value?.key ?? "", error: false }))),
|
||||
),
|
||||
{
|
||||
value: "",
|
||||
@@ -404,39 +427,47 @@ export const KeyInput: FC = () => {
|
||||
onKeyInputBlur()
|
||||
|
||||
try {
|
||||
const decoded = decodeKey(
|
||||
{
|
||||
dynamicBuilder: builder,
|
||||
lookup: builder.lookup,
|
||||
},
|
||||
Binary.fromHex(keyInput.value),
|
||||
)
|
||||
if (!decoded) return
|
||||
const ctx = {
|
||||
dynamicBuilder: builder,
|
||||
lookup: builder.lookup,
|
||||
}
|
||||
const target = decodeKeyTarget(ctx, Binary.fromHex(keyInput.value))
|
||||
if (!target) return
|
||||
|
||||
let newKeysEnabled = keysEnabled
|
||||
if (
|
||||
decoded.pallet.name !== selectedEntry.pallet ||
|
||||
decoded.item.name !== selectedEntry.entry
|
||||
target.pallet.name !== selectedEntry.pallet ||
|
||||
target.item.name !== selectedEntry.entry
|
||||
) {
|
||||
storageEntryState.selectEntry({
|
||||
group: decoded.pallet.name,
|
||||
item: decoded.item.name,
|
||||
group: target.pallet.name,
|
||||
item: target.item.name,
|
||||
})
|
||||
newKeysEnabled =
|
||||
decoded.item.type.tag === "plain"
|
||||
target.item.type.tag === "plain"
|
||||
? 0
|
||||
: decoded.item.type.value.hashers.length
|
||||
: target.item.type.value.hashers.length
|
||||
}
|
||||
|
||||
if (decoded.args.length !== newKeysEnabled) {
|
||||
if (decoded.args.length > newKeysEnabled) {
|
||||
toggleKey(decoded.args.length - 1)
|
||||
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(decoded.args.length)
|
||||
toggleKey(args.length)
|
||||
}
|
||||
}
|
||||
|
||||
decoded.args.forEach((value, idx) =>
|
||||
args.forEach((value, idx) =>
|
||||
setKeyValue({
|
||||
idx,
|
||||
value,
|
||||
|
||||
@@ -32,7 +32,7 @@ import {
|
||||
stringifyArg,
|
||||
} from "./storage.state"
|
||||
import { setValue } from "./StorageDecode"
|
||||
import { setKeysEnabled, setKeyValue } from "./StorageQuery"
|
||||
import { setKeysEnabled, setKeyValue, setSelectedKey } from "./StorageQuery"
|
||||
|
||||
export const StorageSubscriptions: FC = () => {
|
||||
const params = useParams()
|
||||
@@ -174,8 +174,7 @@ const ValuesSubscriptionBox: FC<{ subscription: string }> = ({
|
||||
? status.value.length - 1
|
||||
: status.value.findIndex((v) => v.height > target) - 1
|
||||
const targetValue = status.value[targetValueIdx] as
|
||||
| StorageSubscriptionValue
|
||||
| undefined
|
||||
StorageSubscriptionValue | undefined
|
||||
const hasNext = targetValueIdx < status.value.length - 1
|
||||
const hasPrev = targetValueIdx > 0
|
||||
|
||||
@@ -494,13 +493,19 @@ const useSynchronizeInputs = (id: string) => {
|
||||
group: params.pallet,
|
||||
item: params.item,
|
||||
})
|
||||
setMode(params.value.type)
|
||||
setMode(params.value.type === "rawQuery" ? "query" : params.value.type)
|
||||
// Let entry settle
|
||||
await Promise.resolve()
|
||||
if (cancelled) return
|
||||
if (params.value.type === "query") {
|
||||
if (cancelled) return
|
||||
setKeysEnabled(params.value.value.length)
|
||||
params.value.value.forEach((value, idx) => setKeyValue({ idx, value }))
|
||||
} else if (params.value.type === "rawQuery") {
|
||||
setKeysEnabled(0)
|
||||
setSelectedKey({
|
||||
type: "raw",
|
||||
key: params.value.value,
|
||||
})
|
||||
} else {
|
||||
setValue(params.value.value)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Twox128 } from "@polkadot-api/substrate-bindings"
|
||||
import { fromHex, toHex } from "polkadot-api/utils"
|
||||
|
||||
const textEncoder = new TextEncoder()
|
||||
const twoxHash = (value: string) => toHex(Twox128(textEncoder.encode(value)))
|
||||
const hashersToLength: Record<string, number> = {
|
||||
Identity: 0,
|
||||
Twox64Concat: 8,
|
||||
@@ -13,12 +14,10 @@ const hashersToLength: Record<string, number> = {
|
||||
Twox256: -32,
|
||||
}
|
||||
|
||||
export const decodeKey = (
|
||||
ctx: Pick<RuntimeContext, "lookup" | "dynamicBuilder">,
|
||||
export const decodeKeyTarget = (
|
||||
ctx: Pick<RuntimeContext, "lookup">,
|
||||
key: Uint8Array,
|
||||
) => {
|
||||
const twoxHash = (v: string) => toHex(Twox128(textEncoder.encode(v)))
|
||||
|
||||
const keyHex = toHex(key)
|
||||
const pallet = ctx.lookup.metadata.pallets.find(
|
||||
(p) => p.storage && keyHex.startsWith(twoxHash(p.storage.prefix)),
|
||||
@@ -31,6 +30,17 @@ export const decodeKey = (
|
||||
)
|
||||
if (!item) return null
|
||||
|
||||
return { pallet, item, keyRemaining }
|
||||
}
|
||||
|
||||
export const decodeKeyArgs = (
|
||||
ctx: Pick<RuntimeContext, "dynamicBuilder">,
|
||||
{
|
||||
pallet,
|
||||
item,
|
||||
keyRemaining,
|
||||
}: NonNullable<ReturnType<typeof decodeKeyTarget>>,
|
||||
) => {
|
||||
const codec = ctx.dynamicBuilder.buildStorage(pallet.name, item.name)
|
||||
const hasherLengths =
|
||||
item.type.tag === "plain"
|
||||
@@ -56,6 +66,8 @@ export const decodeKey = (
|
||||
const value = argCodec.dec(argsRemaining)
|
||||
// This is needed not just for the length, but see case AccountId: Can decode 0x, but then can't re-encode back. <- TODO bug?
|
||||
const reEnc = argCodec.enc(value)
|
||||
// If reencoding ends up with a longer key, it means the key was actually cut out
|
||||
if (reEnc.length > argsRemaining.length) return null
|
||||
argsRemaining = argsRemaining.slice(reEnc.length)
|
||||
args.push(value)
|
||||
} catch {
|
||||
@@ -64,14 +76,8 @@ export const decodeKey = (
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
pallet,
|
||||
item,
|
||||
entry: getEntry(ctx, item.type),
|
||||
args,
|
||||
}
|
||||
return args
|
||||
}
|
||||
export type DecodedKey = ReturnType<typeof decodeKey>
|
||||
|
||||
export const getEntry = (
|
||||
ctx: Pick<RuntimeContext, "lookup">,
|
||||
|
||||
@@ -111,6 +111,7 @@ export type StorageSubscription = {
|
||||
item: string
|
||||
value: Enum<{
|
||||
decode: HexString
|
||||
rawQuery: HexString
|
||||
query: unknown[]
|
||||
}>
|
||||
}
|
||||
@@ -132,7 +133,7 @@ const storageSubscriptionId = (
|
||||
sub.pallet,
|
||||
sub.item,
|
||||
sub.value.type,
|
||||
...(sub.value.type === "decode"
|
||||
...(sub.value.type === "decode" || sub.value.type === "rawQuery"
|
||||
? [sub.value.value]
|
||||
: sub.value.value.map((v, i) =>
|
||||
Binary.toHex(
|
||||
@@ -162,12 +163,14 @@ export const idToStorageSubscription = async (
|
||||
value:
|
||||
type === "decode"
|
||||
? Enum("decode", values[0])
|
||||
: Enum(
|
||||
"query",
|
||||
values.map((v, i) =>
|
||||
ctx.dynamicBuilder.buildDefinition(entry.keys[i].type).dec(v),
|
||||
: type === "rawQuery"
|
||||
? Enum("rawQuery", values[0])
|
||||
: Enum(
|
||||
"query",
|
||||
values.map((v, i) =>
|
||||
ctx.dynamicBuilder.buildDefinition(entry.keys[i].type).dec(v),
|
||||
),
|
||||
),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,38 +180,55 @@ export const storageSubscriptionToWorkspaceEntry = async ({
|
||||
item,
|
||||
value,
|
||||
}: StorageSubscription): Promise<WorkspaceEntryData<StorageEntryContext>> => {
|
||||
const [ctx, unsafeApi] = await firstValueFrom(
|
||||
const [ctx, unsafeApi, chainHead] = await firstValueFrom(
|
||||
combineLatest([
|
||||
blockHash ? runtimeCtxAt$(blockHash) : runtimeCtx$,
|
||||
genericUnsafeApi$,
|
||||
chainClient$.pipe(map((v) => v.chainHead)),
|
||||
]),
|
||||
)
|
||||
const entry = getEntry(ctx, getStorageItem(ctx, pallet, item)!.item.type)
|
||||
|
||||
const storageEntry = unsafeApi.query[pallet][item]
|
||||
|
||||
const name = `${pallet}.${item}`
|
||||
const args = value.type === "query" ? value.value : []
|
||||
const isEntries = value.type === "query" && args.length !== entry.keys.length
|
||||
const isEntries =
|
||||
(value.type === "query" && args.length !== entry.keys.length) ||
|
||||
value.type === "rawQuery"
|
||||
|
||||
const at = (blockHash: string) => {
|
||||
const value$ = from(
|
||||
isEntries
|
||||
? storageEntry.getEntries(...args, {
|
||||
at: blockHash,
|
||||
})
|
||||
: storageEntry.getValue(...args, {
|
||||
at: blockHash,
|
||||
}),
|
||||
)
|
||||
const hash$ = chainClient$.pipe(
|
||||
switchMap(({ chainHead }) =>
|
||||
chainHead
|
||||
.storage$(blockHash, "hash", (ctx) =>
|
||||
ctx.dynamicBuilder.buildStorage(pallet, item).keys.enc(...args),
|
||||
const value$ =
|
||||
value.type === "rawQuery"
|
||||
? chainHead
|
||||
.storage$(
|
||||
blockHash,
|
||||
"descendantsValues",
|
||||
() => value.value,
|
||||
null,
|
||||
(values, ctx) => {
|
||||
const codecs = ctx.dynamicBuilder.buildStorage(pallet, item)
|
||||
return values.map(({ key, value }) => ({
|
||||
keyArgs: codecs.keys.dec(key),
|
||||
value: codecs.value.dec(value),
|
||||
}))
|
||||
},
|
||||
)
|
||||
.pipe(map((r) => r.value))
|
||||
: from(
|
||||
isEntries
|
||||
? storageEntry.getEntries(...args, {
|
||||
at: blockHash,
|
||||
})
|
||||
: storageEntry.getValue(...args, {
|
||||
at: blockHash,
|
||||
}),
|
||||
)
|
||||
.pipe(map(({ value }) => value)),
|
||||
),
|
||||
)
|
||||
const hash$ = chainHead
|
||||
.storage$(blockHash, "hash", (ctx) =>
|
||||
ctx.dynamicBuilder.buildStorage(pallet, item).keys.enc(...args),
|
||||
)
|
||||
.pipe(map(({ value }) => value))
|
||||
const ctxType$ = runtimeCtxAt$(blockHash).pipe(
|
||||
map((ctx) => {
|
||||
const ctxEntry = getStorageItem(ctx, pallet, item)
|
||||
|
||||
Reference in New Issue
Block a user