feat: decode ChargeAssetTxPayment

This commit is contained in:
Victor Oliva
2026-05-11 11:23:39 +02:00
parent 945ac8e0a2
commit 722de400ea
3 changed files with 163 additions and 35 deletions
+106
View File
@@ -0,0 +1,106 @@
import { client$ } from "@/state/chains/chain.state"
import { XcmV5Junctions } from "@polkadot-api/descriptors"
import {
Binary,
decAnyMetadata,
unifyMetadata,
} from "@polkadot-api/substrate-bindings"
import { HexString } from "polkadot-api"
import { FC, useEffect, useState } from "react"
import { TokenAmount } from "../TokenAmount"
type ChargeAssetTxPayment = {
tip: bigint
// Add more supported types as we go
asset_id?: {
parents: number
interior: XcmV5Junctions
}
}
const getAsset = (value: ChargeAssetTxPayment) => {
if (value.asset_id?.parents !== 0) return null
if (value.asset_id.interior.type !== "X2") return null
const [first, second] = value.asset_id.interior.value
if (first.type !== "PalletInstance" || second.type !== "GeneralIndex")
return null
return {
pallet: first.value,
index: second.value,
}
}
export const canDecodeAsset = (value: ChargeAssetTxPayment) =>
getAsset(value) !== null
const loadAsset = async (
asset: {
pallet: number
index: bigint
},
block?: HexString,
) => {
const client = await client$.getValue()
const metadata = unifyMetadata(
decAnyMetadata(
await client.getMetadata(
block ?? (await client.getFinalizedBlock()).hash,
),
),
)
const pallet = metadata.pallets.find((p) => p.index === asset.pallet)
if (!pallet) return null
const hasMetadata = pallet.storage?.items.find((it) => it.name === "Metadata")
if (!hasMetadata) return null
const assetMetadata: any = await client
.getUnsafeApi()
.query[pallet.name].Metadata.getValue(Number(asset.index))
if (assetMetadata.decimals == null) return null
return {
pallet: pallet.name,
symbol: Binary.toText(assetMetadata.symbol) as string,
decimals: assetMetadata.decimals as number,
}
}
export const ChargeAssetTx: FC<{
chargeAssetTxPayment: {
tip: bigint
// Add more types as we go
asset_id?: {
parents: number
interior: XcmV5Junctions
}
}
}> = ({ chargeAssetTxPayment }) => {
const asset = getAsset(chargeAssetTxPayment)
const [assetMetadata, setAssetMetadata] = useState<{
pallet: string
symbol: string
decimals: number
} | null>(null)
useEffect(() => {
if (!asset) return
let cancelled = false
loadAsset(asset).then(
(r) => !cancelled && setAssetMetadata(r),
(ex) => console.error(ex),
)
return () => {
cancelled = true
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [asset?.index, asset?.pallet])
if (!asset || !assetMetadata) return null
return (
<span className="tabular-nums">
<TokenAmount>{chargeAssetTxPayment.tip}</TokenAmount> in{" "}
{assetMetadata.symbol
? `${assetMetadata.symbol} (${assetMetadata.pallet} #${asset.index})`
: `${assetMetadata.pallet} #${asset.index}`}
</span>
)
}
+34 -25
View File
@@ -2,14 +2,10 @@ import { ExpandBtn } from "@/components/Expand"
import { JsonDisplay } from "@/components/JsonDisplay"
import { Dot } from "lucide-react"
import { jsonSerialize } from "polkadot-api/utils"
import {
ComponentType,
FC,
PropsWithChildren,
ReactNode,
useState,
} from "react"
import { FC, PropsWithChildren, ReactNode, useState } from "react"
import { InlineMortality, MortalityAnalyzer } from "./MortalityAnalyzer"
import { canDecodeAsset, ChargeAssetTx } from "./ChargeAssetTx"
import { TokenAmount } from "../TokenAmount"
export const SignedExtensions: FC<{
extra: Record<string, unknown>
@@ -30,20 +26,19 @@ export const SignedExtensions: FC<{
const SignedExtension: FC<{ id: string; value: unknown }> = ({ id, value }) => {
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
const { expanded, inline } = knownExtension
const expandedElement = expanded?.(value)
const inlineElement = inline?.(value)
if (expandedElement) {
return (
<ExpandableLine id={id} inlineContent={inlineElement}>
{expandedElement}
</ExpandableLine>
)
}
if (inlineElement) {
return <InlineLine id={id} inlineContent={inlineElement} />
}
}
const inlineJson = JSON.stringify(value, jsonSerialize)
@@ -59,13 +54,27 @@ const SignedExtension: FC<{ id: string; value: unknown }> = ({ id, value }) => {
export const knownSignedExtensions: Record<
string,
{
inline?: ComponentType<{ id: string; value: unknown }>
expanded?: ComponentType<{ id: string; value: unknown }>
inline?: (value: any) => ReactNode
expanded?: (value: any) => ReactNode
}
> = {
CheckMortality: {
expanded: ({ value }) => <MortalityAnalyzer mortality={value as any} />,
inline: ({ value }) => <InlineMortality mortality={value as any} />,
expanded: (value) => <MortalityAnalyzer mortality={value as any} />,
inline: (value) => <InlineMortality mortality={value as any} />,
},
ChargeAssetTxPayment: {
inline: (value) =>
value.asset_id ? (
canDecodeAsset(value) ? (
<ChargeAssetTx chargeAssetTxPayment={value} />
) : null
) : (
<div className="inline-block">
<TokenAmount>{value.tip}</TokenAmount>
<span> {JSON.stringify(value, jsonSerialize)}</span>
</div>
),
expanded: (value) => (value.asset_id ? <JsonDisplay src={value} /> : null),
},
}
+23 -10
View File
@@ -3,6 +3,23 @@ import { chainProperties$ } from "@/state/chain-props.state"
import { useStateObservable } from "@react-rxjs/core"
import { FC } from "react"
export const formatToken = (
amount: bigint,
properties: {
decimals: number
symbol?: string
},
) => {
const formattedValue = (
Number(amount) /
10 ** properties.decimals
).toLocaleString(undefined, {
maximumSignificantDigits: 3,
})
return `${formattedValue} ${properties.symbol}`
}
export const TokenAmount: FC<{
children: bigint
className?: string
@@ -15,16 +32,12 @@ export const TokenAmount: FC<{
return children.toLocaleString()
}
const formattedValue = (
Number(children) /
10 ** properties.tokenDecimals
).toLocaleString(undefined, {
maximumSignificantDigits: 3,
})
return (
<span
className={cn("tabular-nums", className)}
>{`${formattedValue} ${properties.tokenSymbol}`}</span>
<span className={cn("tabular-nums", className)}>
{formatToken(children, {
decimals: properties.tokenDecimals,
symbol: properties.tokenSymbol,
})}
</span>
)
}