diff --git a/package.json b/package.json index e4ab2bb..90e69d1 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ghost-dao-interface", "private": true, - "version": "0.5.38", + "version": "0.5.39", "type": "module", "scripts": { "dev": "vite", diff --git a/src/containers/Bridge/Bridge.jsx b/src/containers/Bridge/Bridge.jsx index b0b9de5..33f194c 100644 --- a/src/containers/Bridge/Bridge.jsx +++ b/src/containers/Bridge/Bridge.jsx @@ -10,6 +10,7 @@ import { useMediaQuery, } from "@mui/material"; import { decodeAddress } from "@polkadot/util-crypto"; +import { fromHex } from "@polkadot-api/utils"; import { getBlockNumber } from "@wagmi/core"; import { useTransaction } from "wagmi"; import { keccak256 } from "viem"; @@ -42,6 +43,8 @@ import { useCurrentSlot, useGenesisSlot, useErasTotalStake, + useLatestBlockNumber, + useEraIndex, } from "../../hooks/ghost"; import { ValidatorTable } from "./ValidatorTable"; @@ -102,24 +105,28 @@ const Bridge = ({ chainId, address, config, connect }) => { const networkIdEncoded = u64.enc(BigInt(chainId)); const amountEncoded = u128.enc(BigInt(watchTransaction.amount)); const addressEncoded = decodeAddress(watchTransaction.receiverAddress, false, 1996); + const transactionHashEncoded = fromHex(watchTransaction.transactionHash); const blockNumber = u64.enc(watchTransactionInfo?.blockNumber ?? 0n); const clapArgsStr = new Uint8Array([ ...addressEncoded, ...amountEncoded, ...blockNumber, + ...transactionHashEncoded, ...networkIdEncoded ]); return keccak256(clapArgsStr) }, [watchTransaction, watchTransactionInfo]) + const latestBlockNumber = useLatestBlockNumber(); + const eraIndex = useEraIndex(); const currentSlot = useCurrentSlot(); const genesisSlot = useGenesisSlot(); const currentSession = useCurrentIndex(); const applauseThreshold = useApplauseThreshold(); const evmNetwork = useEvmNetwork({ evmChainId: chainId }); const totalStakedAmount = useErasTotalStake({ - eraIndex: Math.floor((watchTransaction?.sessionIndex ?? currentSession) / 6) + eraIndex: eraIndex?.index ?? 0, }); const authorities = useAuthorities({ currentSession: watchTransaction?.sessionIndex ?? currentSession @@ -205,16 +212,20 @@ const Bridge = ({ chainId, address, config, connect }) => { const latestCommits = useMemo(() => { return validators?.map((validator, index) => { + const lastUpdatedNumber = Number(blockCommitments?.at(index)?.last_updated ?? 0); + const timestampDelta = (latestBlockNumber - lastUpdatedNumber) * 6000; // ideal 6 seconds for block + const lastUpdatedTimestamp = Math.floor(currentTime - timestampDelta); + return { validator: validator, - lastActive: currentTime - Number(blockCommitments?.at(index)?.last_updated ?? 0), - lastUpdated: blockCommitments?.at(index)?.last_updated, + lastActive: timestampDelta, + lastUpdated: lastUpdatedTimestamp, lastStoredBlock: blockCommitments?.at(index)?.last_stored_block, storedBlockTime: (blockCommitments?.at(index)?.last_stored_block ?? 0n) * networkAvgBlockSpeed(chainId), disabled: disabledValidators?.includes(index), } }) - }, [blockCommitments, disabledValidators, validators, chainId]); + }, [blockCommitments, disabledValidators, validators, latestBlockNumber, chainId]); const latestUpdate = useMemo(() => { const validCommits = latestCommits?.filter(commit => diff --git a/src/hooks/ghost/MetadataProvider.jsx b/src/hooks/ghost/MetadataProvider.jsx index 29b5838..80c0f61 100644 --- a/src/hooks/ghost/MetadataProvider.jsx +++ b/src/hooks/ghost/MetadataProvider.jsx @@ -8,15 +8,24 @@ import { useUnstableProvider } from "./UnstableProvider" const MetadataProvider = createContext(null) export const useMetadata = () => useContext(MetadataProvider) +const CACHE_VERSION = "v2" + export const MetadataProviderProvider = ({ children }) => { const { client, chainId } = useUnstableProvider() const { data: metadata } = useSWR( client && chainId ? ["metadata", client, chainId] : null, async ([_, client]) => { - const storageKey = `metadata-${chainId}` + const storageKey = `metadata-${chainId}-${CACHE_VERSION}` const storedMetadata = sessionStorage.getItem(storageKey) + if (storedMetadata) return unifyMetadata(decAnyMetadata(storedMetadata)) + Object.keys(sessionStorage).forEach(key => { + if (key.startsWith("metadata-")) { + sessionStorage.removeItem(key); + } + }); + const metadata = await new Promise((resolve, reject) => client._request("state_getMetadata", [], { onSuccess: resolve, diff --git a/src/hooks/ghost/index.js b/src/hooks/ghost/index.js index d76ab9c..02d4cea 100644 --- a/src/hooks/ghost/index.js +++ b/src/hooks/ghost/index.js @@ -12,3 +12,5 @@ export * from "./useBlockCommitments"; export * from "./useApplauseDetails"; export * from "./useBabeSlots"; export * from "./useErasTotalStaked"; +export * from "./useLatestBlockNumber"; +export * from "./useEraIndex"; diff --git a/src/hooks/ghost/useEraIndex.js b/src/hooks/ghost/useEraIndex.js new file mode 100644 index 0000000..0c52f0f --- /dev/null +++ b/src/hooks/ghost/useEraIndex.js @@ -0,0 +1,41 @@ +import useSWRSubscription from "swr/subscription" +import { getDynamicBuilder, getLookupFn } from "@polkadot-api/metadata-builders" +import { distinct, filter, map, mergeMap } from "rxjs" + +import { useUnstableProvider } from "./UnstableProvider" +import { useMetadata } from "./MetadataProvider" + +export const useEraIndex = () => { + const { chainHead$, chainId } = useUnstableProvider() + const metadata = useMetadata() + const { data: eraIndex } = useSWRSubscription( + chainHead$ && chainId && metadata + ? ["eraIndex", chainHead$, chainId, metadata] + : null, + ([_, chainHead$, chainId, metadata], { next }) => { + const { finalized$, storage$ } = chainHead$ + const subscription = finalized$.pipe( + filter(Boolean), + mergeMap((blockInfo) => { + const builder = getDynamicBuilder(getLookupFn(metadata)) + const eraIndex = builder.buildStorage("Staking", "ActiveEra") + return storage$(blockInfo?.hash, "value", () => + eraIndex?.keys.enc() + ).pipe( + filter(Boolean), + distinct(), + map((value) => eraIndex?.value.dec(value)) + ) + }), + ) + .subscribe({ + next(eraIndex) { + next(null, eraIndex) + }, + error: next, + }) + return () => subscription.unsubscribe() + } + ) + return eraIndex; +} diff --git a/src/hooks/ghost/useErasTotalStaked.js b/src/hooks/ghost/useErasTotalStaked.js index f08ee1e..a3da566 100644 --- a/src/hooks/ghost/useErasTotalStaked.js +++ b/src/hooks/ghost/useErasTotalStaked.js @@ -5,14 +5,14 @@ import { distinct, filter, map, mergeMap } from "rxjs" import { useUnstableProvider } from "./UnstableProvider" import { useMetadata } from "./MetadataProvider" -export const useErasTotalStake = ({ eraIndex }) => { +export const useErasTotalStake = ({ epochIndex }) => { const { chainHead$, chainId } = useUnstableProvider() const metadata = useMetadata() const { data: eraTotalStake } = useSWRSubscription( chainHead$ && chainId && metadata - ? ["eraTotalStake", chainHead$, eraIndex, chainId, metadata] + ? ["eraTotalStake", chainHead$, epochIndex, chainId, metadata] : null, - ([_, chainHead$, eraIndex, chainId, metadata], { next }) => { + ([_, chainHead$, epochIndex, chainId, metadata], { next }) => { const { finalized$, storage$ } = chainHead$ const subscription = finalized$.pipe( filter(Boolean), @@ -20,7 +20,7 @@ export const useErasTotalStake = ({ eraIndex }) => { const builder = getDynamicBuilder(getLookupFn(metadata)) const eraTotalStake = builder.buildStorage("Staking", "ErasTotalStake") return storage$(blockInfo?.hash, "value", () => - eraTotalStake?.keys.enc(eraIndex) + eraTotalStake?.keys.enc(epochIndex) ).pipe( filter(Boolean), distinct(), diff --git a/src/hooks/ghost/useLatestBlockNumber.js b/src/hooks/ghost/useLatestBlockNumber.js new file mode 100644 index 0000000..0e8f54a --- /dev/null +++ b/src/hooks/ghost/useLatestBlockNumber.js @@ -0,0 +1,18 @@ +import { useState, useEffect } from "react"; +import useSWRSubscription from "swr/subscription" +import { useUnstableProvider } from "./UnstableProvider" + +export const useLatestBlockNumber = () => { + const { chainHead$ } = useUnstableProvider(); + const [blockNumber, setBlockNumber] = useState(null); + + useEffect(() => { + if (!chainHead$) return; + const subscription = chainHead$.best$.subscribe((block) => { + setBlockNumber(block.number); + }); + return () => subscription.unsubscribe(); + }, [chainHead$]); + + return blockNumber; +}