feat: bring priority to body detail

This commit is contained in:
Victor Oliva
2026-05-07 09:21:47 +02:00
parent c38cb75ddf
commit dac53e22e0
7 changed files with 276 additions and 187 deletions

View File

@@ -1,5 +1,5 @@
{
"version": "0.1.0-autogenerated.11438231719699111413",
"version": "0.1.0-autogenerated.6048354284411761388",
"name": "@polkadot-api/descriptors",
"files": [
"dist"

View File

@@ -10,4 +10,5 @@ export const whitelist: Polkadot_peopleWhitelistEntry[] = [
"tx.Balances.transfer_keep_alive",
"query.Session.Validators",
"query.Session.CurrentIndex",
"api.TransactionPaymentApi.query_info",
]

View File

@@ -131,7 +131,7 @@ export const BlockBody: FC = () => {
)}
</Tabs.List>
<Tabs.Content value="tx" className="py-2">
<ol>
<ol className="space-y-2">
{extrinsics.map((extrinsic, idx) => (
<Extrinsic
key={idx}

View File

@@ -11,8 +11,9 @@ import { DecodedExtrinsic } from "@polkadot-api/tx-utils"
import { Edit, FileSearch } from "lucide-react"
import { Enum, HexString, SS58String } from "polkadot-api"
import { toHex } from "polkadot-api/utils"
import { FC, useEffect, useRef, useState } from "react"
import { FC, ReactNode, useEffect, useRef, useState } from "react"
import { twMerge } from "tailwind-merge"
import { PriorityValue } from "../../Extrinsics/Analyzer/Priority"
import { SignedExtensions } from "./SignedExtensions"
export type ApplyExtrinsicEvent = SystemEvent & {
@@ -35,20 +36,26 @@ export const Extrinsic: FC<{
events.includes(highlightedEvent as ApplyExtrinsicEvent))
),
)
const rawExtrinsic = toHex(extrinsic.raw)
const txPayment =
"extra" in extrinsic
? (extrinsic.extra.ChargeAssetTxPayment ??
extrinsic.extra.ChargeTxPayment)
: undefined
return (
<li className="p-2 border rounded mb-2 bg-card text-card-foreground">
<div className="flex justify-between items-center">
<li className="overflow-hidden rounded-lg border border-foreground/10 bg-card text-card-foreground">
<div className="flex items-center justify-between gap-2 px-2 py-2">
<button
onClick={() => setExpanded((e) => !e)}
className="flex gap-1 items-center"
className="flex min-w-0 items-center gap-1 text-left"
>
<ExpandBtn expanded={expanded} />
{extrinsic.idx}. {extrinsic.call.type}.{extrinsic.call.value.type}
</button>
<div className="flex gap-2 items-center">
<CopyBinary value={extrinsic.callData} />
<Link to={`/extrinsics/analyzer#extrinsic=${toHex(extrinsic.raw)}`}>
<Link to={`/extrinsics/analyzer#extrinsic=${rawExtrinsic}`}>
<FileSearch size={15} />
</Link>
<Link to={"/extrinsics#data=" + toHex(extrinsic.callData)}>
@@ -57,21 +64,38 @@ export const Extrinsic: FC<{
</div>
</div>
{expanded ? (
<div className="overflow-hidden">
<div className="flex gap-2 items-center py-2">
Extrinsic Hash: {shortStr(extrinsic.hash, 6)}{" "}
<CopyText text={extrinsic.hash} binary />
<div className="space-y-2 border-t border-foreground/10 px-2 py-2">
<div className="grid gap-2 lg:grid-cols-[minmax(0,1.25fr)_minmax(0,1fr)_auto]">
{extrinsic.type === "signed" ? (
<CompactBlock label="Signer">
<Sender compact sender={extrinsic.address} />
</CompactBlock>
) : null}
<CompactBlock label="Extrinsic Hash">
<div className="flex min-w-0 items-center gap-2">
<span className="truncate font-mono text-sm">
{shortStr(extrinsic.hash, 6)}
</span>
<CopyText text={extrinsic.hash} binary />
</div>
</CompactBlock>
<CompactBlock label="Priority">
<PriorityValue
extrinsic={rawExtrinsic}
txPayment={txPayment ?? {}}
/>
</CompactBlock>
</div>
{extrinsic.type === "signed" && (
<div>
<Sender sender={extrinsic.address} />
<CompactSection title="Call Payload">
<div className="max-h-[80vh] overflow-auto">
<JsonDisplay src={extrinsic.call.value.value} />
</div>
)}
<div className="overflow-auto max-h-[80vh] p-2">
<JsonDisplay src={extrinsic.call.value.value} />
</div>
<div className="p-2 overflow-auto max-h-[80vh] border-t">
</CompactSection>
<CompactSection title={`Events (${events.length})`}>
<ol className="flex flex-col gap-1">
{events.map((evt, i) => (
<EventDisplay
@@ -82,11 +106,12 @@ export const Extrinsic: FC<{
/>
))}
</ol>
</div>
</CompactSection>
{"extra" in extrinsic && (
<div className="p-2 overflow-auto max-h-[80vh] border-t">
<SignedExtensions extra={extrinsic.extra} />
</div>
<CompactSection title="Signed Extensions">
<SignedExtensions extra={extrinsic.extra} title={false} />
</CompactSection>
)}
</div>
) : null}
@@ -141,12 +166,16 @@ export const senderToAddress = (
const Sender: React.FC<{
sender: Enum<{ Id: SS58String }> | SS58String | HexString
}> = ({ sender }) => {
compact?: boolean
}> = ({ sender, compact }) => {
const value = senderToAddress(sender)
return (
value && (
<div className="flex gap-2 items-center py-2">
Signer:
<div
className={
compact ? "flex items-center gap-2" : "flex items-center gap-2 py-2"
}
>
{value.startsWith("0x") ? (
<EthAccountDisplay value={value} />
) : (
@@ -156,3 +185,27 @@ const Sender: React.FC<{
)
)
}
const CompactSection: FC<{ title: string; children: ReactNode }> = ({
title,
children,
}) => (
<div className="rounded-lg border border-foreground/10 bg-foreground/3 px-2 py-2">
<div className="mb-2 text-[11px] font-semibold uppercase tracking-[0.18em] text-foreground/50">
{title}
</div>
{children}
</div>
)
const CompactBlock: FC<{ label: string; children: ReactNode }> = ({
label,
children,
}) => (
<div className="rounded-lg border border-foreground/10 bg-foreground/3 px-2 py-2">
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-foreground/50">
{label}
</div>
<div className="mt-1">{children}</div>
</div>
)

View File

@@ -47,7 +47,7 @@ const SignedExtension: FC<{ id: string; value: unknown }> = ({ id, value }) => {
}
const inlineJson = JSON.stringify(value, jsonSerialize)
return !inlineJson || inlineJson.length < 40 ? (
return !inlineJson || inlineJson.length < 30 ? (
<InlineLine id={id} inlineContent={inlineJson} />
) : (
<ExpandableLine id={id} inlineContent={null}>
@@ -77,10 +77,14 @@ const ExpandableLine: FC<
return (
<li className="space-y-2 rounded-lg border border-foreground/10 bg-foreground/5 px-3 py-2">
<div className="flex gap-2 items-center">
<ExpandBtn expanded={expanded} onClick={() => setExpanded((e) => !e)} />
<ExpandBtn
className="shrink-0"
expanded={expanded}
onClick={() => setExpanded((e) => !e)}
/>
{id}
{inlineContent ? (
<div className="max-w-full whitespace-nowrap font-mono text-sm">
<div className="max-w-full whitespace-nowrap font-mono text-sm overflow-hidden text-ellipsis">
- {inlineContent}
</div>
) : null}
@@ -94,13 +98,13 @@ const InlineLine: FC<{ id: string; inlineContent?: ReactNode }> = ({
id,
inlineContent,
}) => (
<li className="flex flex-wrap items-center gap-2 rounded-lg border border-foreground/10 bg-foreground/5 px-3 py-2">
<li className="flex items-center gap-2 rounded-lg border border-foreground/10 bg-foreground/5 px-3 py-2">
<div className="flex gap-2 items-center">
<Dot size={16} />
{id}
</div>
{inlineContent ? (
<div className="max-w-full whitespace-nowrap font-mono text-sm">
<div className="max-w-full whitespace-nowrap font-mono text-sm overflow-hidden text-ellipsis">
- {inlineContent}
</div>
) : null}

View File

@@ -14,7 +14,7 @@ import { fromHex, toHex } from "polkadot-api/utils"
import { FC, ReactNode, useMemo } from "react"
import { map, merge, switchMap } from "rxjs"
import { senderToAddress } from "../../Explorer/Detail/Extrinsic"
import { AnalyzePriority, analyzePriority$ } from "./Priority"
import { AnalyzePriority } from "./Priority"
import { selectedBlock$, selectedBlockHex$ } from "./selectedBlock"
const extDecoder$ = selectedBlock$.pipeState(
@@ -72,6 +72,10 @@ export const ExtrinsicDecoder: FC<{
const decoded = decodeResult.value
const signerAddress =
decoded.type === "signed" ? senderToAddress(decoded.address) : null
const txPayment =
"extra" in decoded
? (decoded.extra.ChargeAssetTxPayment ?? decoded.extra.ChargeTxPayment)
: null
return (
<BlockContext value={block}>
@@ -109,6 +113,11 @@ export const ExtrinsicDecoder: FC<{
</div>
</SectionCard>
<CallData
call={decoded.call as TxCallData}
callData={decoded.callData}
/>
{decoded.type === "signed" ? (
<SectionCard title="Signed Extensions">
<SignedExtensions extra={decoded.extra} title={false} />
@@ -121,12 +130,8 @@ export const ExtrinsicDecoder: FC<{
</SectionCard>
) : null}
<SectionCard title="Priority Analysis">
<AnalyzePriority extrinsic={extrinsic} />
<AnalyzePriority extrinsic={extrinsic} txPayment={txPayment} />
</SectionCard>
<CallData
call={decoded.call as TxCallData}
callData={decoded.callData}
/>
</div>
</BlockContext>
)
@@ -166,7 +171,7 @@ const CallData: FC<{ call: TxCallData; callData: Uint8Array }> = ({
</SectionCard>
)
export const extrinsicDecoder$ = merge(extDecoder$, analyzePriority$)
export const extrinsicDecoder$ = extDecoder$
const SectionCard: FC<{
children: ReactNode

View File

@@ -1,177 +1,107 @@
import { TokenAmount } from "@/components/TokenAmount"
import { BlockContext } from "@/pages/Explorer/Detail/blockContext"
import { client$ } from "@/state/chains/chain.state"
import { polkadot_people } from "@polkadot-api/descriptors"
import { state, useStateObservable } from "@react-rxjs/core"
import { Binary, HexString } from "polkadot-api"
import { FC, ReactNode, useEffect, useState } from "react"
import { combineLatest, firstValueFrom, switchMap } from "rxjs"
import { selectedBlockHex$ } from "./selectedBlock"
import { TokenAmount } from "@/components/TokenAmount"
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: length.max,
maxWeight: weights.max_block,
}),
() => null,
),
),
),
)
import { FC, ReactNode, useContext, useEffect, useState } from "react"
export 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$)
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
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
const limitingFactor =
maxTxPerBlockWeight < maxTxPerBlockLength ? "Weight" : "Length"
return {
priority,
maxTxPerBlockLength,
maxTxPerBlockWeight,
maxTxPerBlock,
limitingFactor,
}
})()
const blockHex = useContext(BlockContext)?.hash
const details = usePriorityAnalysis({
txPayment,
extrinsic,
blockHex,
})
return (
<div className="space-y-4">
{priority ? (
<div className="flex flex-col gap-2 border-b border-foreground/10 pb-4 lg:flex-row lg:items-end lg:justify-between">
<div>
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-foreground/50">
Computed Priority
</div>
<div className="mt-1 font-mono text-3xl font-semibold tracking-tight text-foreground sm:text-4xl">
{priority.priority.toLocaleString()}
</div>
</div>
</div>
) : null}
<div className="grid gap-4 lg:grid-cols-[minmax(0,1.1fr)_minmax(18rem,0.9fr)]">
<div className="rounded-lg border border-foreground/10 bg-foreground/5 p-3">
<div className="flex flex-col gap-2 border-b border-foreground/10 pb-4 lg:flex-row lg:items-end lg:justify-between">
<div>
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-foreground/50">
Inputs
Computed Priority
</div>
<div className="mt-3 space-y-2">
<DetailRow label="Tip" value={<TokenAmount>{tip}</TokenAmount>} />
<DetailRow
label="Partial Fee"
value={<TokenAmount>{queryInfo.partial_fee}</TokenAmount>}
/>
<DetailRow label="Class" value={queryInfo.class.type} />
<DetailRow label="Encoded Length" value={length.toLocaleString()} />
<DetailRow
label="Weight"
value={`${(Number(queryInfo.weight.proof_size) / 1024).toLocaleString(undefined, { maximumSignificantDigits: 3 })} KB / ${(Number(queryInfo.weight.ref_time) / 1_000_000).toLocaleString(undefined, { maximumSignificantDigits: 3 })} ms`}
/>
</div>
</div>
<div className="rounded-lg border border-foreground/10 bg-foreground/5 p-3">
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-foreground/50">
Capacity
</div>
<div className="mt-3 space-y-2">
<DetailRow
label="Tx/Block by Length"
value={
priority ? priority.maxTxPerBlockLength.toLocaleString() : "N/A"
}
/>
<DetailRow
label="Tx/Block by Weight"
value={
priority ? priority.maxTxPerBlockWeight.toLocaleString() : "N/A"
}
/>
<DetailRow
label="Limited By"
value={priority ? priority.limitingFactor : "N/A"}
/>
<div className="mt-1 font-mono text-3xl font-semibold tracking-tight text-foreground sm:text-4xl">
{details?.priority.toLocaleString() ?? "…"}
</div>
</div>
</div>
{!priority ? (
{details ? (
<div className="grid gap-4 lg:grid-cols-[minmax(0,1.1fr)_minmax(18rem,0.9fr)]">
<div className="rounded-lg border border-foreground/10 bg-foreground/5 p-3">
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-foreground/50">
Inputs
</div>
<div className="mt-3 space-y-2">
<DetailRow
label="Tip"
value={<TokenAmount>{details.tip}</TokenAmount>}
/>
<DetailRow
label="Fee"
value={
<TokenAmount>{details.queryInfo.partial_fee}</TokenAmount>
}
/>
<DetailRow label="Class" value={details.queryInfo.class.type} />
<DetailRow
label="Encoded Length"
value={length.toLocaleString()}
/>
<DetailRow
label="Weight"
value={`${(Number(details.queryInfo.weight.proof_size) / 1024).toLocaleString(undefined, { maximumSignificantDigits: 3 })} KB / ${(Number(details.queryInfo.weight.ref_time) / 1_000_000).toLocaleString(undefined, { maximumSignificantDigits: 3 })} Mref`}
/>
</div>
</div>
<div className="rounded-lg border border-foreground/10 bg-foreground/5 p-3">
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-foreground/50">
Capacity
</div>
<div className="mt-3 space-y-2">
<DetailRow
label="Tx/Block by Length"
value={details.maxTxPerBlockLength.toLocaleString()}
/>
<DetailRow
label="Tx/Block by Weight"
value={details.maxTxPerBlockWeight.toLocaleString()}
/>
<DetailRow label="Limited By" value={details.limitingFactor} />
</div>
</div>
</div>
) : (
<div className="rounded-lg border border-foreground/10 bg-background/60 p-3 text-xs text-muted-foreground">
Waiting for block limits to finish the priority calculation.
</div>
) : null}
)}
</div>
)
}
export const analyzePriority$ = maxBlockSize$
export const PriorityValue: FC<{
txPayment?: { tip?: bigint }
extrinsic: HexString
}> = ({ txPayment, extrinsic }) => {
const blockHex = useContext(BlockContext)?.hash
const details = usePriorityAnalysis({
txPayment,
extrinsic,
blockHex,
})
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
return details ? (
<span className="font-mono text-sm text-foreground">
{details.priority.toLocaleString()}
</span>
) : (
<span className="text-sm text-muted-foreground"></span>
)
}
const DetailRow: FC<{ label: string; value: ReactNode }> = ({
@@ -183,3 +113,99 @@ const DetailRow: FC<{ label: string; value: ReactNode }> = ({
<div className="text-right font-mono text-sm text-foreground">{value}</div>
</div>
)
const getPriority = async (
extrinsic: HexString,
txPayment: { tip?: bigint } | undefined,
at?: HexString,
) => {
const client = await client$.getValue()
const typedApi = client.getTypedApi(polkadot_people)
const extLength = (extrinsic.length - 2) / 8
const tip = txPayment?.tip ?? 0n
try {
const [length, weights, queryInfo] = await Promise.all([
typedApi.constants.System.BlockLength({
at,
}),
typedApi.constants.System.BlockWeights({
at,
}),
typedApi.apis.TransactionPaymentApi.query_info(
Binary.fromOpaque(extrinsic),
extLength,
{
at,
},
),
])
const maxLength =
length.max[
queryInfo.class.type.toLocaleLowerCase() as keyof typeof length.max
]
const maxTxPerBlockWeight = divWeight(weights.max_block, queryInfo.weight)
const maxTxPerBlockLength = BigInt(Math.floor(maxLength / extLength))
const maxTxPerBlock =
maxTxPerBlockWeight < maxTxPerBlockLength
? maxTxPerBlockWeight
: maxTxPerBlockLength
const priority = (tip + 1n) * maxTxPerBlock
const limitingFactor =
maxTxPerBlockWeight < maxTxPerBlockLength ? "Weight" : "Length"
return {
priority,
length: extLength,
queryInfo,
tip,
maxTxPerBlockLength,
maxTxPerBlockWeight,
maxTxPerBlock,
limitingFactor,
}
} catch (ex) {
console.error(ex)
if (at) {
// Might be the block was already unpinned and no archive support, try with latest block
return getPriority(extrinsic, txPayment)
}
return null
}
}
type PriorityDetails = Awaited<ReturnType<typeof getPriority>>
const usePriorityAnalysis = ({
txPayment,
extrinsic,
blockHex,
}: {
txPayment?: { tip?: bigint }
extrinsic: HexString
blockHex?: string
}) => {
const [result, setResult] = useState<PriorityDetails | null>(null)
useEffect(() => {
let cancelled = false
getPriority(extrinsic, txPayment, blockHex).then((r) => {
if (cancelled) return
setResult(r)
})
return () => {
cancelled = true
}
}, [blockHex, extrinsic, txPayment])
return result
}
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
}