@dedot/api 0.18.8 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/chaintypes/substrate/consts.d.ts +2 -2
  2. package/chaintypes/substrate/consts.js +1 -0
  3. package/chaintypes/substrate/errors.d.ts +909 -909
  4. package/chaintypes/substrate/errors.js +1 -0
  5. package/chaintypes/substrate/events.d.ts +664 -664
  6. package/chaintypes/substrate/events.js +1 -0
  7. package/chaintypes/substrate/index.d.ts +13 -16
  8. package/chaintypes/substrate/index.js +1 -0
  9. package/chaintypes/substrate/json-rpc.d.ts +2 -2
  10. package/chaintypes/substrate/json-rpc.js +1 -0
  11. package/chaintypes/substrate/query.d.ts +509 -527
  12. package/chaintypes/substrate/query.js +1 -0
  13. package/chaintypes/substrate/runtime.d.ts +123 -123
  14. package/chaintypes/substrate/runtime.js +1 -0
  15. package/chaintypes/substrate/tx.d.ts +1311 -1311
  16. package/chaintypes/substrate/tx.js +1 -0
  17. package/chaintypes/substrate/types.js +1 -0
  18. package/chaintypes/substrate/view-functions.d.ts +2 -2
  19. package/chaintypes/substrate/view-functions.js +1 -0
  20. package/cjs/chaintypes/substrate/consts.js +1 -0
  21. package/cjs/chaintypes/substrate/errors.js +1 -0
  22. package/cjs/chaintypes/substrate/events.js +1 -0
  23. package/cjs/chaintypes/substrate/index.js +1 -0
  24. package/cjs/chaintypes/substrate/json-rpc.js +1 -0
  25. package/cjs/chaintypes/substrate/query.js +1 -0
  26. package/cjs/chaintypes/substrate/runtime.js +1 -0
  27. package/cjs/chaintypes/substrate/tx.js +1 -0
  28. package/cjs/chaintypes/substrate/types.js +1 -0
  29. package/cjs/chaintypes/substrate/view-functions.js +1 -0
  30. package/cjs/client/BaseSubstrateClient.js +45 -2
  31. package/cjs/client/DedotClient.js +356 -224
  32. package/cjs/client/LegacyClient.js +99 -23
  33. package/cjs/client/V2Client.js +316 -0
  34. package/cjs/client/explorer/LegacyBlockExplorer.js +263 -0
  35. package/cjs/client/explorer/V2BlockExplorer.js +74 -0
  36. package/cjs/client/explorer/index.js +18 -0
  37. package/cjs/client/utils.js +57 -0
  38. package/cjs/executor/EventExecutor.js +1 -0
  39. package/cjs/executor/RuntimeApiExecutor.js +49 -2
  40. package/cjs/executor/StorageQueryExecutor.js +114 -2
  41. package/cjs/executor/ViewFunctionExecutor.js +21 -2
  42. package/cjs/executor/v2/StorageQueryExecutorV2.js +7 -1
  43. package/cjs/executor/validation-helpers.js +181 -0
  44. package/cjs/extrinsic/submittable/BaseSubmittableExtrinsic.js +86 -2
  45. package/cjs/extrinsic/submittable/SubmittableExtrinsic.js +38 -16
  46. package/cjs/extrinsic/submittable/SubmittableExtrinsicV2.js +12 -2
  47. package/cjs/json-rpc/group/ChainHead/ChainHead.js +226 -21
  48. package/cjs/storage/QueryableStorage.js +1 -0
  49. package/client/BaseSubstrateClient.d.ts +34 -18
  50. package/client/BaseSubstrateClient.js +47 -4
  51. package/client/DedotClient.d.ts +346 -49
  52. package/client/DedotClient.js +356 -224
  53. package/client/LegacyClient.d.ts +22 -17
  54. package/client/LegacyClient.js +101 -25
  55. package/client/V2Client.d.ts +75 -0
  56. package/client/V2Client.js +312 -0
  57. package/client/explorer/LegacyBlockExplorer.d.ts +68 -0
  58. package/client/explorer/LegacyBlockExplorer.js +259 -0
  59. package/client/explorer/V2BlockExplorer.d.ts +40 -0
  60. package/client/explorer/V2BlockExplorer.js +70 -0
  61. package/client/explorer/index.d.ts +2 -0
  62. package/client/explorer/index.js +2 -0
  63. package/client/utils.d.ts +18 -0
  64. package/client/utils.js +52 -0
  65. package/executor/ConstantExecutor.d.ts +1 -2
  66. package/executor/ErrorExecutor.d.ts +2 -2
  67. package/executor/EventExecutor.d.ts +2 -2
  68. package/executor/EventExecutor.js +1 -0
  69. package/executor/Executor.d.ts +4 -5
  70. package/executor/RuntimeApiExecutor.d.ts +2 -2
  71. package/executor/RuntimeApiExecutor.js +27 -3
  72. package/executor/StorageQueryExecutor.d.ts +3 -2
  73. package/executor/StorageQueryExecutor.js +92 -3
  74. package/executor/TxExecutor.d.ts +2 -2
  75. package/executor/ViewFunctionExecutor.d.ts +2 -2
  76. package/executor/ViewFunctionExecutor.js +22 -3
  77. package/executor/v2/RuntimeApiExecutorV2.d.ts +2 -3
  78. package/executor/v2/StorageQueryExecutorV2.d.ts +3 -3
  79. package/executor/v2/StorageQueryExecutorV2.js +8 -2
  80. package/executor/v2/TxExecutorV2.d.ts +5 -5
  81. package/executor/v2/ViewFunctionExecutorV2.d.ts +2 -3
  82. package/executor/validation-helpers.d.ts +36 -0
  83. package/executor/validation-helpers.js +174 -0
  84. package/extrinsic/extensions/SignedExtension.d.ts +1 -1
  85. package/extrinsic/submittable/BaseSubmittableExtrinsic.d.ts +7 -3
  86. package/extrinsic/submittable/BaseSubmittableExtrinsic.js +64 -3
  87. package/extrinsic/submittable/SubmittableExtrinsic.d.ts +4 -1
  88. package/extrinsic/submittable/SubmittableExtrinsic.js +39 -17
  89. package/extrinsic/submittable/SubmittableExtrinsicV2.d.ts +6 -3
  90. package/extrinsic/submittable/SubmittableExtrinsicV2.js +13 -3
  91. package/json-rpc/JsonRpcClient.d.ts +6 -6
  92. package/json-rpc/group/Archive.d.ts +1 -1
  93. package/json-rpc/group/ChainHead/ChainHead.d.ts +18 -2
  94. package/json-rpc/group/ChainHead/ChainHead.js +226 -21
  95. package/json-rpc/group/ChainSpec.d.ts +3 -3
  96. package/json-rpc/group/Transaction.d.ts +1 -1
  97. package/json-rpc/group/TransactionWatch.d.ts +1 -1
  98. package/package.json +9 -9
  99. package/proxychain.d.ts +3 -4
  100. package/storage/NewStorageQuery.d.ts +3 -3
  101. package/storage/QueryableStorage.js +1 -0
  102. package/types.d.ts +94 -6
@@ -0,0 +1,312 @@
1
+ import { $H256, $Header, $RuntimeVersion, PortableRegistry, } from '@dedot/codecs';
2
+ import { u32 } from '@dedot/shape';
3
+ import { assert, concatU8a, DedotError, twox64Concat, u8aToHex, xxhashAsU8a } from '@dedot/utils';
4
+ import { ConstantExecutor, ErrorExecutor, EventExecutor, RuntimeApiExecutorV2, StorageQueryExecutorV2, TxExecutorV2, ViewFunctionExecutorV2, } from '../executor/index.js';
5
+ import { SubmittableExtrinsicV2 } from '../extrinsic/submittable/SubmittableExtrinsicV2.js';
6
+ import { Archive, ChainHead, ChainSpec, Transaction, TransactionWatch } from '../json-rpc/index.js';
7
+ import { newProxyChain } from '../proxychain.js';
8
+ import { NewStorageQuery } from '../storage/index.js';
9
+ import { BaseSubstrateClient, ensurePresence } from './BaseSubstrateClient.js';
10
+ import { V2BlockExplorer } from './explorer/index.js';
11
+ /**
12
+ * @name V2Client
13
+ * @description New promised-based API Client for Polkadot & Substrate based on JSON-RPC V2
14
+ *
15
+ * __Unstable, use with caution.__
16
+ */
17
+ export class V2Client// prettier-end-here
18
+ extends BaseSubstrateClient {
19
+ _chainHead;
20
+ _chainSpec;
21
+ _archive;
22
+ _txBroadcaster;
23
+ _blockExplorer;
24
+ #hasher;
25
+ /**
26
+ * Use factory methods (`create`, `new`) to create `V2Client` instances.
27
+ *
28
+ * @param options
29
+ */
30
+ constructor(options) {
31
+ super('v2', options);
32
+ }
33
+ /**
34
+ * Factory method to create a new V2Client instance
35
+ *
36
+ * @param options
37
+ */
38
+ static async create(options) {
39
+ return new V2Client(options).connect();
40
+ }
41
+ /**
42
+ * Alias for __V2Client.create__
43
+ *
44
+ * @param options
45
+ */
46
+ static async new(options) {
47
+ return V2Client.create(options);
48
+ }
49
+ get chainSpec() {
50
+ return ensurePresence(this._chainSpec);
51
+ }
52
+ get chainHead() {
53
+ return ensurePresence(this._chainHead);
54
+ }
55
+ get archive() {
56
+ return this._archive;
57
+ }
58
+ get txBroadcaster() {
59
+ this.chainHead; // Ensure chain head is initialized
60
+ assert(this._txBroadcaster, 'JSON-RPC method to broadcast transactions is not supported by the server/node.');
61
+ return this._txBroadcaster;
62
+ }
63
+ async #initializeTxBroadcaster(rpcMethods) {
64
+ const tx = new Transaction(this, { rpcMethods });
65
+ if (await tx.supported())
66
+ return tx;
67
+ const txWatch = new TransactionWatch(this, { rpcMethods });
68
+ if (await txWatch.supported())
69
+ return txWatch;
70
+ }
71
+ /**
72
+ * Initialize APIs before usage
73
+ */
74
+ async doInitialize() {
75
+ const shouldInitialize = !this._genesisHash;
76
+ if (shouldInitialize) {
77
+ const rpcMethods = (await this.rpc.rpc_methods()).methods;
78
+ this._chainHead = new ChainHead(this, { rpcMethods });
79
+ this._chainSpec = new ChainSpec(this, { rpcMethods });
80
+ // Always initialize Archive, but only set up fallback if supported
81
+ const archive = new Archive(this, { rpcMethods });
82
+ // Set up ChainHead with Archive fallback only if Archive is supported
83
+ if (await archive.supported()) {
84
+ this._archive = archive;
85
+ this._chainHead.withArchive(archive);
86
+ }
87
+ this._txBroadcaster = await this.#initializeTxBroadcaster(rpcMethods);
88
+ this._blockExplorer = new V2BlockExplorer(this);
89
+ }
90
+ // Fetching node information
91
+ let [_, genesisHash] = await Promise.all([
92
+ this.chainHead.follow(true),
93
+ shouldInitialize ? this.chainSpec.genesisHash().catch(() => undefined) : Promise.resolve(this._genesisHash),
94
+ ]);
95
+ this._genesisHash = genesisHash || (await this.#getGenesisHashFallback());
96
+ const newBestRuntime = await this.chainHead.bestRuntimeVersion();
97
+ if (!this._runtimeVersion || newBestRuntime.specVersion !== this._runtimeVersion.specVersion) {
98
+ this._runtimeVersion = newBestRuntime;
99
+ let metadata;
100
+ if (await this.shouldPreloadMetadata()) {
101
+ metadata = await this.fetchMetadata();
102
+ }
103
+ await this.setupMetadata(metadata);
104
+ }
105
+ if (shouldInitialize) {
106
+ this.subscribeRuntimeUpgrades();
107
+ this.chainHead.on('newBlock', (...args) => this.emit('newBlock', ...args));
108
+ this.chainHead.on('bestBlock', (...args) => this.emit('bestBlock', ...args));
109
+ this.chainHead.on('finalizedBlock', (...args) => this.emit('finalizedBlock', ...args));
110
+ this.chainHead.on('bestChainChanged', (...args) => this.emit('bestChainChanged', ...args));
111
+ }
112
+ }
113
+ /**
114
+ * Ref: https://github.com/paritytech/polkadot-sdk/blob/bbd51ce867967f71657b901f1a956ad4f75d352e/substrate/frame/system/src/lib.rs#L909-L913
115
+ * @private
116
+ */
117
+ async #getGenesisHashFallback() {
118
+ const pallet = xxhashAsU8a('System', 128);
119
+ const item = xxhashAsU8a('BlockHash', 128);
120
+ const blockHeightAt0 = twox64Concat(u32.encode(0));
121
+ const key = u8aToHex(concatU8a(pallet, item, blockHeightAt0));
122
+ const storageValue = await this.chainHead.storage([{ type: 'value', key }]);
123
+ const rawGenesisHash = storageValue.at(0)?.value;
124
+ assert(rawGenesisHash, 'Genesis hash not found!');
125
+ // Here we assume that in most case the hash is stored as a H256
126
+ return $H256.tryDecode(rawGenesisHash);
127
+ }
128
+ subscribeRuntimeUpgrades() {
129
+ this.chainHead.on('bestBlock', this.onRuntimeUpgrade);
130
+ this.#subscribeStalingDetection();
131
+ }
132
+ onRuntimeUpgrade = async (block) => {
133
+ const runtimeUpgraded = block.runtime && block.runtime.specVersion !== this._runtimeVersion?.specVersion;
134
+ if (!runtimeUpgraded)
135
+ return;
136
+ this.startRuntimeUpgrade();
137
+ this._runtimeVersion = block.runtime;
138
+ const newMetadata = await this.fetchMetadata(undefined, this._runtimeVersion);
139
+ await this.setupMetadata(newMetadata);
140
+ const blockInfo = {
141
+ hash: block.hash,
142
+ number: block.number,
143
+ parent: block.parent,
144
+ runtimeUpgraded: block.runtimeUpgraded === true,
145
+ };
146
+ this.emit('runtimeUpgraded', this._runtimeVersion, blockInfo);
147
+ this.doneRuntimeUpgrade();
148
+ };
149
+ async setMetadata(metadata) {
150
+ this._metadata = metadata;
151
+ // Here we assume that hasher method of a chain is less likely to change via runtime upgrades
152
+ // So for now, we only need to detect it in initial connection and reuse it later
153
+ if (!this.#hasher && !this.options.hasher) {
154
+ this.#hasher = await this.chainHead.hasher();
155
+ }
156
+ this._registry = new PortableRegistry(metadata.latest, // --
157
+ this.#hasher || this.options.hasher);
158
+ }
159
+ #subscribeStalingDetection() {
160
+ const stalingDetectionFn = this.getStalingDetectionFn();
161
+ if (!stalingDetectionFn)
162
+ return;
163
+ this.chainHead.on('bestBlock', stalingDetectionFn);
164
+ }
165
+ async beforeDisconnect() {
166
+ await this.chainHead.unfollow();
167
+ }
168
+ cleanUp() {
169
+ super.cleanUp();
170
+ this._chainHead = undefined;
171
+ this._chainSpec = undefined;
172
+ this._archive = undefined;
173
+ this._txBroadcaster = undefined;
174
+ this._blockExplorer = undefined;
175
+ }
176
+ /**
177
+ * @description Clear local cache, API at-block cache, and ChainHead cache
178
+ * @param keepMetadataCache Keep the metadata cache, only clear other caches.
179
+ */
180
+ async clearCache(keepMetadataCache = false) {
181
+ await super.clearCache(keepMetadataCache);
182
+ this._chainHead?.clearCache();
183
+ }
184
+ get query() {
185
+ return newProxyChain({
186
+ executor: new StorageQueryExecutorV2(this, this.chainHead),
187
+ });
188
+ }
189
+ get view() {
190
+ return newProxyChain({
191
+ executor: new ViewFunctionExecutorV2(this, this.chainHead),
192
+ });
193
+ }
194
+ get call() {
195
+ return this.callAt();
196
+ }
197
+ callAt(blockHash) {
198
+ return newProxyChain({
199
+ executor: new RuntimeApiExecutorV2(this, this.chainHead, blockHash),
200
+ });
201
+ }
202
+ get tx() {
203
+ return newProxyChain({ executor: new TxExecutorV2(this) });
204
+ }
205
+ get block() {
206
+ return ensurePresence(this._blockExplorer);
207
+ }
208
+ /**
209
+ * Get a new API instance at a specific block hash
210
+ * Supports both pinned blocks (via ChainHead) and historical blocks (via Archive fallback)
211
+ *
212
+ * @param hash
213
+ */
214
+ async at(hash) {
215
+ const cached = this._apiAtCache.get(hash);
216
+ if (cached)
217
+ return cached;
218
+ let parentVersion;
219
+ let parentHash;
220
+ // Try to get block info from ChainHead first (for pinned blocks)
221
+ const targetBlock = this.chainHead.findBlock(hash);
222
+ if (targetBlock) {
223
+ if (hash === this.genesisHash) {
224
+ parentHash = hash;
225
+ parentVersion = targetBlock.runtime;
226
+ }
227
+ else {
228
+ parentHash = targetBlock.parent;
229
+ const parentBlock = this.chainHead.findBlock(parentHash);
230
+ parentVersion = parentBlock?.runtime;
231
+ }
232
+ // fallback to fetching on-chain runtime if we can't find it in the block
233
+ if (!parentVersion) {
234
+ parentVersion = this.toSubstrateRuntimeVersion(await this.callAt(parentHash).core.version());
235
+ }
236
+ }
237
+ else {
238
+ // Block not pinned, try via Archive fallback if supported
239
+ if (this._archive && (await this._archive.supported())) {
240
+ try {
241
+ if (hash === this.genesisHash) {
242
+ parentHash = hash;
243
+ }
244
+ else {
245
+ const rawHeader = await this._archive.header(hash);
246
+ assert(rawHeader, `Header for block ${hash} not found`);
247
+ const header = $Header.tryDecode(rawHeader);
248
+ parentHash = header.parentHash;
249
+ }
250
+ // Fetch runtime version via Archive
251
+ const runtimeRaw = await this._archive.call('Core_version', '0x', parentHash);
252
+ assert(runtimeRaw, 'Runtime Version Not Found');
253
+ parentVersion = this.toSubstrateRuntimeVersion($RuntimeVersion.tryDecode(runtimeRaw));
254
+ }
255
+ catch (error) {
256
+ throw new DedotError(`Unable to fetch runtime version for block ${hash}: ${error}`);
257
+ }
258
+ }
259
+ else {
260
+ throw new DedotError('Block is not pinned and Archive JSON-RPC is not supported by the server/node!');
261
+ }
262
+ }
263
+ let metadata = this.metadata;
264
+ let registry = this.registry;
265
+ if (parentVersion && parentVersion.specVersion !== this.runtimeVersion.specVersion) {
266
+ const cachedMetadata = this.findMetadataInCache(parentVersion.specVersion);
267
+ if (cachedMetadata) {
268
+ metadata = cachedMetadata[0];
269
+ registry = cachedMetadata[1];
270
+ }
271
+ else {
272
+ metadata = await this.fetchMetadata(parentHash, parentVersion);
273
+ registry = new PortableRegistry(metadata.latest, this.#hasher || this.options.hasher);
274
+ }
275
+ }
276
+ const api = {
277
+ rpcVersion: 'v2',
278
+ atBlockHash: hash,
279
+ options: this.options,
280
+ genesisHash: this.genesisHash,
281
+ runtimeVersion: parentVersion,
282
+ metadata,
283
+ registry,
284
+ rpc: this.rpc,
285
+ };
286
+ api.consts = newProxyChain({ executor: new ConstantExecutor(api) });
287
+ api.events = newProxyChain({ executor: new EventExecutor(api) });
288
+ api.errors = newProxyChain({ executor: new ErrorExecutor(api) });
289
+ api.query = newProxyChain({
290
+ executor: new StorageQueryExecutorV2(api, this.chainHead),
291
+ });
292
+ api.call = newProxyChain({ executor: new RuntimeApiExecutorV2(api, this.chainHead) });
293
+ api.view = newProxyChain({
294
+ executor: new ViewFunctionExecutorV2(api, this.chainHead),
295
+ });
296
+ // @ts-ignore Add queryMulti implementation for at-block queries
297
+ api.queryMulti = (queries) => {
298
+ return this.internalQueryMulti(queries, undefined, hash);
299
+ };
300
+ this._apiAtCache.set(hash, api);
301
+ return api;
302
+ }
303
+ getStorageQuery() {
304
+ return new NewStorageQuery(this);
305
+ }
306
+ sendTx(tx, callback) {
307
+ return SubmittableExtrinsicV2.fromTx(this, tx) // --
308
+ .send((result) => {
309
+ callback && callback(result);
310
+ });
311
+ }
312
+ }
@@ -0,0 +1,68 @@
1
+ import { BlockHash, Header } from '@dedot/codecs';
2
+ import { HexString } from '@dedot/utils';
3
+ import type { BlockExplorer, BlockInfo } from '../../types.js';
4
+ import type { LegacyClient } from '../LegacyClient.js';
5
+ /**
6
+ * @name LegacyBlockExplorer
7
+ * @description Block explorer implementation for LegacyClient using legacy JSON-RPC methods
8
+ * Optimized to share RPC subscriptions across multiple subscribers using Signal pattern
9
+ */
10
+ export declare class LegacyBlockExplorer implements BlockExplorer {
11
+ #private;
12
+ constructor(client: LegacyClient<any>);
13
+ /**
14
+ * Convert PinnedBlock to BlockInfo format
15
+ */
16
+ private toBlockInfo;
17
+ /**
18
+ * Fill missing blocks when a gap is detected
19
+ * Fetches and emits all missing blocks between lastNumber and currentNumber
20
+ */
21
+ private fillMissingBlocks;
22
+ /**
23
+ * Start the shared best block subscription if not already active
24
+ */
25
+ private ensureBestBlockSubscription;
26
+ private hasNewRuntime;
27
+ /**
28
+ * Start the shared finalized block subscription if not already active
29
+ */
30
+ private ensureFinalizedBlockSubscription;
31
+ /**
32
+ * Clean up best block subscription when no more listeners
33
+ */
34
+ private cleanupBestBlockSubscription;
35
+ /**
36
+ * Clean up finalized block subscription when no more listeners
37
+ */
38
+ private cleanupFinalizedBlockSubscription;
39
+ private calculateBlockHash;
40
+ /**
41
+ * Get the best block
42
+ */
43
+ best(): Promise<BlockInfo>;
44
+ /**
45
+ * Subscribe to the best block
46
+ * Multiple subscribers will share a single RPC subscription
47
+ * New subscribers immediately receive the current best block if available
48
+ */
49
+ best(callback: (block: BlockInfo) => void): () => void;
50
+ /**
51
+ * Get the finalized block
52
+ */
53
+ finalized(): Promise<BlockInfo>;
54
+ /**
55
+ * Subscribe to the finalized block
56
+ * Multiple subscribers will share a single RPC subscription
57
+ * New subscribers immediately receive the current finalized block if available
58
+ */
59
+ finalized(callback: (block: BlockInfo) => void): () => void;
60
+ /**
61
+ * Get the header of a block by number or hash
62
+ */
63
+ header(hash: BlockHash): Promise<Header>;
64
+ /**
65
+ * Get the body (transactions) of a block by number or hash
66
+ */
67
+ body(hash: BlockHash): Promise<HexString[]>;
68
+ }
@@ -0,0 +1,259 @@
1
+ import { $Header } from '@dedot/codecs';
2
+ import { assert, AsyncQueue, noop, Signal } from '@dedot/utils';
3
+ /**
4
+ * @name LegacyBlockExplorer
5
+ * @description Block explorer implementation for LegacyClient using legacy JSON-RPC methods
6
+ * Optimized to share RPC subscriptions across multiple subscribers using Signal pattern
7
+ */
8
+ export class LegacyBlockExplorer {
9
+ #client;
10
+ // Signals for block subscriptions
11
+ #bestBlockSignal;
12
+ #finalizedBlockSignal;
13
+ // Track RPC subscription cleanup functions
14
+ #bestBlockUnsub;
15
+ #finalizedBlockUnsub;
16
+ constructor(client) {
17
+ this.#client = client;
18
+ this.#bestBlockSignal = new Signal();
19
+ this.#finalizedBlockSignal = new Signal();
20
+ }
21
+ /**
22
+ * Convert PinnedBlock to BlockInfo format
23
+ */
24
+ toBlockInfo(header) {
25
+ return {
26
+ hash: header.hash || this.calculateBlockHash(header),
27
+ number: header.number,
28
+ parent: header.parentHash,
29
+ runtimeUpgraded: this.hasNewRuntime(header),
30
+ };
31
+ }
32
+ /**
33
+ * Fill missing blocks when a gap is detected
34
+ * Fetches and emits all missing blocks between lastNumber and currentNumber
35
+ */
36
+ async fillMissingBlocks(lastNumber, currentNumber, subject, blockType) {
37
+ // No gap if no previous block or current is next sequential block
38
+ if (lastNumber === undefined || currentNumber <= lastNumber + 1) {
39
+ return;
40
+ }
41
+ // Gap detected - backfill missing blocks
42
+ const gapSize = currentNumber - lastNumber - 1;
43
+ // Create list of missing block numbers
44
+ const missingNums = Array.from({ length: gapSize }, (_, i) => lastNumber + 1 + i);
45
+ try {
46
+ // Fetch all hashes in parallel
47
+ const hashes = await Promise.all(missingNums.map((num) => this.#client.rpc.chain_getBlockHash(num)));
48
+ // Fetch all headers in parallel
49
+ const headers = await Promise.all(hashes.map((hash) => this.#client.rpc.chain_getHeader(hash)));
50
+ // Emit blocks in order
51
+ for (let i = 0; i < missingNums.length; i++) {
52
+ const hash = hashes[i];
53
+ const header = headers[i];
54
+ if (hash && header) {
55
+ subject.next(this.toBlockInfo({ ...header, hash }));
56
+ }
57
+ }
58
+ }
59
+ catch (error) {
60
+ console.error(`Failed to backfill missing ${blockType} blocks:`, error);
61
+ // Continue despite backfill failure
62
+ }
63
+ }
64
+ /**
65
+ * Start the shared best block subscription if not already active
66
+ */
67
+ ensureBestBlockSubscription() {
68
+ if (this.#bestBlockUnsub) {
69
+ // Subscription already active
70
+ return;
71
+ }
72
+ // Use closure pattern to handle async subscription without race conditions
73
+ let done = false;
74
+ let unsub;
75
+ const blockQueue = new AsyncQueue();
76
+ // Start RPC subscription (non-blocking)
77
+ this.#client.rpc
78
+ .chain_subscribeNewHeads((header) => {
79
+ if (done) {
80
+ // Unsubscribe was called before subscription completed
81
+ unsub && unsub();
82
+ return;
83
+ }
84
+ // Enqueue block processing to ensure sequential handling
85
+ blockQueue
86
+ .enqueue(async () => {
87
+ const currentHash = this.calculateBlockHash(header);
88
+ // Detect gaps in best block stream (due to reconnection)
89
+ const { number: lastNumber, hash: lastHash } = this.#bestBlockSignal.value || {};
90
+ if (lastHash === currentHash) {
91
+ return;
92
+ }
93
+ // Fill missing blocks if gap detected
94
+ await this.fillMissingBlocks(lastNumber, header.number, this.#bestBlockSignal, 'best');
95
+ // Emit current block
96
+ const blockInfo = this.toBlockInfo({ ...header, hash: currentHash });
97
+ this.#bestBlockSignal.next(blockInfo);
98
+ })
99
+ .catch((error) => {
100
+ console.error('Error processing best block:', error);
101
+ });
102
+ })
103
+ .then((rpcUnsub) => {
104
+ unsub = rpcUnsub;
105
+ });
106
+ // Immediately set unsub wrapper to prevent race conditions
107
+ this.#bestBlockUnsub = () => {
108
+ done = true;
109
+ blockQueue.cancel();
110
+ unsub && unsub().catch(noop);
111
+ };
112
+ }
113
+ hasNewRuntime(header) {
114
+ return header.digest.logs.some((log) => log.type === 'RuntimeEnvironmentUpdated');
115
+ }
116
+ /**
117
+ * Start the shared finalized block subscription if not already active
118
+ */
119
+ ensureFinalizedBlockSubscription() {
120
+ if (this.#finalizedBlockUnsub) {
121
+ // Subscription already active
122
+ return;
123
+ }
124
+ // Use closure pattern to handle async subscription without race conditions
125
+ let done = false;
126
+ let unsub;
127
+ const blockQueue = new AsyncQueue();
128
+ // Start RPC subscription (non-blocking)
129
+ this.#client.rpc
130
+ .chain_subscribeFinalizedHeads((header) => {
131
+ if (done) {
132
+ // Unsubscribe was called before subscription completed
133
+ unsub && unsub();
134
+ return;
135
+ }
136
+ // Enqueue block processing to ensure sequential handling
137
+ blockQueue
138
+ .enqueue(async () => {
139
+ // Detect gaps in finalized block stream (due to reconnection)
140
+ const lastNumber = this.#finalizedBlockSignal.value?.number;
141
+ if (lastNumber === header.number) {
142
+ return;
143
+ }
144
+ // Fill missing blocks if gap detected
145
+ await this.fillMissingBlocks(lastNumber, header.number, this.#finalizedBlockSignal, 'finalized');
146
+ // Emit current block
147
+ const blockInfo = this.toBlockInfo(header);
148
+ this.#finalizedBlockSignal.next(blockInfo);
149
+ })
150
+ .catch((error) => {
151
+ console.error('Error processing finalized block:', error);
152
+ });
153
+ })
154
+ .then((rpcUnsub) => {
155
+ unsub = rpcUnsub;
156
+ });
157
+ // Immediately set unsub wrapper to prevent race conditions
158
+ this.#finalizedBlockUnsub = () => {
159
+ done = true;
160
+ blockQueue.cancel();
161
+ unsub && unsub().catch(noop);
162
+ };
163
+ }
164
+ /**
165
+ * Clean up best block subscription when no more listeners
166
+ */
167
+ cleanupBestBlockSubscription() {
168
+ if (this.#bestBlockSignal.listenerCount === 0 && this.#bestBlockUnsub) {
169
+ this.#bestBlockUnsub();
170
+ this.#bestBlockUnsub = undefined;
171
+ }
172
+ }
173
+ /**
174
+ * Clean up finalized block subscription when no more listeners
175
+ */
176
+ cleanupFinalizedBlockSubscription() {
177
+ if (this.#finalizedBlockSignal.listenerCount === 0 && this.#finalizedBlockUnsub) {
178
+ this.#finalizedBlockUnsub();
179
+ this.#finalizedBlockUnsub = undefined;
180
+ }
181
+ }
182
+ calculateBlockHash(header) {
183
+ return this.#client.registry.hashAsHex($Header.tryEncode(header));
184
+ }
185
+ best(callback) {
186
+ if (callback) {
187
+ // Subscribe mode - use shared subscription
188
+ // Ensure RPC subscription is active (creates on first listener)
189
+ this.ensureBestBlockSubscription();
190
+ // Subscribe to subject (automatically emits current value if available)
191
+ const unsub = this.#bestBlockSignal.subscribe(callback);
192
+ // Return cleanup function
193
+ return () => {
194
+ unsub();
195
+ this.cleanupBestBlockSubscription();
196
+ };
197
+ }
198
+ else {
199
+ // One-time query - return cached value if available
200
+ if (this.#bestBlockSignal.value) {
201
+ return Promise.resolve(this.#bestBlockSignal.value);
202
+ }
203
+ // Fallback to RPC call if no cached value
204
+ return this.#client.rpc.chain_getHeader().then(async (header) => {
205
+ assert(header, 'Header not found');
206
+ return {
207
+ hash: this.calculateBlockHash(header),
208
+ number: header.number,
209
+ parent: header.parentHash,
210
+ };
211
+ });
212
+ }
213
+ }
214
+ finalized(callback) {
215
+ if (callback) {
216
+ // Subscribe mode - use shared subscription
217
+ // Ensure RPC subscription is active (creates on first listener)
218
+ this.ensureFinalizedBlockSubscription();
219
+ // Subscribe to subject (automatically emits current value if available)
220
+ const unsub = this.#finalizedBlockSignal.subscribe(callback);
221
+ // Return cleanup function
222
+ return () => {
223
+ unsub();
224
+ this.cleanupFinalizedBlockSubscription();
225
+ };
226
+ }
227
+ else {
228
+ // One-time query - return cached value if available
229
+ if (this.#finalizedBlockSignal.value) {
230
+ return Promise.resolve(this.#finalizedBlockSignal.value);
231
+ }
232
+ // Fallback to RPC call if no cached value
233
+ return this.#client.rpc.chain_getFinalizedHead().then(async (hash) => {
234
+ const header = await this.header(hash);
235
+ return {
236
+ hash,
237
+ number: header.number,
238
+ parent: header.parentHash,
239
+ };
240
+ });
241
+ }
242
+ }
243
+ /**
244
+ * Get the header of a block by number or hash
245
+ */
246
+ async header(hash) {
247
+ const header = await this.#client.rpc.chain_getHeader(hash);
248
+ assert(header, `Header not found for block ${hash}`);
249
+ return header;
250
+ }
251
+ /**
252
+ * Get the body (transactions) of a block by number or hash
253
+ */
254
+ async body(hash) {
255
+ const block = await this.#client.rpc.chain_getBlock(hash);
256
+ assert(block, `Block not found for ${hash}`);
257
+ return block.block.extrinsics;
258
+ }
259
+ }
@@ -0,0 +1,40 @@
1
+ import { BlockHash, Header } from '@dedot/codecs';
2
+ import { HexString } from '@dedot/utils';
3
+ import type { BlockExplorer, BlockInfo } from '../../types.js';
4
+ import type { V2Client } from '../V2Client.js';
5
+ /**
6
+ * @name V2BlockExplorer
7
+ * @description Block explorer implementation for V2Client using JSON-RPC v2 (ChainHead API)
8
+ */
9
+ export declare class V2BlockExplorer implements BlockExplorer {
10
+ #private;
11
+ constructor(client: V2Client<any>);
12
+ /**
13
+ * Convert PinnedBlock to BlockInfo format
14
+ */
15
+ private toBlockInfo;
16
+ /**
17
+ * Get the best block
18
+ */
19
+ best(): Promise<BlockInfo>;
20
+ /**
21
+ * Subscribe to the best block
22
+ */
23
+ best(callback: (block: BlockInfo) => void): () => void;
24
+ /**
25
+ * Get the finalized block
26
+ */
27
+ finalized(): Promise<BlockInfo>;
28
+ /**
29
+ * Subscribe to the finalized block
30
+ */
31
+ finalized(callback: (block: BlockInfo) => void): () => void;
32
+ /**
33
+ * Get the header of a block by number or hash
34
+ */
35
+ header(hash: BlockHash): Promise<Header>;
36
+ /**
37
+ * Get the body (transactions) of a block by number or hash
38
+ */
39
+ body(hash: BlockHash): Promise<HexString[]>;
40
+ }