diff --git a/.papi/descriptors/package.json b/.papi/descriptors/package.json index f9dadde..79e4920 100644 --- a/.papi/descriptors/package.json +++ b/.papi/descriptors/package.json @@ -1,5 +1,5 @@ { - "version": "0.1.0-autogenerated.8379875965201711085", + "version": "0.1.0-autogenerated.403250971526209557", "name": "@polkadot-api/descriptors", "files": [ "dist" diff --git a/.papi/whitelist.ts b/.papi/whitelist.ts index 3068661..6ef714c 100644 --- a/.papi/whitelist.ts +++ b/.papi/whitelist.ts @@ -4,6 +4,9 @@ export const whitelist: Polkadot_peopleWhitelistEntry[] = [ "query.Identity.IdentityOf", "query.Identity.SuperOf", "query.System.Account", + "query.System.Number", + "const.System.BlockLength", + "const.System.BlockWeights", "tx.Balances.transfer_keep_alive", "query.Session.Validators", "query.Session.CurrentIndex", diff --git a/src/pages/Explorer/Detail/BlockBody.tsx b/src/pages/Explorer/Detail/BlockBody.tsx index 5134c94..b4c2653 100644 --- a/src/pages/Explorer/Detail/BlockBody.tsx +++ b/src/pages/Explorer/Detail/BlockBody.tsx @@ -1,9 +1,13 @@ import { Chopsticks } from "@/components/Icons" import { Loading } from "@/components/Loading" +import { getHashParams } from "@/hashParams" import { groupBy } from "@/lib/groupBy" import { runtimeCtx$, runtimeCtxAt$ } from "@/state/chains/chain.state" +import { Blake2256 } from "@polkadot-api/substrate-bindings" +import { getExtrinsicDecoder } from "@polkadot-api/tx-utils" import * as Tabs from "@radix-ui/react-tabs" import { state, useStateObservable } from "@react-rxjs/core" +import { toHex } from "polkadot-api/utils" import { FC, useState } from "react" import { useLocation } from "react-router-dom" import { catchError, combineLatest, filter, map, take } from "rxjs" @@ -12,9 +16,6 @@ import { BlockInfo, blockInfoState$ } from "../block.state" import { BlockEvents } from "./BlockEvents" import { BlockStorageDiff } from "./BlockStorageDiff" import { ApplyExtrinsicEvent, Extrinsic } from "./Extrinsic" -import { getHashParams } from "@/hashParams" -import { Blake2256 } from "@polkadot-api/substrate-bindings" -import { toHex } from "polkadot-api/utils" const blockExtrinsics$ = state((hash: string) => { const body$ = blockInfoState$(hash).pipe( @@ -25,10 +26,13 @@ const blockExtrinsics$ = state((hash: string) => { return combineLatest([ body$, - runtimeCtxAt$(hash).pipe(catchError(() => runtimeCtx$)), + runtimeCtxAt$(hash).pipe( + catchError(() => runtimeCtx$), + map((v) => getExtrinsicDecoder(v.metadataRaw)), + ), ]).pipe( take(1), - map(([body, { txDecoder }]) => + map(([body, txDecoder]) => body.map((raw, idx) => ({ idx, hash: toHex(Blake2256(raw)), diff --git a/src/pages/Explorer/Detail/Extrinsic.tsx b/src/pages/Explorer/Detail/Extrinsic.tsx index 97a5a7c..59e7739 100644 --- a/src/pages/Explorer/Detail/Extrinsic.tsx +++ b/src/pages/Explorer/Detail/Extrinsic.tsx @@ -92,7 +92,7 @@ export const Extrinsic: FC<{ ) } -const SignedExtensions: FC<{ extra: Record }> = ({ +export const SignedExtensions: FC<{ extra: Record }> = ({ extra, }) => (
@@ -173,7 +173,7 @@ export const EventDisplay: FC<{ ) } -const Sender: React.FC<{ +export const Sender: React.FC<{ sender: Enum<{ Id: SS58String }> | SS58String | HexString }> = ({ sender }) => { const value: string | null = diff --git a/src/pages/Extrinsics/ExtrinsicAnalyzer.tsx b/src/pages/Extrinsics/ExtrinsicAnalyzer.tsx new file mode 100644 index 0000000..acc971c --- /dev/null +++ b/src/pages/Extrinsics/ExtrinsicAnalyzer.tsx @@ -0,0 +1,318 @@ +import { CopyBinary } from "@/codec-components/ViewCodec/CopyBinary" +import { JsonDisplay } from "@/components/JsonDisplay" +import { TextInputField } from "@/components/TextInputField" +import { Button } from "@/components/ui/button" +import { client$ } from "@/state/chains/chain.state" +import { polkadot_people } from "@polkadot-api/descriptors" +import { DecodedExtrinsic, getExtrinsicDecoder } from "@polkadot-api/tx-utils" +import { state, Subscribe, useStateObservable } from "@react-rxjs/core" +import { ChevronLeft } from "lucide-react" +import { Binary, HexString, TxCallData } from "polkadot-api" +import { toHex } from "polkadot-api/utils" +import { FC, useEffect, useMemo, useState } from "react" +import { combineLatest, firstValueFrom, map, merge, switchMap } from "rxjs" +import { Sender, SignedExtensions } from "../Explorer/Detail/Extrinsic" +import { BlockPicker, selectedBlock$ } from "../Storage/BlockPicker" + +const extDecoder$ = state( + selectedBlock$.pipe( + map((block) => getExtrinsicDecoder(block.ctx.metadataRaw)), + ), +) +const selectedBlockHex$ = state(selectedBlock$.pipe(map((v) => v.hash))) + +export const ExtrinsicAnalyzer: FC<{ onClose: () => void }> = ({ onClose }) => { + const [extrinsicHex, setExtrinsicHex] = useState("") + + return ( +
+
+ +

Analyze Extrinsic

+
+
+
+ +
+
+ +
+
+ + {extrinsicHex ? : null} + +
+ ) +} + +const DecodeExtrinsic: FC<{ + extrinsic: HexString +}> = ({ extrinsic }) => { + const extrinsicDecoder = useStateObservable(extDecoder$) + const decodeResult = useMemo(() => { + try { + return { + type: "success" as const, + value: extrinsicDecoder(extrinsic), + } + } catch (ex) { + console.error(ex) + return { + type: "error" as const, + value: ex as Error, + } + } + }, [extrinsicDecoder, extrinsic]) + + if (decodeResult.type === "error") { + return
Can't decode: {decodeResult.value.message}
+ } + + const decoded = decodeResult.value + + return ( +
+

+ {decoded.type} Transaction v{decoded.version} +

+ {decoded.type === "signed" ? ( + + ) : decoded.type === "general" ? ( +
TODO
+ ) : ( + + )} + +
+ ) +} + +const SignedInfo: FC<{ + extrinsic: HexString + decoded: DecodedExtrinsic & { type: "signed" } +}> = ({ extrinsic, decoded }) => { + const mortality = decoded.extra.CheckMortality + const txPayment = + decoded.extra.ChargeAssetTxPayment ?? decoded.extra.ChargeTxPayment + + return ( +
+ + + {mortality ? : null} + +
+ ) +} + +const CallData: FC<{ call: TxCallData; callData: Uint8Array }> = ({ + call, + callData, +}) => ( +
+
+
+ {call.type}.{call.value.type} +
+
+
+ {toHex(callData)} +
+ +
+
+ +
+) + +const selectedBlockNumber$ = state( + combineLatest([selectedBlockHex$, client$]).pipe( + switchMap(([selectedBlock, client]) => + selectedBlock + ? (client + .getTypedApi(polkadot_people) + .query.System.Number.getValue({ + at: selectedBlock, + }) + .catch(() => null) as Promise) + : client.finalizedBlock$.pipe(map((block) => block.number)), + ), + ), +) + +const AnalyzeMortality: FC<{ mortality: { type: string; value: number } }> = ({ + mortality, +}) => { + const selectedBlockNumber = useStateObservable(selectedBlockNumber$) + + if (mortality.type === "Immortal") return null + const parseResult = /^Mortal(\d+)$/.exec(mortality.type) + if (parseResult === null) return null + const first = BigInt(parseResult[1]) + const second = BigInt(mortality.value) + // from polkadot-sdk primitives runtime generic era fn decode + const encoded = first + (second << 8n) + const period = Number(2n << (encoded % (1n << 4n))) + const factor = period >> 12 || 1 + const phase = Number(encoded >> 4n) * factor + + // From fn birth + const birthBlock = selectedBlockNumber + ? Math.floor((Math.max(selectedBlockNumber, phase) - phase) / period) * + period + + phase + : null + + const referencedBlocks = birthBlock + ? new Array(5) + .fill(0) + .map((_, i) => (birthBlock - period * i).toLocaleString()) + : null + + return ( +
+ Mortality: period={period} phase={phase}{" "} + {referencedBlocks ? ( + <> + currentBlock={selectedBlockNumber?.toLocaleString()} nextPeriod= + {(birthBlock! + period).toLocaleString()} oldPeriods= + {referencedBlocks.join(" ")} + + ) : null} +
+ ) +} + +const maxBlockSize$ = state( + combineLatest([selectedBlockHex$, client$]).pipe( + switchMap(([selectedBlock, client]) => + Promise.all([ + client.getTypedApi(polkadot_people).constants.System.BlockLength({ + at: selectedBlock, + }), + client.getTypedApi(polkadot_people).constants.System.BlockWeights({ + at: selectedBlock, + }), + ]).then( + ([length, weights]) => ({ + length, + maxWeight: weights.max_block, + }), + () => null, + ), + ), + ), +) + +const AnalyzePriority: FC<{ + txPayment?: { tip?: bigint } + extrinsic: HexString +}> = ({ txPayment, extrinsic }) => { + const blockHex = useStateObservable(selectedBlockHex$) + const maxBlockSize = useStateObservable(maxBlockSize$) + const [queryInfo, setQueryInfo] = useState<{ + weight: Weight + class: { type: "Normal" | "Operational" | "Mandatory" } + partial_fee: bigint + } | null>(null) + const length = (extrinsic.length - 2) / 8 + const tip = txPayment?.tip ?? 0n + + useEffect(() => { + let cancelled = false + ;(async () => { + setQueryInfo(null) + const client = await firstValueFrom(client$) + console.log(extrinsic.length) + const result = await client + .getUnsafeApi() + .apis.TransactionPaymentApi.query_info( + Binary.fromOpaque(extrinsic), + length, + { + at: blockHex, + }, + ) + if (!cancelled) { + setQueryInfo(result as any) + } + })() + return () => { + cancelled = true + } + }, [extrinsic, blockHex, length]) + + if (!queryInfo) return null + + const priority = (() => { + if (!maxBlockSize) return null + console.log(queryInfo) + const maxLength = + maxBlockSize.length[ + queryInfo.class.type.toLocaleLowerCase() as keyof typeof maxBlockSize.length + ] + + const maxTxPerBlockWeight = divWeight( + maxBlockSize.maxWeight, + queryInfo.weight, + ) + const maxTxPerBlockLength = BigInt(Math.floor(maxLength / length)) + const maxTxPerBlock = + maxTxPerBlockWeight < maxTxPerBlockLength + ? maxTxPerBlockWeight + : maxTxPerBlockLength + const priority = (tip + 1n) * maxTxPerBlock + + return { priority, maxTxPerBlockLength, maxTxPerBlockWeight } + })() + + return ( +
+ Priority: tip={tip.toLocaleString()} fee= + {queryInfo.partial_fee.toLocaleString()} class={queryInfo.class.type}{" "} + weight= + {queryInfo.weight.proof_size.toLocaleString() + + "/" + + queryInfo.weight.ref_time.toLocaleString()}{" "} + {priority ? ( + <> + txPerBlockLength={priority.maxTxPerBlockLength.toLocaleString()}{" "} + txPerBlockWeight={priority.maxTxPerBlockWeight.toLocaleString()}{" "} + priority={priority.priority.toLocaleString()} + + ) : null} +
+ ) +} + +const extAnalyzer$ = merge( + extDecoder$, + selectedBlockHex$, + selectedBlockNumber$, + maxBlockSize$, +) + +type Weight = { ref_time: bigint; proof_size: bigint } +const divWeight = (a: Weight, b: Weight) => { + const ref_time = b.ref_time === 0n ? 1n : a.ref_time / b.ref_time + const proof_size = b.proof_size === 0n ? 1n : a.proof_size / b.proof_size + return ref_time < proof_size ? ref_time : proof_size +} diff --git a/src/pages/Extrinsics/Extrinsics.tsx b/src/pages/Extrinsics/Extrinsics.tsx index 7f015b9..da0e0e0 100644 --- a/src/pages/Extrinsics/Extrinsics.tsx +++ b/src/pages/Extrinsics/Extrinsics.tsx @@ -1,4 +1,5 @@ import { BinaryDisplay } from "@/codec-components/LookupTypeEdit" +import { ActionButton } from "@/components/ActionButton" import { ButtonGroup } from "@/components/ButtonGroup" import { LoadingMetadata } from "@/components/Loading" import { withSubscribe } from "@/components/withSuspense" @@ -8,18 +9,18 @@ import { CodecComponentType, CodecComponentValue, } from "@polkadot-api/react-builder" -import { fromHex, toHex } from "polkadot-api/utils" import { state, useStateObservable } from "@react-rxjs/core" +import { FileSearch, Settings } from "lucide-react" +import { fromHex, toHex } from "polkadot-api/utils" import { useLayoutEffect, useState } from "react" import { useLocation } from "react-router-dom" import { map } from "rxjs" import { twMerge } from "tailwind-merge" +import { ExtrinsicAnalyzer } from "./ExtrinsicAnalyzer" +import { CustomSignedExt, customSignedExtensions$ } from "./CustomSignedExt" import { EditMode } from "./EditMode" import { JsonMode } from "./JsonMode" import { ExtrinsicModal } from "./SubmitTx/SubmitTx" -import { ActionButton } from "@/components/ActionButton" -import { Settings } from "lucide-react" -import { CustomSignedExt, customSignedExtensions$ } from "./CustomSignedExt" const extrinsicProps$ = state( runtimeCtx$.pipe( @@ -55,7 +56,7 @@ const customExtensionsCount$ = state( export const Extrinsics = withSubscribe( () => { const [viewMode, setViewMode] = useState<"edit" | "json">("edit") - const [editingExtensions, setEditingExtensions] = useState(false) + const [page, setPage] = useState<"analyze" | "extensions" | null>(null) const extrinsicProps = useStateObservable(extrinsicProps$) const location = useLocation() @@ -83,8 +84,10 @@ export const Extrinsics = withSubscribe( } }, [binaryValue]) - if (editingExtensions) - return setEditingExtensions(false)} /> + if (page === "extensions") + return setPage(null)} /> + if (page === "analyze") + return setPage(null)} /> return (
+ setPage("analyze")} + > + + setEditingExtensions(true)} + onClick={() => setPage("extensions")} > {customExtensionsCount$} diff --git a/src/pages/Storage/BlockPicker.tsx b/src/pages/Storage/BlockPicker.tsx index 7ad0337..abbe253 100644 --- a/src/pages/Storage/BlockPicker.tsx +++ b/src/pages/Storage/BlockPicker.tsx @@ -1,6 +1,9 @@ import { Spinner } from "@/components/Icons" -import { runtimeCtx$, runtimeCtxAt$ } from "@/state/chains/chain.state" -import { RuntimeContext } from "@polkadot-api/observable-client" +import { + CachedRuntime, + runtimeCtx$, + runtimeCtxAt$, +} from "@/state/chains/chain.state" import { Input } from "@polkahub/ui-components" import { liftSuspense, @@ -29,7 +32,7 @@ const loadedCtx$ = state( debounceTime(200), switchMap((v) => { const inner$: Observable<{ - ctx: Pick + ctx: CachedRuntime hash?: string }> = !v.trim() || v === "Latest" diff --git a/src/state/chains/chain.state.ts b/src/state/chains/chain.state.ts index 595d348..672c730 100644 --- a/src/state/chains/chain.state.ts +++ b/src/state/chains/chain.state.ts @@ -1,18 +1,21 @@ import { createChopsticksProvider } from "@/chopsticks/chopsticks" import { getHashParams, setHashParams } from "@/hashParams" import { getDynamicBuilder, getLookupFn } from "@polkadot-api/metadata-builders" +import type { + ChainHead$, + RuntimeContext, +} from "@polkadot-api/observable-client" import { decAnyMetadata, HexString, unifyMetadata, } from "@polkadot-api/substrate-bindings" -import { getExtrinsicDecoder } from "@polkadot-api/tx-utils" -import { fromHex, toHex } from "polkadot-api/utils" import { liftSuspense, sinkSuspense, state, SUSPENSE } from "@react-rxjs/core" import { createSignal } from "@react-rxjs/utils" import { get, update } from "idb-keyval" import { createClient } from "polkadot-api" import { withLogsRecorder } from "polkadot-api/logs-provider" +import { fromHex, toHex } from "polkadot-api/utils" import { catchError, concat, @@ -47,7 +50,6 @@ import { getWebsocketProvider, WebsocketSource, } from "./websocket" -import type { ChainHead$ } from "@polkadot-api/observable-client" export type ChainSource = WebsocketSource | SmoldotSource @@ -254,14 +256,6 @@ const uncachedRuntimeCtx$ = chainClient$.pipeState( filter(Boolean), ) -const withTxDecoder: ( - input: Observable, -) => Observable }> = - map((ctx) => ({ - ...ctx, - txDecoder: getExtrinsicDecoder(ctx.metadataRaw), - })) - export const runtimeCtxAt$ = state((atBlock: string) => chainClient$.pipe( take(1), @@ -272,17 +266,20 @@ export const runtimeCtxAt$ = state((atBlock: string) => client.chainHead.getRuntimeContext$(atBlock) ) }), - withTxDecoder, ), ) +export type CachedRuntime = Pick< + RuntimeContext, + "metadataRaw" | "lookup" | "dynamicBuilder" +> export const runtimeCtx$ = chainClient$.pipeState( switchMap(({ id }) => get(IDB_KEY).then((cache) => cache ? [...cache.entries()].find(([, v]) => v.id === id) : undefined, ), ), - switchMap((cached) => { + switchMap((cached): Observable => { if (!cached) return uncachedRuntimeCtx$ const metadata = unifyMetadata(decAnyMetadata(cached[1].data)) const lookup = getLookupFn(metadata) @@ -296,7 +293,6 @@ export const runtimeCtx$ = chainClient$.pipeState( }), ) }), - withTxDecoder, ) export const lookup$ = runtimeCtx$.pipeState(map((ctx) => ctx.lookup))