improve UI
This commit is contained in:
@@ -18,7 +18,9 @@ export const TokenAmount: FC<{
|
||||
const formattedValue = (
|
||||
Number(children) /
|
||||
10 ** properties.tokenDecimals
|
||||
).toLocaleString(undefined)
|
||||
).toLocaleString(undefined, {
|
||||
maximumSignificantDigits: 3,
|
||||
})
|
||||
|
||||
return (
|
||||
<span
|
||||
|
||||
@@ -92,9 +92,12 @@ export const useSyncHashParam = <T extends any[]>(
|
||||
|
||||
useLayoutEffect(
|
||||
() =>
|
||||
setHashParams({
|
||||
[key]: getFn(...dependencies),
|
||||
}),
|
||||
setHashParams(
|
||||
{
|
||||
[key]: getFn(...dependencies),
|
||||
},
|
||||
location,
|
||||
),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
dependencies,
|
||||
)
|
||||
@@ -116,6 +119,6 @@ export const useHashParamState = <T extends string | null>(
|
||||
|
||||
return [
|
||||
getHashParams(location).get(key) ?? init?.() ?? (null as any),
|
||||
(value: string | null) => setHashParams({ [key]: value }),
|
||||
(value: string | null) => setHashParams({ [key]: value }, location),
|
||||
] as const
|
||||
}
|
||||
|
||||
@@ -130,15 +130,19 @@ export const EventDisplay: FC<{
|
||||
)
|
||||
}
|
||||
|
||||
export const Sender: React.FC<{
|
||||
export const senderToAddress = (
|
||||
sender: Enum<{ Id: SS58String }> | SS58String | HexString,
|
||||
) =>
|
||||
typeof sender === "string"
|
||||
? sender
|
||||
: "type" in sender && sender.type === "Id"
|
||||
? sender.value
|
||||
: null
|
||||
|
||||
const Sender: React.FC<{
|
||||
sender: Enum<{ Id: SS58String }> | SS58String | HexString
|
||||
}> = ({ sender }) => {
|
||||
const value: string | null =
|
||||
typeof sender === "string"
|
||||
? sender
|
||||
: "type" in sender && sender.type === "Id"
|
||||
? sender.value
|
||||
: null
|
||||
const value = senderToAddress(sender)
|
||||
return (
|
||||
value && (
|
||||
<div className="flex gap-2 items-center py-2">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { client$ } from "@/state/chains/chain.state"
|
||||
import { useStateObservable, withDefault } from "@react-rxjs/core"
|
||||
import { jsonSerialize } from "polkadot-api/utils"
|
||||
import { FC, useContext } from "react"
|
||||
import { map, switchMap } from "rxjs"
|
||||
import { BlockContext } from "./blockContext"
|
||||
@@ -17,9 +18,31 @@ export const MortalityAnalyzer: FC<{
|
||||
const finalizedNumber = useStateObservable(finalizedNumber$)
|
||||
const selectedBlockNumber = selectedBlock?.number ?? finalizedNumber
|
||||
|
||||
if (mortality.type === "Immortal") return <div>Immortal</div>
|
||||
if (mortality.type === "Immortal") {
|
||||
return (
|
||||
<div className="rounded-lg border border-foreground/10 bg-background/60 px-3 py-2">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-foreground/50">
|
||||
Lifetime
|
||||
</div>
|
||||
<div className="mt-1 text-sm font-medium text-foreground">Immortal</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const parseResult = /^Mortal(\d+)$/.exec(mortality.type)
|
||||
if (parseResult === null) return null
|
||||
if (parseResult === null) {
|
||||
return (
|
||||
<div className="rounded-lg border border-foreground/10 bg-background/60 px-3 py-2">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-foreground/50">
|
||||
Lifetime
|
||||
</div>
|
||||
<div className="mt-1 font-mono text-sm text-foreground">
|
||||
{mortality.type} {mortality.value}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const first = BigInt(parseResult[1])
|
||||
const second = BigInt(mortality.value)
|
||||
// from polkadot-sdk primitives runtime generic era fn decode
|
||||
@@ -34,13 +57,126 @@ export const MortalityAnalyzer: FC<{
|
||||
period +
|
||||
phase
|
||||
: null
|
||||
const deathBlock = birthBlock == null ? null : birthBlock + period
|
||||
const blocksRemaining =
|
||||
birthBlock == null || deathBlock == null || selectedBlockNumber == null
|
||||
? null
|
||||
: deathBlock - selectedBlockNumber
|
||||
const contextProgress =
|
||||
birthBlock == null || deathBlock == null || selectedBlockNumber == null
|
||||
? null
|
||||
: getTimelineProgress(birthBlock, deathBlock, selectedBlockNumber)
|
||||
|
||||
return (
|
||||
<div>
|
||||
period={period} phase={phase}{" "}
|
||||
{birthBlock != null
|
||||
? `currentBlock=${selectedBlockNumber?.toLocaleString()} from=${birthBlock.toLocaleString()} to=${(birthBlock + period).toLocaleString()}`
|
||||
: null}
|
||||
<div className="space-y-3 rounded-lg border border-foreground/10 bg-background/60 px-3 py-3">
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-foreground/50">
|
||||
Lifetime
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2 sm:grid-cols-2 xl:grid-cols-4">
|
||||
<Metric label="Period" value={period.toLocaleString()} />
|
||||
<Metric label="Phase" value={phase.toLocaleString()} />
|
||||
</div>
|
||||
|
||||
<div className="rounded-md border border-foreground/10 bg-foreground/5 px-3 py-3">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-foreground/50">
|
||||
Validity Window
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{blocksRemaining == null
|
||||
? null
|
||||
: `${blocksRemaining.toLocaleString()} blocks remaining`}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<div className="relative h-3 rounded-full bg-emerald-500/50">
|
||||
{contextProgress != null ? (
|
||||
<div
|
||||
className="absolute top-1/2 -translate-y-2.5"
|
||||
style={{ left: `${contextProgress}%` }}
|
||||
>
|
||||
<div className="h-5 w-0.5 bg-foreground/60" />
|
||||
<div
|
||||
className="text-xs whitespace-nowrap font-semibold text-foreground/60"
|
||||
style={{
|
||||
transform: `translateX(${contextProgress < 10 ? "-1%" : contextProgress > 90 ? "-95%" : "-50%"})`,
|
||||
}}
|
||||
>
|
||||
{selectedBlockNumber?.toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="mt-5 grid grid-cols-[1fr_1fr] items-start gap-2 text-xs text-muted-foreground">
|
||||
<div>
|
||||
<div className="font-semibold uppercase tracking-[0.14em] text-foreground/50">
|
||||
Start
|
||||
</div>
|
||||
<div className="mt-1 font-mono text-sm text-foreground">
|
||||
{formatOptional(birthBlock)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="font-semibold uppercase tracking-[0.14em] text-foreground/50">
|
||||
End
|
||||
</div>
|
||||
<div className="mt-1 font-mono text-sm text-foreground">
|
||||
{formatOptional(deathBlock)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Metric: FC<{ label: string; value: string }> = ({ label, value }) => (
|
||||
<div className="rounded-md border border-foreground/10 bg-foreground/5 px-3 py-2">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-foreground/50">
|
||||
{label}
|
||||
</div>
|
||||
<div className="mt-1 break-all font-mono text-sm text-foreground">
|
||||
{value}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const formatOptional = (value: number | null) =>
|
||||
value == null ? "N/A" : value.toLocaleString()
|
||||
|
||||
const getTimelineProgress = (
|
||||
start: number,
|
||||
end: number,
|
||||
value: number,
|
||||
): number => {
|
||||
if (end <= start) return 0
|
||||
const raw = ((value - start) / (end - start)) * 100
|
||||
return Math.max(0, Math.min(100, raw))
|
||||
}
|
||||
|
||||
export const InlineMortality: FC<{
|
||||
mortality: { type: string; value: number }
|
||||
}> = ({ mortality }) => {
|
||||
if (mortality.type === "Immortal") return "Immortal"
|
||||
|
||||
const parseResult = /^Mortal(\d+)$/.exec(mortality.type)
|
||||
if (parseResult === null) return JSON.stringify(mortality, jsonSerialize)
|
||||
|
||||
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
|
||||
|
||||
return JSON.stringify({ period, phase }) + " " + JSON.stringify(mortality)
|
||||
}
|
||||
|
||||
@@ -1,73 +1,108 @@
|
||||
import { ExpandBtn } from "@/components/Expand"
|
||||
import { JsonDisplay } from "@/components/JsonDisplay"
|
||||
import { Dot } from "lucide-react"
|
||||
import { toHex } from "polkadot-api/utils"
|
||||
import { ComponentType, FC, useState } from "react"
|
||||
import { MortalityAnalyzer } from "./MortalityAnalyzer"
|
||||
import { jsonSerialize } from "polkadot-api/utils"
|
||||
import {
|
||||
ComponentType,
|
||||
FC,
|
||||
PropsWithChildren,
|
||||
ReactNode,
|
||||
useState,
|
||||
} from "react"
|
||||
import { InlineMortality, MortalityAnalyzer } from "./MortalityAnalyzer"
|
||||
|
||||
export const SignedExtensions: FC<{ extra: Record<string, unknown> }> = ({
|
||||
extra,
|
||||
}) => (
|
||||
export const SignedExtensions: FC<{
|
||||
extra: Record<string, unknown>
|
||||
title?: boolean
|
||||
}> = ({ extra, title = true }) => (
|
||||
<div className="space-y-2">
|
||||
<h3>Signed extensions</h3>
|
||||
<ul className="space-y-2">
|
||||
{Object.entries(extra).map(([key, value]) => {
|
||||
const KnownExtension = knownSignedExtensions[key]
|
||||
return KnownExtension ? (
|
||||
<KnownExtension key={key} id={key} value={value} />
|
||||
) : (
|
||||
<SignedExtension key={key} id={key} value={value} />
|
||||
)
|
||||
})}
|
||||
{title ? (
|
||||
<h3 className="text-sm font-semibold">Signed extensions</h3>
|
||||
) : null}
|
||||
<ul className="space-y-3">
|
||||
{Object.entries(extra).map(([key, value]) => (
|
||||
<SignedExtension key={key} id={key} value={value} />
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
|
||||
const SignedExtension: FC<{ id: string; value: unknown }> = ({ id, value }) => {
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const inlineJson = JSON.stringify(value, (_, v) =>
|
||||
typeof v === "bigint" ? String(v) : v instanceof Uint8Array ? toHex(v) : v,
|
||||
)
|
||||
|
||||
if (!inlineJson || inlineJson.length < 40) {
|
||||
return (
|
||||
<li className="flex items-center flex-wrap gap-1">
|
||||
<div className="flex gap-2 items-center">
|
||||
<Dot size={16} />
|
||||
{id}
|
||||
</div>
|
||||
{inlineJson ? (
|
||||
<div className="whitespace-nowrap">
|
||||
- <span className="font-mono text-sm">{inlineJson}</span>
|
||||
</div>
|
||||
) : null}
|
||||
</li>
|
||||
)
|
||||
const knownExtension = knownSignedExtensions[id]
|
||||
if (knownExtension) {
|
||||
const { expanded: ExpandedView, inline: InlineView } = knownExtension
|
||||
return ExpandedView ? (
|
||||
<ExpandableLine
|
||||
id={id}
|
||||
inlineContent={InlineView ? <InlineView id={id} value={value} /> : null}
|
||||
>
|
||||
<ExpandedView id={id} value={value} />
|
||||
</ExpandableLine>
|
||||
) : InlineView ? (
|
||||
<InlineLine
|
||||
id={id}
|
||||
inlineContent={InlineView ? <InlineView id={id} value={value} /> : null}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
return (
|
||||
<li className="space-y-2">
|
||||
<div className="flex gap-2 items-center">
|
||||
<ExpandBtn expanded={expanded} onClick={() => setExpanded((e) => !e)} />
|
||||
{id}
|
||||
</div>
|
||||
{expanded && <JsonDisplay src={value} />}
|
||||
</li>
|
||||
|
||||
const inlineJson = JSON.stringify(value, jsonSerialize)
|
||||
return !inlineJson || inlineJson.length < 40 ? (
|
||||
<InlineLine id={id} inlineContent={inlineJson} />
|
||||
) : (
|
||||
<ExpandableLine id={id} inlineContent={null}>
|
||||
<JsonDisplay src={value} />
|
||||
</ExpandableLine>
|
||||
)
|
||||
}
|
||||
|
||||
export const knownSignedExtensions: Record<
|
||||
string,
|
||||
ComponentType<{ id: string; value: unknown }>
|
||||
{
|
||||
inline?: ComponentType<{ id: string; value: unknown }>
|
||||
expanded?: ComponentType<{ id: string; value: unknown }>
|
||||
}
|
||||
> = {
|
||||
CheckMortality: ({ id, value }) => {
|
||||
return (
|
||||
<li className="space-y-2">
|
||||
<div className="flex gap-2 items-center">
|
||||
<Dot size={16} />
|
||||
{id}
|
||||
</div>
|
||||
<MortalityAnalyzer mortality={value as any} />
|
||||
</li>
|
||||
)
|
||||
CheckMortality: {
|
||||
expanded: ({ value }) => <MortalityAnalyzer mortality={value as any} />,
|
||||
inline: ({ value }) => <InlineMortality mortality={value as any} />,
|
||||
},
|
||||
}
|
||||
|
||||
const ExpandableLine: FC<
|
||||
PropsWithChildren<{ id: string; inlineContent?: ReactNode }>
|
||||
> = ({ id, inlineContent, children }) => {
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
|
||||
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)} />
|
||||
{id}
|
||||
{inlineContent ? (
|
||||
<div className="max-w-full whitespace-nowrap font-mono text-sm">
|
||||
- {inlineContent}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{expanded && children}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
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">
|
||||
<div className="flex gap-2 items-center">
|
||||
<Dot size={16} />
|
||||
{id}
|
||||
</div>
|
||||
{inlineContent ? (
|
||||
<div className="max-w-full whitespace-nowrap font-mono text-sm">
|
||||
- {inlineContent}
|
||||
</div>
|
||||
) : null}
|
||||
</li>
|
||||
)
|
||||
|
||||
@@ -12,27 +12,41 @@ export const ExtrinsicAnalyzer: FC = () => {
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="p-2 space-y-2">
|
||||
<h2 className="text-lg font-bold">Analyze Extrinsic</h2>
|
||||
<div className="flex items-center gap-1 flex-wrap">
|
||||
<div>
|
||||
<label>
|
||||
Block
|
||||
<div className="space-y-4 p-3 md:p-4">
|
||||
<header className="space-y-2">
|
||||
<h2 className="text-2xl font-semibold tracking-tight">
|
||||
Analyze Extrinsic
|
||||
</h2>
|
||||
<p className="max-w-3xl text-sm text-muted-foreground">
|
||||
Decode a SCALE-encoded extrinsic against the selected block metadata,
|
||||
inspect its signer and extensions, and review the exact call payload
|
||||
and fee priority data.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<section className="rounded-xl border border-foreground/10 bg-card p-4 shadow-sm">
|
||||
<div className="flex gap-4 flex-col lg:flex-row">
|
||||
<div className="space-y-2">
|
||||
<div className="text-xs font-semibold uppercase tracking-[0.2em] text-foreground/60">
|
||||
Reference Block
|
||||
</div>
|
||||
<BlockPicker />
|
||||
</label>
|
||||
</div>
|
||||
<div className="grow">
|
||||
<label>
|
||||
Extrinsic
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 w-full">
|
||||
<div className="text-xs font-semibold uppercase tracking-[0.2em] text-foreground/60">
|
||||
Extrinsic Bytes (hex)
|
||||
</div>
|
||||
<TextInputField
|
||||
className="w-full"
|
||||
className="w-full rounded-md bg-input"
|
||||
value={extrinsicHex}
|
||||
onChange={setExtrinsicHex}
|
||||
placeholder="Extrinsic Hex"
|
||||
placeholder="0x..."
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Subscribe source$={extrinsicDecoder$} fallback={null}>
|
||||
{extrinsicHex ? <ExtrinsicDecoder extrinsic={extrinsicHex} /> : null}
|
||||
</Subscribe>
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { CopyBinary } from "@/codec-components/ViewCodec/CopyBinary"
|
||||
import { AccountIdDisplay } from "@/components/AccountIdDisplay"
|
||||
import { JsonDisplay } from "@/components/JsonDisplay"
|
||||
import { blockInfoState$ } from "@/pages/Explorer/block.state"
|
||||
import { BlockContext } from "@/pages/Explorer/Detail/blockContext"
|
||||
import { SignedExtensions } from "@/pages/Explorer/Detail/SignedExtensions"
|
||||
import { DecodedExtrinsic, getExtrinsicDecoder } from "@polkadot-api/tx-utils"
|
||||
import { getExtrinsicDecoder } from "@polkadot-api/tx-utils"
|
||||
import { useStateObservable, withDefault } from "@react-rxjs/core"
|
||||
import { HexString, TxCallData } from "polkadot-api"
|
||||
import { toHex } from "polkadot-api/utils"
|
||||
import { FC, useMemo } from "react"
|
||||
import { FC, ReactNode, useMemo } from "react"
|
||||
import { map, merge, switchMap } from "rxjs"
|
||||
import { Sender } from "../../Explorer/Detail/Extrinsic"
|
||||
import { senderToAddress } from "../../Explorer/Detail/Extrinsic"
|
||||
import { AnalyzePriority, analyzePriority$ } from "./Priority"
|
||||
import { selectedBlock$, selectedBlockHex$ } from "./selectedBlock"
|
||||
|
||||
@@ -43,24 +44,66 @@ export const ExtrinsicDecoder: FC<{
|
||||
}, [extrinsicDecoder, extrinsic])
|
||||
|
||||
if (decodeResult.type === "error") {
|
||||
return <div>Can't decode: {decodeResult.value.message}</div>
|
||||
return (
|
||||
<div className="rounded-xl border border-red-500/30 bg-red-500/5 p-4 text-sm text-red-700 dark:text-red-300">
|
||||
<div className="text-xs font-semibold uppercase tracking-[0.2em]">
|
||||
Decode Error
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
Can't decode: {decodeResult.value.message}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const decoded = decodeResult.value
|
||||
const signerAddress =
|
||||
decoded.type === "signed" ? senderToAddress(decoded.address) : null
|
||||
|
||||
return (
|
||||
<BlockContext value={block}>
|
||||
<div>
|
||||
<h2 className="capitalize text-xl font-bold">
|
||||
{decoded.type} Transaction v{decoded.version}
|
||||
</h2>
|
||||
<div className="space-y-4">
|
||||
<SectionCard className="space-y-4">
|
||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div className="space-y-2">
|
||||
<div className="text-xs font-semibold uppercase tracking-[0.2em] text-foreground/60">
|
||||
Decoded Extrinsic
|
||||
</div>
|
||||
<h2 className="text-2xl font-semibold tracking-tight capitalize">
|
||||
{decoded.type} Transaction v{decoded.version}
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{decoded.call.type}.{decoded.call.value.type}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{signerAddress ? (
|
||||
<div className="rounded-lg border border-foreground/10 bg-foreground/5 px-3 py-2">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-foreground/50">
|
||||
Signer
|
||||
</div>
|
||||
<div className="mt-1">
|
||||
<AccountIdDisplay value={signerAddress} />
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</SectionCard>
|
||||
|
||||
{decoded.type === "signed" ? (
|
||||
<SignedInfo extrinsic={extrinsic} decoded={decoded} />
|
||||
<SectionCard title="Signed Extensions">
|
||||
<SignedExtensions extra={decoded.extra} title={false} />
|
||||
</SectionCard>
|
||||
) : decoded.type === "general" ? (
|
||||
<div>TODO</div>
|
||||
) : (
|
||||
<SectionCard>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
General transaction analysis is not implemented yet.
|
||||
</div>
|
||||
</SectionCard>
|
||||
) : null}
|
||||
<SectionCard title="Priority Analysis">
|
||||
<AnalyzePriority extrinsic={extrinsic} />
|
||||
)}
|
||||
</SectionCard>
|
||||
<CallData
|
||||
call={decoded.call as TxCallData}
|
||||
callData={decoded.callData}
|
||||
@@ -70,40 +113,60 @@ export const ExtrinsicDecoder: FC<{
|
||||
)
|
||||
}
|
||||
|
||||
const SignedInfo: FC<{
|
||||
extrinsic: HexString
|
||||
decoded: DecodedExtrinsic & { type: "signed" }
|
||||
}> = ({ extrinsic, decoded }) => {
|
||||
const txPayment =
|
||||
decoded.extra.ChargeAssetTxPayment ?? decoded.extra.ChargeTxPayment
|
||||
|
||||
return (
|
||||
<div className="space-y-2 mb-4">
|
||||
<Sender sender={decoded.address} />
|
||||
<SignedExtensions extra={decoded.extra} />
|
||||
<AnalyzePriority extrinsic={extrinsic} txPayment={txPayment ?? {}} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const CallData: FC<{ call: TxCallData; callData: Uint8Array }> = ({
|
||||
call,
|
||||
callData,
|
||||
}) => (
|
||||
<div>
|
||||
<div className="flex gap-2 items-baseline">
|
||||
<div className="text-lg">
|
||||
{call.type}.{call.value.type}
|
||||
</div>
|
||||
<div className="flex gap-1 items-center overflow-hidden">
|
||||
<div className="shrink overflow-hidden text-ellipsis text-muted-foreground">
|
||||
{toHex(callData)}
|
||||
<SectionCard title="Call Payload">
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
||||
<div>
|
||||
<div className="text-lg font-semibold tracking-tight">
|
||||
{call.type}.{call.value.type}
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Decoded arguments for the call embedded in this extrinsic.
|
||||
</p>
|
||||
</div>
|
||||
<CopyBinary value={callData} />
|
||||
<div className="min-w-0 rounded-lg border border-foreground/10 bg-foreground/5 px-3 py-2 lg:max-w-xl">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-foreground/50">
|
||||
Call Data
|
||||
</div>
|
||||
<div className="mt-1 flex items-start gap-2">
|
||||
<div className="shrink overflow-auto font-mono text-xs text-muted-foreground">
|
||||
{toHex(callData)}
|
||||
</div>
|
||||
<CopyBinary value={callData} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="overflow-auto rounded-xl border border-foreground/10 bg-background/60 p-3">
|
||||
<JsonDisplay src={call.value.value} />
|
||||
</div>
|
||||
</div>
|
||||
<JsonDisplay src={call.value.value} />
|
||||
</div>
|
||||
</SectionCard>
|
||||
)
|
||||
|
||||
export const extrinsicDecoder$ = merge(extDecoder$, analyzePriority$)
|
||||
|
||||
const SectionCard: FC<{
|
||||
children: ReactNode
|
||||
title?: string
|
||||
className?: string
|
||||
}> = ({ children, title, className }) => (
|
||||
<section
|
||||
className={[
|
||||
"rounded-xl border border-foreground/10 bg-card p-4 shadow-sm",
|
||||
className,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ")}
|
||||
>
|
||||
{title ? (
|
||||
<div className="mb-3 text-xs font-semibold uppercase tracking-[0.2em] text-foreground/60">
|
||||
{title}
|
||||
</div>
|
||||
) : null}
|
||||
{children}
|
||||
</section>
|
||||
)
|
||||
|
||||
@@ -2,9 +2,10 @@ 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, useEffect, useState } from "react"
|
||||
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(
|
||||
@@ -83,24 +84,82 @@ export const AnalyzePriority: FC<{
|
||||
? maxTxPerBlockWeight
|
||||
: maxTxPerBlockLength
|
||||
const priority = (tip + 1n) * maxTxPerBlock
|
||||
const limitingFactor =
|
||||
maxTxPerBlockWeight < maxTxPerBlockLength ? "Weight" : "Length"
|
||||
|
||||
return { priority, maxTxPerBlockLength, maxTxPerBlockWeight }
|
||||
return {
|
||||
priority,
|
||||
maxTxPerBlockLength,
|
||||
maxTxPerBlockWeight,
|
||||
maxTxPerBlock,
|
||||
limitingFactor,
|
||||
}
|
||||
})()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<b>Priority:</b> tip={tip.toLocaleString()} fee=
|
||||
{queryInfo.partial_fee.toLocaleString()} class={queryInfo.class.type}{" "}
|
||||
weight=
|
||||
{queryInfo.weight.proof_size.toLocaleString() +
|
||||
"/" +
|
||||
queryInfo.weight.ref_time.toLocaleString()}{" "}
|
||||
<div className="space-y-4">
|
||||
{priority ? (
|
||||
<>
|
||||
txPerBlockLength={priority.maxTxPerBlockLength.toLocaleString()}{" "}
|
||||
txPerBlockWeight={priority.maxTxPerBlockWeight.toLocaleString()}{" "}
|
||||
priority={priority.priority.toLocaleString()}
|
||||
</>
|
||||
<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="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>{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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!priority ? (
|
||||
<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>
|
||||
)
|
||||
@@ -114,3 +173,13 @@ const divWeight = (a: Weight, b: Weight) => {
|
||||
const proof_size = b.proof_size === 0n ? 1n : a.proof_size / b.proof_size
|
||||
return ref_time < proof_size ? ref_time : proof_size
|
||||
}
|
||||
|
||||
const DetailRow: FC<{ label: string; value: ReactNode }> = ({
|
||||
label,
|
||||
value,
|
||||
}) => (
|
||||
<div className="flex items-baseline justify-between gap-4 border-b border-foreground/8 pb-2 last:border-b-0 last:pb-0">
|
||||
<div className="text-sm text-muted-foreground">{label}</div>
|
||||
<div className="text-right font-mono text-sm text-foreground">{value}</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user