UNPKG

17 kBJavaScriptView Raw
1import { firstValueFrom, map, of, switchMap } from 'rxjs';
2import { Metadata, TypeRegistry } from '@polkadot/types';
3import { getSpecAlias, getSpecExtensions, getSpecHasher, getSpecRpc, getSpecTypes, getUpgradeVersion } from '@polkadot/types-known';
4import { assertReturn, BN_ZERO, isUndefined, logger, noop, objectSpread, u8aEq, u8aToHex, u8aToU8a } from '@polkadot/util';
5import { cryptoWaitReady } from '@polkadot/util-crypto';
6import { Decorate } from './Decorate.js';
7const KEEPALIVE_INTERVAL = 10000;
8const WITH_VERSION_SHORTCUT = false;
9const l = logger('api/init');
10function textToString(t) {
11 return t.toString();
12}
13export class Init extends Decorate {
14 __internal__atLast = null;
15 __internal__healthTimer = null;
16 __internal__registries = [];
17 __internal__updateSub = null;
18 __internal__waitingRegistries = {};
19 constructor(options, type, decorateMethod) {
20 super(options, type, decorateMethod);
21 // all injected types added to the registry for overrides
22 this.registry.setKnownTypes(options);
23 // We only register the types (global) if this is not a cloned instance.
24 // Do right up-front, so we get in the user types before we are actually
25 // doing anything on-chain, this ensures we have the overrides in-place
26 if (!options.source) {
27 this.registerTypes(options.types);
28 }
29 else {
30 this.__internal__registries = options.source.__internal__registries;
31 }
32 this._rpc = this._decorateRpc(this._rpcCore, this._decorateMethod);
33 this._rx.rpc = this._decorateRpc(this._rpcCore, this._rxDecorateMethod);
34 if (this.supportMulti) {
35 this._queryMulti = this._decorateMulti(this._decorateMethod);
36 this._rx.queryMulti = this._decorateMulti(this._rxDecorateMethod);
37 }
38 this._rx.signer = options.signer;
39 this._rpcCore.setRegistrySwap((blockHash) => this.getBlockRegistry(blockHash));
40 this._rpcCore.setResolveBlockHash((blockNumber) => firstValueFrom(this._rpcCore.chain.getBlockHash(blockNumber)));
41 if (this.hasSubscriptions) {
42 this._rpcCore.provider.on('disconnected', () => this.__internal__onProviderDisconnect());
43 this._rpcCore.provider.on('error', (e) => this.__internal__onProviderError(e));
44 this._rpcCore.provider.on('connected', () => this.__internal__onProviderConnect());
45 }
46 else if (!this._options.noInitWarn) {
47 l.warn('Api will be available in a limited mode since the provider does not support subscriptions');
48 }
49 // If the provider was instantiated earlier, and has already emitted a
50 // 'connected' event, then the `on('connected')` won't fire anymore. To
51 // cater for this case, we call manually `this._onProviderConnect`.
52 if (this._rpcCore.provider.isConnected) {
53 this.__internal__onProviderConnect().catch(noop);
54 }
55 }
56 /**
57 * @description Decorates a registry based on the runtime version
58 */
59 _initRegistry(registry, chain, version, metadata, chainProps) {
60 registry.clearCache();
61 registry.setChainProperties(chainProps || this.registry.getChainProperties());
62 registry.setKnownTypes(this._options);
63 registry.register(getSpecTypes(registry, chain, version.specName, version.specVersion));
64 registry.setHasher(getSpecHasher(registry, chain, version.specName));
65 // for bundled types, pull through the aliases defined
66 if (registry.knownTypes.typesBundle) {
67 registry.knownTypes.typesAlias = getSpecAlias(registry, chain, version.specName);
68 }
69 registry.setMetadata(metadata, undefined, objectSpread({}, getSpecExtensions(registry, chain, version.specName), this._options.signedExtensions), this._options.noInitWarn);
70 }
71 /**
72 * @description Returns the default versioned registry
73 */
74 _getDefaultRegistry() {
75 return assertReturn(this.__internal__registries.find(({ isDefault }) => isDefault), 'Initialization error, cannot find the default registry');
76 }
77 /**
78 * @description Returns a decorated API instance at a specific point in time
79 */
80 async at(blockHash, knownVersion) {
81 const u8aHash = u8aToU8a(blockHash);
82 const u8aHex = u8aToHex(u8aHash);
83 const registry = await this.getBlockRegistry(u8aHash, knownVersion);
84 if (!this.__internal__atLast || this.__internal__atLast[0] !== u8aHex) {
85 // always create a new decoration - since we are pointing to a specific hash, this
86 // means that all queries needs to use that hash (not a previous one already existing)
87 this.__internal__atLast = [u8aHex, this._createDecorated(registry, true, null, u8aHash).decoratedApi];
88 }
89 return this.__internal__atLast[1];
90 }
91 async _createBlockRegistry(blockHash, header, version) {
92 const registry = new TypeRegistry(blockHash);
93 const metadata = new Metadata(registry, await firstValueFrom(this._rpcCore.state.getMetadata.raw(header.parentHash)));
94 const runtimeChain = this._runtimeChain;
95 if (!runtimeChain) {
96 throw new Error('Invalid initializion order, runtimeChain is not available');
97 }
98 this._initRegistry(registry, runtimeChain, version, metadata);
99 // add our new registry
100 const result = { counter: 0, lastBlockHash: blockHash, metadata, registry, runtimeVersion: version };
101 this.__internal__registries.push(result);
102 return result;
103 }
104 _cacheBlockRegistryProgress(key, creator) {
105 // look for waiting resolves
106 let waiting = this.__internal__waitingRegistries[key];
107 if (isUndefined(waiting)) {
108 // nothing waiting, construct new
109 waiting = this.__internal__waitingRegistries[key] = new Promise((resolve, reject) => {
110 creator()
111 .then((registry) => {
112 delete this.__internal__waitingRegistries[key];
113 resolve(registry);
114 })
115 .catch((error) => {
116 delete this.__internal__waitingRegistries[key];
117 reject(error);
118 });
119 });
120 }
121 return waiting;
122 }
123 _getBlockRegistryViaVersion(blockHash, version) {
124 if (version) {
125 // check for pre-existing registries. We also check specName, e.g. it
126 // could be changed like in Westmint with upgrade from shell -> westmint
127 const existingViaVersion = this.__internal__registries.find(({ runtimeVersion: { specName, specVersion } }) => specName.eq(version.specName) &&
128 specVersion.eq(version.specVersion));
129 if (existingViaVersion) {
130 existingViaVersion.counter++;
131 existingViaVersion.lastBlockHash = blockHash;
132 return existingViaVersion;
133 }
134 }
135 return null;
136 }
137 async _getBlockRegistryViaHash(blockHash) {
138 // ensure we have everything required
139 if (!this._genesisHash || !this._runtimeVersion) {
140 throw new Error('Cannot retrieve data on an uninitialized chain');
141 }
142 // We have to assume that on the RPC layer the calls used here does not call back into
143 // the registry swap, so getHeader & getRuntimeVersion should not be historic
144 const header = this.registry.createType('HeaderPartial', this._genesisHash.eq(blockHash)
145 ? { number: BN_ZERO, parentHash: this._genesisHash }
146 : await firstValueFrom(this._rpcCore.chain.getHeader.raw(blockHash)));
147 if (header.parentHash.isEmpty) {
148 throw new Error('Unable to retrieve header and parent from supplied hash');
149 }
150 // get the runtime version, either on-chain or via an known upgrade history
151 const [firstVersion, lastVersion] = getUpgradeVersion(this._genesisHash, header.number);
152 const version = this.registry.createType('RuntimeVersionPartial', WITH_VERSION_SHORTCUT && (firstVersion && (lastVersion ||
153 firstVersion.specVersion.eq(this._runtimeVersion.specVersion)))
154 ? { apis: firstVersion.apis, specName: this._runtimeVersion.specName, specVersion: firstVersion.specVersion }
155 : await firstValueFrom(this._rpcCore.state.getRuntimeVersion.raw(header.parentHash)));
156 return (
157 // try to find via version
158 this._getBlockRegistryViaVersion(blockHash, version) ||
159 // return new or in-flight result
160 await this._cacheBlockRegistryProgress(version.toHex(), () => this._createBlockRegistry(blockHash, header, version)));
161 }
162 /**
163 * @description Sets up a registry based on the block hash defined
164 */
165 async getBlockRegistry(blockHash, knownVersion) {
166 return (
167 // try to find via blockHash
168 this.__internal__registries.find(({ lastBlockHash }) => lastBlockHash && u8aEq(lastBlockHash, blockHash)) ||
169 // try to find via version
170 this._getBlockRegistryViaVersion(blockHash, knownVersion) ||
171 // return new or in-flight result
172 await this._cacheBlockRegistryProgress(u8aToHex(blockHash), () => this._getBlockRegistryViaHash(blockHash)));
173 }
174 async _loadMeta() {
175 // on re-connection to the same chain, we don't want to re-do everything from chain again
176 if (this._isReady) {
177 return true;
178 }
179 this._unsubscribeUpdates();
180 // only load from on-chain if we are not a clone (default path), alternatively
181 // just use the values from the source instance provided
182 [this._genesisHash, this._runtimeMetadata] = this._options.source?._isReady
183 ? await this._metaFromSource(this._options.source)
184 : await this._metaFromChain(this._options.metadata);
185 return this._initFromMeta(this._runtimeMetadata);
186 }
187 // eslint-disable-next-line @typescript-eslint/require-await
188 async _metaFromSource(source) {
189 this._extrinsicType = source.extrinsicVersion;
190 this._runtimeChain = source.runtimeChain;
191 this._runtimeVersion = source.runtimeVersion;
192 // manually build a list of all available methods in this RPC, we are
193 // going to filter on it to align the cloned RPC without making a call
194 const sections = Object.keys(source.rpc);
195 const rpcs = [];
196 for (let s = 0, scount = sections.length; s < scount; s++) {
197 const section = sections[s];
198 const methods = Object.keys(source.rpc[section]);
199 for (let m = 0, mcount = methods.length; m < mcount; m++) {
200 rpcs.push(`${section}_${methods[m]}`);
201 }
202 }
203 this._filterRpc(rpcs, getSpecRpc(this.registry, source.runtimeChain, source.runtimeVersion.specName));
204 return [source.genesisHash, source.runtimeMetadata];
205 }
206 // subscribe to metadata updates, inject the types on changes
207 _subscribeUpdates() {
208 if (this.__internal__updateSub || !this.hasSubscriptions) {
209 return;
210 }
211 this.__internal__updateSub = this._rpcCore.state.subscribeRuntimeVersion().pipe(switchMap((version) =>
212 // only retrieve the metadata when the on-chain version has been changed
213 this._runtimeVersion?.specVersion.eq(version.specVersion)
214 ? of(false)
215 : this._rpcCore.state.getMetadata().pipe(map((metadata) => {
216 l.log(`Runtime version updated to spec=${version.specVersion.toString()}, tx=${version.transactionVersion.toString()}`);
217 this._runtimeMetadata = metadata;
218 this._runtimeVersion = version;
219 this._rx.runtimeVersion = version;
220 // update the default registry version
221 const thisRegistry = this._getDefaultRegistry();
222 const runtimeChain = this._runtimeChain;
223 if (!runtimeChain) {
224 throw new Error('Invalid initializion order, runtimeChain is not available');
225 }
226 // setup the data as per the current versions
227 thisRegistry.metadata = metadata;
228 thisRegistry.runtimeVersion = version;
229 this._initRegistry(this.registry, runtimeChain, version, metadata);
230 this._injectMetadata(thisRegistry, true);
231 return true;
232 })))).subscribe();
233 }
234 async _metaFromChain(optMetadata) {
235 const [genesisHash, runtimeVersion, chain, chainProps, rpcMethods, chainMetadata] = await Promise.all([
236 firstValueFrom(this._rpcCore.chain.getBlockHash(0)),
237 firstValueFrom(this._rpcCore.state.getRuntimeVersion()),
238 firstValueFrom(this._rpcCore.system.chain()),
239 firstValueFrom(this._rpcCore.system.properties()),
240 firstValueFrom(this._rpcCore.rpc.methods()),
241 optMetadata
242 ? Promise.resolve(null)
243 : firstValueFrom(this._rpcCore.state.getMetadata())
244 ]);
245 // set our chain version & genesisHash as returned
246 this._runtimeChain = chain;
247 this._runtimeVersion = runtimeVersion;
248 this._rx.runtimeVersion = runtimeVersion;
249 // retrieve metadata, either from chain or as pass-in via options
250 const metadataKey = `${genesisHash.toHex() || '0x'}-${runtimeVersion.specVersion.toString()}`;
251 const metadata = chainMetadata || (optMetadata?.[metadataKey]
252 ? new Metadata(this.registry, optMetadata[metadataKey])
253 : await firstValueFrom(this._rpcCore.state.getMetadata()));
254 // initializes the registry & RPC
255 this._initRegistry(this.registry, chain, runtimeVersion, metadata, chainProps);
256 this._filterRpc(rpcMethods.methods.map(textToString), getSpecRpc(this.registry, chain, runtimeVersion.specName));
257 this._subscribeUpdates();
258 // setup the initial registry, when we have none
259 if (!this.__internal__registries.length) {
260 this.__internal__registries.push({ counter: 0, isDefault: true, metadata, registry: this.registry, runtimeVersion });
261 }
262 // get unique types & validate
263 metadata.getUniqTypes(this._options.throwOnUnknown || false);
264 return [genesisHash, metadata];
265 }
266 _initFromMeta(metadata) {
267 const runtimeVersion = this._runtimeVersion;
268 if (!runtimeVersion) {
269 throw new Error('Invalid initializion order, runtimeVersion is not available');
270 }
271 this._extrinsicType = metadata.asLatest.extrinsic.version.toNumber();
272 this._rx.extrinsicType = this._extrinsicType;
273 this._rx.genesisHash = this._genesisHash;
274 this._rx.runtimeVersion = runtimeVersion;
275 // inject metadata and adjust the types as detected
276 this._injectMetadata(this._getDefaultRegistry(), true);
277 // derive is last, since it uses the decorated rx
278 this._rx.derive = this._decorateDeriveRx(this._rxDecorateMethod);
279 this._derive = this._decorateDerive(this._decorateMethod);
280 return true;
281 }
282 _subscribeHealth() {
283 this._unsubscribeHealth();
284 // Only enable the health keepalive on WS, not needed on HTTP
285 this.__internal__healthTimer = this.hasSubscriptions
286 ? setInterval(() => {
287 firstValueFrom(this._rpcCore.system.health.raw()).catch(noop);
288 }, KEEPALIVE_INTERVAL)
289 : null;
290 }
291 _unsubscribeHealth() {
292 if (this.__internal__healthTimer) {
293 clearInterval(this.__internal__healthTimer);
294 this.__internal__healthTimer = null;
295 }
296 }
297 _unsubscribeUpdates() {
298 if (this.__internal__updateSub) {
299 this.__internal__updateSub.unsubscribe();
300 this.__internal__updateSub = null;
301 }
302 }
303 _unsubscribe() {
304 this._unsubscribeHealth();
305 this._unsubscribeUpdates();
306 }
307 async __internal__onProviderConnect() {
308 this._isConnected.next(true);
309 this.emit('connected');
310 try {
311 const cryptoReady = this._options.initWasm === false
312 ? true
313 : await cryptoWaitReady();
314 const hasMeta = await this._loadMeta();
315 this._subscribeHealth();
316 if (hasMeta && !this._isReady && cryptoReady) {
317 this._isReady = true;
318 this.emit('ready', this);
319 }
320 }
321 catch (_error) {
322 const error = new Error(`FATAL: Unable to initialize the API: ${_error.message}`);
323 l.error(error);
324 this.emit('error', error);
325 }
326 }
327 __internal__onProviderDisconnect() {
328 this._isConnected.next(false);
329 this._unsubscribe();
330 this.emit('disconnected');
331 }
332 __internal__onProviderError(error) {
333 this.emit('error', error);
334 }
335}