@acala-network/chopsticks-core 1.3.1 → 1.4.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/dist/cjs/api.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { ProviderInterface, ProviderInterfaceCallback } from '@polkadot/rpc-provider/types';
2
2
  import type { ExtDef } from '@polkadot/types/extrinsic/signedExtensions/types';
3
3
  import type { HexString } from '@polkadot/util/types';
4
+ import type { Database } from './database.js';
4
5
  import type { ChainProperties, Header, SignedBlock } from './index.js';
5
6
  /**
6
7
  * API class. Calls provider to get on-chain data.
@@ -18,6 +19,7 @@ export declare class Api {
18
19
  #private;
19
20
  readonly signedExtensions: ExtDef;
20
21
  constructor(provider: ProviderInterface, signedExtensions?: ExtDef);
22
+ setDb(db: Database, scope: string): void;
21
23
  disconnect(): Promise<void>;
22
24
  get isReady(): Promise<void> | undefined;
23
25
  get chain(): Promise<string>;
package/dist/cjs/api.js CHANGED
@@ -51,6 +51,16 @@ function _class_private_field_set(receiver, privateMap, value) {
51
51
  _class_apply_descriptor_set(receiver, descriptor, value);
52
52
  return value;
53
53
  }
54
+ function _class_private_method_get(receiver, privateSet, fn) {
55
+ if (!privateSet.has(receiver)) {
56
+ throw new TypeError("attempted to get private field on non-instance");
57
+ }
58
+ return fn;
59
+ }
60
+ function _class_private_method_init(obj, privateSet) {
61
+ _check_private_redeclaration(obj, privateSet);
62
+ privateSet.add(obj);
63
+ }
54
64
  function _define_property(obj, key, value) {
55
65
  if (key in obj) {
56
66
  Object.defineProperty(obj, key, {
@@ -69,8 +79,19 @@ function _interop_require_default(obj) {
69
79
  default: obj
70
80
  };
71
81
  }
72
- var _provider = /*#__PURE__*/ new WeakMap(), _ready = /*#__PURE__*/ new WeakMap(), _chain = /*#__PURE__*/ new WeakMap(), _chainProperties = /*#__PURE__*/ new WeakMap(), _apiHooks = /*#__PURE__*/ new WeakMap();
82
+ const CACHEABLE_METHODS = new Set([
83
+ 'system_chain',
84
+ 'system_properties',
85
+ 'system_name',
86
+ 'chain_getBlockHash',
87
+ 'chain_getHeader'
88
+ ]);
89
+ var _provider = /*#__PURE__*/ new WeakMap(), _ready = /*#__PURE__*/ new WeakMap(), _chain = /*#__PURE__*/ new WeakMap(), _chainProperties = /*#__PURE__*/ new WeakMap(), _db = /*#__PURE__*/ new WeakMap(), _scope = /*#__PURE__*/ new WeakMap(), _apiHooks = /*#__PURE__*/ new WeakMap(), _cachedSend = /*#__PURE__*/ new WeakSet();
73
90
  class Api {
91
+ setDb(db, scope) {
92
+ _class_private_field_set(this, _db, db);
93
+ _class_private_field_set(this, _scope, scope);
94
+ }
74
95
  async disconnect() {
75
96
  return _class_private_field_get(this, _provider).disconnect();
76
97
  }
@@ -112,21 +133,21 @@ class Api {
112
133
  return _class_private_field_get(this, _provider).send(method, params, isCacheable);
113
134
  }
114
135
  async getSystemName() {
115
- return this.send('system_name', []);
136
+ return _class_private_method_get(this, _cachedSend, cachedSend).call(this, 'system_name', []);
116
137
  }
117
138
  async getSystemProperties() {
118
- return this.send('system_properties', []);
139
+ return _class_private_method_get(this, _cachedSend, cachedSend).call(this, 'system_properties', []);
119
140
  }
120
141
  async getSystemChain() {
121
- return this.send('system_chain', []);
142
+ return _class_private_method_get(this, _cachedSend, cachedSend).call(this, 'system_chain', []);
122
143
  }
123
144
  async getBlockHash(blockNumber) {
124
- return this.send('chain_getBlockHash', Number.isInteger(blockNumber) ? [
145
+ return _class_private_method_get(this, _cachedSend, cachedSend).call(this, 'chain_getBlockHash', Number.isInteger(blockNumber) ? [
125
146
  blockNumber
126
147
  ] : [], !!blockNumber);
127
148
  }
128
149
  async getHeader(hash) {
129
- return this.send('chain_getHeader', hash ? [
150
+ return _class_private_method_get(this, _cachedSend, cachedSend).call(this, 'chain_getHeader', hash ? [
130
151
  hash
131
152
  ] : [], !!hash);
132
153
  }
@@ -226,6 +247,7 @@ class Api {
226
247
  return _class_private_field_get(this, _provider).subscribe('chain_finalizedHead', 'chain_subscribeFinalizedHeads', [], cb);
227
248
  }
228
249
  constructor(provider, signedExtensions){
250
+ _class_private_method_init(this, _cachedSend);
229
251
  _class_private_field_init(this, _provider, {
230
252
  writable: true,
231
253
  value: void 0
@@ -242,6 +264,14 @@ class Api {
242
264
  writable: true,
243
265
  value: void 0
244
266
  });
267
+ _class_private_field_init(this, _db, {
268
+ writable: true,
269
+ value: void 0
270
+ });
271
+ _class_private_field_init(this, _scope, {
272
+ writable: true,
273
+ value: void 0
274
+ });
245
275
  _define_property(this, "signedExtensions", void 0);
246
276
  _class_private_field_init(this, _apiHooks, {
247
277
  writable: true,
@@ -251,3 +281,16 @@ class Api {
251
281
  this.signedExtensions = signedExtensions || {};
252
282
  }
253
283
  }
284
+ async function cachedSend(method, params, isCacheable) {
285
+ if (_class_private_field_get(this, _db)?.queryRpcCall && _class_private_field_get(this, _scope) && CACHEABLE_METHODS.has(method)) {
286
+ const key = JSON.stringify(params);
287
+ const cached = await _class_private_field_get(this, _db).queryRpcCall(_class_private_field_get(this, _scope), method, key);
288
+ if (cached !== null) return JSON.parse(cached);
289
+ _class_private_field_get(this, _apiHooks)?.fetching?.();
290
+ const result = await _class_private_field_get(this, _provider).send(method, params, isCacheable);
291
+ await _class_private_field_get(this, _db).saveRpcCall?.(_class_private_field_get(this, _scope), method, key, JSON.stringify(result));
292
+ return result;
293
+ }
294
+ _class_private_field_get(this, _apiHooks)?.fetching?.();
295
+ return _class_private_field_get(this, _provider).send(method, params, isCacheable);
296
+ }
@@ -1,4 +1,4 @@
1
- import type { Header, TransactionValidityError } from '@polkadot/types/interfaces';
1
+ import type { ApplyExtrinsicResult, Header, TransactionValidityError } from '@polkadot/types/interfaces';
2
2
  import type { HexString } from '@polkadot/util/types';
3
3
  import type { TaskCallResponse } from '../wasm-executor/index.js';
4
4
  import { Block } from './block.js';
@@ -10,6 +10,21 @@ export type BuildBlockCallbacks = {
10
10
  onPhaseApplied?: (phase: 'initialize' | 'finalize' | number, resp: TaskCallResponse) => void;
11
11
  };
12
12
  export declare const buildBlock: (head: Block, inherentProviders: InherentProvider[], params: BuildBlockParams, callbacks?: BuildBlockCallbacks) => Promise<[Block, HexString[]]>;
13
+ export type DryRunResult = {
14
+ outcome: ApplyExtrinsicResult;
15
+ storageDiff: [HexString, HexString | null][];
16
+ };
17
+ /**
18
+ * Dry-run a batch of extrinsics against the same initialized block.
19
+ *
20
+ * Core_initialize_block and the inherents are executed once. Each extrinsic
21
+ * is then applied on a temporary storage layer that is pushed before the call
22
+ * and popped after, so no extrinsic side-effects leak into the next.
23
+ *
24
+ * Caveat: because account nonces revert with each pop, all extrinsics must be
25
+ * pre-signed with the same base nonce.
26
+ */
27
+ export declare const dryRunExtrinsicsAmortized: (head: Block, inherentProviders: InherentProvider[], extrinsics: HexString[], params: BuildBlockParams) => Promise<DryRunResult[]>;
13
28
  export declare const dryRunExtrinsic: (head: Block, inherentProviders: InherentProvider[], extrinsic: HexString | {
14
29
  call: HexString;
15
30
  address: string;
@@ -15,6 +15,9 @@ _export(exports, {
15
15
  get dryRunExtrinsic () {
16
16
  return dryRunExtrinsic;
17
17
  },
18
+ get dryRunExtrinsicsAmortized () {
19
+ return dryRunExtrinsicsAmortized;
20
+ },
18
21
  get dryRunInherents () {
19
22
  return dryRunInherents;
20
23
  },
@@ -378,6 +381,25 @@ const buildBlock = async (head, inherentProviders, params, callbacks)=>{
378
381
  pendingExtrinsics
379
382
  ];
380
383
  };
384
+ const dryRunExtrinsicsAmortized = async (head, inherentProviders, extrinsics, params)=>{
385
+ const registry = await head.registry;
386
+ const header = await newHeader(head);
387
+ const { block: newBlock } = await initNewBlock(head, header, inherentProviders, params);
388
+ const results = [];
389
+ for (const extrinsic of extrinsics){
390
+ newBlock.pushStorageLayer();
391
+ const resp = await newBlock.call('BlockBuilder_apply_extrinsic', [
392
+ extrinsic
393
+ ]);
394
+ const outcome = registry.createType('ApplyExtrinsicResult', resp.result);
395
+ results.push({
396
+ outcome,
397
+ storageDiff: resp.storageDiff
398
+ });
399
+ newBlock.popStorageLayer();
400
+ }
401
+ return results;
402
+ };
381
403
  const dryRunExtrinsic = async (head, inherentProviders, extrinsic, params)=>{
382
404
  const registry = await head.registry;
383
405
  const header = await newHeader(head);
@@ -69,6 +69,16 @@ export declare class Block {
69
69
  * Pop a layer from the storage stack.
70
70
  */
71
71
  popStorageLayer(): void;
72
+ /**
73
+ * Truncate the storage layer stack back to a target depth.
74
+ *
75
+ * Used by snapshot-restore mechanisms to undo accumulated dev.setStorage
76
+ * calls when reverting to a previously captured state, without losing the
77
+ * underlying block.
78
+ */
79
+ resetStorageLayers(targetCount: number): void;
80
+ /** The current depth of the storage layer stack. */
81
+ get storageLayerCount(): number;
72
82
  /**
73
83
  * Get storage diff.
74
84
  */
@@ -170,6 +170,18 @@ class Block {
170
170
  _class_private_field_get(this, _storages).pop();
171
171
  }
172
172
  /**
173
+ * Truncate the storage layer stack back to a target depth.
174
+ *
175
+ * Used by snapshot-restore mechanisms to undo accumulated dev.setStorage
176
+ * calls when reverting to a previously captured state, without losing the
177
+ * underlying block.
178
+ */ resetStorageLayers(targetCount) {
179
+ while(_class_private_field_get(this, _storages).length > targetCount)_class_private_field_get(this, _storages).pop();
180
+ }
181
+ /** The current depth of the storage layer stack. */ get storageLayerCount() {
182
+ return _class_private_field_get(this, _storages).length;
183
+ }
184
+ /**
173
185
  * Get storage diff.
174
186
  */ async storageDiff() {
175
187
  const storage = {};
@@ -95,20 +95,40 @@ class HeadState {
95
95
  }
96
96
  }
97
97
  const diff = await _class_private_field_get(this, _head).storageDiff();
98
+ const newValues = {
99
+ ...diff
100
+ };
101
+ const watchedKeys = new Set();
102
+ for (const [keys] of Object.values(_class_private_field_get(this, _storageListeners))){
103
+ for (const key of keys)watchedKeys.add(key);
104
+ }
105
+ for (const key of watchedKeys){
106
+ if (newValues[key] === undefined && _class_private_field_get(this, _oldValues)[key] !== undefined) {
107
+ newValues[key] = await head.get(key) ?? null;
108
+ }
109
+ }
98
110
  for (const [keys, cb] of Object.values(_class_private_field_get(this, _storageListeners))){
99
- const changed = keys.filter((key)=>diff[key]).map((key)=>[
100
- key,
101
- diff[key]
102
- ]);
103
- if (changed.length > 0) {
111
+ const changes = [];
112
+ for (const key of keys){
113
+ if (newValues[key] === undefined) continue;
114
+ if (newValues[key] !== _class_private_field_get(this, _oldValues)[key]) {
115
+ changes.push([
116
+ key,
117
+ newValues[key]
118
+ ]);
119
+ }
120
+ }
121
+ if (changes.length > 0) {
104
122
  try {
105
- await cb(head, changed);
123
+ await cb(head, changes);
106
124
  } catch (error) {
107
125
  logger.error(error, 'setHead storage diff callback error');
108
126
  }
109
127
  }
110
128
  }
111
- Object.assign(_class_private_field_get(this, _oldValues), diff);
129
+ for (const [key, value] of Object.entries(newValues)){
130
+ _class_private_field_get(this, _oldValues)[key] = value;
131
+ }
112
132
  }
113
133
  constructor(head){
114
134
  _class_private_field_init(this, _headListeners, {
@@ -151,6 +151,15 @@ export declare class Blockchain {
151
151
  outcome: ApplyExtrinsicResult;
152
152
  storageDiff: [HexString, HexString | null][];
153
153
  }>;
154
+ /**
155
+ * Dry-run a batch of extrinsics in block `at`, amortizing block initialization
156
+ * across the whole batch. Each extrinsic is applied independently — its storage
157
+ * changes do not accumulate into subsequent extrinsics.
158
+ */
159
+ dryRunExtrinsicsAmortized(extrinsics: HexString[], at?: HexString): Promise<{
160
+ outcome: ApplyExtrinsicResult;
161
+ storageDiff: [HexString, HexString | null][];
162
+ }[]>;
154
163
  /**
155
164
  * Dry run hrmp messages in block `at`.
156
165
  * Return the storage diff.
@@ -326,6 +326,24 @@ class Blockchain {
326
326
  };
327
327
  }
328
328
  /**
329
+ * Dry-run a batch of extrinsics in block `at`, amortizing block initialization
330
+ * across the whole batch. Each extrinsic is applied independently — its storage
331
+ * changes do not accumulate into subsequent extrinsics.
332
+ */ async dryRunExtrinsicsAmortized(extrinsics, at) {
333
+ await this.api.isReady;
334
+ const head = at ? await this.getBlock(at) : this.head;
335
+ if (!head) {
336
+ throw new Error(`Cannot find block ${at}`);
337
+ }
338
+ const params = {
339
+ transactions: [],
340
+ downwardMessages: [],
341
+ upwardMessages: [],
342
+ horizontalMessages: {}
343
+ };
344
+ return (0, _blockbuilder.dryRunExtrinsicsAmortized)(head, _class_private_field_get(this, _inherentProviders), extrinsics, params);
345
+ }
346
+ /**
329
347
  * Dry run hrmp messages in block `at`.
330
348
  * Return the storage diff.
331
349
  */ async dryRunHrmp(hrmp, at) {
@@ -87,7 +87,7 @@ var StorageValueKind = /*#__PURE__*/ function(StorageValueKind) {
87
87
  StorageValueKind["DeletedPrefix"] = "DeletedPrefix";
88
88
  return StorageValueKind;
89
89
  }({});
90
- var _api = /*#__PURE__*/ new WeakMap(), _at = /*#__PURE__*/ new WeakMap(), _db = /*#__PURE__*/ new WeakMap(), _keyCache = /*#__PURE__*/ new WeakMap(), _defaultChildKeyCache = /*#__PURE__*/ new WeakMap();
90
+ var _api = /*#__PURE__*/ new WeakMap(), _at = /*#__PURE__*/ new WeakMap(), _db = /*#__PURE__*/ new WeakMap(), _keyCache = /*#__PURE__*/ new WeakMap(), _defaultChildKeyCache = /*#__PURE__*/ new WeakMap(), _inflight = /*#__PURE__*/ new WeakMap();
91
91
  class RemoteStorageLayer {
92
92
  deleted(_key) {
93
93
  return false;
@@ -99,13 +99,20 @@ class RemoteStorageLayer {
99
99
  return res.value ?? undefined;
100
100
  }
101
101
  }
102
+ const inflight = _class_private_field_get(this, _inflight).get(key);
103
+ if (inflight) return inflight;
102
104
  logger.trace({
103
105
  at: _class_private_field_get(this, _at),
104
106
  key
105
107
  }, 'RemoteStorageLayer get');
106
- const data = await _class_private_field_get(this, _api).getStorage(key, _class_private_field_get(this, _at));
107
- _class_private_field_get(this, _db)?.saveStorage(_class_private_field_get(this, _at), key, data);
108
- return data ?? undefined;
108
+ const fetch = _class_private_field_get(this, _api).getStorage(key, _class_private_field_get(this, _at)).then((data)=>{
109
+ _class_private_field_get(this, _db)?.saveStorage(_class_private_field_get(this, _at), key, data);
110
+ return data ?? undefined;
111
+ }).finally(()=>{
112
+ _class_private_field_get(this, _inflight).delete(key);
113
+ });
114
+ _class_private_field_get(this, _inflight).set(key, fetch);
115
+ return fetch;
109
116
  }
110
117
  async getMany(keys, _cache) {
111
118
  const result = [];
@@ -165,12 +172,33 @@ class RemoteStorageLayer {
165
172
  }, 'RemoteStorageLayer getKeysPaged');
166
173
  const isChild = (0, _index.isPrefixedChildKey)(prefix);
167
174
  const minPrefixLen = isChild ? _index.CHILD_PREFIX_LENGTH : _index.PREFIX_LENGTH;
168
- // can't handle keyCache without prefix
169
- if (prefix === startKey || prefix.length < minPrefixLen || startKey.length < minPrefixLen) {
175
+ // KeyCache groups by the first `minPrefixLen` chars; it cannot correctly answer queries
176
+ // whose prefix is longer than that grouping width, so proxy directly to upstream.
177
+ if (prefix.length < minPrefixLen || startKey.length < minPrefixLen || prefix.length > minPrefixLen || startKey.length > minPrefixLen) {
170
178
  return _class_private_field_get(this, _api).getKeysPaged(prefix, pageSize, startKey, _class_private_field_get(this, _at));
171
179
  }
180
+ const startKeyEqualsPrefix = startKey === prefix;
181
+ let cachedKeys;
182
+ if (_class_private_field_get(this, _db)?.queryPagedKeys && startKeyEqualsPrefix) {
183
+ const cached = await _class_private_field_get(this, _db).queryPagedKeys(_class_private_field_get(this, _at), prefix);
184
+ if (cached) {
185
+ cachedKeys = cached;
186
+ isChild ? _class_private_field_get(this, _defaultChildKeyCache).feed([
187
+ startKey,
188
+ ...cached
189
+ ]) : _class_private_field_get(this, _keyCache).feed([
190
+ startKey,
191
+ ...cached
192
+ ]);
193
+ }
194
+ }
172
195
  let batchComplete = false;
196
+ let fetchedNewKeys = false;
173
197
  const keysPaged = [];
198
+ // Seed with cached keys so a cache-hit-then-extend doesn't truncate the persisted set.
199
+ const allFetchedKeys = cachedKeys ? [
200
+ ...cachedKeys
201
+ ] : [];
174
202
  while(keysPaged.length < pageSize){
175
203
  const nextKey = isChild ? await _class_private_field_get(this, _defaultChildKeyCache).next(startKey) : await _class_private_field_get(this, _keyCache).next(startKey);
176
204
  if (nextKey) {
@@ -203,15 +231,7 @@ class RemoteStorageLayer {
203
231
  break;
204
232
  }
205
233
  if (_class_private_field_get(this, _db)) {
206
- // filter out keys that are not in the db]
207
- const newBatch = [];
208
- for (const key of batch){
209
- const res = await _class_private_field_get(this, _db).queryStorage(_class_private_field_get(this, _at), key);
210
- if (res) {
211
- continue;
212
- }
213
- newBatch.push(key);
214
- }
234
+ const newBatch = await Promise.all(batch.map((key)=>_class_private_field_get(this, _db).queryStorage(_class_private_field_get(this, _at), key).then((r)=>r ? null : key))).then((rs)=>rs.filter((k)=>k !== null));
215
235
  if (newBatch.length > 0) {
216
236
  // batch fetch storage values and save to db, they may be used later
217
237
  _class_private_field_get(this, _api).getStorageBatch(prefix, newBatch, _class_private_field_get(this, _at)).then((storage)=>{
@@ -220,8 +240,15 @@ class RemoteStorageLayer {
220
240
  }
221
241
  });
222
242
  }
243
+ if (startKeyEqualsPrefix) {
244
+ allFetchedKeys.push(...batch);
245
+ fetchedNewKeys = true;
246
+ }
223
247
  }
224
248
  }
249
+ if (_class_private_field_get(this, _db)?.savePagedKeys && startKeyEqualsPrefix && fetchedNewKeys) {
250
+ await _class_private_field_get(this, _db).savePagedKeys(_class_private_field_get(this, _at), prefix, allFetchedKeys);
251
+ }
225
252
  return keysPaged;
226
253
  }
227
254
  constructor(api, at, db){
@@ -245,6 +272,10 @@ class RemoteStorageLayer {
245
272
  writable: true,
246
273
  value: new _keycache.default(_index.CHILD_PREFIX_LENGTH)
247
274
  });
275
+ _class_private_field_init(this, _inflight, {
276
+ writable: true,
277
+ value: new Map()
278
+ });
248
279
  _class_private_field_set(this, _api, api);
249
280
  _class_private_field_set(this, _at, at);
250
281
  _class_private_field_set(this, _db, db);
@@ -361,6 +392,7 @@ class StorageLayer {
361
392
  return knownBest;
362
393
  }
363
394
  async getKeysPaged(prefix, pageSize, startKey) {
395
+ if (pageSize > BATCH_SIZE) throw new Error(`pageSize must be less or equal to ${BATCH_SIZE}`);
364
396
  if (!startKey || startKey === '0x') {
365
397
  startKey = prefix;
366
398
  }
@@ -24,4 +24,8 @@ export declare class Database {
24
24
  saveStorage: (blockHash: HexString, key: HexString, value: HexString | null) => Promise<void>;
25
25
  saveStorageBatch?: (entries: KeyValueEntry[]) => Promise<void>;
26
26
  queryStorage: (blockHash: HexString, key: HexString) => Promise<KeyValueEntry | null>;
27
+ queryPagedKeys?: (blockHash: HexString, prefix: HexString) => Promise<HexString[] | null>;
28
+ savePagedKeys?: (blockHash: HexString, prefix: HexString, keys: HexString[]) => Promise<void>;
29
+ queryRpcCall?: (scope: string, method: string, params: string) => Promise<string | null>;
30
+ saveRpcCall?: (scope: string, method: string, params: string, result: string) => Promise<void>;
27
31
  }
package/dist/cjs/setup.js CHANGED
@@ -38,6 +38,10 @@ const processOptions = async (options)=>{
38
38
  const api = new _api.Api(provider);
39
39
  // setup api hooks
40
40
  api.onFetching(options.hooks?.apiFetching);
41
+ if (options.db) {
42
+ const scope = typeof options.endpoint === 'string' ? options.endpoint : JSON.stringify(options.endpoint ?? '');
43
+ api.setDb(options.db, scope);
44
+ }
41
45
  await api.isReady;
42
46
  let blockHash;
43
47
  if (options.block == null) {
package/dist/esm/api.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { ProviderInterface, ProviderInterfaceCallback } from '@polkadot/rpc-provider/types';
2
2
  import type { ExtDef } from '@polkadot/types/extrinsic/signedExtensions/types';
3
3
  import type { HexString } from '@polkadot/util/types';
4
+ import type { Database } from './database.js';
4
5
  import type { ChainProperties, Header, SignedBlock } from './index.js';
5
6
  /**
6
7
  * API class. Calls provider to get on-chain data.
@@ -18,6 +19,7 @@ export declare class Api {
18
19
  #private;
19
20
  readonly signedExtensions: ExtDef;
20
21
  constructor(provider: ProviderInterface, signedExtensions?: ExtDef);
22
+ setDb(db: Database, scope: string): void;
21
23
  disconnect(): Promise<void>;
22
24
  get isReady(): Promise<void> | undefined;
23
25
  get chain(): Promise<string>;
package/dist/esm/api.js CHANGED
@@ -1,6 +1,13 @@
1
1
  import RpcError from '@polkadot/rpc-provider/coder/error';
2
2
  import _ from 'lodash';
3
3
  import { prefixedChildKey, splitChildKey, stripChildPrefix } from './utils/index.js';
4
+ const CACHEABLE_METHODS = new Set([
5
+ 'system_chain',
6
+ 'system_properties',
7
+ 'system_name',
8
+ 'chain_getBlockHash',
9
+ 'chain_getHeader'
10
+ ]);
4
11
  /**
5
12
  * API class. Calls provider to get on-chain data.
6
13
  * Either `endpoint` or `genesis` porvider must be provided.
@@ -17,12 +24,31 @@ import { prefixedChildKey, splitChildKey, stripChildPrefix } from './utils/index
17
24
  #ready;
18
25
  #chain;
19
26
  #chainProperties;
27
+ #db;
28
+ #scope;
20
29
  signedExtensions;
21
30
  #apiHooks = {};
22
31
  constructor(provider, signedExtensions){
23
32
  this.#provider = provider;
24
33
  this.signedExtensions = signedExtensions || {};
25
34
  }
35
+ setDb(db, scope) {
36
+ this.#db = db;
37
+ this.#scope = scope;
38
+ }
39
+ async #cachedSend(method, params, isCacheable) {
40
+ if (this.#db?.queryRpcCall && this.#scope && CACHEABLE_METHODS.has(method)) {
41
+ const key = JSON.stringify(params);
42
+ const cached = await this.#db.queryRpcCall(this.#scope, method, key);
43
+ if (cached !== null) return JSON.parse(cached);
44
+ this.#apiHooks?.fetching?.();
45
+ const result = await this.#provider.send(method, params, isCacheable);
46
+ await this.#db.saveRpcCall?.(this.#scope, method, key, JSON.stringify(result));
47
+ return result;
48
+ }
49
+ this.#apiHooks?.fetching?.();
50
+ return this.#provider.send(method, params, isCacheable);
51
+ }
26
52
  async disconnect() {
27
53
  return this.#provider.disconnect();
28
54
  }
@@ -64,21 +90,21 @@ import { prefixedChildKey, splitChildKey, stripChildPrefix } from './utils/index
64
90
  return this.#provider.send(method, params, isCacheable);
65
91
  }
66
92
  async getSystemName() {
67
- return this.send('system_name', []);
93
+ return this.#cachedSend('system_name', []);
68
94
  }
69
95
  async getSystemProperties() {
70
- return this.send('system_properties', []);
96
+ return this.#cachedSend('system_properties', []);
71
97
  }
72
98
  async getSystemChain() {
73
- return this.send('system_chain', []);
99
+ return this.#cachedSend('system_chain', []);
74
100
  }
75
101
  async getBlockHash(blockNumber) {
76
- return this.send('chain_getBlockHash', Number.isInteger(blockNumber) ? [
102
+ return this.#cachedSend('chain_getBlockHash', Number.isInteger(blockNumber) ? [
77
103
  blockNumber
78
104
  ] : [], !!blockNumber);
79
105
  }
80
106
  async getHeader(hash) {
81
- return this.send('chain_getHeader', hash ? [
107
+ return this.#cachedSend('chain_getHeader', hash ? [
82
108
  hash
83
109
  ] : [], !!hash);
84
110
  }
@@ -1,4 +1,4 @@
1
- import type { Header, TransactionValidityError } from '@polkadot/types/interfaces';
1
+ import type { ApplyExtrinsicResult, Header, TransactionValidityError } from '@polkadot/types/interfaces';
2
2
  import type { HexString } from '@polkadot/util/types';
3
3
  import type { TaskCallResponse } from '../wasm-executor/index.js';
4
4
  import { Block } from './block.js';
@@ -10,6 +10,21 @@ export type BuildBlockCallbacks = {
10
10
  onPhaseApplied?: (phase: 'initialize' | 'finalize' | number, resp: TaskCallResponse) => void;
11
11
  };
12
12
  export declare const buildBlock: (head: Block, inherentProviders: InherentProvider[], params: BuildBlockParams, callbacks?: BuildBlockCallbacks) => Promise<[Block, HexString[]]>;
13
+ export type DryRunResult = {
14
+ outcome: ApplyExtrinsicResult;
15
+ storageDiff: [HexString, HexString | null][];
16
+ };
17
+ /**
18
+ * Dry-run a batch of extrinsics against the same initialized block.
19
+ *
20
+ * Core_initialize_block and the inherents are executed once. Each extrinsic
21
+ * is then applied on a temporary storage layer that is pushed before the call
22
+ * and popped after, so no extrinsic side-effects leak into the next.
23
+ *
24
+ * Caveat: because account nonces revert with each pop, all extrinsics must be
25
+ * pre-signed with the same base nonce.
26
+ */
27
+ export declare const dryRunExtrinsicsAmortized: (head: Block, inherentProviders: InherentProvider[], extrinsics: HexString[], params: BuildBlockParams) => Promise<DryRunResult[]>;
13
28
  export declare const dryRunExtrinsic: (head: Block, inherentProviders: InherentProvider[], extrinsic: HexString | {
14
29
  call: HexString;
15
30
  address: string;
@@ -354,6 +354,34 @@ export const buildBlock = async (head, inherentProviders, params, callbacks)=>{
354
354
  pendingExtrinsics
355
355
  ];
356
356
  };
357
+ /**
358
+ * Dry-run a batch of extrinsics against the same initialized block.
359
+ *
360
+ * Core_initialize_block and the inherents are executed once. Each extrinsic
361
+ * is then applied on a temporary storage layer that is pushed before the call
362
+ * and popped after, so no extrinsic side-effects leak into the next.
363
+ *
364
+ * Caveat: because account nonces revert with each pop, all extrinsics must be
365
+ * pre-signed with the same base nonce.
366
+ */ export const dryRunExtrinsicsAmortized = async (head, inherentProviders, extrinsics, params)=>{
367
+ const registry = await head.registry;
368
+ const header = await newHeader(head);
369
+ const { block: newBlock } = await initNewBlock(head, header, inherentProviders, params);
370
+ const results = [];
371
+ for (const extrinsic of extrinsics){
372
+ newBlock.pushStorageLayer();
373
+ const resp = await newBlock.call('BlockBuilder_apply_extrinsic', [
374
+ extrinsic
375
+ ]);
376
+ const outcome = registry.createType('ApplyExtrinsicResult', resp.result);
377
+ results.push({
378
+ outcome,
379
+ storageDiff: resp.storageDiff
380
+ });
381
+ newBlock.popStorageLayer();
382
+ }
383
+ return results;
384
+ };
357
385
  export const dryRunExtrinsic = async (head, inherentProviders, extrinsic, params)=>{
358
386
  const registry = await head.registry;
359
387
  const header = await newHeader(head);
@@ -69,6 +69,16 @@ export declare class Block {
69
69
  * Pop a layer from the storage stack.
70
70
  */
71
71
  popStorageLayer(): void;
72
+ /**
73
+ * Truncate the storage layer stack back to a target depth.
74
+ *
75
+ * Used by snapshot-restore mechanisms to undo accumulated dev.setStorage
76
+ * calls when reverting to a previously captured state, without losing the
77
+ * underlying block.
78
+ */
79
+ resetStorageLayers(targetCount: number): void;
80
+ /** The current depth of the storage layer stack. */
81
+ get storageLayerCount(): number;
72
82
  /**
73
83
  * Get storage diff.
74
84
  */
@@ -160,6 +160,18 @@ import { RemoteStorageLayer, StorageLayer, StorageValueKind } from './storage-la
160
160
  this.#storages.pop();
161
161
  }
162
162
  /**
163
+ * Truncate the storage layer stack back to a target depth.
164
+ *
165
+ * Used by snapshot-restore mechanisms to undo accumulated dev.setStorage
166
+ * calls when reverting to a previously captured state, without losing the
167
+ * underlying block.
168
+ */ resetStorageLayers(targetCount) {
169
+ while(this.#storages.length > targetCount)this.#storages.pop();
170
+ }
171
+ /** The current depth of the storage layer stack. */ get storageLayerCount() {
172
+ return this.#storages.length;
173
+ }
174
+ /**
163
175
  * Get storage diff.
164
176
  */ async storageDiff() {
165
177
  const storage = {};
@@ -43,19 +43,39 @@ export class HeadState {
43
43
  }
44
44
  }
45
45
  const diff = await this.#head.storageDiff();
46
+ const newValues = {
47
+ ...diff
48
+ };
49
+ const watchedKeys = new Set();
50
+ for (const [keys] of Object.values(this.#storageListeners)){
51
+ for (const key of keys)watchedKeys.add(key);
52
+ }
53
+ for (const key of watchedKeys){
54
+ if (newValues[key] === undefined && this.#oldValues[key] !== undefined) {
55
+ newValues[key] = await head.get(key) ?? null;
56
+ }
57
+ }
46
58
  for (const [keys, cb] of Object.values(this.#storageListeners)){
47
- const changed = keys.filter((key)=>diff[key]).map((key)=>[
48
- key,
49
- diff[key]
50
- ]);
51
- if (changed.length > 0) {
59
+ const changes = [];
60
+ for (const key of keys){
61
+ if (newValues[key] === undefined) continue;
62
+ if (newValues[key] !== this.#oldValues[key]) {
63
+ changes.push([
64
+ key,
65
+ newValues[key]
66
+ ]);
67
+ }
68
+ }
69
+ if (changes.length > 0) {
52
70
  try {
53
- await cb(head, changed);
71
+ await cb(head, changes);
54
72
  } catch (error) {
55
73
  logger.error(error, 'setHead storage diff callback error');
56
74
  }
57
75
  }
58
76
  }
59
- Object.assign(this.#oldValues, diff);
77
+ for (const [key, value] of Object.entries(newValues)){
78
+ this.#oldValues[key] = value;
79
+ }
60
80
  }
61
81
  }
@@ -151,6 +151,15 @@ export declare class Blockchain {
151
151
  outcome: ApplyExtrinsicResult;
152
152
  storageDiff: [HexString, HexString | null][];
153
153
  }>;
154
+ /**
155
+ * Dry-run a batch of extrinsics in block `at`, amortizing block initialization
156
+ * across the whole batch. Each extrinsic is applied independently — its storage
157
+ * changes do not accumulate into subsequent extrinsics.
158
+ */
159
+ dryRunExtrinsicsAmortized(extrinsics: HexString[], at?: HexString): Promise<{
160
+ outcome: ApplyExtrinsicResult;
161
+ storageDiff: [HexString, HexString | null][];
162
+ }[]>;
154
163
  /**
155
164
  * Dry run hrmp messages in block `at`.
156
165
  * Return the storage diff.
@@ -7,7 +7,7 @@ import { defaultLogger } from '../logger.js';
7
7
  import { OffchainWorker } from '../offchain.js';
8
8
  import { compactHex } from '../utils/index.js';
9
9
  import { Block } from './block.js';
10
- import { dryRunExtrinsic, dryRunInherents } from './block-builder.js';
10
+ import { dryRunExtrinsic, dryRunExtrinsicsAmortized, dryRunInherents } from './block-builder.js';
11
11
  import { HeadState } from './head-state.js';
12
12
  import { TxPool } from './txpool.js';
13
13
  const logger = defaultLogger.child({
@@ -330,6 +330,24 @@ const logger = defaultLogger.child({
330
330
  };
331
331
  }
332
332
  /**
333
+ * Dry-run a batch of extrinsics in block `at`, amortizing block initialization
334
+ * across the whole batch. Each extrinsic is applied independently — its storage
335
+ * changes do not accumulate into subsequent extrinsics.
336
+ */ async dryRunExtrinsicsAmortized(extrinsics, at) {
337
+ await this.api.isReady;
338
+ const head = at ? await this.getBlock(at) : this.head;
339
+ if (!head) {
340
+ throw new Error(`Cannot find block ${at}`);
341
+ }
342
+ const params = {
343
+ transactions: [],
344
+ downwardMessages: [],
345
+ upwardMessages: [],
346
+ horizontalMessages: {}
347
+ };
348
+ return dryRunExtrinsicsAmortized(head, this.#inherentProviders, extrinsics, params);
349
+ }
350
+ /**
333
351
  * Dry run hrmp messages in block `at`.
334
352
  * Return the storage diff.
335
353
  */ async dryRunHrmp(hrmp, at) {
@@ -17,6 +17,7 @@ export class RemoteStorageLayer {
17
17
  #db;
18
18
  #keyCache = new KeyCache(PREFIX_LENGTH);
19
19
  #defaultChildKeyCache = new KeyCache(CHILD_PREFIX_LENGTH);
20
+ #inflight = new Map();
20
21
  constructor(api, at, db){
21
22
  this.#api = api;
22
23
  this.#at = at;
@@ -32,13 +33,20 @@ export class RemoteStorageLayer {
32
33
  return res.value ?? undefined;
33
34
  }
34
35
  }
36
+ const inflight = this.#inflight.get(key);
37
+ if (inflight) return inflight;
35
38
  logger.trace({
36
39
  at: this.#at,
37
40
  key
38
41
  }, 'RemoteStorageLayer get');
39
- const data = await this.#api.getStorage(key, this.#at);
40
- this.#db?.saveStorage(this.#at, key, data);
41
- return data ?? undefined;
42
+ const fetch = this.#api.getStorage(key, this.#at).then((data)=>{
43
+ this.#db?.saveStorage(this.#at, key, data);
44
+ return data ?? undefined;
45
+ }).finally(()=>{
46
+ this.#inflight.delete(key);
47
+ });
48
+ this.#inflight.set(key, fetch);
49
+ return fetch;
42
50
  }
43
51
  async getMany(keys, _cache) {
44
52
  const result = [];
@@ -98,12 +106,33 @@ export class RemoteStorageLayer {
98
106
  }, 'RemoteStorageLayer getKeysPaged');
99
107
  const isChild = isPrefixedChildKey(prefix);
100
108
  const minPrefixLen = isChild ? CHILD_PREFIX_LENGTH : PREFIX_LENGTH;
101
- // can't handle keyCache without prefix
102
- if (prefix === startKey || prefix.length < minPrefixLen || startKey.length < minPrefixLen) {
109
+ // KeyCache groups by the first `minPrefixLen` chars; it cannot correctly answer queries
110
+ // whose prefix is longer than that grouping width, so proxy directly to upstream.
111
+ if (prefix.length < minPrefixLen || startKey.length < minPrefixLen || prefix.length > minPrefixLen || startKey.length > minPrefixLen) {
103
112
  return this.#api.getKeysPaged(prefix, pageSize, startKey, this.#at);
104
113
  }
114
+ const startKeyEqualsPrefix = startKey === prefix;
115
+ let cachedKeys;
116
+ if (this.#db?.queryPagedKeys && startKeyEqualsPrefix) {
117
+ const cached = await this.#db.queryPagedKeys(this.#at, prefix);
118
+ if (cached) {
119
+ cachedKeys = cached;
120
+ isChild ? this.#defaultChildKeyCache.feed([
121
+ startKey,
122
+ ...cached
123
+ ]) : this.#keyCache.feed([
124
+ startKey,
125
+ ...cached
126
+ ]);
127
+ }
128
+ }
105
129
  let batchComplete = false;
130
+ let fetchedNewKeys = false;
106
131
  const keysPaged = [];
132
+ // Seed with cached keys so a cache-hit-then-extend doesn't truncate the persisted set.
133
+ const allFetchedKeys = cachedKeys ? [
134
+ ...cachedKeys
135
+ ] : [];
107
136
  while(keysPaged.length < pageSize){
108
137
  const nextKey = isChild ? await this.#defaultChildKeyCache.next(startKey) : await this.#keyCache.next(startKey);
109
138
  if (nextKey) {
@@ -136,15 +165,7 @@ export class RemoteStorageLayer {
136
165
  break;
137
166
  }
138
167
  if (this.#db) {
139
- // filter out keys that are not in the db]
140
- const newBatch = [];
141
- for (const key of batch){
142
- const res = await this.#db.queryStorage(this.#at, key);
143
- if (res) {
144
- continue;
145
- }
146
- newBatch.push(key);
147
- }
168
+ const newBatch = await Promise.all(batch.map((key)=>this.#db.queryStorage(this.#at, key).then((r)=>r ? null : key))).then((rs)=>rs.filter((k)=>k !== null));
148
169
  if (newBatch.length > 0) {
149
170
  // batch fetch storage values and save to db, they may be used later
150
171
  this.#api.getStorageBatch(prefix, newBatch, this.#at).then((storage)=>{
@@ -153,8 +174,15 @@ export class RemoteStorageLayer {
153
174
  }
154
175
  });
155
176
  }
177
+ if (startKeyEqualsPrefix) {
178
+ allFetchedKeys.push(...batch);
179
+ fetchedNewKeys = true;
180
+ }
156
181
  }
157
182
  }
183
+ if (this.#db?.savePagedKeys && startKeyEqualsPrefix && fetchedNewKeys) {
184
+ await this.#db.savePagedKeys(this.#at, prefix, allFetchedKeys);
185
+ }
158
186
  return keysPaged;
159
187
  }
160
188
  }
@@ -290,6 +318,7 @@ export class StorageLayer {
290
318
  return knownBest;
291
319
  }
292
320
  async getKeysPaged(prefix, pageSize, startKey) {
321
+ if (pageSize > BATCH_SIZE) throw new Error(`pageSize must be less or equal to ${BATCH_SIZE}`);
293
322
  if (!startKey || startKey === '0x') {
294
323
  startKey = prefix;
295
324
  }
@@ -24,4 +24,8 @@ export declare class Database {
24
24
  saveStorage: (blockHash: HexString, key: HexString, value: HexString | null) => Promise<void>;
25
25
  saveStorageBatch?: (entries: KeyValueEntry[]) => Promise<void>;
26
26
  queryStorage: (blockHash: HexString, key: HexString) => Promise<KeyValueEntry | null>;
27
+ queryPagedKeys?: (blockHash: HexString, prefix: HexString) => Promise<HexString[] | null>;
28
+ savePagedKeys?: (blockHash: HexString, prefix: HexString, keys: HexString[]) => Promise<void>;
29
+ queryRpcCall?: (scope: string, method: string, params: string) => Promise<string | null>;
30
+ saveRpcCall?: (scope: string, method: string, params: string, result: string) => Promise<void>;
27
31
  }
package/dist/esm/setup.js CHANGED
@@ -20,6 +20,10 @@ export const processOptions = async (options)=>{
20
20
  const api = new Api(provider);
21
21
  // setup api hooks
22
22
  api.onFetching(options.hooks?.apiFetching);
23
+ if (options.db) {
24
+ const scope = typeof options.endpoint === 'string' ? options.endpoint : JSON.stringify(options.endpoint ?? '');
25
+ api.setDb(options.db, scope);
26
+ }
23
27
  await api.isReady;
24
28
  let blockHash;
25
29
  if (options.block == null) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@acala-network/chopsticks-core",
3
- "version": "1.3.1",
3
+ "version": "1.4.0",
4
4
  "author": "Acala Developers <hello@acala.network>",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -14,7 +14,7 @@
14
14
  "depcheck": "npx depcheck"
15
15
  },
16
16
  "dependencies": {
17
- "@acala-network/chopsticks-executor": "1.3.1",
17
+ "@acala-network/chopsticks-executor": "1.4.0",
18
18
  "@polkadot/rpc-provider": "^16.4.1",
19
19
  "@polkadot/types": "^16.4.1",
20
20
  "@polkadot/types-codec": "^16.4.1",