@subsquid/fuel-stream 0.0.1

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.
Files changed (56) hide show
  1. package/lib/archive/data-schema.d.ts +332 -0
  2. package/lib/archive/data-schema.d.ts.map +1 -0
  3. package/lib/archive/data-schema.js +293 -0
  4. package/lib/archive/data-schema.js.map +1 -0
  5. package/lib/archive/source.d.ts +14 -0
  6. package/lib/archive/source.d.ts.map +1 -0
  7. package/lib/archive/source.js +82 -0
  8. package/lib/archive/source.js.map +1 -0
  9. package/lib/data/data-partial.d.ts +20 -0
  10. package/lib/data/data-partial.d.ts.map +1 -0
  11. package/lib/data/data-partial.js +3 -0
  12. package/lib/data/data-partial.js.map +1 -0
  13. package/lib/data/data-request.d.ts +35 -0
  14. package/lib/data/data-request.d.ts.map +1 -0
  15. package/lib/data/data-request.js +3 -0
  16. package/lib/data/data-request.js.map +1 -0
  17. package/lib/data/model.d.ts +45 -0
  18. package/lib/data/model.d.ts.map +1 -0
  19. package/lib/data/model.js +19 -0
  20. package/lib/data/model.js.map +1 -0
  21. package/lib/data/util.d.ts +20 -0
  22. package/lib/data/util.d.ts.map +1 -0
  23. package/lib/data/util.js +3 -0
  24. package/lib/data/util.js.map +1 -0
  25. package/lib/fields.d.ts +6 -0
  26. package/lib/fields.d.ts.map +1 -0
  27. package/lib/fields.js +32 -0
  28. package/lib/fields.js.map +1 -0
  29. package/lib/filter.d.ts +6 -0
  30. package/lib/filter.d.ts.map +1 -0
  31. package/lib/filter.js +172 -0
  32. package/lib/filter.js.map +1 -0
  33. package/lib/graphql.d.ts +14 -0
  34. package/lib/graphql.d.ts.map +1 -0
  35. package/lib/graphql.js +40 -0
  36. package/lib/graphql.js.map +1 -0
  37. package/lib/index.d.ts +3 -0
  38. package/lib/index.d.ts.map +1 -0
  39. package/lib/index.js +19 -0
  40. package/lib/index.js.map +1 -0
  41. package/lib/source.d.ts +118 -0
  42. package/lib/source.d.ts.map +1 -0
  43. package/lib/source.js +296 -0
  44. package/lib/source.js.map +1 -0
  45. package/package.json +34 -0
  46. package/src/archive/data-schema.ts +320 -0
  47. package/src/archive/source.ts +88 -0
  48. package/src/data/data-partial.ts +25 -0
  49. package/src/data/data-request.ts +44 -0
  50. package/src/data/model.ts +114 -0
  51. package/src/data/util.ts +42 -0
  52. package/src/fields.ts +35 -0
  53. package/src/filter.ts +229 -0
  54. package/src/graphql.ts +51 -0
  55. package/src/index.ts +2 -0
  56. package/src/source.ts +416 -0
package/src/filter.ts ADDED
@@ -0,0 +1,229 @@
1
+ import {EntityFilter, FilterBuilder} from '@subsquid/util-internal-processor-tools'
2
+ import {assertNotNull, groupBy, weakMemo} from '@subsquid/util-internal'
3
+ import {getRequestAt, RangeRequest} from '@subsquid/util-internal-range'
4
+ import {Block, TransactionInput, TransactionOutput, Receipt, Transaction} from '@subsquid/fuel-normalization'
5
+ import {DataRequest} from './data/data-request'
6
+
7
+
8
+ class IncludeSet {
9
+ public readonly receipts = new Set<Receipt>()
10
+ public readonly transactions = new Set<Transaction>()
11
+ public readonly inputs = new Set<TransactionInput>()
12
+ public readonly outputs = new Set<TransactionOutput>()
13
+
14
+ addReceipt(receipt?: Receipt): void {
15
+ if (receipt) {
16
+ this.receipts.add(receipt)
17
+ }
18
+ }
19
+
20
+ addTransaction(tx?: Transaction): void {
21
+ if (tx) {
22
+ this.transactions.add(tx)
23
+ }
24
+ }
25
+
26
+ addInput(input?: TransactionInput): void {
27
+ if (input) {
28
+ this.inputs.add(input)
29
+ }
30
+ }
31
+
32
+ addOutput(output?: TransactionOutput): void {
33
+ if (output) {
34
+ this.outputs.add(output)
35
+ }
36
+ }
37
+ }
38
+
39
+
40
+ interface ReceiptRelations {
41
+ transaction?: boolean
42
+ }
43
+
44
+
45
+ interface TransactionRelations {
46
+ receipts?: boolean
47
+ inputs?: boolean
48
+ outputs?: boolean
49
+ }
50
+
51
+
52
+ interface InputRelations {
53
+ transaction?: boolean
54
+ }
55
+
56
+
57
+ interface OutputRelations {
58
+ transaction?: boolean
59
+ }
60
+
61
+
62
+ function buildReceiptFilter(dataRequest: DataRequest): EntityFilter<Receipt, ReceiptRelations> {
63
+ let receipts = new EntityFilter<Receipt, ReceiptRelations>()
64
+
65
+ dataRequest.receipts?.forEach(req => {
66
+ let {type, logDataContract, ...relations} = req
67
+ let filter = new FilterBuilder<Receipt>()
68
+ filter.propIn('receiptType', type)
69
+ filter.propIn('contract', logDataContract)
70
+ receipts.add(filter, relations)
71
+ })
72
+
73
+ return receipts
74
+ }
75
+
76
+
77
+ function buildTransactionFilter(dataRequest: DataRequest): EntityFilter<Transaction, TransactionRelations> {
78
+ let transactions = new EntityFilter<Transaction, TransactionRelations>()
79
+
80
+ dataRequest.transactions?.forEach(req => {
81
+ let {type, ...relations} = req
82
+ let filter = new FilterBuilder<Transaction>()
83
+ filter.propIn('type', type)
84
+ transactions.add(filter, relations)
85
+ })
86
+
87
+ return transactions
88
+ }
89
+
90
+
91
+ function buildInputFilter(dataRequest: DataRequest): EntityFilter<TransactionInput, InputRelations> {
92
+ let inputs = new EntityFilter<TransactionInput, InputRelations>()
93
+
94
+ dataRequest.inputs?.forEach(req => {
95
+ let {
96
+ type,
97
+ coinOwner,
98
+ coinAssetId,
99
+ contractContract,
100
+ messageSender,
101
+ messageRecipient,
102
+ ...relations
103
+ } = req
104
+ let filter = new FilterBuilder<TransactionInput>()
105
+ filter.propIn('type', req.type)
106
+ filter.getIn(input => input.type == 'InputCoin' && assertNotNull(input.owner), coinOwner)
107
+ filter.getIn(input => input.type == 'InputCoin' && assertNotNull(input.assetId), coinAssetId)
108
+ filter.getIn(input => input.type == 'InputContract' && assertNotNull(input.contract), contractContract)
109
+ filter.getIn(input => input.type == 'InputMessage' && assertNotNull(input.sender), messageSender)
110
+ filter.getIn(input => input.type == 'InputMessage' && assertNotNull(input.recipient), messageRecipient)
111
+ inputs.add(filter, relations)
112
+ })
113
+
114
+ return inputs
115
+ }
116
+
117
+
118
+ function buildOutputFilter(dataRequest: DataRequest): EntityFilter<TransactionOutput, OutputRelations> {
119
+ let outputs = new EntityFilter<TransactionOutput, OutputRelations>()
120
+
121
+ dataRequest.outputs?.forEach(req => {
122
+ let {type, ...relations} = req
123
+ let filter = new FilterBuilder<TransactionOutput>()
124
+ filter.propIn('type', type)
125
+ outputs.add(filter, relations)
126
+ })
127
+
128
+ return outputs
129
+ }
130
+
131
+
132
+ const getItemFilter = weakMemo((dataRequest: DataRequest) => {
133
+ return {
134
+ receipts: buildReceiptFilter(dataRequest),
135
+ transactions: buildTransactionFilter(dataRequest),
136
+ inputs: buildInputFilter(dataRequest),
137
+ outputs: buildOutputFilter(dataRequest),
138
+ }
139
+ })
140
+
141
+
142
+ export function filterBlock(block: Block, dataRequest: DataRequest): void {
143
+ let items = getItemFilter(dataRequest)
144
+
145
+ let include = new IncludeSet()
146
+
147
+ let transactions = new Map(block.transactions.map(tx => [tx.index, tx]))
148
+ let inputsByTx = groupBy(block.inputs, input => input.transactionIndex)
149
+ let outputsByTx = groupBy(block.outputs, ouput => ouput.transactionIndex)
150
+ let receiptsByTx = groupBy(block.receipts, receipt => receipt.transactionIndex)
151
+
152
+ if (items.receipts.present()) {
153
+ for (let receipt of block.receipts) {
154
+ let rel = items.receipts.match(receipt)
155
+ if (rel == null) continue
156
+ include.addReceipt(receipt)
157
+ if (rel.transaction) {
158
+ let tx = assertNotNull(transactions.get(receipt.transactionIndex))
159
+ include.addTransaction(tx)
160
+ }
161
+ }
162
+ }
163
+
164
+ if (items.transactions.present()) {
165
+ for (let tx of block.transactions) {
166
+ let rel = items.transactions.match(tx)
167
+ if (rel == null) continue
168
+ include.addTransaction(tx)
169
+ if (rel.receipts) {
170
+ let receipts = assertNotNull(receiptsByTx.get(tx.index))
171
+ for (let receipt of receipts) {
172
+ include.addReceipt(receipt)
173
+ }
174
+ }
175
+ if (rel.inputs) {
176
+ let inputs = assertNotNull(inputsByTx.get(tx.index))
177
+ for (let input of inputs) {
178
+ include.addInput(input)
179
+ }
180
+ }
181
+ if (rel.outputs) {
182
+ let outputs = assertNotNull(outputsByTx.get(tx.index))
183
+ for (let output of outputs) {
184
+ include.addOutput(output)
185
+ }
186
+ }
187
+ }
188
+ }
189
+
190
+ if (items.inputs.present()) {
191
+ for (let input of block.inputs) {
192
+ let rel = items.inputs.match(input)
193
+ if (rel == null) continue
194
+ include.addInput(input)
195
+ if (rel.transaction) {
196
+ let tx = assertNotNull(transactions.get(input.transactionIndex))
197
+ include.addTransaction(tx)
198
+ }
199
+ }
200
+ }
201
+
202
+ if (items.outputs.present()) {
203
+ for (let output of block.outputs) {
204
+ let rel = items.outputs.match(output)
205
+ if (rel == null) continue
206
+ include.addOutput(output)
207
+ if (rel.transaction) {
208
+ let tx = assertNotNull(transactions.get(output.transactionIndex))
209
+ include.addTransaction(tx)
210
+ }
211
+ }
212
+ }
213
+
214
+ block.receipts = block.receipts.filter(receipt => include.receipts.has(receipt))
215
+ block.transactions = block.transactions.filter(tx => include.transactions.has(tx))
216
+ block.inputs = block.inputs.filter(input => include.inputs.has(input))
217
+ block.outputs = block.outputs.filter(output => include.outputs.has(output))
218
+ }
219
+
220
+
221
+ export function filterBlockBatch(requests: RangeRequest<DataRequest>[], blocks: Block[]): void {
222
+ for (let block of blocks) {
223
+ let dataRequest = getRequestAt(requests, block.header.height) || NO_DATA_REQUEST
224
+ filterBlock(block, dataRequest)
225
+ }
226
+ }
227
+
228
+
229
+ const NO_DATA_REQUEST: DataRequest = {}
package/src/graphql.ts ADDED
@@ -0,0 +1,51 @@
1
+ import {mapRangeRequestList, RangeRequestList} from '@subsquid/util-internal-range'
2
+ import {HttpDataSource} from '@subsquid/fuel-data/lib/data-source'
3
+ import {BlockHeader, DataRequest as RawDataRequest} from '@subsquid/fuel-data/lib/raw-data'
4
+ import {mapRawBlock} from '@subsquid/fuel-normalization'
5
+ import {DataRequest} from './data/data-request'
6
+ import {PartialBlock} from './data/data-partial'
7
+ import {filterBlockBatch} from './filter'
8
+
9
+
10
+ export class GraphqlDataSource {
11
+ constructor(private baseDataSource: HttpDataSource) { }
12
+
13
+ async getFinalizedHeight(): Promise<number> {
14
+ return this.baseDataSource.getFinalizedHeight()
15
+ }
16
+
17
+ getBlockHash(height: number): Promise<string | undefined> {
18
+ return this.baseDataSource.getBlockHash(height)
19
+ }
20
+
21
+ getBlockHeader(height: number): Promise<BlockHeader | undefined> {
22
+ return this.baseDataSource.getBlockHeader(height)
23
+ }
24
+
25
+ async *getBlockStream(
26
+ requests: RangeRequestList<DataRequest>,
27
+ stopOnHead?: boolean
28
+ ): AsyncIterable<PartialBlock[]> {
29
+ for await (let batch of this.baseDataSource.getFinalizedBlocks(
30
+ mapRangeRequestList(requests, toRawDataRequest),
31
+ stopOnHead
32
+ )) {
33
+ let blocks = batch.blocks.map(b => mapRawBlock(b))
34
+ filterBlockBatch(requests, blocks)
35
+ yield blocks
36
+ }
37
+ }
38
+ }
39
+
40
+
41
+ function toRawDataRequest(req: DataRequest): RawDataRequest {
42
+ return {
43
+ transactions: !!req.transactions?.length
44
+ || !!req.inputs?.length
45
+ || !!req.outputs?.length
46
+ || !!req.receipts?.length,
47
+ inputs: !!req.inputs?.length,
48
+ outputs: !!req.outputs?.length,
49
+ receipts: !!req.receipts?.length
50
+ }
51
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './data/model'
2
+ export * from './source'
package/src/source.ts ADDED
@@ -0,0 +1,416 @@
1
+ import {BlockHeader} from '@subsquid/fuel-normalization'
2
+ import {HttpAgent, HttpClient} from '@subsquid/http-client'
3
+ import {Logger} from '@subsquid/logger'
4
+ import {def, addErrorContext, last} from '@subsquid/util-internal'
5
+ import {ArchiveClient} from '@subsquid/util-internal-archive-client'
6
+ import {getOrGenerateSquidId} from '@subsquid/util-internal-processor-tools'
7
+ import {
8
+ applyRangeBound,
9
+ mergeRangeRequests,
10
+ getSize,
11
+ Range,
12
+ RangeRequest,
13
+ RangeRequestList,
14
+ FiniteRange
15
+ } from '@subsquid/util-internal-range'
16
+ import {BlockHeader as RawBlockHeader} from '@subsquid/fuel-data/lib/raw-data'
17
+ import {HttpDataSource} from '@subsquid/fuel-data/lib/data-source'
18
+ import assert from 'assert'
19
+ import {FuelGateway} from './archive/source'
20
+ import {getFields} from './fields'
21
+ import {Block, FieldSelection} from './data/model'
22
+ import {GraphqlDataSource} from './graphql'
23
+ import {PartialBlock} from './data/data-partial'
24
+ import {
25
+ DataRequest,
26
+ ReceiptRequest,
27
+ InputRequest,
28
+ OutputRequest,
29
+ TransactionRequest
30
+ } from './data/data-request'
31
+
32
+
33
+ export interface GraphqlSettings {
34
+ /**
35
+ * GraphQL endpoint URL
36
+ */
37
+ url: string
38
+ /**
39
+ * Maximum number of concurrent `blocks` queries.
40
+ *
41
+ * Default is `10`
42
+ */
43
+ strideConcurrency?: number
44
+ /**
45
+ * `blocks` query size.
46
+ *
47
+ * Default is `5`.
48
+ */
49
+ strideSize?: number
50
+ }
51
+
52
+
53
+ export interface GatewaySettings {
54
+ /**
55
+ * Subsquid Network Gateway url
56
+ */
57
+ url: string
58
+ /**
59
+ * Request timeout in ms
60
+ */
61
+ requestTimeout?: number
62
+ }
63
+
64
+
65
+ interface BlockRange {
66
+ range?: Range
67
+ }
68
+
69
+
70
+
71
+ /**
72
+ * API and data that is passed to the data handler
73
+ */
74
+ export interface DataHandlerContext<Store, F extends FieldSelection = {}> {
75
+ /**
76
+ * An instance of a structured logger.
77
+ */
78
+ log: Logger
79
+ /**
80
+ * Storage interface provided by the database
81
+ */
82
+ store: Store
83
+ /**
84
+ * List of blocks to map and process
85
+ */
86
+ blocks: Block<F>[]
87
+ /**
88
+ * Signals, that the processor reached the head of a chain.
89
+ *
90
+ * The head block is always included in `.blocks`.
91
+ */
92
+ isHead: boolean
93
+ }
94
+
95
+
96
+ export type FuelBatchProcessorFields<T> = T extends DataSourceBuilder<infer F> ? F : never
97
+
98
+
99
+ export class DataSourceBuilder<F extends FieldSelection = {}> {
100
+ private requests: RangeRequest<DataRequest>[] = []
101
+ private fields?: FieldSelection
102
+ private blockRange?: Range
103
+ private gateway?: GatewaySettings
104
+ private graphql?: GraphqlSettings
105
+ private running = false
106
+
107
+ private assertNotRunning(): void {
108
+ if (this.running) {
109
+ throw new Error('Settings modifications are not allowed after start of processing')
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Set Subsquid Network Gateway endpoint (ex Archive).
115
+ *
116
+ * Subsquid Network allows to get data from finalized blocks up to
117
+ * infinite times faster and more efficient than via regular GraphQL.
118
+ *
119
+ * @example
120
+ * processor.setGateway('https://v2.archive.subsquid.io/network/fuel-mainnet')
121
+ */
122
+ setGateway(url: string | GatewaySettings): this {
123
+ this.assertNotRunning()
124
+ if (typeof url == 'string') {
125
+ this.gateway = {url}
126
+ } else {
127
+ this.gateway = url
128
+ }
129
+ return this
130
+ }
131
+
132
+ /**
133
+ * Set up GraphQL data ingestion
134
+ */
135
+ setGraphql(settings?: GraphqlSettings): this {
136
+ this.assertNotRunning()
137
+ this.graphql = settings
138
+ return this
139
+ }
140
+
141
+ /**
142
+ * Limits the range of blocks to be processed.
143
+ *
144
+ * When the upper bound is specified,
145
+ * the processor will terminate with exit code 0 once it reaches it.
146
+ */
147
+ setBlockRange(range?: Range): this {
148
+ this.assertNotRunning()
149
+ this.blockRange = range
150
+ return this
151
+ }
152
+
153
+ /**
154
+ * Configure a set of fetched fields
155
+ */
156
+ setFields<F extends FieldSelection>(fields: F): DataSourceBuilder<F> {
157
+ this.assertNotRunning()
158
+ this.fields = fields
159
+ return this as any
160
+ }
161
+
162
+ private add(range: Range | undefined, request: DataRequest): void {
163
+ this.requests.push({
164
+ range: range || {from: 0},
165
+ request
166
+ })
167
+ }
168
+
169
+ /**
170
+ * By default, the processor will fetch only blocks
171
+ * which contain requested items. This method
172
+ * modifies such behaviour to fetch all chain blocks.
173
+ *
174
+ * Optionally a range of blocks can be specified
175
+ * for which the setting should be effective.
176
+ */
177
+ includeAllBlocks(range?: Range): this {
178
+ this.assertNotRunning()
179
+ this.add(range, {includeAllBlocks: true})
180
+ return this
181
+ }
182
+
183
+ addTransaction(options: TransactionRequest & BlockRange): this {
184
+ this.assertNotRunning()
185
+ let {range, ...req} = options
186
+ this.add(range, {
187
+ transactions: [req]
188
+ })
189
+ return this
190
+ }
191
+
192
+ addReceipt(options: ReceiptRequest & BlockRange): this {
193
+ this.assertNotRunning()
194
+ let {range, ...req} = options
195
+ this.add(range, {
196
+ receipts: [req]
197
+ })
198
+ return this
199
+ }
200
+
201
+ addInput(options: InputRequest & BlockRange): this {
202
+ this.assertNotRunning()
203
+ let {range, ...req} = options
204
+ this.add(range, {
205
+ inputs: [req]
206
+ })
207
+ return this
208
+ }
209
+
210
+ addOutput(options: OutputRequest & BlockRange): this {
211
+ this.assertNotRunning()
212
+ let {range, ...req} = options
213
+ this.add(range, {
214
+ outputs: [req]
215
+ })
216
+ return this
217
+ }
218
+
219
+ @def
220
+ private getRequests(): RangeRequest<DataRequest>[] {
221
+ function concat<T>(a?: T[], b?: T[]): T[] | undefined {
222
+ let result: T[] = []
223
+ if (a) {
224
+ result.push(...a)
225
+ }
226
+ if (b) {
227
+ result.push(...b)
228
+ }
229
+ return result.length == 0 ? undefined : result
230
+ }
231
+
232
+ let requests = mergeRangeRequests(this.requests, (a, b) => {
233
+ return {
234
+ includeAllBlocks: a.includeAllBlocks || b.includeAllBlocks,
235
+ transactions: concat(a.transactions, b.transactions),
236
+ receipts: concat(a.receipts, b.receipts),
237
+ inputs: concat(a.inputs, b.inputs),
238
+ outputs: concat(a.outputs, b.outputs),
239
+ }
240
+ })
241
+
242
+ let fields = getFields(this.fields)
243
+
244
+ requests = requests.map(({range, request}) => {
245
+ return {
246
+ range,
247
+ request: {
248
+ fields,
249
+ ...request
250
+ }
251
+ }
252
+ })
253
+
254
+ return applyRangeBound(requests, this.blockRange)
255
+ }
256
+
257
+ build(): DataSource<Block<F>> {
258
+ return new FuelDataSource(
259
+ this.getRequests(),
260
+ this.gateway,
261
+ this.graphql
262
+ ) as DataSource<Block<F>>
263
+ }
264
+ }
265
+
266
+
267
+ export interface DataSource<Block> {
268
+ getFinalizedHeight(): Promise<number>
269
+ getBlockHash(height: number): Promise<string | undefined>
270
+ getBlocksCountInRange(range: FiniteRange): number
271
+ getBlockStream(fromBlockHeight?: number): AsyncIterable<Block[]>
272
+ }
273
+
274
+
275
+ export type GetDataSourceBlock<T> = T extends DataSource<infer B> ? B : never
276
+
277
+
278
+ class FuelDataSource implements DataSource<PartialBlock> {
279
+ private graphql?: GraphqlDataSource
280
+ private isConsistent?: boolean
281
+ private ranges: Range[]
282
+
283
+ constructor(
284
+ private requests: RangeRequestList<DataRequest>,
285
+ private gatewaySettings?: GatewaySettings,
286
+ graphqlSettings?: GraphqlSettings
287
+ ) {
288
+ assert(this.gatewaySettings || graphqlSettings, 'either gateway or GraphQL should be provided')
289
+ if (graphqlSettings) {
290
+ this.graphql = this.createGraphqlDataSource(graphqlSettings)
291
+ }
292
+ this.ranges = this.requests.map(req => req.range)
293
+ }
294
+
295
+ getFinalizedHeight(): Promise<number> {
296
+ if (this.graphql) {
297
+ return this.graphql.getFinalizedHeight()
298
+ } else {
299
+ return this.createGateway().getFinalizedHeight()
300
+ }
301
+ }
302
+
303
+ async getBlockHash(height: number): Promise<string | undefined> {
304
+ await this.assertConsistency()
305
+ if (this.gatewaySettings == null) {
306
+ assert(this.graphql)
307
+ return this.graphql.getBlockHash(height)
308
+ } else {
309
+ let gateway = this.createGateway()
310
+ let head = await gateway.getFinalizedHeight()
311
+ if (head >= height) return gateway.getBlockHash(height)
312
+ if (this.graphql) return this.graphql.getBlockHash(height)
313
+ }
314
+ }
315
+
316
+ private async assertConsistency(): Promise<void> {
317
+ if (this.isConsistent || this.gatewaySettings == null || this.graphql == null) return
318
+ let blocks = await this.performConsistencyCheck().catch(err => {
319
+ throw addErrorContext(
320
+ new Error(`Failed to check consistency between Subsquid Gateway and GraphQL endpoints`),
321
+ {reason: err}
322
+ )
323
+ })
324
+ if (blocks == null) {
325
+ this.isConsistent = true
326
+ } else {
327
+ throw addErrorContext(
328
+ new Error(`Provided Subsquid Gateway and GraphQL endpoints don't agree on block №${blocks.archiveBlock.height}`),
329
+ blocks
330
+ )
331
+ }
332
+ }
333
+
334
+ private async performConsistencyCheck(): Promise<{
335
+ archiveBlock: BlockHeader
336
+ gqlBlock: RawBlockHeader | null
337
+ } | undefined> {
338
+ let archive = this.createGateway()
339
+ let height = await archive.getFinalizedHeight()
340
+ let archiveBlock = await archive.getBlockHeader(height)
341
+ let gqlBlock = await this.graphql!.getBlockHeader(archiveBlock.height)
342
+ if (gqlBlock?.id === archiveBlock.hash && Number(gqlBlock.height) === archiveBlock.height) return
343
+ return {archiveBlock, gqlBlock: gqlBlock || null}
344
+ }
345
+
346
+ getBlocksCountInRange(range: FiniteRange): number {
347
+ return getSize(this.ranges, range)
348
+ }
349
+
350
+ async *getBlockStream(fromBlockHeight?: number): AsyncIterable<PartialBlock[]> {
351
+ await this.assertConsistency()
352
+
353
+ let requests = fromBlockHeight == null
354
+ ? this.requests
355
+ : applyRangeBound(this.requests, {from: fromBlockHeight})
356
+
357
+ if (requests.length == 0) return
358
+
359
+ if (this.gatewaySettings) {
360
+ let agent = new HttpAgent({keepAlive: true})
361
+ try {
362
+ let archive = this.createGateway(agent)
363
+ let height = await archive.getFinalizedHeight()
364
+ let from = requests[0].range.from
365
+ if (height > from || !this.graphql) {
366
+ for await (let batch of archive.getBlockStream(requests, !!this.graphql)) {
367
+ yield batch
368
+ from = last(batch).header.height + 1
369
+ }
370
+ requests = applyRangeBound(requests, {from})
371
+ }
372
+ } finally {
373
+ agent.close()
374
+ }
375
+ }
376
+
377
+ if (requests.length == 0) return
378
+
379
+ assert(this.graphql)
380
+
381
+ yield* this.graphql.getBlockStream(requests)
382
+ }
383
+
384
+ private createGateway(agent?: HttpAgent): FuelGateway {
385
+ assert(this.gatewaySettings)
386
+
387
+ let http = new HttpClient({
388
+ headers: {
389
+ 'x-squid-id': this.getSquidId()
390
+ },
391
+ agent
392
+ })
393
+
394
+ return new FuelGateway(
395
+ new ArchiveClient({
396
+ http,
397
+ url: this.gatewaySettings.url,
398
+ queryTimeout: this.gatewaySettings.requestTimeout,
399
+ })
400
+ )
401
+ }
402
+
403
+ private createGraphqlDataSource(settings: GraphqlSettings): GraphqlDataSource {
404
+ let dataSource = new HttpDataSource({
405
+ client: new HttpClient({baseUrl: settings.url}),
406
+ strideConcurrency: settings.strideConcurrency,
407
+ strideSize: settings.strideSize,
408
+ })
409
+ return new GraphqlDataSource(dataSource)
410
+ }
411
+
412
+ @def
413
+ private getSquidId(): string {
414
+ return getOrGenerateSquidId()
415
+ }
416
+ }