@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.
- package/chaintypes/substrate/index.d.ts +21 -1
- package/cjs/client/BaseSubstrateClient.js +12 -3
- package/cjs/client/DedotClient.js +51 -11
- package/cjs/client/LegacyClient.js +4 -5
- package/cjs/executor/Executor.js +1 -0
- package/cjs/executor/v2/StorageQueryExecutorV2.js +1 -3
- package/cjs/executor/v2/TxExecutorV2.js +5 -3
- package/cjs/json-rpc/group/Archive.js +232 -0
- package/cjs/json-rpc/group/ChainHead/ChainHead.js +149 -52
- package/cjs/json-rpc/group/ChainHead/error.js +5 -0
- package/cjs/json-rpc/group/index.js +1 -0
- package/cjs/json-rpc/subscriptionsInfo.js +4 -0
- package/cjs/storage/LegacyStorageQuery.js +5 -0
- package/cjs/storage/NewStorageQuery.js +5 -0
- package/client/BaseSubstrateClient.d.ts +8 -6
- package/client/BaseSubstrateClient.js +13 -4
- package/client/DedotClient.d.ts +11 -4
- package/client/DedotClient.js +54 -14
- package/client/LegacyClient.d.ts +2 -2
- package/client/LegacyClient.js +4 -5
- package/executor/Executor.d.ts +5 -5
- package/executor/Executor.js +1 -0
- package/executor/StorageQueryExecutor.d.ts +2 -2
- package/executor/v2/RuntimeApiExecutorV2.d.ts +2 -2
- package/executor/v2/StorageQueryExecutorV2.d.ts +4 -4
- package/executor/v2/StorageQueryExecutorV2.js +1 -3
- package/executor/v2/TxExecutorV2.d.ts +2 -1
- package/executor/v2/TxExecutorV2.js +5 -3
- package/executor/v2/ViewFunctionExecutorV2.d.ts +2 -2
- package/extrinsic/extensions/SignedExtension.d.ts +3 -3
- package/extrinsic/submittable/BaseSubmittableExtrinsic.d.ts +2 -2
- package/extrinsic/submittable/SubmittableExtrinsicV2.d.ts +2 -2
- package/json-rpc/group/Archive.d.ts +134 -0
- package/json-rpc/group/Archive.js +228 -0
- package/json-rpc/group/ChainHead/ChainHead.d.ts +21 -0
- package/json-rpc/group/ChainHead/ChainHead.js +150 -53
- package/json-rpc/group/ChainHead/error.d.ts +3 -0
- package/json-rpc/group/ChainHead/error.js +5 -0
- package/json-rpc/group/index.d.ts +1 -0
- package/json-rpc/group/index.js +1 -0
- package/json-rpc/subscriptionsInfo.js +4 -0
- package/package.json +9 -9
- package/storage/BaseStorageQuery.d.ts +5 -6
- package/storage/LegacyStorageQuery.d.ts +4 -3
- package/storage/LegacyStorageQuery.js +5 -0
- package/storage/NewStorageQuery.d.ts +4 -3
- package/storage/NewStorageQuery.js +5 -0
- package/storage/QueryableStorage.d.ts +2 -2
- 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
|
|
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
|
-
//
|
|
226
|
-
|
|
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
|
|
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
|
-
|
|
534
|
+
const operation = async () => {
|
|
482
535
|
const atHash = this.#ensurePinnedHash(at);
|
|
483
536
|
const cacheKey = `${atHash}::body`;
|
|
484
|
-
|
|
485
|
-
|
|
537
|
+
const cached = this.#cache.get(cacheKey);
|
|
538
|
+
if (cached) {
|
|
539
|
+
return cached;
|
|
486
540
|
}
|
|
487
|
-
const
|
|
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(
|
|
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
|
-
|
|
574
|
+
const operation = async () => {
|
|
511
575
|
const atHash = this.#ensurePinnedHash(at);
|
|
512
576
|
const cacheKey = `${atHash}::call::${func}::${params}`;
|
|
513
|
-
|
|
514
|
-
|
|
577
|
+
const cached = this.#cache.get(cacheKey);
|
|
578
|
+
if (cached) {
|
|
579
|
+
return cached;
|
|
515
580
|
}
|
|
516
|
-
const
|
|
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(
|
|
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
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
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
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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
|
-
|
|
584
|
-
|
|
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
|
|
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
|
-
|
|
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
|
package/client/DedotClient.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { BlockHash } from '@dedot/codecs';
|
|
2
2
|
import type { JsonRpcProvider } from '@dedot/providers';
|
|
3
|
-
import { GenericSubstrateApi, RpcV2,
|
|
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
|
-
*
|
|
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
|
|
70
|
+
protected getStorageQuery(): BaseStorageQuery;
|
|
64
71
|
}
|
package/client/DedotClient.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
165
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
|
235
|
+
this._apiAtCache.set(hash, api);
|
|
196
236
|
return api;
|
|
197
237
|
}
|
|
198
238
|
getStorageQuery() {
|
package/client/LegacyClient.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { BlockHash } from '@dedot/codecs';
|
|
2
2
|
import type { JsonRpcProvider } from '@dedot/providers';
|
|
3
|
-
import { GenericSubstrateApi, RpcLegacy,
|
|
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
|
|
134
|
+
protected getStorageQuery(): BaseStorageQuery;
|
|
135
135
|
}
|