From 30d9df9fe604f0bde44694f019049b1deda1b756 Mon Sep 17 00:00:00 2001 From: chaojun Date: Fri, 27 Mar 2026 20:58:58 +0800 Subject: [PATCH 1/6] feat: refactor preimage hooks --- .../components/preImages/desktop.js | 6 +- .../components/preImages/mobile.js | 4 +- .../hooks/useOldPreimagePapiNew.js | 111 ++++++++ packages/next-common/hooks/usePreimageNew.js | 258 ++++++++++++++++++ .../next-common/hooks/usePreimageNewCommon.js | 81 ++++++ .../next-common/hooks/usePreimagePapiNew.js | 113 ++++++++ .../utils/consts/settings/hydradx.js | 1 + 7 files changed, 569 insertions(+), 5 deletions(-) create mode 100644 packages/next-common/hooks/useOldPreimagePapiNew.js create mode 100644 packages/next-common/hooks/usePreimageNew.js create mode 100644 packages/next-common/hooks/usePreimageNewCommon.js create mode 100644 packages/next-common/hooks/usePreimagePapiNew.js diff --git a/packages/next-common/components/preImages/desktop.js b/packages/next-common/components/preImages/desktop.js index 4ebc49fd0b..9b93e6908d 100644 --- a/packages/next-common/components/preImages/desktop.js +++ b/packages/next-common/components/preImages/desktop.js @@ -2,9 +2,9 @@ import useColumns from "next-common/components/styledList/useColumns"; import { SecondaryCard } from "next-common/components/styled/containers/secondaryCard"; import { useState } from "react"; import useOldPreimage from "next-common/hooks/useOldPreimage"; -import useOldPreimagePapi from "next-common/hooks/useOldPreimagePapi"; -import usePreimage from "next-common/hooks/usePreimage"; -import usePreimagePapi from "next-common/hooks/usePreimagePapi"; +import useOldPreimagePapi from "next-common/hooks/useOldPreimagePapiNew"; +import usePreimage from "next-common/hooks/usePreimageNew"; +import usePreimagePapi from "next-common/hooks/usePreimagePapiNew"; import { useChainSettings } from "next-common/context/chain"; import { useDispatch, useSelector } from "react-redux"; import { diff --git a/packages/next-common/components/preImages/mobile.js b/packages/next-common/components/preImages/mobile.js index a0acd60c80..e7e47064b1 100644 --- a/packages/next-common/components/preImages/mobile.js +++ b/packages/next-common/components/preImages/mobile.js @@ -1,7 +1,7 @@ import { SecondaryCard } from "next-common/components/styled/containers/secondaryCard"; import React, { useState, useEffect, useRef } from "react"; -import usePreimage from "next-common/hooks/usePreimage"; -import usePreimagePapi from "next-common/hooks/usePreimagePapi"; +import usePreimage from "next-common/hooks/usePreimageNew"; +import usePreimagePapi from "next-common/hooks/usePreimagePapiNew"; import useOldPreimage from "next-common/hooks/useOldPreimage"; import useOldPreimagePapi from "next-common/hooks/useOldPreimagePapi"; import { useChainSettings } from "next-common/context/chain"; diff --git a/packages/next-common/hooks/useOldPreimagePapiNew.js b/packages/next-common/hooks/useOldPreimagePapiNew.js new file mode 100644 index 0000000000..2c2b5f9b96 --- /dev/null +++ b/packages/next-common/hooks/useOldPreimagePapiNew.js @@ -0,0 +1,111 @@ +import { useAsync } from "react-use"; +import { Binary } from "polkadot-api"; +import { useContextPapi } from "next-common/context/papi"; +import { + fetchPapiPreimageBytes, + decodePreimageWithPapi, + toPreimageCount, + toPreimageLength, + convertPapiDepositTuple, + getPapiStatusName, +} from "./useOldPreimageCommon"; +import { + parsePapiHashOrBounded, + buildNoBytesResult, + buildBasePapiResult, + resolveInlinePapiPreimage, +} from "./usePreimageNewCommon"; + +const oldPapiPreimageResultCache = new Map(); + +function parsePapiStatusFor(rawStatus) { + if (!rawStatus) return { status: null }; + + const statusName = getPapiStatusName(rawStatus); + const { type, value = {} } = rawStatus; + const result = { status: rawStatus, statusName }; + + if (type === "Requested") { + result.count = toPreimageCount(value.count); + result.deposit = convertPapiDepositTuple(value.deposit); + result.proposalLength = toPreimageLength(value.len); + } else if (type === "Unrequested") { + result.deposit = convertPapiDepositTuple(value.deposit); + result.proposalLength = toPreimageLength(value.len); + } else { + console.error(`Unhandled PAPI Preimage.StatusFor type: ${type}`); + } + + return result; +} + +async function fetchOldPapiPreimage(proposalHash, papi, client) { + const base = buildBasePapiResult(proposalHash, null); + + if (!papi.query?.Preimage?.StatusFor) return base; + + const rawStatus = await papi.query.Preimage.StatusFor.getValue( + Binary.fromHex(proposalHash), + ); + + const parsedStatus = parsePapiStatusFor(rawStatus); + const withStatus = { ...base, ...parsedStatus }; + + if (!parsedStatus.status) return withStatus; + + const bytes = await fetchPapiPreimageBytes( + papi, + proposalHash, + withStatus.proposalLength, + ); + + if (!bytes) return buildNoBytesResult(withStatus); + + let decoded; + try { + decoded = await decodePreimageWithPapi(withStatus, bytes, client); + } catch { + // ignore + } + + if (!decoded) { + return { + ...withStatus, + isCompleted: true, + proposalError: "Unable to decode preimage bytes into a valid Call", + }; + } + + return decoded; +} + +export default function useOldPreimagePapi(hashOrBounded) { + const { api: papi, client } = useContextPapi(); + + const { value, loading } = useAsync(async () => { + if (!hashOrBounded || !papi || !client) return null; + + const { proposalHash, inlineData } = parsePapiHashOrBounded(hashOrBounded); + if (!proposalHash) return null; + + if (oldPapiPreimageResultCache.has(proposalHash)) { + return oldPapiPreimageResultCache.get(proposalHash); + } + + const result = inlineData + ? await resolveInlinePapiPreimage(proposalHash, inlineData, client) + : await fetchOldPapiPreimage(proposalHash, papi, client); + + if (result?.isCompleted) { + oldPapiPreimageResultCache.set(proposalHash, result); + } + + return result; + }, [hashOrBounded, papi, client]); + + const isStatusLoaded = Boolean(papi) && Boolean(client) && !loading; + const isPreimageLoaded = + Boolean(papi) && Boolean(client) && !loading && Boolean(value?.isCompleted); + + return [value ?? {}, isStatusLoaded, isPreimageLoaded]; +} diff --git a/packages/next-common/hooks/usePreimageNew.js b/packages/next-common/hooks/usePreimageNew.js new file mode 100644 index 0000000000..868d8e1c98 --- /dev/null +++ b/packages/next-common/hooks/usePreimageNew.js @@ -0,0 +1,258 @@ +import { useAsync } from "react-use"; +import { useContextApi } from "next-common/context/api"; +import { + BN, + BN_ZERO, + formatNumber, + isString, + isU8a, + u8aToHex, +} from "@polkadot/util"; +import { Option } from "@polkadot/types"; +import { buildNoBytesResult } from "./usePreimageNewCommon"; + +const preimageResultCache = new Map(); + +function parseHashOrBounded(hashOrBounded, api) { + if (isString(hashOrBounded)) { + return { proposalHash: hashOrBounded }; + } + + if (isU8a(hashOrBounded)) { + return { proposalHash: hashOrBounded.toHex() }; + } + + if (hashOrBounded.isInline) { + const inlineData = hashOrBounded.asInline.toU8a(true); + const proposalHash = u8aToHex(api?.registry.hash(inlineData)); + return { proposalHash, inlineData }; + } + + if (hashOrBounded.isLegacy) { + return { proposalHash: hashOrBounded.asLegacy.hash_.toHex() }; + } + + if (hashOrBounded.isLookup) { + return { proposalHash: hashOrBounded.asLookup.hash_.toHex() }; + } + + console.error( + `Unhandled FrameSupportPreimagesBounded type: ${hashOrBounded.type}`, + ); + return {}; +} + +function isHashOnlyStorageKey(api) { + if (!api?.query.preimage?.preimageFor?.creator.meta.type.isMap) { + return false; + } + const { type } = api.registry.lookup.getTypeDef( + api.query.preimage.preimageFor.creator.meta.type.asMap.key, + ); + return type === "H256"; +} + +function parseTicket(rawTicket) { + if (!rawTicket) return undefined; + return { who: rawTicket[0].toString(), amount: rawTicket[1] }; +} + +function parseRequestStatus(optStatus) { + const status = optStatus.unwrapOr(null); + if (!status) return { status: null }; + + if (status.isRequested) { + const req = status.asRequested; + // 旧版 runtime:asRequested 是 Option,无结构化字段 + if (req instanceof Option) { + return { status, statusName: "requested" }; + } + return { + status, + statusName: "requested", + count: req.count.toNumber(), + ticket: parseTicket(req.maybeTicket.unwrapOr(null)), + proposalLength: req.maybeLen.unwrapOr(BN_ZERO), + }; + } + + if (status.isUnrequested) { + const unreq = status.asUnrequested; + // 旧版 runtime:asUnrequested 是 Option + if (unreq instanceof Option) { + return { + status, + statusName: "unrequested", + ticket: parseTicket(unreq.unwrapOr(null)), + }; + } + return { + status, + statusName: "unrequested", + ticket: parseTicket(unreq.ticket), + proposalLength: unreq.len, + }; + } + + console.error(`Unhandled PalletPreimageRequestStatus type: ${status.type}`); + return { status }; +} + +function buildPreimageForKey(proposalHash, proposalLength, hashOnly) { + if (hashOnly) { + return [proposalHash]; + } + return [[proposalHash, proposalLength || BN_ZERO]]; +} + +function extractCallData(optBytes) { + if (!optBytes) return null; + + const callData = isU8a(optBytes) + ? optBytes + : optBytes.unwrapOr?.(null) ?? optBytes; + + if (!callData) return null; + if (isU8a(callData) && callData.length === 0) return null; + if (isString(callData) && callData === "0x") return null; + if (typeof callData?.toHex === "function" && callData.toHex() === "0x") + return null; + + return callData; +} + +function decodeCallBytes(callData, registry, proposalLength) { + let proposal = null; + let proposalError = null; + let proposalWarning = null; + let resolvedLength; + + try { + proposal = registry.createType("Call", callData); + const callLength = proposal.encodedLength; + + if (proposalLength) { + const storeLength = proposalLength.toNumber(); + if (callLength !== storeLength) { + proposalWarning = `Decoded call length does not match on-chain stored preimage length (${formatNumber( + callLength, + )} bytes vs ${formatNumber(storeLength)} bytes)`; + } + } else { + // status 未提供 proposalLength,从解码的 Call 中推导 + resolvedLength = new BN(callLength); + } + } catch { + proposalError = "Unable to decode preimage bytes into a valid Call"; + } + + return { proposal, proposalError, proposalWarning, resolvedLength }; +} + +function buildBaseResult(proposalHash, hashOnly, registry, inlineData) { + return { + proposalHash, + count: 0, + isCompleted: false, + isHashParam: hashOnly, + registry, + status: null, + ticket: undefined, + statusName: null, + // 若为 inline data,proposalLength 可从字节长度得出 + proposalLength: inlineData ? new BN(inlineData.length) : null, + proposal: null, + proposalError: null, + proposalWarning: null, + }; +} + +function buildCompletedResult(base, decoded) { + return { + ...base, + isCompleted: true, + proposal: decoded.proposal ?? null, + proposalError: decoded.proposalError ?? null, + proposalWarning: decoded.proposalWarning ?? null, + // 若 decoded 提供了推导出的 length 则使用,否则沿用 base 中的 + proposalLength: decoded.resolvedLength ?? base.proposalLength, + }; +} + +function resolveInlinePreimage(proposalHash, hashOnly, api, inlineData) { + const base = buildBaseResult( + proposalHash, + hashOnly, + api.registry, + inlineData, + ); + const callData = extractCallData(inlineData); + const decoded = callData + ? decodeCallBytes(callData, api.registry, base.proposalLength) + : { proposal: null, proposalError: null, proposalWarning: null }; + return buildCompletedResult(base, decoded); +} + +async function fetchPreimage(proposalHash, api, hashOnly) { + const base = buildBaseResult(proposalHash, hashOnly, api.registry, null); + + if (!api.query.preimage?.requestStatusFor) return base; + + const optStatus = await api.query.preimage.requestStatusFor(proposalHash); + const parsedStatus = parseRequestStatus(optStatus); + const withStatus = { ...base, ...parsedStatus }; + + if (!parsedStatus.status) return withStatus; + + if (!api.query.preimage?.preimageFor) return withStatus; + + const bytesKey = buildPreimageForKey( + proposalHash, + withStatus.proposalLength, + hashOnly, + ); + const optBytes = await api.query.preimage.preimageFor(...bytesKey); + const callData = extractCallData(optBytes); + + if (!callData) return buildNoBytesResult(withStatus); + + const decoded = decodeCallBytes( + callData, + api.registry, + withStatus.proposalLength, + ); + return buildCompletedResult(withStatus, decoded); +} + +export default function usePreimage(hashOrBounded) { + const api = useContextApi(); + + const { value, loading } = useAsync(async () => { + if (!hashOrBounded || !api) return null; + + const { proposalHash, inlineData } = parseHashOrBounded(hashOrBounded, api); + if (!proposalHash) return null; + + if (preimageResultCache.has(proposalHash)) { + return preimageResultCache.get(proposalHash); + } + + const hashOnly = isHashOnlyStorageKey(api); + + const result = inlineData + ? resolveInlinePreimage(proposalHash, hashOnly, api, inlineData) + : await fetchPreimage(proposalHash, api, hashOnly); + + if (result?.isCompleted) { + preimageResultCache.set(proposalHash, result); + } + + return result; + }, [hashOrBounded, api]); + + const isStatusLoaded = Boolean(api) && !loading; + const isPreimageLoaded = + Boolean(api) && !loading && Boolean(value?.isCompleted); + + return [value ?? {}, isStatusLoaded, isPreimageLoaded]; +} diff --git a/packages/next-common/hooks/usePreimageNewCommon.js b/packages/next-common/hooks/usePreimageNewCommon.js new file mode 100644 index 0000000000..6967a2fb0f --- /dev/null +++ b/packages/next-common/hooks/usePreimageNewCommon.js @@ -0,0 +1,81 @@ +import { BN, BN_ZERO, isString, isU8a } from "@polkadot/util"; +import { blake2AsHex } from "@polkadot/util-crypto"; +import { decodePreimageWithPapi } from "./useOldPreimageCommon"; + +export function parsePapiHashOrBounded(hashOrBounded) { + if (isString(hashOrBounded)) { + return { proposalHash: hashOrBounded }; + } + + if (isU8a(hashOrBounded)) { + return { proposalHash: hashOrBounded.toHex() }; + } + + if (hashOrBounded.isInline) { + const inlineData = hashOrBounded.asInline.toU8a(true); + const proposalHash = blake2AsHex(inlineData); + return { proposalHash, inlineData }; + } + + if (hashOrBounded.isLegacy) { + return { proposalHash: hashOrBounded.asLegacy.hash_.toHex() }; + } + + if (hashOrBounded.isLookup) { + return { proposalHash: hashOrBounded.asLookup.hash_.toHex() }; + } + + console.error( + `Unhandled FrameSupportPreimagesBounded type: ${hashOrBounded.type}`, + ); + return {}; +} + +export function buildNoBytesResult(base) { + return { + ...base, + isCompleted: true, + proposal: null, + proposalError: null, + proposalWarning: "No preimage bytes found", + proposalLength: base.proposalLength || BN_ZERO, + }; +} + +export function buildBasePapiResult(proposalHash, inlineData) { + return { + proposalHash, + count: 0, + isCompleted: false, + isHashParam: false, + registry: null, + status: null, + ticket: undefined, + deposit: undefined, + statusName: null, + proposalLength: inlineData ? new BN(inlineData.length) : null, + proposal: null, + proposalError: null, + proposalWarning: null, + }; +} + +export async function resolveInlinePapiPreimage( + proposalHash, + inlineData, + client, +) { + const base = buildBasePapiResult(proposalHash, inlineData); + + const decoded = await decodePreimageWithPapi(base, inlineData, client); + + if (!decoded) { + return { + ...base, + isCompleted: true, + proposalError: "Unable to decode preimage bytes into a valid Call", + }; + } + + return decoded; +} diff --git a/packages/next-common/hooks/usePreimagePapiNew.js b/packages/next-common/hooks/usePreimagePapiNew.js new file mode 100644 index 0000000000..96d10acb2a --- /dev/null +++ b/packages/next-common/hooks/usePreimagePapiNew.js @@ -0,0 +1,113 @@ +import { useAsync } from "react-use"; +import { Binary } from "polkadot-api"; +import { useContextPapi } from "next-common/context/papi"; +import { + fetchPapiPreimageBytes, + decodePreimageWithPapi, + toPreimageCount, + toPreimageLength, + convertPapiDepositTuple, + getPapiStatusName, +} from "./useOldPreimageCommon"; +import { + parsePapiHashOrBounded, + buildNoBytesResult, + buildBasePapiResult, + resolveInlinePapiPreimage, +} from "./usePreimageNewCommon"; + +const papiPreimageResultCache = new Map(); + +function parsePapiRequestStatus(rawStatus) { + if (!rawStatus) return { status: null }; + + const statusName = getPapiStatusName(rawStatus); // 首字母小写 + const { type, value = {} } = rawStatus; + const result = { status: rawStatus, statusName }; + + if (type === "Requested") { + result.count = toPreimageCount(value.count); + result.ticket = convertPapiDepositTuple( + value.maybeTicket ?? value.maybe_ticket, + ); + result.proposalLength = toPreimageLength(value.maybeLen ?? value.maybe_len); + } else if (type === "Unrequested") { + result.ticket = convertPapiDepositTuple(value.ticket); + result.proposalLength = toPreimageLength(value.len); + } else { + console.error(`Unhandled PAPI Preimage.RequestStatusFor type: ${type}`); + } + + return result; +} + +async function fetchPapiPreimage(proposalHash, papi, client) { + const base = buildBasePapiResult(proposalHash, null); + + if (!papi.query?.Preimage?.RequestStatusFor) return base; + + const rawStatus = await papi.query.Preimage.RequestStatusFor.getValue( + Binary.fromHex(proposalHash), + ); + + const parsedStatus = parsePapiRequestStatus(rawStatus); + const withStatus = { ...base, ...parsedStatus }; + + if (!parsedStatus.status) return withStatus; + + const bytes = await fetchPapiPreimageBytes( + papi, + proposalHash, + withStatus.proposalLength, + ); + + if (!bytes) return buildNoBytesResult(withStatus); + + let decoded; + try { + decoded = await decodePreimageWithPapi(withStatus, bytes, client); + } catch { + // ignore + } + + if (!decoded) { + return { + ...withStatus, + isCompleted: true, + proposalError: "Unable to decode preimage bytes into a valid Call", + }; + } + + return decoded; +} + +export default function usePreimagePapi(hashOrBounded) { + const { api: papi, client } = useContextPapi(); + + const { value, loading } = useAsync(async () => { + if (!hashOrBounded || !papi || !client) return null; + + const { proposalHash, inlineData } = parsePapiHashOrBounded(hashOrBounded); + if (!proposalHash) return null; + + if (papiPreimageResultCache.has(proposalHash)) { + return papiPreimageResultCache.get(proposalHash); + } + + const result = inlineData + ? await resolveInlinePapiPreimage(proposalHash, inlineData, client) + : await fetchPapiPreimage(proposalHash, papi, client); + + if (result?.isCompleted) { + papiPreimageResultCache.set(proposalHash, result); + } + + return result; + }, [hashOrBounded, papi, client]); + + const isStatusLoaded = Boolean(papi) && Boolean(client) && !loading; + const isPreimageLoaded = + Boolean(papi) && Boolean(client) && !loading && Boolean(value?.isCompleted); + + return [value ?? {}, isStatusLoaded, isPreimageLoaded]; +} diff --git a/packages/next-common/utils/consts/settings/hydradx.js b/packages/next-common/utils/consts/settings/hydradx.js index 678823c201..b6cf57a250 100644 --- a/packages/next-common/utils/consts/settings/hydradx.js +++ b/packages/next-common/utils/consts/settings/hydradx.js @@ -158,6 +158,7 @@ const hydradx = { openSquare: { voting: "hydration", }, + enablePapi: true, }; export default hydradx; From 9dae34c2c6bcae0cc88ec5e522ffa2e991217881 Mon Sep 17 00:00:00 2001 From: chaojun Date: Mon, 30 Mar 2026 13:43:58 +0800 Subject: [PATCH 2/6] refactor useOldPreimage --- .../components/preImages/desktop.js | 2 +- .../components/preImages/mobile.js | 4 +- .../next-common/hooks/useOldPreimageNew.js | 127 ++++++++++++++ packages/next-common/hooks/usePreimageNew.js | 155 ++---------------- .../next-common/hooks/usePreimageNewCommon.js | 146 ++++++++++++++++- 5 files changed, 287 insertions(+), 147 deletions(-) create mode 100644 packages/next-common/hooks/useOldPreimageNew.js diff --git a/packages/next-common/components/preImages/desktop.js b/packages/next-common/components/preImages/desktop.js index 9b93e6908d..9e0da26e9d 100644 --- a/packages/next-common/components/preImages/desktop.js +++ b/packages/next-common/components/preImages/desktop.js @@ -1,7 +1,7 @@ import useColumns from "next-common/components/styledList/useColumns"; import { SecondaryCard } from "next-common/components/styled/containers/secondaryCard"; import { useState } from "react"; -import useOldPreimage from "next-common/hooks/useOldPreimage"; +import useOldPreimage from "next-common/hooks/useOldPreimageNew"; import useOldPreimagePapi from "next-common/hooks/useOldPreimagePapiNew"; import usePreimage from "next-common/hooks/usePreimageNew"; import usePreimagePapi from "next-common/hooks/usePreimagePapiNew"; diff --git a/packages/next-common/components/preImages/mobile.js b/packages/next-common/components/preImages/mobile.js index e7e47064b1..bf7674799d 100644 --- a/packages/next-common/components/preImages/mobile.js +++ b/packages/next-common/components/preImages/mobile.js @@ -2,8 +2,8 @@ import { SecondaryCard } from "next-common/components/styled/containers/secondar import React, { useState, useEffect, useRef } from "react"; import usePreimage from "next-common/hooks/usePreimageNew"; import usePreimagePapi from "next-common/hooks/usePreimagePapiNew"; -import useOldPreimage from "next-common/hooks/useOldPreimage"; -import useOldPreimagePapi from "next-common/hooks/useOldPreimagePapi"; +import useOldPreimage from "next-common/hooks/useOldPreimageNew"; +import useOldPreimagePapi from "next-common/hooks/useOldPreimagePapiNew"; import { useChainSettings } from "next-common/context/chain"; import { useDispatch } from "react-redux"; import { incPreImagesTrigger } from "next-common/store/reducers/preImagesSlice"; diff --git a/packages/next-common/hooks/useOldPreimageNew.js b/packages/next-common/hooks/useOldPreimageNew.js new file mode 100644 index 0000000000..d168a39083 --- /dev/null +++ b/packages/next-common/hooks/useOldPreimageNew.js @@ -0,0 +1,127 @@ +import { useAsync } from "react-use"; +import { useContextApi } from "next-common/context/api"; +import { BN_ZERO } from "@polkadot/util"; +import { Option } from "@polkadot/types"; +import { + buildNoBytesResult, + buildBaseResult, + buildCompletedResult, + buildPreimageForKey, + decodeCallBytes, + extractCallData, + isHashOnlyStorageKey, + parseHashOrBounded, + resolveInlinePreimage, +} from "./usePreimageNewCommon"; + +const oldPreimageResultCache = new Map(); + +function parseDeposit(rawDeposit) { + if (!rawDeposit) return undefined; + return { who: rawDeposit[0].toString(), amount: rawDeposit[1] }; +} + +function parseStatusFor(optStatus) { + const status = optStatus.unwrapOr(null); + if (!status) return { status: null }; + + if (status.isRequested) { + const req = status.asRequested; + // 旧版 runtime:asRequested 是 Option,无结构化字段 + if (req instanceof Option) { + return { status, statusName: "requested" }; + } + return { + status, + statusName: "requested", + count: req.count.toNumber(), + deposit: parseDeposit(req.deposit.unwrapOr(null)), + proposalLength: req.len.unwrapOr(BN_ZERO), + }; + } + + if (status.isUnrequested) { + const unreq = status.asUnrequested; + // 旧版 runtime:asUnrequested 是 Option + if (unreq instanceof Option) { + return { + status, + statusName: "unrequested", + deposit: parseDeposit(unreq.unwrapOr(null)), + }; + } + return { + status, + statusName: "unrequested", + deposit: parseDeposit(unreq.deposit), + proposalLength: unreq.len, + }; + } + + console.error(`Unhandled PalletPreimageRequestStatus type: ${status.type}`); + return { status }; +} + +async function fetchOldPreimage(proposalHash, api, hashOnly) { + const base = buildBaseResult(proposalHash, hashOnly, api.registry, null); + + if (!api.query.preimage?.statusFor) return base; + + const optStatus = await api.query.preimage.statusFor(proposalHash); + const parsedStatus = parseStatusFor(optStatus); + const withStatus = { ...base, ...parsedStatus }; + + if (!parsedStatus.status) return withStatus; + + if (!api.query.preimage?.preimageFor) return withStatus; + + const bytesKey = buildPreimageForKey( + proposalHash, + withStatus.proposalLength, + hashOnly, + ); + const optBytes = await api.query.preimage.preimageFor(...bytesKey); + const callData = extractCallData(optBytes); + + if (!callData) return buildNoBytesResult(withStatus); + + const decoded = decodeCallBytes( + callData, + api.registry, + withStatus.proposalLength, + ); + return buildCompletedResult(withStatus, decoded); +} + +export default function useOldPreimage(hashOrBounded) { + const api = useContextApi(); + + const { value, loading } = useAsync(async () => { + if (!hashOrBounded || !api) return null; + + const { proposalHash, inlineData } = parseHashOrBounded(hashOrBounded, api); + if (!proposalHash) return null; + + if (oldPreimageResultCache.has(proposalHash)) { + return oldPreimageResultCache.get(proposalHash); + } + + const hashOnly = isHashOnlyStorageKey(api); + + const result = inlineData + ? resolveInlinePreimage(proposalHash, hashOnly, api, inlineData) + : await fetchOldPreimage(proposalHash, api, hashOnly); + + if (result?.isCompleted) { + oldPreimageResultCache.set(proposalHash, result); + } + + return result; + }, [hashOrBounded, api]); + + const isStatusLoaded = Boolean(api) && !loading; + const isPreimageLoaded = + Boolean(api) && !loading && Boolean(value?.isCompleted); + + return [value ?? {}, isStatusLoaded, isPreimageLoaded]; +} diff --git a/packages/next-common/hooks/usePreimageNew.js b/packages/next-common/hooks/usePreimageNew.js index 868d8e1c98..3c3010db5f 100644 --- a/packages/next-common/hooks/usePreimageNew.js +++ b/packages/next-common/hooks/usePreimageNew.js @@ -1,57 +1,21 @@ import { useAsync } from "react-use"; import { useContextApi } from "next-common/context/api"; -import { - BN, - BN_ZERO, - formatNumber, - isString, - isU8a, - u8aToHex, -} from "@polkadot/util"; +import { BN_ZERO } from "@polkadot/util"; import { Option } from "@polkadot/types"; -import { buildNoBytesResult } from "./usePreimageNewCommon"; +import { + buildNoBytesResult, + buildBaseResult, + buildCompletedResult, + buildPreimageForKey, + decodeCallBytes, + extractCallData, + isHashOnlyStorageKey, + parseHashOrBounded, + resolveInlinePreimage, +} from "./usePreimageNewCommon"; const preimageResultCache = new Map(); -function parseHashOrBounded(hashOrBounded, api) { - if (isString(hashOrBounded)) { - return { proposalHash: hashOrBounded }; - } - - if (isU8a(hashOrBounded)) { - return { proposalHash: hashOrBounded.toHex() }; - } - - if (hashOrBounded.isInline) { - const inlineData = hashOrBounded.asInline.toU8a(true); - const proposalHash = u8aToHex(api?.registry.hash(inlineData)); - return { proposalHash, inlineData }; - } - - if (hashOrBounded.isLegacy) { - return { proposalHash: hashOrBounded.asLegacy.hash_.toHex() }; - } - - if (hashOrBounded.isLookup) { - return { proposalHash: hashOrBounded.asLookup.hash_.toHex() }; - } - - console.error( - `Unhandled FrameSupportPreimagesBounded type: ${hashOrBounded.type}`, - ); - return {}; -} - -function isHashOnlyStorageKey(api) { - if (!api?.query.preimage?.preimageFor?.creator.meta.type.isMap) { - return false; - } - const { type } = api.registry.lookup.getTypeDef( - api.query.preimage.preimageFor.creator.meta.type.asMap.key, - ); - return type === "H256"; -} - function parseTicket(rawTicket) { if (!rawTicket) return undefined; return { who: rawTicket[0].toString(), amount: rawTicket[1] }; @@ -98,101 +62,6 @@ function parseRequestStatus(optStatus) { return { status }; } -function buildPreimageForKey(proposalHash, proposalLength, hashOnly) { - if (hashOnly) { - return [proposalHash]; - } - return [[proposalHash, proposalLength || BN_ZERO]]; -} - -function extractCallData(optBytes) { - if (!optBytes) return null; - - const callData = isU8a(optBytes) - ? optBytes - : optBytes.unwrapOr?.(null) ?? optBytes; - - if (!callData) return null; - if (isU8a(callData) && callData.length === 0) return null; - if (isString(callData) && callData === "0x") return null; - if (typeof callData?.toHex === "function" && callData.toHex() === "0x") - return null; - - return callData; -} - -function decodeCallBytes(callData, registry, proposalLength) { - let proposal = null; - let proposalError = null; - let proposalWarning = null; - let resolvedLength; - - try { - proposal = registry.createType("Call", callData); - const callLength = proposal.encodedLength; - - if (proposalLength) { - const storeLength = proposalLength.toNumber(); - if (callLength !== storeLength) { - proposalWarning = `Decoded call length does not match on-chain stored preimage length (${formatNumber( - callLength, - )} bytes vs ${formatNumber(storeLength)} bytes)`; - } - } else { - // status 未提供 proposalLength,从解码的 Call 中推导 - resolvedLength = new BN(callLength); - } - } catch { - proposalError = "Unable to decode preimage bytes into a valid Call"; - } - - return { proposal, proposalError, proposalWarning, resolvedLength }; -} - -function buildBaseResult(proposalHash, hashOnly, registry, inlineData) { - return { - proposalHash, - count: 0, - isCompleted: false, - isHashParam: hashOnly, - registry, - status: null, - ticket: undefined, - statusName: null, - // 若为 inline data,proposalLength 可从字节长度得出 - proposalLength: inlineData ? new BN(inlineData.length) : null, - proposal: null, - proposalError: null, - proposalWarning: null, - }; -} - -function buildCompletedResult(base, decoded) { - return { - ...base, - isCompleted: true, - proposal: decoded.proposal ?? null, - proposalError: decoded.proposalError ?? null, - proposalWarning: decoded.proposalWarning ?? null, - // 若 decoded 提供了推导出的 length 则使用,否则沿用 base 中的 - proposalLength: decoded.resolvedLength ?? base.proposalLength, - }; -} - -function resolveInlinePreimage(proposalHash, hashOnly, api, inlineData) { - const base = buildBaseResult( - proposalHash, - hashOnly, - api.registry, - inlineData, - ); - const callData = extractCallData(inlineData); - const decoded = callData - ? decodeCallBytes(callData, api.registry, base.proposalLength) - : { proposal: null, proposalError: null, proposalWarning: null }; - return buildCompletedResult(base, decoded); -} - async function fetchPreimage(proposalHash, api, hashOnly) { const base = buildBaseResult(proposalHash, hashOnly, api.registry, null); diff --git a/packages/next-common/hooks/usePreimageNewCommon.js b/packages/next-common/hooks/usePreimageNewCommon.js index 6967a2fb0f..1edf1f8e33 100644 --- a/packages/next-common/hooks/usePreimageNewCommon.js +++ b/packages/next-common/hooks/usePreimageNewCommon.js @@ -1,4 +1,11 @@ -import { BN, BN_ZERO, isString, isU8a } from "@polkadot/util"; +import { + BN, + BN_ZERO, + formatNumber, + isString, + isU8a, + u8aToHex, +} from "@polkadot/util"; import { blake2AsHex } from "@polkadot/util-crypto"; import { decodePreimageWithPapi } from "./useOldPreimageCommon"; @@ -79,3 +86,140 @@ export async function resolveInlinePapiPreimage( return decoded; } + +// ── polkadot-js shared (used by usePreimage + useOldPreimage) ──────────────── + +// 将任意 hashOrBounded 输入统一为 { proposalHash, inlineData? } +// inline hash 通过 api.registry.hash 计算 +export function parseHashOrBounded(hashOrBounded, api) { + if (isString(hashOrBounded)) { + return { proposalHash: hashOrBounded }; + } + + if (isU8a(hashOrBounded)) { + return { proposalHash: hashOrBounded.toHex() }; + } + + if (hashOrBounded.isInline) { + const inlineData = hashOrBounded.asInline.toU8a(true); + const proposalHash = u8aToHex(api?.registry.hash(inlineData)); + return { proposalHash, inlineData }; + } + + if (hashOrBounded.isLegacy) { + return { proposalHash: hashOrBounded.asLegacy.hash_.toHex() }; + } + + if (hashOrBounded.isLookup) { + return { proposalHash: hashOrBounded.asLookup.hash_.toHex() }; + } + + console.error( + `Unhandled FrameSupportPreimagesBounded type: ${hashOrBounded.type}`, + ); + return {}; +} + +// 判断 preimageFor 存储的 key 类型:H256-only(旧版)还是 (H256, u32)(新版) +export function isHashOnlyStorageKey(api) { + if (!api?.query.preimage?.preimageFor?.creator.meta.type.isMap) { + return false; + } + const { type } = api.registry.lookup.getTypeDef( + api.query.preimage.preimageFor.creator.meta.type.asMap.key, + ); + return type === "H256"; +} + +export function buildPreimageForKey(proposalHash, proposalLength, hashOnly) { + if (hashOnly) { + return [proposalHash]; + } + return [[proposalHash, proposalLength || BN_ZERO]]; +} + +export function extractCallData(optBytes) { + if (!optBytes) return null; + + const callData = isU8a(optBytes) + ? optBytes + : optBytes.unwrapOr?.(null) ?? optBytes; + + if (!callData) return null; + if (isU8a(callData) && callData.length === 0) return null; + if (isString(callData) && callData === "0x") return null; + if (typeof callData?.toHex === "function" && callData.toHex() === "0x") + return null; + + return callData; +} + +export function decodeCallBytes(callData, registry, proposalLength) { + let proposal = null; + let proposalError = null; + let proposalWarning = null; + let resolvedLength; + + try { + proposal = registry.createType("Call", callData); + const callLength = proposal.encodedLength; + + if (proposalLength) { + const storeLength = proposalLength.toNumber(); + if (callLength !== storeLength) { + proposalWarning = `Decoded call length does not match on-chain stored preimage length (${formatNumber( + callLength, + )} bytes vs ${formatNumber(storeLength)} bytes)`; + } + } else { + resolvedLength = new BN(callLength); + } + } catch { + proposalError = "Unable to decode preimage bytes into a valid Call"; + } + + return { proposal, proposalError, proposalWarning, resolvedLength }; +} + +export function buildBaseResult(proposalHash, hashOnly, registry, inlineData) { + return { + proposalHash, + count: 0, + isCompleted: false, + isHashParam: hashOnly, + registry, + status: null, + ticket: undefined, + deposit: undefined, + statusName: null, + proposalLength: inlineData ? new BN(inlineData.length) : null, + proposal: null, + proposalError: null, + proposalWarning: null, + }; +} + +export function buildCompletedResult(base, decoded) { + return { + ...base, + isCompleted: true, + proposal: decoded.proposal ?? null, + proposalError: decoded.proposalError ?? null, + proposalWarning: decoded.proposalWarning ?? null, + proposalLength: decoded.resolvedLength ?? base.proposalLength, + }; +} + +export function resolveInlinePreimage(proposalHash, hashOnly, api, inlineData) { + const base = buildBaseResult( + proposalHash, + hashOnly, + api.registry, + inlineData, + ); + const callData = extractCallData(inlineData); + const decoded = callData + ? decodeCallBytes(callData, api.registry, base.proposalLength) + : { proposal: null, proposalError: null, proposalWarning: null }; + return buildCompletedResult(base, decoded); +} From 2320196611fb9210b98963f7a1eefda70f30a629 Mon Sep 17 00:00:00 2001 From: chaojun Date: Mon, 30 Mar 2026 14:55:07 +0800 Subject: [PATCH 3/6] Remove enable papi flag --- packages/next-common/utils/consts/settings/hydradx.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/next-common/utils/consts/settings/hydradx.js b/packages/next-common/utils/consts/settings/hydradx.js index b6cf57a250..678823c201 100644 --- a/packages/next-common/utils/consts/settings/hydradx.js +++ b/packages/next-common/utils/consts/settings/hydradx.js @@ -158,7 +158,6 @@ const hydradx = { openSquare: { voting: "hydration", }, - enablePapi: true, }; export default hydradx; From d1dff2ac209ccbf046a87333767d8dd3f5a40b09 Mon Sep 17 00:00:00 2001 From: chaojun Date: Mon, 30 Mar 2026 15:00:10 +0800 Subject: [PATCH 4/6] Update --- packages/next-common/hooks/common/useSubStorage.js | 2 +- packages/next-common/hooks/useOldPreimageCommon.js | 4 ++-- packages/next-common/utils/callDecoder/decoder.mjs | 3 ++- packages/next-common/utils/callDecoder/typeName.mjs | 4 +++- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/next-common/hooks/common/useSubStorage.js b/packages/next-common/hooks/common/useSubStorage.js index b6babd219e..b15feb933a 100644 --- a/packages/next-common/hooks/common/useSubStorage.js +++ b/packages/next-common/hooks/common/useSubStorage.js @@ -151,7 +151,7 @@ export default function useSubStorage( const paramsKey = normalizedParams .map((p) => { - if (p === null || p === undefined) return "null"; + if (isNil(p)) return "null"; if (typeof p === "object") { try { return JSON.stringify(p); diff --git a/packages/next-common/hooks/useOldPreimageCommon.js b/packages/next-common/hooks/useOldPreimageCommon.js index d7fa844d5a..4dc8d7ebaf 100644 --- a/packages/next-common/hooks/useOldPreimageCommon.js +++ b/packages/next-common/hooks/useOldPreimageCommon.js @@ -103,7 +103,7 @@ function unwrapMaybeValue(value) { export function toPreimageLength(value) { const unwrappedValue = unwrapMaybeValue(value); - if (unwrappedValue === null || unwrappedValue === undefined) { + if (isNil(unwrappedValue)) { return null; } @@ -116,7 +116,7 @@ export function toPreimageLength(value) { export function toPreimageCount(value) { const unwrappedValue = unwrapMaybeValue(value); - if (unwrappedValue === null || unwrappedValue === undefined) { + if (isNil(unwrappedValue)) { return undefined; } diff --git a/packages/next-common/utils/callDecoder/decoder.mjs b/packages/next-common/utils/callDecoder/decoder.mjs index 1fb2d9a532..85286a922c 100644 --- a/packages/next-common/utils/callDecoder/decoder.mjs +++ b/packages/next-common/utils/callDecoder/decoder.mjs @@ -1,3 +1,4 @@ +import { isNil } from "lodash-es"; import { toTypedCallTree } from "./treeBuilder.mjs"; import { normalizeCallTree } from "./treeNormalize.mjs"; import { @@ -141,7 +142,7 @@ function getCallDocs(metadata, section, method) { ); const callsType = pallet?.calls?.type; - if (callsType === undefined) { + if (isNil(callsType)) { return null; } diff --git a/packages/next-common/utils/callDecoder/typeName.mjs b/packages/next-common/utils/callDecoder/typeName.mjs index 2dfd86ad4e..3fcbe712c8 100644 --- a/packages/next-common/utils/callDecoder/typeName.mjs +++ b/packages/next-common/utils/callDecoder/typeName.mjs @@ -1,6 +1,8 @@ +import { isNil } from "lodash-es"; + function getPathBasedTypeName(metadata, typeId) { // try path-based naming - if (typeId === undefined || typeId === null) { + if (isNil(typeId)) { return null; } const rawLookup = metadata?.lookup?.[typeId]; From 2064ffbb8894be852e93383e200f582b763b4c40 Mon Sep 17 00:00:00 2001 From: chaojun Date: Mon, 30 Mar 2026 15:03:10 +0800 Subject: [PATCH 5/6] Update --- packages/next-common/hooks/usePreimageNewCommon.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/next-common/hooks/usePreimageNewCommon.js b/packages/next-common/hooks/usePreimageNewCommon.js index 1edf1f8e33..66b9879d5b 100644 --- a/packages/next-common/hooks/usePreimageNewCommon.js +++ b/packages/next-common/hooks/usePreimageNewCommon.js @@ -89,8 +89,6 @@ export async function resolveInlinePapiPreimage( // ── polkadot-js shared (used by usePreimage + useOldPreimage) ──────────────── -// 将任意 hashOrBounded 输入统一为 { proposalHash, inlineData? } -// inline hash 通过 api.registry.hash 计算 export function parseHashOrBounded(hashOrBounded, api) { if (isString(hashOrBounded)) { return { proposalHash: hashOrBounded }; @@ -120,7 +118,6 @@ export function parseHashOrBounded(hashOrBounded, api) { return {}; } -// 判断 preimageFor 存储的 key 类型:H256-only(旧版)还是 (H256, u32)(新版) export function isHashOnlyStorageKey(api) { if (!api?.query.preimage?.preimageFor?.creator.meta.type.isMap) { return false; From 5726040cbe908d59840dcc186028d2d340481fd3 Mon Sep 17 00:00:00 2001 From: chaojun Date: Mon, 30 Mar 2026 15:51:48 +0800 Subject: [PATCH 6/6] Update --- packages/next-common/hooks/useOldPreimageNew.js | 4 ++-- packages/next-common/hooks/usePreimageNew.js | 4 ++-- packages/next-common/hooks/usePreimagePapiNew.js | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/next-common/hooks/useOldPreimageNew.js b/packages/next-common/hooks/useOldPreimageNew.js index d168a39083..1390f36f59 100644 --- a/packages/next-common/hooks/useOldPreimageNew.js +++ b/packages/next-common/hooks/useOldPreimageNew.js @@ -27,7 +27,7 @@ function parseStatusFor(optStatus) { if (status.isRequested) { const req = status.asRequested; - // 旧版 runtime:asRequested 是 Option,无结构化字段 + // Older versions: asRequested is an option with no structured fields. if (req instanceof Option) { return { status, statusName: "requested" }; } @@ -42,7 +42,7 @@ function parseStatusFor(optStatus) { if (status.isUnrequested) { const unreq = status.asUnrequested; - // 旧版 runtime:asUnrequested 是 Option + // Older versions, asUnrequested is an option. if (unreq instanceof Option) { return { status, diff --git a/packages/next-common/hooks/usePreimageNew.js b/packages/next-common/hooks/usePreimageNew.js index 3c3010db5f..794dcb7452 100644 --- a/packages/next-common/hooks/usePreimageNew.js +++ b/packages/next-common/hooks/usePreimageNew.js @@ -27,7 +27,7 @@ function parseRequestStatus(optStatus) { if (status.isRequested) { const req = status.asRequested; - // 旧版 runtime:asRequested 是 Option,无结构化字段 + // Older versions: asRequested is an option with no structured fields. if (req instanceof Option) { return { status, statusName: "requested" }; } @@ -42,7 +42,7 @@ function parseRequestStatus(optStatus) { if (status.isUnrequested) { const unreq = status.asUnrequested; - // 旧版 runtime:asUnrequested 是 Option + // Older versions, asUnrequested is an option. if (unreq instanceof Option) { return { status, diff --git a/packages/next-common/hooks/usePreimagePapiNew.js b/packages/next-common/hooks/usePreimagePapiNew.js index 96d10acb2a..8b87c92fe1 100644 --- a/packages/next-common/hooks/usePreimagePapiNew.js +++ b/packages/next-common/hooks/usePreimagePapiNew.js @@ -21,7 +21,7 @@ const papiPreimageResultCache = new Map(); function parsePapiRequestStatus(rawStatus) { if (!rawStatus) return { status: null }; - const statusName = getPapiStatusName(rawStatus); // 首字母小写 + const statusName = getPapiStatusName(rawStatus); // Lowercase first letter const { type, value = {} } = rawStatus; const result = { status: rawStatus, statusName };