@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.
- package/chaintypes/substrate/consts.d.ts +2 -2
- package/chaintypes/substrate/consts.js +1 -0
- package/chaintypes/substrate/errors.d.ts +909 -909
- package/chaintypes/substrate/errors.js +1 -0
- package/chaintypes/substrate/events.d.ts +664 -664
- package/chaintypes/substrate/events.js +1 -0
- package/chaintypes/substrate/index.d.ts +13 -16
- package/chaintypes/substrate/index.js +1 -0
- package/chaintypes/substrate/json-rpc.d.ts +2 -2
- package/chaintypes/substrate/json-rpc.js +1 -0
- package/chaintypes/substrate/query.d.ts +509 -527
- package/chaintypes/substrate/query.js +1 -0
- package/chaintypes/substrate/runtime.d.ts +123 -123
- package/chaintypes/substrate/runtime.js +1 -0
- package/chaintypes/substrate/tx.d.ts +1311 -1311
- package/chaintypes/substrate/tx.js +1 -0
- package/chaintypes/substrate/types.js +1 -0
- package/chaintypes/substrate/view-functions.d.ts +2 -2
- package/chaintypes/substrate/view-functions.js +1 -0
- package/cjs/chaintypes/substrate/consts.js +1 -0
- package/cjs/chaintypes/substrate/errors.js +1 -0
- package/cjs/chaintypes/substrate/events.js +1 -0
- package/cjs/chaintypes/substrate/index.js +1 -0
- package/cjs/chaintypes/substrate/json-rpc.js +1 -0
- package/cjs/chaintypes/substrate/query.js +1 -0
- package/cjs/chaintypes/substrate/runtime.js +1 -0
- package/cjs/chaintypes/substrate/tx.js +1 -0
- package/cjs/chaintypes/substrate/types.js +1 -0
- package/cjs/chaintypes/substrate/view-functions.js +1 -0
- package/cjs/client/BaseSubstrateClient.js +45 -2
- package/cjs/client/DedotClient.js +356 -224
- package/cjs/client/LegacyClient.js +99 -23
- package/cjs/client/V2Client.js +316 -0
- package/cjs/client/explorer/LegacyBlockExplorer.js +263 -0
- package/cjs/client/explorer/V2BlockExplorer.js +74 -0
- package/cjs/client/explorer/index.js +18 -0
- package/cjs/client/utils.js +57 -0
- package/cjs/executor/EventExecutor.js +1 -0
- package/cjs/executor/RuntimeApiExecutor.js +49 -2
- package/cjs/executor/StorageQueryExecutor.js +114 -2
- package/cjs/executor/ViewFunctionExecutor.js +21 -2
- package/cjs/executor/v2/StorageQueryExecutorV2.js +7 -1
- package/cjs/executor/validation-helpers.js +181 -0
- package/cjs/extrinsic/submittable/BaseSubmittableExtrinsic.js +86 -2
- package/cjs/extrinsic/submittable/SubmittableExtrinsic.js +38 -16
- package/cjs/extrinsic/submittable/SubmittableExtrinsicV2.js +12 -2
- package/cjs/json-rpc/group/ChainHead/ChainHead.js +226 -21
- package/cjs/storage/QueryableStorage.js +1 -0
- package/client/BaseSubstrateClient.d.ts +34 -18
- package/client/BaseSubstrateClient.js +47 -4
- package/client/DedotClient.d.ts +346 -49
- package/client/DedotClient.js +356 -224
- package/client/LegacyClient.d.ts +22 -17
- package/client/LegacyClient.js +101 -25
- package/client/V2Client.d.ts +75 -0
- package/client/V2Client.js +312 -0
- package/client/explorer/LegacyBlockExplorer.d.ts +68 -0
- package/client/explorer/LegacyBlockExplorer.js +259 -0
- package/client/explorer/V2BlockExplorer.d.ts +40 -0
- package/client/explorer/V2BlockExplorer.js +70 -0
- package/client/explorer/index.d.ts +2 -0
- package/client/explorer/index.js +2 -0
- package/client/utils.d.ts +18 -0
- package/client/utils.js +52 -0
- package/executor/ConstantExecutor.d.ts +1 -2
- package/executor/ErrorExecutor.d.ts +2 -2
- package/executor/EventExecutor.d.ts +2 -2
- package/executor/EventExecutor.js +1 -0
- package/executor/Executor.d.ts +4 -5
- package/executor/RuntimeApiExecutor.d.ts +2 -2
- package/executor/RuntimeApiExecutor.js +27 -3
- package/executor/StorageQueryExecutor.d.ts +3 -2
- package/executor/StorageQueryExecutor.js +92 -3
- package/executor/TxExecutor.d.ts +2 -2
- package/executor/ViewFunctionExecutor.d.ts +2 -2
- package/executor/ViewFunctionExecutor.js +22 -3
- package/executor/v2/RuntimeApiExecutorV2.d.ts +2 -3
- package/executor/v2/StorageQueryExecutorV2.d.ts +3 -3
- package/executor/v2/StorageQueryExecutorV2.js +8 -2
- package/executor/v2/TxExecutorV2.d.ts +5 -5
- package/executor/v2/ViewFunctionExecutorV2.d.ts +2 -3
- package/executor/validation-helpers.d.ts +36 -0
- package/executor/validation-helpers.js +174 -0
- package/extrinsic/extensions/SignedExtension.d.ts +1 -1
- package/extrinsic/submittable/BaseSubmittableExtrinsic.d.ts +7 -3
- package/extrinsic/submittable/BaseSubmittableExtrinsic.js +64 -3
- package/extrinsic/submittable/SubmittableExtrinsic.d.ts +4 -1
- package/extrinsic/submittable/SubmittableExtrinsic.js +39 -17
- package/extrinsic/submittable/SubmittableExtrinsicV2.d.ts +6 -3
- package/extrinsic/submittable/SubmittableExtrinsicV2.js +13 -3
- package/json-rpc/JsonRpcClient.d.ts +6 -6
- package/json-rpc/group/Archive.d.ts +1 -1
- package/json-rpc/group/ChainHead/ChainHead.d.ts +18 -2
- package/json-rpc/group/ChainHead/ChainHead.js +226 -21
- package/json-rpc/group/ChainSpec.d.ts +3 -3
- package/json-rpc/group/Transaction.d.ts +1 -1
- package/json-rpc/group/TransactionWatch.d.ts +1 -1
- package/package.json +9 -9
- package/proxychain.d.ts +3 -4
- package/storage/NewStorageQuery.d.ts +3 -3
- package/storage/QueryableStorage.js +1 -0
- 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
|
+
}
|