@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.
- package/lib/archive/data-schema.d.ts +332 -0
- package/lib/archive/data-schema.d.ts.map +1 -0
- package/lib/archive/data-schema.js +293 -0
- package/lib/archive/data-schema.js.map +1 -0
- package/lib/archive/source.d.ts +14 -0
- package/lib/archive/source.d.ts.map +1 -0
- package/lib/archive/source.js +82 -0
- package/lib/archive/source.js.map +1 -0
- package/lib/data/data-partial.d.ts +20 -0
- package/lib/data/data-partial.d.ts.map +1 -0
- package/lib/data/data-partial.js +3 -0
- package/lib/data/data-partial.js.map +1 -0
- package/lib/data/data-request.d.ts +35 -0
- package/lib/data/data-request.d.ts.map +1 -0
- package/lib/data/data-request.js +3 -0
- package/lib/data/data-request.js.map +1 -0
- package/lib/data/model.d.ts +45 -0
- package/lib/data/model.d.ts.map +1 -0
- package/lib/data/model.js +19 -0
- package/lib/data/model.js.map +1 -0
- package/lib/data/util.d.ts +20 -0
- package/lib/data/util.d.ts.map +1 -0
- package/lib/data/util.js +3 -0
- package/lib/data/util.js.map +1 -0
- package/lib/fields.d.ts +6 -0
- package/lib/fields.d.ts.map +1 -0
- package/lib/fields.js +32 -0
- package/lib/fields.js.map +1 -0
- package/lib/filter.d.ts +6 -0
- package/lib/filter.d.ts.map +1 -0
- package/lib/filter.js +172 -0
- package/lib/filter.js.map +1 -0
- package/lib/graphql.d.ts +14 -0
- package/lib/graphql.d.ts.map +1 -0
- package/lib/graphql.js +40 -0
- package/lib/graphql.js.map +1 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +19 -0
- package/lib/index.js.map +1 -0
- package/lib/source.d.ts +118 -0
- package/lib/source.d.ts.map +1 -0
- package/lib/source.js +296 -0
- package/lib/source.js.map +1 -0
- package/package.json +34 -0
- package/src/archive/data-schema.ts +320 -0
- package/src/archive/source.ts +88 -0
- package/src/data/data-partial.ts +25 -0
- package/src/data/data-request.ts +44 -0
- package/src/data/model.ts +114 -0
- package/src/data/util.ts +42 -0
- package/src/fields.ts +35 -0
- package/src/filter.ts +229 -0
- package/src/graphql.ts +51 -0
- package/src/index.ts +2 -0
- 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
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
|
+
}
|