@acala-network/chopsticks-core 1.2.0 → 1.2.2

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.
@@ -22,6 +22,7 @@ _export(exports, {
22
22
  return _default;
23
23
  }
24
24
  });
25
+ const _archive_v1 = /*#__PURE__*/ _interop_require_wildcard(require("./archive_v1.js"));
25
26
  const _chainHead_v1 = /*#__PURE__*/ _interop_require_wildcard(require("./chainHead_v1.js"));
26
27
  const _chainSpec_v1 = /*#__PURE__*/ _interop_require_wildcard(require("./chainSpec_v1.js"));
27
28
  const _transaction_v1 = /*#__PURE__*/ _interop_require_wildcard(require("./transaction_v1.js"));
@@ -67,6 +68,7 @@ function _interop_require_wildcard(obj, nodeInterop) {
67
68
  return newObj;
68
69
  }
69
70
  const handlers = {
71
+ ..._archive_v1,
70
72
  ..._chainHead_v1,
71
73
  ..._transaction_v1,
72
74
  ..._chainSpec_v1
@@ -0,0 +1,16 @@
1
+ import type { HexString } from '@polkadot/util/types';
2
+ import type { Block } from '../../blockchain/block.js';
3
+ export declare function getDescendantValues(block: Block, params: DescendantValuesParams): Promise<{
4
+ items: Array<{
5
+ key: string;
6
+ value?: HexString;
7
+ }>;
8
+ next: DescendantValuesParams | null;
9
+ }>;
10
+ export declare const PAGE_SIZE = 1000;
11
+ export type DescendantValuesParams = {
12
+ prefix: string;
13
+ startKey: string;
14
+ isDescendantHashes?: boolean;
15
+ };
16
+ export declare function afterResponse(fn: () => void): Promise<void>;
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", {
3
+ value: true
4
+ });
5
+ function _export(target, all) {
6
+ for(var name in all)Object.defineProperty(target, name, {
7
+ enumerable: true,
8
+ get: Object.getOwnPropertyDescriptor(all, name).get
9
+ });
10
+ }
11
+ _export(exports, {
12
+ get PAGE_SIZE () {
13
+ return PAGE_SIZE;
14
+ },
15
+ get afterResponse () {
16
+ return afterResponse;
17
+ },
18
+ get getDescendantValues () {
19
+ return getDescendantValues;
20
+ }
21
+ });
22
+ async function getDescendantValues(block, params) {
23
+ const keys = await block.getKeysPaged({
24
+ ...params,
25
+ pageSize: PAGE_SIZE
26
+ });
27
+ const items = (await block.getMany(keys)).map((value, idx)=>({
28
+ key: keys[idx],
29
+ value
30
+ }));
31
+ if (keys.length < PAGE_SIZE) {
32
+ return {
33
+ items,
34
+ next: null
35
+ };
36
+ }
37
+ return {
38
+ items,
39
+ next: {
40
+ ...params,
41
+ startKey: keys[PAGE_SIZE - 1]
42
+ }
43
+ };
44
+ }
45
+ const PAGE_SIZE = 1000;
46
+ async function afterResponse(fn) {
47
+ await new Promise((resolve)=>setTimeout(resolve, 0));
48
+ fn();
49
+ }
@@ -48,6 +48,10 @@ export declare class Block {
48
48
  * Get the block storage by key.
49
49
  */
50
50
  get(key: string): Promise<HexString | undefined>;
51
+ /**
52
+ * Get the block storage by key.
53
+ */
54
+ getMany(keys: string[]): Promise<Array<HexString | undefined>>;
51
55
  read<T extends string>(type: T, query: StorageEntry, ...args: any[]): Promise<import("@polkadot/types/types").DetectCodec<import("@polkadot/types-codec/types").Codec, T> | undefined>;
52
56
  /**
53
57
  * Get paged storage keys.
@@ -117,6 +117,19 @@ import { RemoteStorageLayer, StorageLayer, StorageValueKind } from './storage-la
117
117
  return val;
118
118
  }
119
119
  }
120
+ /**
121
+ * Get the block storage by key.
122
+ */ async getMany(keys) {
123
+ const vals = await this.storage.getMany(keys, true);
124
+ return vals.map((val)=>{
125
+ switch(val){
126
+ case StorageValueKind.Deleted:
127
+ return undefined;
128
+ default:
129
+ return val;
130
+ }
131
+ });
132
+ }
120
133
  async read(type, query, ...args) {
121
134
  const key = compactHex(query(...args));
122
135
  const value = await this.get(key);
@@ -2,9 +2,13 @@ import { GenericExtrinsic } from '@polkadot/types';
2
2
  import { hexToU8a, u8aConcat, u8aToHex } from '@polkadot/util';
3
3
  import { blake2AsHex, blake2AsU8a } from '@polkadot/util-crypto';
4
4
  import _ from 'lodash';
5
+ import { defaultLogger } from '../../../logger.js';
5
6
  import { compactHex, getCurrentSlot, getParaId } from '../../../utils/index.js';
6
7
  import { dmqMqcHead, hrmpChannels, hrmpEgressChannelIndex, hrmpIngressChannelIndex, paraHead, upgradeGoAheadSignal, WELL_KNOWN_KEYS } from '../../../utils/proof.js';
7
8
  import { createProof, decodeProof } from '../../../wasm-executor/index.js';
9
+ const logger = defaultLogger.child({
10
+ name: 'parachain-validation-data'
11
+ });
8
12
  const MOCK_VALIDATION_DATA = {
9
13
  validationData: {
10
14
  relayParentNumber: 1000,
@@ -31,7 +35,7 @@ const MOCK_VALIDATION_DATA = {
31
35
  horizontalMessages: [],
32
36
  downwardMessages: []
33
37
  };
34
- const getValidationData = async (parent)=>{
38
+ const getValidationData = async (parent, fallback = true)=>{
35
39
  const meta = await parent.meta;
36
40
  if (parent.number === 0) {
37
41
  const { trieRootHash, nodes } = await createProof(MOCK_VALIDATION_DATA.relayChainState.trieNodes, []);
@@ -46,15 +50,39 @@ const getValidationData = async (parent)=>{
46
50
  }
47
51
  };
48
52
  }
49
- const extrinsics = await parent.extrinsics;
50
- const validationDataExtrinsic = extrinsics.find((extrinsic)=>{
51
- const firstArg = meta.registry.createType('GenericExtrinsic', extrinsic)?.args?.[0];
52
- return firstArg && 'validationData' in firstArg;
53
- });
54
- if (!validationDataExtrinsic) {
55
- throw new Error('Missing validation data from block');
53
+ try {
54
+ const extrinsics = await parent.extrinsics;
55
+ const validationDataExtrinsic = extrinsics.find((extrinsic)=>{
56
+ const firstArg = meta.registry.createType('GenericExtrinsic', extrinsic)?.args?.[0];
57
+ return firstArg && 'validationData' in firstArg;
58
+ });
59
+ if (!validationDataExtrinsic) {
60
+ throw new Error('Missing validation data from block');
61
+ }
62
+ return meta.registry.createType('GenericExtrinsic', validationDataExtrinsic).args[0].toJSON();
63
+ } catch (e) {
64
+ logger.warn('Failed to get validation data from block %d %s', parent.number, e);
65
+ if (fallback) {
66
+ // this could fail due to wasm override that breaks the validation data format
67
+ // so we will try parent's parent
68
+ const grandParent = await parent.parentBlock;
69
+ if (grandParent) {
70
+ const data = await getValidationData(grandParent, false);
71
+ return {
72
+ ...data,
73
+ validationData: {
74
+ ...data.validationData,
75
+ relayParentNumber: data.validationData.relayParentNumber + 2
76
+ }
77
+ };
78
+ } else {
79
+ throw e;
80
+ }
81
+ } else {
82
+ // fallback failed, throw error
83
+ throw e;
84
+ }
56
85
  }
57
- return meta.registry.createType('GenericExtrinsic', validationDataExtrinsic).args[0].toJSON();
58
86
  };
59
87
  export class SetValidationData {
60
88
  async createInherents(newBlock, params) {
@@ -82,7 +110,12 @@ export class SetValidationData {
82
110
  if (key === WELL_KNOWN_KEYS.CURRENT_SLOT) {
83
111
  // increment current slot
84
112
  const relayCurrentSlot = decoded[key] ? meta.registry.createType('Slot', hexToU8a(decoded[key])).toNumber() : await getCurrentSlot(parent) * relaySlotIncrease;
85
- const newSlot = meta.registry.createType('Slot', relayCurrentSlot + relaySlotIncrease);
113
+ const newSlot = meta.registry.createType('Slot', relayCurrentSlot + relaySlotIncrease + 1) // +1 to be safe
114
+ ;
115
+ logger.debug({
116
+ relayCurrentSlot,
117
+ newSlot: newSlot.toNumber()
118
+ }, 'Updating relay current slot');
86
119
  newEntries.push([
87
120
  key,
88
121
  u8aToHex(newSlot.toU8a())
@@ -7,10 +7,18 @@ export declare enum StorageValueKind {
7
7
  }
8
8
  export type StorageValue = string | StorageValueKind | undefined;
9
9
  export interface StorageLayerProvider {
10
+ /**
11
+ * Returns true if key is deleted
12
+ */
13
+ deleted(key: string): boolean;
10
14
  /**
11
15
  * Get the value of a storage key.
12
16
  */
13
17
  get(key: string, cache: boolean): Promise<StorageValue>;
18
+ /**
19
+ * Get the value of many storage keys.
20
+ */
21
+ getMany(keys: string[], _cache: boolean): Promise<StorageValue[]>;
14
22
  /**
15
23
  * Get paged storage keys.
16
24
  */
@@ -23,14 +31,18 @@ export interface StorageLayerProvider {
23
31
  export declare class RemoteStorageLayer implements StorageLayerProvider {
24
32
  #private;
25
33
  constructor(api: Api, at: HexString, db: Database | undefined);
34
+ deleted(_key: string): boolean;
26
35
  get(key: string, _cache: boolean): Promise<StorageValue>;
36
+ getMany(keys: string[], _cache: boolean): Promise<StorageValue[]>;
27
37
  findNextKey(prefix: string, startKey: string, _knownBest?: string): Promise<string | undefined>;
28
38
  getKeysPaged(prefix: string, pageSize: number, startKey: string): Promise<string[]>;
29
39
  }
30
40
  export declare class StorageLayer implements StorageLayerProvider {
31
41
  #private;
32
42
  constructor(parent?: StorageLayerProvider);
43
+ deleted(key: string): boolean;
33
44
  get(key: string, cache: boolean): Promise<StorageValue | undefined>;
45
+ getMany(keys: string[], cache: boolean): Promise<StorageValue[]>;
34
46
  set(key: string, value: StorageValue): void;
35
47
  setAll(values: Record<string, StorageValue | null> | [string, StorageValue | null][]): void;
36
48
  findNextKey(prefix: string, startKey: string, knownBest?: string): Promise<string | undefined>;
@@ -22,6 +22,9 @@ export class RemoteStorageLayer {
22
22
  this.#at = at;
23
23
  this.#db = db;
24
24
  }
25
+ deleted(_key) {
26
+ return false;
27
+ }
25
28
  async get(key, _cache) {
26
29
  if (this.#db) {
27
30
  const res = await this.#db.queryStorage(this.#at, key);
@@ -37,6 +40,48 @@ export class RemoteStorageLayer {
37
40
  this.#db?.saveStorage(this.#at, key, data);
38
41
  return data ?? undefined;
39
42
  }
43
+ async getMany(keys, _cache) {
44
+ const result = [];
45
+ let pending = keys.map((key, idx)=>({
46
+ key,
47
+ idx
48
+ }));
49
+ if (this.#db) {
50
+ const results = await Promise.all(pending.map(({ key })=>this.#db.queryStorage(this.#at, key)));
51
+ const oldPending = pending;
52
+ pending = [];
53
+ results.forEach((res, idx)=>{
54
+ if (res) {
55
+ result[idx] = res.value ?? undefined;
56
+ } else {
57
+ pending.push({
58
+ key: oldPending[idx].key,
59
+ idx
60
+ });
61
+ }
62
+ });
63
+ }
64
+ if (pending.length) {
65
+ logger.trace({
66
+ at: this.#at,
67
+ keys
68
+ }, 'RemoteStorageLayer getMany');
69
+ const data = await this.#api.getStorageBatch('0x', pending.map(({ key })=>key), this.#at);
70
+ data.forEach(([, res], idx)=>{
71
+ result[pending[idx].idx] = res ?? undefined;
72
+ });
73
+ if (this.#db?.saveStorageBatch) {
74
+ this.#db?.saveStorageBatch(data.map(([key, value])=>({
75
+ key,
76
+ value,
77
+ blockHash: this.#at
78
+ })));
79
+ } else if (this.#db) {
80
+ data.forEach(([key, value])=>this.#db?.saveStorage(this.#at, key, value));
81
+ }
82
+ }
83
+ return result;
84
+ }
40
85
  async findNextKey(prefix, startKey, _knownBest) {
41
86
  const keys = await this.getKeysPaged(prefix, 1, startKey);
42
87
  return keys[0];
@@ -134,6 +179,18 @@ export class StorageLayer {
134
179
  this.#keys.splice(idx, 1);
135
180
  }
136
181
  }
182
+ deleted(key) {
183
+ if (this.#store.has(key)) {
184
+ return this.#store.get(key) === "Deleted";
185
+ }
186
+ if (this.#deletedPrefix.some((dp)=>key.startsWith(dp))) {
187
+ return true;
188
+ }
189
+ if (this.#parent) {
190
+ return this.#parent.deleted(key);
191
+ }
192
+ return false;
193
+ }
137
194
  async get(key, cache) {
138
195
  if (this.#store.has(key)) {
139
196
  return this.#store.get(key);
@@ -150,6 +207,33 @@ export class StorageLayer {
150
207
  }
151
208
  return undefined;
152
209
  }
210
+ async getMany(keys, cache) {
211
+ const result = [];
212
+ const pending = [];
213
+ const preloadedPromises = keys.map(async (key, idx)=>{
214
+ if (this.#store.has(key)) {
215
+ result[idx] = await this.#store.get(key);
216
+ } else if (this.#deletedPrefix.some((dp)=>key.startsWith(dp))) {
217
+ result[idx] = "Deleted";
218
+ } else {
219
+ pending.push({
220
+ key,
221
+ idx
222
+ });
223
+ }
224
+ });
225
+ if (pending.length && this.#parent) {
226
+ const vals = await this.#parent.getMany(pending.map((p)=>p.key), false);
227
+ vals.forEach((val, idx)=>{
228
+ if (cache) {
229
+ this.#store.set(pending[idx].key, val);
230
+ }
231
+ result[pending[idx].idx] = val;
232
+ });
233
+ }
234
+ await Promise.all(preloadedPromises);
235
+ return result;
236
+ }
153
237
  set(key, value) {
154
238
  switch(value){
155
239
  case "Deleted":
@@ -212,7 +296,7 @@ export class StorageLayer {
212
296
  const next = await this.findNextKey(prefix, startKey, undefined);
213
297
  if (!next) break;
214
298
  startKey = next;
215
- if (await this.get(next, false) === "Deleted") continue;
299
+ if (this.deleted(next)) continue;
216
300
  keys.push(next);
217
301
  }
218
302
  return keys;
@@ -0,0 +1,74 @@
1
+ import type { HexString } from '@polkadot/util/types';
2
+ import { type Handler } from '../shared.js';
3
+ import type { StorageItemRequest } from './chainHead_v1.js';
4
+ /**
5
+ * Retrieve the body of a specific block
6
+ *
7
+ * @param context
8
+ * @param params - [`hash`]
9
+ *
10
+ * @return An array of the SCALE-encoded transactions of a block, or `null` if the block is not found.
11
+ */
12
+ export declare const archive_v1_body: Handler<[HexString], HexString[] | null>;
13
+ export type CallResult = {
14
+ success: true;
15
+ value: HexString;
16
+ } | {
17
+ success: false;
18
+ error: any;
19
+ };
20
+ export declare const archive_v1_call: Handler<[HexString, string, HexString], CallResult | null>;
21
+ /**
22
+ * Retrieve the height of the finalized block.
23
+ *
24
+ * @param context
25
+ *
26
+ * @return The `number` of the height of the head (a.k.a. finalized) block.
27
+ */
28
+ export declare const archive_v1_finalizedHeight: Handler<undefined, number>;
29
+ /**
30
+ * Retrieve the genesis hash
31
+ *
32
+ * @param context
33
+ *
34
+ * @return An {@link HexString} with the hash of the genesis block.
35
+ */
36
+ export declare const archive_v1_genesisHash: Handler<undefined, HexString>;
37
+ /**
38
+ * Retrieve the hash of a specific height
39
+ *
40
+ * @param context
41
+ * @param params - [`height`]
42
+ *
43
+ * @return An array of {@link HexString} with the hashes of the blocks associated to the
44
+ * given height.
45
+ */
46
+ export declare const archive_v1_hashByHeight: Handler<[number], HexString[]>;
47
+ /**
48
+ * Retrieve the header for a specific block
49
+ *
50
+ * @param context
51
+ * @param params - [`hash`]
52
+ *
53
+ * @return SCALE-encoded header, or `null` if the block is not found.
54
+ */
55
+ export declare const archive_v1_header: Handler<[HexString], HexString | null>;
56
+ /**
57
+ * Query the storage for a given block
58
+ *
59
+ * @param context
60
+ * @param params - [`hash`, `items`, `childTrie`]
61
+ *
62
+ * @return the operationId to capture the notifications where to receive the result
63
+ *
64
+ * The query type `closestDescendantMerkleValue` is not up to spec.
65
+ * According to the spec, the result should be the Merkle value of the key or
66
+ * the closest descendant of the key.
67
+ * As chopsticks doesn't have direct access to the Merkle tree, it will return
68
+ * a string that will change every time that one of the descendant changes, but
69
+ * it won't be the actual Merkle value.
70
+ * This should be enough for applications that don't rely on the actual Merkle
71
+ * value, but just use it to detect for storage changes.
72
+ */
73
+ export declare const archive_v1_storage: Handler<[HexString, StorageItemRequest[], HexString | null], string>;
74
+ export declare const archive_v1_stopStorage: Handler<[string], null>;
@@ -0,0 +1,215 @@
1
+ import { blake2AsHex } from '@polkadot/util-crypto';
2
+ import { randomId } from '../../blockchain/head-state.js';
3
+ import { ResponseError } from '../shared.js';
4
+ import { archive_unstable_body, archive_unstable_call } from '../substrate/archive.js';
5
+ import { afterResponse, getDescendantValues } from './storage-common.js';
6
+ /**
7
+ * Retrieve the body of a specific block
8
+ *
9
+ * @param context
10
+ * @param params - [`hash`]
11
+ *
12
+ * @return An array of the SCALE-encoded transactions of a block, or `null` if the block is not found.
13
+ */ export const archive_v1_body = async (...args)=>archive_unstable_body(...args).catch(()=>null);
14
+ /**
15
+ * Perform a runtime call for a block
16
+ *
17
+ * @param context
18
+ * @param params - [`hash`, `function`, `callParameters`]
19
+ *
20
+ * @return A {@link CallResult} with the result of the runtime call, or `null` if the block
21
+ * is not found.
22
+ */ function isBlockNotFound(error) {
23
+ return error instanceof ResponseError && error.code === 1;
24
+ }
25
+ export const archive_v1_call = async (...args)=>archive_unstable_call.call(undefined, ...args).then(({ value })=>({
26
+ success: true,
27
+ value
28
+ }), (error)=>isBlockNotFound(error) ? null : {
29
+ success: false,
30
+ error
31
+ });
32
+ /**
33
+ * Retrieve the height of the finalized block.
34
+ *
35
+ * @param context
36
+ *
37
+ * @return The `number` of the height of the head (a.k.a. finalized) block.
38
+ */ export const archive_v1_finalizedHeight = (context)=>{
39
+ return Promise.resolve(context.chain.head.number);
40
+ };
41
+ /**
42
+ * Retrieve the genesis hash
43
+ *
44
+ * @param context
45
+ *
46
+ * @return An {@link HexString} with the hash of the genesis block.
47
+ */ export const archive_v1_genesisHash = async (context)=>{
48
+ const genesisBlock = await context.chain.getBlockAt(0);
49
+ return genesisBlock.hash;
50
+ };
51
+ /**
52
+ * Retrieve the hash of a specific height
53
+ *
54
+ * @param context
55
+ * @param params - [`height`]
56
+ *
57
+ * @return An array of {@link HexString} with the hashes of the blocks associated to the
58
+ * given height.
59
+ */ export const archive_v1_hashByHeight = async (context, [height])=>{
60
+ const block = await context.chain.getBlockAt(height);
61
+ return block ? [
62
+ block.hash
63
+ ] : [];
64
+ };
65
+ /**
66
+ * Retrieve the header for a specific block
67
+ *
68
+ * @param context
69
+ * @param params - [`hash`]
70
+ *
71
+ * @return SCALE-encoded header, or `null` if the block is not found.
72
+ */ export const archive_v1_header = async (context, [hash])=>{
73
+ const block = await context.chain.getBlock(hash);
74
+ return block ? (await block.header).toHex() : null;
75
+ };
76
+ /**
77
+ * Contains the storage operations.
78
+ */ const storageOperations = new Map();
79
+ /**
80
+ * Query the storage for a given block
81
+ *
82
+ * @param context
83
+ * @param params - [`hash`, `items`, `childTrie`]
84
+ *
85
+ * @return the operationId to capture the notifications where to receive the result
86
+ *
87
+ * The query type `closestDescendantMerkleValue` is not up to spec.
88
+ * According to the spec, the result should be the Merkle value of the key or
89
+ * the closest descendant of the key.
90
+ * As chopsticks doesn't have direct access to the Merkle tree, it will return
91
+ * a string that will change every time that one of the descendant changes, but
92
+ * it won't be the actual Merkle value.
93
+ * This should be enough for applications that don't rely on the actual Merkle
94
+ * value, but just use it to detect for storage changes.
95
+ */ export const archive_v1_storage = async (context, [hash, items, _childTrie], { subscribe })=>{
96
+ const operationId = randomId();
97
+ const callback = subscribe('chainHead_v1_storageEvent', operationId, ()=>storageOperations.delete(operationId));
98
+ storageOperations.set(operationId, {
99
+ callback,
100
+ hash,
101
+ params: [],
102
+ storageDiffs: new Map()
103
+ });
104
+ afterResponse(async ()=>{
105
+ const block = await context.chain.getBlock(hash);
106
+ if (!block) {
107
+ storageOperations.get(operationId)?.callback({
108
+ event: 'storageError',
109
+ operationId,
110
+ error: 'Block not found'
111
+ });
112
+ return;
113
+ }
114
+ const handleStorageItemRequest = async (sir)=>{
115
+ switch(sir.type){
116
+ case 'value':
117
+ {
118
+ const value = await block.get(sir.key);
119
+ if (value) {
120
+ storageOperations.get(operationId)?.callback({
121
+ event: 'storage',
122
+ key: sir.key,
123
+ value
124
+ });
125
+ }
126
+ return null;
127
+ }
128
+ case 'hash':
129
+ {
130
+ const value = await block.get(sir.key);
131
+ if (value) {
132
+ storageOperations.get(operationId)?.callback({
133
+ event: 'storage',
134
+ key: sir.key,
135
+ hash
136
+ });
137
+ }
138
+ return null;
139
+ }
140
+ case 'descendantsValues':
141
+ {
142
+ let items;
143
+ let next = {
144
+ prefix: sir.key,
145
+ startKey: '0x'
146
+ };
147
+ do {
148
+ ;
149
+ ({ items, next } = await getDescendantValues(block, next));
150
+ for (const { key, value } of items){
151
+ storageOperations.get(operationId)?.callback({
152
+ event: 'storage',
153
+ key,
154
+ value
155
+ });
156
+ }
157
+ }while (next !== null)
158
+ return null;
159
+ }
160
+ case 'descendantsHashes':
161
+ {
162
+ let items;
163
+ let next = {
164
+ prefix: sir.key,
165
+ startKey: '0x'
166
+ };
167
+ do {
168
+ ;
169
+ ({ items, next } = await getDescendantValues(block, next));
170
+ for (const { key, value } of items){
171
+ if (value === undefined) {
172
+ continue;
173
+ }
174
+ storageOperations.get(operationId)?.callback({
175
+ event: 'storage',
176
+ key,
177
+ hash: blake2AsHex(value)
178
+ });
179
+ }
180
+ }while (next !== null)
181
+ return null;
182
+ }
183
+ case 'closestDescendantMerkleValue':
184
+ {
185
+ const subscription = storageOperations.get(operationId);
186
+ if (!subscription) return null;
187
+ if (!subscription.storageDiffs.has(sir.key)) {
188
+ // Set up a diff watch for this key
189
+ subscription.storageDiffs.set(sir.key, 0);
190
+ }
191
+ subscription.callback({
192
+ event: 'storage',
193
+ operationId,
194
+ items: [
195
+ {
196
+ key: sir.key,
197
+ closestDescendantMerkleValue: String(subscription.storageDiffs.get(sir.key))
198
+ }
199
+ ]
200
+ });
201
+ return null;
202
+ }
203
+ }
204
+ };
205
+ await Promise.all(items.map(handleStorageItemRequest));
206
+ storageOperations.get(operationId)?.callback({
207
+ event: 'storageDone'
208
+ });
209
+ });
210
+ return operationId;
211
+ };
212
+ export const archive_v1_stopStorage = async (_, [operationId], { unsubscribe })=>{
213
+ unsubscribe(operationId);
214
+ return null;
215
+ };