refactor: use top-level client to get blocks

This commit is contained in:
Victor Oliva
2025-04-18 17:10:49 +02:00
parent 6d1eb9f6e2
commit 51d82f2640
9 changed files with 449 additions and 535 deletions

View File

@@ -14,10 +14,8 @@
"semi": false
},
"dependencies": {
"@ledgerhq/hw-transport-webusb": "^6.29.4",
"@noble/hashes": "^1.8.0",
"@polkadot-api/descriptors": "file:.papi/descriptors",
"@polkadot-api/ledger-signer": "^0.1.12",
"@polkadot-api/metadata-builders": "^0.11.0",
"@polkadot-api/observable-client": "^0.9.0",
"@polkadot-api/react-builder": "0.2.6",
@@ -84,7 +82,6 @@
"packageManager": "pnpm@9.12.2+sha512.22721b3a11f81661ae1ec68ce1a7b879425a1ca5b991c975b074ac220b187ce56c708fe5db69f4c962c989452eee76c82877f4ee80f474cebd61ee13461b6228",
"pnpm": {
"patchedDependencies": {
"@polkadot-api/observable-client": "patches/@polkadot-api__observable-client.patch",
"react18-json-view": "patches/react18-json-view.patch"
}
}

View File

@@ -1,29 +0,0 @@
diff --git a/dist/esm/chainHead/chainHead.mjs b/dist/esm/chainHead/chainHead.mjs
index 299d6849e6b8f9b97f97fc444e2fbf5e31ca49cc..a0508f9ef99f549207e024e16d2e13903a047189 100644
--- a/dist/esm/chainHead/chainHead.mjs
+++ b/dist/esm/chainHead/chainHead.mjs
@@ -175,7 +175,7 @@ const getChainHead$ = (chainHead) => {
usingBlock
);
const _body$ = withOptionalHash$(commonEnhancer(lazyFollower("body"), "body"));
- const body$ = (hash) => upsertCachedStream(hash, "body", _body$(hash, true));
+ const body$ = (hash, canonical) => upsertCachedStream(hash, "body", _body$(hash, canonical));
const _storage$ = commonEnhancer(lazyFollower("storage"), "storage");
const storage$ = withOptionalHash$(
(hash, withCanonicalChain2, type, keyMapper, childTrie = null, mapper) => pinnedBlocks$.pipe(
@@ -289,11 +289,11 @@ const getChainHead$ = (chainHead) => {
metadata$,
genesis$,
header$,
- body$,
- call$: withCanonicalChain(call$),
- storage$: withCanonicalChain(storage$),
+ body$: withCanonicalChain(body$, false),
+ call$: withCanonicalChain(call$, false),
+ storage$: withCanonicalChain(storage$, false),
storageQueries$,
- eventsAt$: withCanonicalChain(eventsAt$),
+ eventsAt$: withCanonicalChain(eventsAt$, false),
holdBlock,
trackTx$,
trackTxWithoutEvents$,

714
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
import { CopyText } from "@/components/Copy"
import { Popover } from "@/components/Popover"
import { Link } from "@/hashParams"
import { chainHead$ } from "@/state/chains/chain.state"
import { client$ } from "@/state/chains/chain.state"
import { state, useStateObservable } from "@react-rxjs/core"
import { FC } from "react"
import { combineLatest, debounceTime, map, switchMap } from "rxjs"
@@ -10,7 +10,9 @@ import { BlockInfo, blocksByHeight$, finalized$ } from "./block.state"
import { BlockPopover } from "./BlockPopover"
import * as Finalizing from "./FinalizingTable"
const best$ = chainHead$.pipeState(switchMap((chainHead) => chainHead.best$))
const best$ = client$.pipeState(
switchMap((client) => client.bestBlocks$.pipe(map(([best]) => best))),
)
interface PositionedBlock {
block: BlockInfo

View File

@@ -1,5 +1,5 @@
import { chainHead$ } from "@/state/chains/chain.state"
import { CircularProgress } from "@/components/CircularProgress"
import { client$ } from "@/state/chains/chain.state"
import { state, useStateObservable } from "@react-rxjs/core"
import {
animationFrames,
@@ -10,7 +10,9 @@ import {
} from "rxjs"
import { targetBlockTime$ } from "./blockTime.state"
const best$ = chainHead$.pipeState(switchMap((chainHead) => chainHead.best$))
const best$ = client$.pipeState(
switchMap((client) => client.bestBlocks$.pipe(map(([best]) => best))),
)
const bestBlockTime$ = best$.pipeState(
// Only count when increasing height
map((block) => block.number),

View File

@@ -1,4 +1,6 @@
import { CircularProgress } from "@/components/CircularProgress"
import { groupBy } from "@/lib/groupBy"
import { chainClient$, client$, runtimeCtx$ } from "@/state/chains/chain.state"
import { state, useStateObservable } from "@react-rxjs/core"
import {
catchError,
@@ -11,17 +13,11 @@ import {
withLatestFrom,
} from "rxjs"
import { targetBlockTime$ } from "./blockTime.state"
import {
chainClient$,
chainHead$,
runtimeCtx$,
} from "@/state/chains/chain.state"
import { groupBy } from "@/lib/groupBy"
const bestBlock$ = chainHead$.pipeState(
const bestBlock$ = client$.pipeState(
// Only count when increasing height
switchMap((chainHead) => chainHead.best$),
map((block) => block.number),
switchMap((client) => client.bestBlocks$),
map(([block]) => block.number),
distinctUntilChanged((prev, current) => prev >= current),
)

View File

@@ -1,18 +1,18 @@
import { chainHead$, runtimeCtx$ } from "@/state/chains/chain.state"
import { client$, runtimeCtx$ } from "@/state/chains/chain.state"
import { useStateObservable, withDefault } from "@react-rxjs/core"
import { FC, PropsWithChildren } from "react"
import { map, switchMap } from "rxjs"
import { twMerge } from "tailwind-merge"
import { BlockTime } from "./BlockTime"
import { EpochRemainingTime } from "./EpochTime"
import { useStateObservable, withDefault } from "@react-rxjs/core"
const finalized$ = chainHead$.pipeState(
switchMap((chainHead) => chainHead.finalized$),
const finalized$ = client$.pipeState(
switchMap((chainHead) => chainHead.finalizedBlock$),
map((v) => v.number.toLocaleString()),
)
const best$ = chainHead$.pipeState(
switchMap((chainHead) => chainHead.best$),
map((v) => v.number.toLocaleString()),
const best$ = client$.pipeState(
switchMap((chainHead) => chainHead.bestBlocks$),
map(([v]) => v.number.toLocaleString()),
)
// epoch is only available for relay chains

View File

@@ -1,32 +1,25 @@
import { chainClient$, chainHead$ } from "@/state/chains/chain.state"
import {
ChainHead$,
PinnedBlocks,
SystemEvent,
} from "@polkadot-api/observable-client"
import { StopError } from "@polkadot-api/substrate-client"
import { state, withDefault } from "@react-rxjs/core"
import { chainClient$, client$ } from "@/state/chains/chain.state"
import { SystemEvent } from "@polkadot-api/observable-client"
import { state } from "@react-rxjs/core"
import { partitionByKey, toKeySet } from "@react-rxjs/utils"
import { HexString } from "polkadot-api"
import { HexString, PolkadotClient } from "polkadot-api"
import {
catchError,
combineLatest,
concat,
concatMap,
defer,
distinctUntilChanged,
EMPTY,
filter,
forkJoin,
from,
map,
merge,
mergeMap,
NEVER,
Observable,
ObservedValueOf,
of,
repeat,
retry,
scan,
skip,
startWith,
@@ -40,9 +33,8 @@ import {
withLatestFrom,
} from "rxjs"
export const finalized$ = chainHead$.pipeState(
switchMap((chainHead) => chainHead.finalized$),
withDefault(null),
export const finalized$ = client$.pipeState(
switchMap((client) => client.finalizedBlock$),
)
export enum BlockState {
@@ -67,101 +59,53 @@ export interface BlockInfo {
status: BlockState
}
export const [blockInfo$, recordedBlocks$] = partitionByKey(
chainHead$.pipe(
switchMap((chainHead) =>
chainHead.follow$.pipe(
withInitializedNumber(),
scan(
(acc, evt) => {
switch (evt.type) {
case "initialized": {
const blockNumbers: Record<string, number> = {}
evt.finalizedBlockHashes.forEach((hash, i) => {
const parent = blockNumbers[evt.finalizedBlockHashes[i - 1]]
blockNumbers[hash] = parent != null ? parent + 1 : evt.number
})
return {
value: evt.finalizedBlockHashes.map((hash, i) => ({
hash,
parent: evt.finalizedBlockHashes[i - 1] || evt.parentHash,
number: blockNumbers[hash],
})),
blockNumbers,
}
}
case "newBlock": {
const number = acc.blockNumbers[evt.parentBlockHash] + 1
acc.blockNumbers[evt.blockHash] = number
return {
value: [
{
hash: evt.blockHash,
parent: evt.parentBlockHash,
number,
},
],
blockNumbers: acc.blockNumbers,
}
}
}
return { value: [], blockNumbers: acc.blockNumbers }
},
{ value: [], blockNumbers: {} } as {
value: Array<{
hash: string
parent: string
number: number
}>
blockNumbers: Record<string, number>
},
),
mergeMap(({ value }) => value),
retryOnStopError(),
),
),
),
client$.pipe(switchMap((client) => client.blocks$)),
(v) => v.hash,
(initialized$) =>
initialized$.pipe(
(block$) =>
block$.pipe(
take(1),
withLatestFrom(chainHead$),
withLatestFrom(client$),
switchMap(
([{ hash, parent, number }, chainHead]): Observable<BlockInfo> =>
([{ hash, parent, number }, client]): Observable<BlockInfo> =>
concat(
combineLatest({
hash: of(hash),
parent: of(parent),
number: of(number),
body: chainHead.body$(hash).pipe(
body: from(client.getBlockBody(hash)).pipe(
startWith(null),
catchError((err) => {
console.error("fetch body failed", err)
return of(null)
}),
),
events: chainHead.eventsAt$(hash).pipe(
events: from(
client.getUnsafeApi().query.System.Events.getValue({
at: hash,
}),
).pipe(
startWith(null),
catchError((err) => {
console.error("fetch events failed", err)
return of(null)
}),
),
header: chainHead.header$(hash).pipe(
header: from(client.getBlockHeader(hash)).pipe(
startWith(null),
catchError((err) => {
console.error("fetch header failed", err)
return of(null)
}),
),
status: getBlockStatus$(chainHead, hash, number),
status: getBlockStatus$(client, hash, number),
}),
NEVER,
),
),
takeUntil(
merge(
// Reset when chainHead is changed
chainHead$.pipe(skip(1)),
// Reset when client is changed
client$.pipe(skip(1)),
// Or after 1 hour
timer(60 * 60 * 1000),
),
@@ -295,81 +239,29 @@ export const blocksByHeight$ = state(
),
)
function withInitializedNumber() {
return (source$: Observable<ObservedValueOf<ChainHead$["follow$"]>>) =>
source$.pipe(
withLatestFrom(chainHead$),
concatMap(([event, chainHead]) => {
return event.type !== "initialized"
? of(event)
: chainHead.header$(event.finalizedBlockHashes[0]).pipe(
map((header) => ({
...event,
number: header.number,
parentHash: header.parentHash,
})),
)
}),
)
}
const getBlockStatus$ = (
chainHead: ChainHead$,
client: PolkadotClient,
hash: string,
number: number,
): Observable<BlockState> =>
chainHead.pinnedBlocks$.pipe(
take(1),
switchMap((pinnedBlocks) => {
const block = (hash: string) => pinnedBlocks.blocks.get(hash)
const blockNum = (hash: string) => block(hash)?.number
const finalized = blockNum(pinnedBlocks.finalized)!
if (number <= finalized) {
// assume we are in the list of finalized blocks on `initialized`
return of(BlockState.Finalized)
}
const getInitialState = (pinnedBlocks: PinnedBlocks) => {
let bestBranch = pinnedBlocks.best
while (blockNum(bestBranch)! > finalized && hash !== bestBranch) {
bestBranch = block(bestBranch)!.parent
}
return block(bestBranch)?.hash === hash
? BlockState.Best
: BlockState.Fork
}
const initialState = getInitialState(pinnedBlocks)
return chainHead.follow$.pipe(
concatMap((evt) => {
switch (evt.type) {
case "bestBlockChanged":
return chainHead.pinnedBlocks$.pipe(take(1), map(getInitialState))
case "finalized":
if (evt.finalizedBlockHashes.includes(hash))
return of(BlockState.Finalized)
if (evt.prunedBlockHashes.includes(hash))
return of(BlockState.Pruned)
}
return of(null)
}),
filter((v) => v !== null),
retryOnStopError(),
takeWhile(
(v) => v !== BlockState.Finalized && v !== BlockState.Pruned,
true,
),
startWith(initialState),
)
}),
merge(
client.finalizedBlock$.pipe(
take(1),
// If the latest finalized is ahead, assume it is finalized (?)
filter((b) => b.number > number),
map(() => BlockState.Finalized),
),
combineLatest([client.bestBlocks$, client.finalizedBlock$]).pipe(
map(([best, finalized]) => {
if (finalized.hash === hash) return BlockState.Finalized
if (finalized.number === number) return BlockState.Pruned
if (best.some((b) => b.hash === hash)) return BlockState.Best
return BlockState.Fork
}),
),
).pipe(
takeWhile(
(v) => v !== BlockState.Finalized && v !== BlockState.Pruned,
true,
),
)
const retryOnStopError = <T>() =>
retry<T>({
delay(error) {
if (error instanceof StopError) {
return of(null)
}
throw error
},
})

View File

@@ -146,9 +146,7 @@ export const chainClient$ = state(
sinkSuspense(),
),
)
export const chainHead$ = state(
chainClient$.pipe(map(({ chainHead }) => chainHead)),
)
export const client$ = state(chainClient$.pipe(map(({ client }) => client)))
export const unsafeApi$ = chainClient$.pipeState(
map(({ client }) => client.getUnsafeApi()),