@dedot/api 0.15.2 → 0.16.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 (49) hide show
  1. package/chaintypes/substrate/index.d.ts +21 -1
  2. package/cjs/client/BaseSubstrateClient.js +12 -3
  3. package/cjs/client/DedotClient.js +51 -11
  4. package/cjs/client/LegacyClient.js +4 -5
  5. package/cjs/executor/Executor.js +1 -0
  6. package/cjs/executor/v2/StorageQueryExecutorV2.js +1 -3
  7. package/cjs/executor/v2/TxExecutorV2.js +5 -3
  8. package/cjs/json-rpc/group/Archive.js +232 -0
  9. package/cjs/json-rpc/group/ChainHead/ChainHead.js +149 -52
  10. package/cjs/json-rpc/group/ChainHead/error.js +5 -0
  11. package/cjs/json-rpc/group/index.js +1 -0
  12. package/cjs/json-rpc/subscriptionsInfo.js +4 -0
  13. package/cjs/storage/LegacyStorageQuery.js +5 -0
  14. package/cjs/storage/NewStorageQuery.js +5 -0
  15. package/client/BaseSubstrateClient.d.ts +8 -6
  16. package/client/BaseSubstrateClient.js +13 -4
  17. package/client/DedotClient.d.ts +11 -4
  18. package/client/DedotClient.js +54 -14
  19. package/client/LegacyClient.d.ts +2 -2
  20. package/client/LegacyClient.js +4 -5
  21. package/executor/Executor.d.ts +5 -5
  22. package/executor/Executor.js +1 -0
  23. package/executor/StorageQueryExecutor.d.ts +2 -2
  24. package/executor/v2/RuntimeApiExecutorV2.d.ts +2 -2
  25. package/executor/v2/StorageQueryExecutorV2.d.ts +4 -4
  26. package/executor/v2/StorageQueryExecutorV2.js +1 -3
  27. package/executor/v2/TxExecutorV2.d.ts +2 -1
  28. package/executor/v2/TxExecutorV2.js +5 -3
  29. package/executor/v2/ViewFunctionExecutorV2.d.ts +2 -2
  30. package/extrinsic/extensions/SignedExtension.d.ts +3 -3
  31. package/extrinsic/submittable/BaseSubmittableExtrinsic.d.ts +2 -2
  32. package/extrinsic/submittable/SubmittableExtrinsicV2.d.ts +2 -2
  33. package/json-rpc/group/Archive.d.ts +134 -0
  34. package/json-rpc/group/Archive.js +228 -0
  35. package/json-rpc/group/ChainHead/ChainHead.d.ts +21 -0
  36. package/json-rpc/group/ChainHead/ChainHead.js +150 -53
  37. package/json-rpc/group/ChainHead/error.d.ts +3 -0
  38. package/json-rpc/group/ChainHead/error.js +5 -0
  39. package/json-rpc/group/index.d.ts +1 -0
  40. package/json-rpc/group/index.js +1 -0
  41. package/json-rpc/subscriptionsInfo.js +4 -0
  42. package/package.json +9 -9
  43. package/storage/BaseStorageQuery.d.ts +5 -6
  44. package/storage/LegacyStorageQuery.d.ts +4 -3
  45. package/storage/LegacyStorageQuery.js +5 -0
  46. package/storage/NewStorageQuery.d.ts +4 -3
  47. package/storage/NewStorageQuery.js +5 -0
  48. package/storage/QueryableStorage.d.ts +2 -2
  49. package/types.d.ts +11 -6
@@ -7,6 +7,8 @@ const JsonRpcGroup_js_1 = require("../JsonRpcGroup.js");
7
7
  const BlockUsage_js_1 = require("./BlockUsage.js");
8
8
  const error_js_1 = require("./error.js");
9
9
  exports.MIN_FINALIZED_QUEUE_SIZE = 10; // finalized queue size
10
+ const CHAINHEAD_CACHE_CAPACITY = 256;
11
+ const CHAINHEAD_CACHE_TTL = 30_000; // 30 seconds
10
12
  class ChainHead extends JsonRpcGroup_js_1.JsonRpcGroup {
11
13
  #unsub;
12
14
  #subscriptionId;
@@ -23,6 +25,17 @@ class ChainHead extends JsonRpcGroup_js_1.JsonRpcGroup {
23
25
  #blockUsage;
24
26
  #cache;
25
27
  #operationQueue;
28
+ /**
29
+ * Archive instance used as fallback when ChainHead blocks are not pinned.
30
+ *
31
+ * When ChainHead operations fail with ChainHeadBlockNotPinnedError, the system
32
+ * automatically attempts the same operation using the Archive API. This provides
33
+ * seamless access to historical blockchain data even when blocks are no longer
34
+ * maintained in the ChainHead's pinned block set.
35
+ *
36
+ * @private
37
+ */
38
+ #archive;
26
39
  constructor(client, options) {
27
40
  super(client, { prefix: 'chainHead', supportedVersions: ['unstable', 'v1'], ...options });
28
41
  this.#handlers = {};
@@ -32,10 +45,22 @@ class ChainHead extends JsonRpcGroup_js_1.JsonRpcGroup {
32
45
  this.#followResponseQueue = new utils_1.AsyncQueue();
33
46
  this.#retryQueue = new utils_1.AsyncQueue();
34
47
  this.#blockUsage = new BlockUsage_js_1.BlockUsage();
35
- this.#cache = new Map();
48
+ this.#cache = new utils_1.LRUCache(CHAINHEAD_CACHE_CAPACITY, CHAINHEAD_CACHE_TTL);
36
49
  // This helps us to not accidentally putting too much stress on the JSON-RPC server, especially smoldot/light-client
37
50
  this.#operationQueue = new utils_1.ThrottleQueue(this.#__unsafe__isSmoldot() ? 25 : 250);
38
51
  }
52
+ /**
53
+ * Attach an Archive instance as fallback for operations that fail due to unpinned blocks.
54
+ * When a ChainHeadBlockNotPinnedError occurs, the operation will automatically fallback
55
+ * to the Archive API to attempt to retrieve the data from historical blocks.
56
+ *
57
+ * @param archive - Archive instance to use as fallback
58
+ * @returns this ChainHead instance for method chaining
59
+ */
60
+ withArchive(archive) {
61
+ this.#archive = archive;
62
+ return this;
63
+ }
39
64
  async runtimeVersion() {
40
65
  await this.#ensureFollowed();
41
66
  return this.#finalizedRuntime;
@@ -222,8 +247,9 @@ class ChainHead extends JsonRpcGroup_js_1.JsonRpcGroup {
222
247
  if (!this.isPinned(hash))
223
248
  return;
224
249
  delete this.#pinnedBlocks[hash];
225
- // clear cache
226
- Array.from(this.#cache.keys())
250
+ // Clear cache entries related to the pruned block
251
+ // Filter and remove only cache entries for this specific block
252
+ this.#cache.keys()
227
253
  .filter((key) => key.startsWith(`${hash}::`))
228
254
  .forEach((key) => this.#cache.delete(key));
229
255
  });
@@ -344,11 +370,38 @@ class ChainHead extends JsonRpcGroup_js_1.JsonRpcGroup {
344
370
  return hash;
345
371
  }
346
372
  else {
347
- throw new error_js_1.ChainHeadBlockNotPinnedError(`Block hash ${hash} is not pinned`);
373
+ throw new error_js_1.ChainHeadBlockNotPinnedError(`Block hash ${hash} is not pinned`, hash);
348
374
  }
349
375
  }
350
376
  return (0, utils_1.ensurePresence)(this.#bestHash || this.#finalizedHash);
351
377
  }
378
+ /**
379
+ * Executes a ChainHead operation with automatic Archive fallback.
380
+ *
381
+ * This method first attempts the primary ChainHead operation. If it fails with
382
+ * ChainHeadBlockNotPinnedError (indicating the block is no longer pinned), and
383
+ * an Archive instance is available, it automatically retries the operation using
384
+ * the Archive API.
385
+ *
386
+ * @param operation - Primary ChainHead operation to attempt
387
+ * @param fallback - Archive operation to fallback to
388
+ * @param hash - Block hash being accessed (for logging)
389
+ * @returns Result from either ChainHead or Archive operation
390
+ * @throws Original error if not a pinning error or no Archive available
391
+ * @private
392
+ */
393
+ async #tryWithArchive(operation, fallback) {
394
+ try {
395
+ return await operation();
396
+ }
397
+ catch (error) {
398
+ if (error instanceof error_js_1.ChainHeadBlockNotPinnedError && this.#archive) {
399
+ const errorHash = error.hash;
400
+ return await fallback(this.#archive, errorHash);
401
+ }
402
+ throw error;
403
+ }
404
+ }
352
405
  #getOperationHandler(result) {
353
406
  const handler = this.#handlers[result.operationId];
354
407
  if (handler)
@@ -403,7 +456,7 @@ class ChainHead extends JsonRpcGroup_js_1.JsonRpcGroup {
403
456
  this.#followResponseQueue.clear();
404
457
  this.#retryQueue.clear();
405
458
  this.#blockUsage.clear();
406
- this.#cache.clear();
459
+ this.clearCache();
407
460
  this.#operationQueue.cancel();
408
461
  }
409
462
  async #ensureFollowed() {
@@ -478,21 +531,32 @@ class ChainHead extends JsonRpcGroup_js_1.JsonRpcGroup {
478
531
  async body(at) {
479
532
  await this.#ensureFollowed();
480
533
  const shouldRetryOnPrunedBlock = !at;
481
- try {
534
+ const operation = async () => {
482
535
  const atHash = this.#ensurePinnedHash(at);
483
536
  const cacheKey = `${atHash}::body`;
484
- if (this.#cache.has(cacheKey)) {
485
- return this.#cache.get(cacheKey);
537
+ const cached = this.#cache.get(cacheKey);
538
+ if (cached) {
539
+ return cached;
486
540
  }
487
- const operation = async () => {
541
+ const bodyOperation = async () => {
488
542
  await this.#ensureFollowed();
489
543
  const hash = this.#ensurePinnedHash(atHash);
490
544
  const resp = await this.send('body', this.#subscriptionId, hash);
491
545
  return this.#awaitOperation(resp, hash);
492
546
  };
493
- const resp = await this.#operationQueue.add(() => this.#performOperationWithRetry(operation, atHash));
547
+ const resp = await this.#operationQueue.add(() => this.#performOperationWithRetry(bodyOperation, atHash));
494
548
  this.#cache.set(cacheKey, resp);
495
549
  return resp;
550
+ };
551
+ const fallback = async (archive, hash) => {
552
+ const result = await archive.body(hash);
553
+ if (result === undefined) {
554
+ throw new error_js_1.ChainHeadOperationError(`Block ${hash} not found in Archive`);
555
+ }
556
+ return result;
557
+ };
558
+ try {
559
+ return await this.#tryWithArchive(operation, fallback);
496
560
  }
497
561
  catch (e) {
498
562
  if (e instanceof error_js_1.ChainHeadBlockPrunedError && shouldRetryOnPrunedBlock) {
@@ -507,21 +571,26 @@ class ChainHead extends JsonRpcGroup_js_1.JsonRpcGroup {
507
571
  async call(func, params = '0x', at) {
508
572
  await this.#ensureFollowed();
509
573
  const shouldRetryOnPrunedBlock = !at;
510
- try {
574
+ const operation = async () => {
511
575
  const atHash = this.#ensurePinnedHash(at);
512
576
  const cacheKey = `${atHash}::call::${func}::${params}`;
513
- if (this.#cache.has(cacheKey)) {
514
- return this.#cache.get(cacheKey);
577
+ const cached = this.#cache.get(cacheKey);
578
+ if (cached) {
579
+ return cached;
515
580
  }
516
- const operation = async () => {
581
+ const callOperation = async () => {
517
582
  await this.#ensureFollowed();
518
583
  const hash = this.#ensurePinnedHash(atHash);
519
584
  const resp = await this.send('call', this.#subscriptionId, hash, func, params);
520
585
  return this.#awaitOperation(resp, hash);
521
586
  };
522
- const resp = await this.#operationQueue.add(() => this.#performOperationWithRetry(operation, atHash));
587
+ const resp = await this.#operationQueue.add(() => this.#performOperationWithRetry(callOperation, atHash));
523
588
  this.#cache.set(cacheKey, resp);
524
589
  return resp;
590
+ };
591
+ const fallback = (archive, hash) => archive.call(func, params, hash);
592
+ try {
593
+ return await this.#tryWithArchive(operation, fallback);
525
594
  }
526
595
  catch (e) {
527
596
  if (e instanceof error_js_1.ChainHeadBlockPrunedError && shouldRetryOnPrunedBlock) {
@@ -535,14 +604,19 @@ class ChainHead extends JsonRpcGroup_js_1.JsonRpcGroup {
535
604
  */
536
605
  async header(at) {
537
606
  await this.#ensureFollowed();
538
- const hash = this.#ensurePinnedHash(at);
539
- const cacheKey = `${hash}::header`;
540
- if (this.#cache.has(cacheKey)) {
541
- return this.#cache.get(cacheKey);
542
- }
543
- const resp = await this.#getHeader(hash);
544
- this.#cache.set(cacheKey, resp);
545
- return resp;
607
+ const operation = async () => {
608
+ const hash = this.#ensurePinnedHash(at);
609
+ const cacheKey = `${hash}::header`;
610
+ const cached = this.#cache.get(cacheKey);
611
+ if (cached) {
612
+ return cached;
613
+ }
614
+ const resp = await this.#getHeader(hash);
615
+ this.#cache.set(cacheKey, resp);
616
+ return resp;
617
+ };
618
+ const fallback = (archive, errorHash) => archive.header(errorHash);
619
+ return await this.#tryWithArchive(operation, fallback);
546
620
  }
547
621
  async #getHeader(at) {
548
622
  return await this.send('header', this.#subscriptionId, at);
@@ -553,35 +627,47 @@ class ChainHead extends JsonRpcGroup_js_1.JsonRpcGroup {
553
627
  async storage(items, childTrie, at) {
554
628
  await this.#ensureFollowed();
555
629
  const shouldRetryOnPrunedBlock = !at;
556
- const hash = this.#ensurePinnedHash(at);
557
- try {
558
- // JSON.stringify(items) might get big, we probably should do a twox hashing in such case
559
- const cacheKey = `${hash}::storage::${JSON.stringify(items)}::${childTrie ?? null}`;
560
- if (this.#cache.has(cacheKey)) {
561
- return this.#cache.get(cacheKey);
562
- }
563
- this.#blockUsage.use(hash);
564
- let results = [];
565
- if (this.#__unsafe__isSmoldot()) {
566
- const fetchItem = async (item) => {
567
- const [batch, newDiscardedItems] = await this.#getStorage([item], childTrie ?? null, hash);
568
- if (newDiscardedItems.length > 0) {
569
- return fetchItem(item);
630
+ const operation = async () => {
631
+ const hash = this.#ensurePinnedHash(at);
632
+ try {
633
+ // JSON.stringify(items) might get big, we probably should do a twox hashing in such case
634
+ const cacheKey = `${hash}::storage::${JSON.stringify(items)}::${childTrie ?? null}`;
635
+ const cached = this.#cache.get(cacheKey);
636
+ if (cached) {
637
+ return cached;
638
+ }
639
+ this.#blockUsage.use(hash);
640
+ let results = [];
641
+ if (this.#__unsafe__isSmoldot()) {
642
+ const fetchItem = async (item) => {
643
+ const [batch, newDiscardedItems] = await this.#getStorage([item], childTrie ?? null, hash);
644
+ if (newDiscardedItems.length > 0) {
645
+ return fetchItem(item);
646
+ }
647
+ return batch;
648
+ };
649
+ results = (await Promise.all(items.map((one) => fetchItem(one)))).flat();
650
+ }
651
+ else {
652
+ let queryItems = items;
653
+ while (queryItems.length > 0) {
654
+ const [newBatch, newDiscardedItems] = await this.#getStorage(queryItems, childTrie ?? null, hash);
655
+ results.push(...newBatch);
656
+ queryItems = newDiscardedItems;
570
657
  }
571
- return batch;
572
- };
573
- results = (await Promise.all(items.map((one) => fetchItem(one)))).flat();
574
- }
575
- else {
576
- let queryItems = items;
577
- while (queryItems.length > 0) {
578
- const [newBatch, newDiscardedItems] = await this.#getStorage(queryItems, childTrie ?? null, hash);
579
- results.push(...newBatch);
580
- queryItems = newDiscardedItems;
581
658
  }
659
+ this.#cache.set(cacheKey, results);
660
+ return results;
661
+ }
662
+ finally {
663
+ this.#blockUsage.release(hash);
582
664
  }
583
- this.#cache.set(cacheKey, results);
584
- return results;
665
+ };
666
+ const fallback = (archive, hash) => {
667
+ return archive.storage(items, childTrie, hash);
668
+ };
669
+ try {
670
+ return await this.#tryWithArchive(operation, fallback);
585
671
  }
586
672
  catch (e) {
587
673
  if (e instanceof error_js_1.ChainHeadBlockPrunedError && shouldRetryOnPrunedBlock) {
@@ -589,9 +675,6 @@ class ChainHead extends JsonRpcGroup_js_1.JsonRpcGroup {
589
675
  }
590
676
  throw e;
591
677
  }
592
- finally {
593
- this.#blockUsage.release(hash);
594
- }
595
678
  }
596
679
  async #getStorage(items, childTrie, at) {
597
680
  const operation = () => this.#getStorageOperation(items, childTrie, this.#ensurePinnedHash(at));
@@ -636,5 +719,19 @@ class ChainHead extends JsonRpcGroup_js_1.JsonRpcGroup {
636
719
  // @ts-ignore a trick internally to check whether a provider is using smoldot connection
637
720
  return typeof this.client.provider['chain'] === 'function';
638
721
  }
722
+ /**
723
+ * Clears the internal cache used for storing query results (both chainHead & archive instances)
724
+ * This can be useful for memory management or when you want to force fresh data retrieval.
725
+ *
726
+ * @example
727
+ * ```typescript
728
+ * // Clear all cached results
729
+ * chainHead.clearCache();
730
+ * ```
731
+ */
732
+ clearCache() {
733
+ this.#cache.clear();
734
+ this.#archive?.clearCache();
735
+ }
639
736
  }
640
737
  exports.ChainHead = ChainHead;
@@ -59,6 +59,11 @@ class ChainHeadInvalidRuntimeError extends ChainHeadError {
59
59
  exports.ChainHeadInvalidRuntimeError = ChainHeadInvalidRuntimeError;
60
60
  class ChainHeadBlockNotPinnedError extends ChainHeadError {
61
61
  name = 'ChainHeadBlockNotPinnedError';
62
+ hash;
63
+ constructor(message, hash) {
64
+ super(message);
65
+ this.hash = hash;
66
+ }
62
67
  }
63
68
  exports.ChainHeadBlockNotPinnedError = ChainHeadBlockNotPinnedError;
64
69
  class ChainHeadBlockPrunedError extends ChainHeadError {
@@ -15,6 +15,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./JsonRpcGroup.js"), exports);
18
+ __exportStar(require("./Archive.js"), exports);
18
19
  __exportStar(require("./ChainSpec.js"), exports);
19
20
  __exportStar(require("./Transaction.js"), exports);
20
21
  __exportStar(require("./TransactionWatch.js"), exports);
@@ -17,4 +17,8 @@ exports.subscriptionsInfo = {
17
17
  'transactionWatch_unstable_unwatch',
18
18
  ],
19
19
  transactionWatch_v1_submitAndWatch: ['transactionWatch_v1_watchEvent', 'transactionWatch_v1_unwatch'],
20
+ archive_v1_storage: ['archive_v1_storageEvent', 'archive_v1_stopStorage'],
21
+ archive_v1_storageDiff: ['archive_v1_storageDiffEvent', 'archive_v1_stopStorageDiff'],
22
+ archive_unstable_storage: ['archive_unstable_storageEvent', 'archive_unstable_stopStorage'],
23
+ archive_unstable_storageDiff: ['archive_unstable_storageDiffEvent', 'archive_unstable_stopStorageDiff'],
20
24
  };
@@ -15,6 +15,11 @@ const BaseStorageQuery_js_1 = require("./BaseStorageQuery.js");
15
15
  * - Efficient change tracking for subscriptions
16
16
  */
17
17
  class LegacyStorageQuery extends BaseStorageQuery_js_1.BaseStorageQuery {
18
+ client;
19
+ constructor(client) {
20
+ super(client);
21
+ this.client = client;
22
+ }
18
23
  /**
19
24
  * Query multiple storage items in a single call using state_queryStorageAt
20
25
  *
@@ -16,6 +16,11 @@ const BaseStorageQuery_js_1 = require("./BaseStorageQuery.js");
16
16
  * - Efficient change detection for subscriptions
17
17
  */
18
18
  class NewStorageQuery extends BaseStorageQuery_js_1.BaseStorageQuery {
19
+ client;
20
+ constructor(client) {
21
+ super(client);
22
+ this.client = client;
23
+ }
19
24
  /**
20
25
  * Query multiple storage items in a single call using chainHead_storage
21
26
  *
@@ -2,7 +2,7 @@ import { BlockHash, Hash, Metadata, PortableRegistry, RuntimeVersion } from '@de
2
2
  import type { JsonRpcProvider } from '@dedot/providers';
3
3
  import { type IStorage } from '@dedot/storage';
4
4
  import { Callback, GenericStorageQuery, GenericSubstrateApi, InjectedSigner, Query, QueryFnResult, RpcVersion, Unsub, VersionedGenericSubstrateApi } from '@dedot/types';
5
- import { Deferred } from '@dedot/utils';
5
+ import { Deferred, LRUCache } from '@dedot/utils';
6
6
  import type { SubstrateApi } from '../chaintypes/index.js';
7
7
  import { JsonRpcClient } from '../json-rpc/index.js';
8
8
  import { BaseStorageQuery } from '../storage/index.js';
@@ -15,12 +15,13 @@ export declare function ensurePresence<T>(value: T): NonNullable<T>;
15
15
  export declare abstract class BaseSubstrateClient<Rv extends RpcVersion, ChainApi extends VersionedGenericSubstrateApi = SubstrateApi, Events extends string = ApiEvent> extends JsonRpcClient<ChainApi, Events> implements ISubstrateClient<ChainApi[Rv], Events> {
16
16
  rpcVersion: RpcVersion;
17
17
  protected _options: ApiOptions;
18
- protected _registry?: PortableRegistry;
18
+ protected _registry?: PortableRegistry<ChainApi[Rv]>;
19
19
  protected _metadata?: Metadata;
20
20
  protected _genesisHash?: Hash;
21
21
  protected _runtimeVersion?: SubstrateRuntimeVersion;
22
22
  protected _localCache?: IStorage;
23
23
  protected _runtimeUpgrading?: Deferred<void>;
24
+ protected _apiAtCache: LRUCache;
24
25
  protected constructor(rpcVersion: RpcVersion, options: JsonRpcClientOptions | JsonRpcProvider);
25
26
  protected normalizeOptions(options: ApiOptions | JsonRpcProvider): ApiOptions;
26
27
  protected initializeLocalCache(): Promise<void>;
@@ -37,9 +38,10 @@ export declare abstract class BaseSubstrateClient<Rv extends RpcVersion, ChainAp
37
38
  protected fetchMetadata(hash?: BlockHash, runtime?: SubstrateRuntimeVersion): Promise<Metadata>;
38
39
  protected cleanUp(): void;
39
40
  /**
40
- * @description Clear local cache
41
+ * @description Clear local cache and API at-block cache
42
+ * @param keepMetadataCache Keep the metadata cache, only clear other caches.
41
43
  */
42
- clearCache(): Promise<void>;
44
+ clearCache(keepMetadataCache?: boolean): Promise<void>;
43
45
  protected doConnect(): Promise<this>;
44
46
  protected onConnected: () => Promise<void>;
45
47
  protected onDisconnected: () => Promise<void>;
@@ -61,7 +63,7 @@ export declare abstract class BaseSubstrateClient<Rv extends RpcVersion, ChainAp
61
63
  disconnect(): Promise<void>;
62
64
  get options(): ApiOptions;
63
65
  get metadata(): Metadata;
64
- get registry(): PortableRegistry;
66
+ get registry(): PortableRegistry<ChainApi[Rv]>;
65
67
  get genesisHash(): Hash;
66
68
  get runtimeVersion(): SubstrateRuntimeVersion;
67
69
  getRuntimeVersion(): Promise<SubstrateRuntimeVersion>;
@@ -106,5 +108,5 @@ export declare abstract class BaseSubstrateClient<Rv extends RpcVersion, ChainAp
106
108
  }, callback: Callback<{
107
109
  [K in keyof Fns]: QueryFnResult<Fns[K]>;
108
110
  }>): Promise<Unsub>;
109
- protected getStorageQuery(): BaseStorageQuery<RpcVersion>;
111
+ protected getStorageQuery(): BaseStorageQuery;
110
112
  }
@@ -1,12 +1,14 @@
1
1
  import { $Metadata, PortableRegistry } from '@dedot/codecs';
2
2
  import { LocalStorage } from '@dedot/storage';
3
- import { calcRuntimeApiHash, deferred, ensurePresence as _ensurePresence, u8aToHex } from '@dedot/utils';
3
+ import { calcRuntimeApiHash, deferred, ensurePresence as _ensurePresence, u8aToHex, LRUCache, } from '@dedot/utils';
4
4
  import { ConstantExecutor, ErrorExecutor, EventExecutor } from '../executor/index.js';
5
5
  import { isJsonRpcProvider, JsonRpcClient } from '../json-rpc/index.js';
6
6
  import { newProxyChain } from '../proxychain.js';
7
7
  import { QueryableStorage } from '../storage/index.js';
8
8
  const SUPPORTED_METADATA_VERSIONS = [16, 15, 14];
9
9
  const MetadataApiHash = calcRuntimeApiHash('Metadata'); // 0x37e397fc7c91f5e4
10
+ const API_AT_CACHE_CAPACITY = 64;
11
+ const API_AT_CACHE_TTL = 300_000; // 5 minutes
10
12
  const MESSAGE = 'Make sure to call `.connect()` method first before using the API interfaces.';
11
13
  export function ensurePresence(value) {
12
14
  return _ensurePresence(value, MESSAGE);
@@ -24,10 +26,12 @@ export class BaseSubstrateClient extends JsonRpcClient {
24
26
  _runtimeVersion;
25
27
  _localCache;
26
28
  _runtimeUpgrading;
29
+ _apiAtCache;
27
30
  constructor(rpcVersion, options) {
28
31
  super(options);
29
32
  this.rpcVersion = rpcVersion;
30
33
  this._options = this.normalizeOptions(options);
34
+ this._apiAtCache = new LRUCache(API_AT_CACHE_CAPACITY, API_AT_CACHE_TTL);
31
35
  }
32
36
  /// --- Internal logics
33
37
  normalizeOptions(options) {
@@ -185,12 +189,17 @@ export class BaseSubstrateClient extends JsonRpcClient {
185
189
  this._genesisHash = undefined;
186
190
  this._runtimeVersion = undefined;
187
191
  this._localCache = undefined;
192
+ this._apiAtCache.clear();
188
193
  }
189
194
  /**
190
- * @description Clear local cache
195
+ * @description Clear local cache and API at-block cache
196
+ * @param keepMetadataCache Keep the metadata cache, only clear other caches.
191
197
  */
192
- async clearCache() {
193
- await this._localCache?.clear();
198
+ async clearCache(keepMetadataCache = false) {
199
+ if (!keepMetadataCache) {
200
+ await this._localCache?.clear();
201
+ }
202
+ this._apiAtCache.clear();
194
203
  }
195
204
  async doConnect() {
196
205
  // @ts-ignore
@@ -1,8 +1,8 @@
1
1
  import { BlockHash } from '@dedot/codecs';
2
2
  import type { JsonRpcProvider } from '@dedot/providers';
3
- import { GenericSubstrateApi, RpcV2, RpcVersion, VersionedGenericSubstrateApi } from '@dedot/types';
3
+ import { GenericSubstrateApi, RpcV2, VersionedGenericSubstrateApi } from '@dedot/types';
4
4
  import type { SubstrateApi } from '../chaintypes/index.js';
5
- import { ChainHead, ChainSpec, PinnedBlock } from '../json-rpc/index.js';
5
+ import { Archive, ChainHead, ChainSpec, PinnedBlock } from '../json-rpc/index.js';
6
6
  import { BaseStorageQuery } from '../storage/index.js';
7
7
  import type { ApiOptions, DedotClientEvent, ISubstrateClientAt, TxBroadcaster } from '../types.js';
8
8
  import { BaseSubstrateClient } from './BaseSubstrateClient.js';
@@ -17,6 +17,7 @@ export declare class DedotClient<ChainApi extends VersionedGenericSubstrateApi =
17
17
  #private;
18
18
  protected _chainHead?: ChainHead;
19
19
  protected _chainSpec?: ChainSpec;
20
+ protected _archive?: Archive;
20
21
  protected _txBroadcaster?: TxBroadcaster;
21
22
  /**
22
23
  * Use factory methods (`create`, `new`) to create `DedotClient` instances.
@@ -38,6 +39,7 @@ export declare class DedotClient<ChainApi extends VersionedGenericSubstrateApi =
38
39
  static new<ChainApi extends VersionedGenericSubstrateApi = SubstrateApi>(options: ApiOptions | JsonRpcProvider): Promise<DedotClient<ChainApi>>;
39
40
  get chainSpec(): ChainSpec;
40
41
  get chainHead(): ChainHead;
42
+ archive(): Promise<Archive>;
41
43
  get txBroadcaster(): TxBroadcaster;
42
44
  /**
43
45
  * Initialize APIs before usage
@@ -48,6 +50,11 @@ export declare class DedotClient<ChainApi extends VersionedGenericSubstrateApi =
48
50
  protected beforeDisconnect(): Promise<void>;
49
51
  protected onDisconnected: () => Promise<void>;
50
52
  protected cleanUp(): void;
53
+ /**
54
+ * @description Clear local cache, API at-block cache, and ChainHead cache
55
+ * @param keepMetadataCache Keep the metadata cache, only clear other caches.
56
+ */
57
+ clearCache(keepMetadataCache?: boolean): Promise<void>;
51
58
  get query(): ChainApi[RpcV2]['query'];
52
59
  get view(): ChainApi[RpcV2]['view'];
53
60
  get call(): ChainApi[RpcV2]['call'];
@@ -55,10 +62,10 @@ export declare class DedotClient<ChainApi extends VersionedGenericSubstrateApi =
55
62
  get tx(): ChainApi[RpcV2]['tx'];
56
63
  /**
57
64
  * Get a new API instance at a specific block hash
58
- * For now, this only supports pinned block hashes from the chain head
65
+ * Supports both pinned blocks (via ChainHead) and historical blocks (via Archive fallback)
59
66
  *
60
67
  * @param hash
61
68
  */
62
69
  at<ChainApiAt extends GenericSubstrateApi = ChainApi[RpcV2]>(hash: BlockHash): Promise<ISubstrateClientAt<ChainApiAt>>;
63
- protected getStorageQuery(): BaseStorageQuery<RpcVersion>;
70
+ protected getStorageQuery(): BaseStorageQuery;
64
71
  }
@@ -1,8 +1,8 @@
1
- import { $H256, PortableRegistry } from '@dedot/codecs';
1
+ import { $H256, $RuntimeVersion, PortableRegistry } from '@dedot/codecs';
2
2
  import { u32 } from '@dedot/shape';
3
- import { assert, concatU8a, noop, twox64Concat, u8aToHex, xxhashAsU8a } from '@dedot/utils';
3
+ import { assert, concatU8a, noop, twox64Concat, u8aToHex, xxhashAsU8a, DedotError } from '@dedot/utils';
4
4
  import { ConstantExecutor, ErrorExecutor, EventExecutor, RuntimeApiExecutorV2, StorageQueryExecutorV2, ViewFunctionExecutorV2, TxExecutorV2, } from '../executor/index.js';
5
- import { ChainHead, ChainSpec, Transaction, TransactionWatch } from '../json-rpc/index.js';
5
+ import { Archive, ChainHead, ChainSpec, Transaction, TransactionWatch } from '../json-rpc/index.js';
6
6
  import { newProxyChain } from '../proxychain.js';
7
7
  import { NewStorageQuery } from '../storage/index.js';
8
8
  import { BaseSubstrateClient, ensurePresence } from './BaseSubstrateClient.js';
@@ -16,8 +16,8 @@ export class DedotClient// prettier-end-here
16
16
  extends BaseSubstrateClient {
17
17
  _chainHead;
18
18
  _chainSpec;
19
+ _archive;
19
20
  _txBroadcaster;
20
- #apiAtCache = {};
21
21
  /**
22
22
  * Use factory methods (`create`, `new`) to create `DedotClient` instances.
23
23
  *
@@ -48,6 +48,11 @@ export class DedotClient// prettier-end-here
48
48
  get chainHead() {
49
49
  return ensurePresence(this._chainHead);
50
50
  }
51
+ async archive() {
52
+ assert(this._archive, 'Archive instance is not initialized');
53
+ assert(await this._archive.supported(), 'Archive JSON-RPC is not supported by the connected server');
54
+ return this._archive;
55
+ }
51
56
  get txBroadcaster() {
52
57
  this.chainHead; // Ensure chain head is initialized
53
58
  assert(this._txBroadcaster, 'JSON-RPC method to broadcast transactions is not supported by the server/node.');
@@ -68,6 +73,12 @@ export class DedotClient// prettier-end-here
68
73
  const rpcMethods = (await this.rpc.rpc_methods()).methods;
69
74
  this._chainHead = new ChainHead(this, { rpcMethods });
70
75
  this._chainSpec = new ChainSpec(this, { rpcMethods });
76
+ // Always initialize Archive, but only set up fallback if supported
77
+ this._archive = new Archive(this, { rpcMethods });
78
+ // Set up ChainHead with Archive fallback only if Archive is supported
79
+ if (await this._archive.supported()) {
80
+ this._chainHead.withArchive(this._archive);
81
+ }
71
82
  this._txBroadcaster = await this.#initializeTxBroadcaster(rpcMethods);
72
83
  // Fetching node information
73
84
  let [_, genesisHash] = await Promise.all([
@@ -130,8 +141,16 @@ export class DedotClient// prettier-end-here
130
141
  super.cleanUp();
131
142
  this._chainHead = undefined;
132
143
  this._chainSpec = undefined;
144
+ this._archive = undefined;
133
145
  this._txBroadcaster = undefined;
134
- this.#apiAtCache = {};
146
+ }
147
+ /**
148
+ * @description Clear local cache, API at-block cache, and ChainHead cache
149
+ * @param keepMetadataCache Keep the metadata cache, only clear other caches.
150
+ */
151
+ async clearCache(keepMetadataCache = false) {
152
+ await super.clearCache(keepMetadataCache);
153
+ this._chainHead?.clearCache();
135
154
  }
136
155
  get query() {
137
156
  return newProxyChain({
@@ -156,19 +175,40 @@ export class DedotClient// prettier-end-here
156
175
  }
157
176
  /**
158
177
  * Get a new API instance at a specific block hash
159
- * For now, this only supports pinned block hashes from the chain head
178
+ * Supports both pinned blocks (via ChainHead) and historical blocks (via Archive fallback)
160
179
  *
161
180
  * @param hash
162
181
  */
163
182
  async at(hash) {
164
- if (this.#apiAtCache[hash])
165
- return this.#apiAtCache[hash];
183
+ const cached = this._apiAtCache.get(hash);
184
+ if (cached)
185
+ return cached;
186
+ let targetVersion;
187
+ // Try to get block info from ChainHead first (for pinned blocks)
166
188
  const targetBlock = this.chainHead.findBlock(hash);
167
- assert(targetBlock, 'Block is not pinned!');
168
- let targetVersion = targetBlock.runtime;
169
- if (!targetVersion) {
170
- // fallback to fetching on-chain runtime if we can't find it in the block
171
- targetVersion = this.toSubstrateRuntimeVersion(await this.callAt(hash).core.version());
189
+ if (targetBlock) {
190
+ targetVersion = targetBlock.runtime;
191
+ if (!targetVersion) {
192
+ // fallback to fetching on-chain runtime if we can't find it in the block
193
+ targetVersion = this.toSubstrateRuntimeVersion(await this.callAt(hash).core.version());
194
+ }
195
+ }
196
+ else {
197
+ // Block not pinned, try via Archive fallback if supported
198
+ if (this._archive && (await this._archive.supported())) {
199
+ try {
200
+ // Fetch runtime version via Archive
201
+ const runtimeRaw = await this._archive.call('Core_version', '0x', hash);
202
+ assert(runtimeRaw, 'Runtime Version Not Found');
203
+ targetVersion = this.toSubstrateRuntimeVersion($RuntimeVersion.tryDecode(runtimeRaw));
204
+ }
205
+ catch (error) {
206
+ throw new DedotError(`Unable to fetch runtime version for block ${hash}: ${error}`);
207
+ }
208
+ }
209
+ else {
210
+ throw new DedotError('Block is not pinned and Archive JSON-RPC is not supported by the server/node!');
211
+ }
172
212
  }
173
213
  let metadata = this.metadata;
174
214
  let registry = this.registry;
@@ -192,7 +232,7 @@ export class DedotClient// prettier-end-here
192
232
  api.query = newProxyChain({ executor: new StorageQueryExecutorV2(api, this.chainHead) });
193
233
  api.call = newProxyChain({ executor: new RuntimeApiExecutorV2(api, this.chainHead) });
194
234
  api.view = newProxyChain({ executor: new ViewFunctionExecutorV2(api, this.chainHead) });
195
- this.#apiAtCache[hash] = api;
235
+ this._apiAtCache.set(hash, api);
196
236
  return api;
197
237
  }
198
238
  getStorageQuery() {
@@ -1,6 +1,6 @@
1
1
  import { BlockHash } from '@dedot/codecs';
2
2
  import type { JsonRpcProvider } from '@dedot/providers';
3
- import { GenericSubstrateApi, RpcLegacy, RpcVersion, VersionedGenericSubstrateApi } from '@dedot/types';
3
+ import { GenericSubstrateApi, RpcLegacy, VersionedGenericSubstrateApi } from '@dedot/types';
4
4
  import type { SubstrateApi } from '../chaintypes/index.js';
5
5
  import { BaseStorageQuery } from '../storage/index.js';
6
6
  import type { ApiOptions, ISubstrateClientAt } from '../types.js';
@@ -131,5 +131,5 @@ export declare class LegacyClient<ChainApi extends VersionedGenericSubstrateApi
131
131
  * @param hash
132
132
  */
133
133
  at<ChainApiAt extends GenericSubstrateApi = ChainApi[RpcLegacy]>(hash: BlockHash): Promise<ISubstrateClientAt<ChainApiAt>>;
134
- protected getStorageQuery(): BaseStorageQuery<RpcVersion>;
134
+ protected getStorageQuery(): BaseStorageQuery;
135
135
  }