@subsquid/solana-stream 0.0.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.
Files changed (77) hide show
  1. package/README.md +4 -0
  2. package/lib/archive/schema.d.ts +167 -0
  3. package/lib/archive/schema.d.ts.map +1 -0
  4. package/lib/archive/schema.js +117 -0
  5. package/lib/archive/schema.js.map +1 -0
  6. package/lib/archive/source.d.ts +15 -0
  7. package/lib/archive/source.d.ts.map +1 -0
  8. package/lib/archive/source.js +98 -0
  9. package/lib/archive/source.js.map +1 -0
  10. package/lib/data/fields.d.ts +8 -0
  11. package/lib/data/fields.d.ts.map +1 -0
  12. package/lib/data/fields.js +47 -0
  13. package/lib/data/fields.js.map +1 -0
  14. package/lib/data/model.d.ts +79 -0
  15. package/lib/data/model.d.ts.map +1 -0
  16. package/lib/data/model.js +44 -0
  17. package/lib/data/model.js.map +1 -0
  18. package/lib/data/partial.d.ts +26 -0
  19. package/lib/data/partial.d.ts.map +1 -0
  20. package/lib/data/partial.js +3 -0
  21. package/lib/data/partial.js.map +1 -0
  22. package/lib/data/request.d.ts +104 -0
  23. package/lib/data/request.d.ts.map +1 -0
  24. package/lib/data/request.js +3 -0
  25. package/lib/data/request.js.map +1 -0
  26. package/lib/data/type-util.d.ts +20 -0
  27. package/lib/data/type-util.d.ts.map +1 -0
  28. package/lib/data/type-util.js +3 -0
  29. package/lib/data/type-util.js.map +1 -0
  30. package/lib/index.d.ts +5 -0
  31. package/lib/index.d.ts.map +1 -0
  32. package/lib/index.js +21 -0
  33. package/lib/index.js.map +1 -0
  34. package/lib/instruction.d.ts +13 -0
  35. package/lib/instruction.d.ts.map +1 -0
  36. package/lib/instruction.js +24 -0
  37. package/lib/instruction.js.map +1 -0
  38. package/lib/rpc/client.d.ts +17 -0
  39. package/lib/rpc/client.d.ts.map +1 -0
  40. package/lib/rpc/client.js +15 -0
  41. package/lib/rpc/client.js.map +1 -0
  42. package/lib/rpc/filter.d.ts +4 -0
  43. package/lib/rpc/filter.d.ts.map +1 -0
  44. package/lib/rpc/filter.js +337 -0
  45. package/lib/rpc/filter.js.map +1 -0
  46. package/lib/rpc/mapping.d.ts +5 -0
  47. package/lib/rpc/mapping.d.ts.map +1 -0
  48. package/lib/rpc/mapping.js +13 -0
  49. package/lib/rpc/mapping.js.map +1 -0
  50. package/lib/rpc/project.d.ts +5 -0
  51. package/lib/rpc/project.d.ts.map +1 -0
  52. package/lib/rpc/project.js +61 -0
  53. package/lib/rpc/project.js.map +1 -0
  54. package/lib/rpc/source.d.ts +15 -0
  55. package/lib/rpc/source.d.ts.map +1 -0
  56. package/lib/rpc/source.js +82 -0
  57. package/lib/rpc/source.js.map +1 -0
  58. package/lib/source.d.ts +107 -0
  59. package/lib/source.d.ts.map +1 -0
  60. package/lib/source.js +280 -0
  61. package/lib/source.js.map +1 -0
  62. package/package.json +38 -0
  63. package/src/archive/schema.ts +137 -0
  64. package/src/archive/source.ts +105 -0
  65. package/src/data/fields.ts +50 -0
  66. package/src/data/model.ts +154 -0
  67. package/src/data/partial.ts +31 -0
  68. package/src/data/request.ts +138 -0
  69. package/src/data/type-util.ts +42 -0
  70. package/src/index.ts +4 -0
  71. package/src/instruction.ts +28 -0
  72. package/src/rpc/client.ts +26 -0
  73. package/src/rpc/filter.ts +351 -0
  74. package/src/rpc/mapping.ts +13 -0
  75. package/src/rpc/project.ts +61 -0
  76. package/src/rpc/source.ts +90 -0
  77. package/src/source.ts +392 -0
package/src/source.ts ADDED
@@ -0,0 +1,392 @@
1
+ import {HttpAgent, HttpClient} from '@subsquid/http-client'
2
+ import {BlockInfo} from '@subsquid/solana-rpc'
3
+ import {Base58Bytes} from '@subsquid/solana-rpc-data'
4
+ import {addErrorContext, def, 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
+ FiniteRange,
10
+ getSize,
11
+ mergeRangeRequests,
12
+ Range,
13
+ RangeRequest,
14
+ RangeRequestList
15
+ } from '@subsquid/util-internal-range'
16
+ import assert from 'assert'
17
+ import {SolanaArchive} from './archive/source'
18
+ import {getFields} from './data/fields'
19
+ import {Block, BlockHeader, FieldSelection} from './data/model'
20
+ import {PartialBlock} from './data/partial'
21
+ import {
22
+ BalanceRequest,
23
+ DataRequest,
24
+ InstructionRequest,
25
+ LogRequest,
26
+ RewardRequest,
27
+ TokenBalanceRequest,
28
+ TransactionRequest
29
+ } from './data/request'
30
+ import {SolanaRpcClient} from './rpc/client'
31
+ import {RpcDataSource} from './rpc/source'
32
+
33
+
34
+ export interface GatewaySettings {
35
+ /**
36
+ * Subsquid Network Gateway url
37
+ */
38
+ url: string
39
+ /**
40
+ * Request timeout in ms
41
+ */
42
+ requestTimeout?: number
43
+ }
44
+
45
+
46
+ export interface RpcSettings {
47
+ /**
48
+ * RPC client
49
+ */
50
+ client: SolanaRpcClient
51
+ /**
52
+ * `getBlock` batch call size.
53
+ *
54
+ * Default is `5`.
55
+ */
56
+ strideSize?: number
57
+ /**
58
+ * Maximum number of concurrent `getBlock` batch calls.
59
+ *
60
+ * Default is `10`
61
+ */
62
+ strideConcurrency?: number
63
+ /**
64
+ * Minimum distance from finalized head below which concurrent
65
+ * fetch procedure is allowed.
66
+ *
67
+ * Default is `50` blocks.
68
+ *
69
+ * Concurrent fetch procedure can perform multiple `getBlock` batch calls simultaneously and is faster,
70
+ * but assumes consistent behaviour of RPC endpoint.
71
+ *
72
+ * The latter might not be the case due to load balancing,
73
+ * when one request is sent to node `A` with head slot `X` and
74
+ * another to node `B` with head slot `X - 10`.
75
+ */
76
+ concurrentFetchThreshold?: number
77
+ }
78
+
79
+
80
+ interface BlockRange {
81
+ range?: Range
82
+ }
83
+
84
+
85
+ export class DataSourceBuilder<F extends FieldSelection = {}> {
86
+ private requests: RangeRequest<DataRequest>[] = []
87
+ private fields?: FieldSelection
88
+ private blockRange?: Range
89
+ private archive?: GatewaySettings
90
+ private rpc?: RpcSettings
91
+
92
+ /**
93
+ * Set Subsquid Network Gateway endpoint (ex Archive).
94
+ *
95
+ * Subsquid Network allows to get data from finalized blocks up to
96
+ * infinite times faster and more efficient than via regular RPC.
97
+ *
98
+ * @example
99
+ * source.setGateway('https://v2.archive.subsquid.io/network/solana-mainnet')
100
+ */
101
+ setGateway(url: string | GatewaySettings): this {
102
+ if (typeof url == 'string') {
103
+ this.archive = {url}
104
+ } else {
105
+ this.archive = url
106
+ }
107
+ return this
108
+ }
109
+
110
+ /**
111
+ * Set up RPC data ingestion
112
+ */
113
+ setRpc(settings?: RpcSettings): this {
114
+ this.rpc = settings
115
+ return this
116
+ }
117
+
118
+ /**
119
+ * Limits the range of blocks to fetch.
120
+ *
121
+ * Note, that block heights should be used instead of slots.
122
+ */
123
+ setBlockRange(range?: Range): this {
124
+ this.blockRange = range
125
+ return this
126
+ }
127
+
128
+ /**
129
+ * Configure a set of fetched fields
130
+ */
131
+ setFields<F extends FieldSelection>(fields: F): DataSourceBuilder<F> {
132
+ this.fields = fields
133
+ return this as any
134
+ }
135
+
136
+ private add(range: Range | undefined, request: DataRequest): void {
137
+ this.requests.push({
138
+ range: range || {from: 0},
139
+ request
140
+ })
141
+ }
142
+
143
+ /**
144
+ * By default, blocks that doesn't contain requested items can be omitted.
145
+ * This method modifies such behaviour to fetch all chain blocks.
146
+ *
147
+ * Optionally a range of blocks can be specified
148
+ * for which the setting should be effective.
149
+ */
150
+ includeAllBlocks(range?: Range): this {
151
+ this.add(range, {includeAllBlocks: true})
152
+ return this
153
+ }
154
+
155
+ addTransaction(options: TransactionRequest & BlockRange): this {
156
+ let {range, ...req} = options
157
+ this.add(range, {
158
+ transactions: [req]
159
+ })
160
+ return this
161
+ }
162
+
163
+ addInstruction(options: InstructionRequest & BlockRange): this {
164
+ let {range, ...req} = options
165
+ this.add(range, {
166
+ instructions: [req]
167
+ })
168
+ return this
169
+ }
170
+
171
+ addLog(options: LogRequest & BlockRange): this {
172
+ let {range, ...req} = options
173
+ this.add(range, {
174
+ logs: [req]
175
+ })
176
+ return this
177
+ }
178
+
179
+ addBalance(options: BalanceRequest & BlockRange): this {
180
+ let {range, ...req} = options
181
+ this.add(range, {
182
+ balances: [req]
183
+ })
184
+ return this
185
+ }
186
+
187
+ addTokenBalance(options: TokenBalanceRequest & BlockRange): this {
188
+ let {range, ...req} = options
189
+ this.add(range, {
190
+ tokenBalances: [req]
191
+ })
192
+ return this
193
+ }
194
+
195
+ addReward(options: RewardRequest & BlockRange): this {
196
+ let {range, ...req} = options
197
+ this.add(range, {
198
+ rewards: [req]
199
+ })
200
+ return this
201
+ }
202
+
203
+ private getRequests(): RangeRequestList<DataRequest> {
204
+ function concat<T>(a?: T[], b?: T[]): T[] | undefined {
205
+ let result: T[] = []
206
+ if (a) {
207
+ result.push(...a)
208
+ }
209
+ if (b) {
210
+ result.push(...b)
211
+ }
212
+ return result.length == 0 ? undefined : result
213
+ }
214
+
215
+ let requests = mergeRangeRequests(this.requests, (a, b) => {
216
+ return {
217
+ includeAllBlocks: a.includeAllBlocks || b.includeAllBlocks,
218
+ transactions: concat(a.transactions, b.transactions),
219
+ instructions: concat(a.instructions, b.instructions),
220
+ logs: concat(a.logs, b.logs),
221
+ balances: concat(a.balances, b.balances),
222
+ tokenBalances: concat(a.tokenBalances, b.tokenBalances),
223
+ rewards: concat(a.rewards, b.rewards)
224
+ }
225
+ })
226
+
227
+ let fields = getFields(this.fields)
228
+
229
+ requests = requests.map(({range, request}) => {
230
+ return {
231
+ range,
232
+ request: {
233
+ fields,
234
+ ...request
235
+ }
236
+ }
237
+ })
238
+
239
+ return applyRangeBound(requests, this.blockRange)
240
+ }
241
+
242
+ build(): DataSource<Block<F>> {
243
+ return new SolanaDataSource(
244
+ this.getRequests(),
245
+ this.archive,
246
+ this.rpc
247
+ ) as DataSource<Block<F>>
248
+ }
249
+ }
250
+
251
+
252
+ export interface DataSource<Block> {
253
+ getFinalizedHeight(): Promise<number>
254
+ getBlockHash(height: number): Promise<Base58Bytes | undefined>
255
+ getBlocksCountInRange(range: FiniteRange): number
256
+ getBlockStream(fromBlockHeight?: number): AsyncIterable<Block[]>
257
+ }
258
+
259
+
260
+ export type GetDataSourceBlock<T> = T extends DataSource<infer B> ? B : never
261
+
262
+
263
+ class SolanaDataSource implements DataSource<PartialBlock> {
264
+ private rpc?: RpcDataSource
265
+ private isConsistent?: boolean
266
+ private ranges: Range[]
267
+
268
+ constructor(
269
+ private requests: RangeRequestList<DataRequest>,
270
+ private archiveSettings?: GatewaySettings,
271
+ rpcSettings?: RpcSettings
272
+ ) {
273
+ assert(this.archiveSettings || rpcSettings, 'either archive or RPC should be provided')
274
+ if (rpcSettings) {
275
+ this.rpc = new RpcDataSource(rpcSettings)
276
+ }
277
+ this.ranges = this.requests.map(req => req.range)
278
+ }
279
+
280
+ getFinalizedHeight(): Promise<number> {
281
+ if (this.rpc) {
282
+ return this.rpc.getFinalizedHeight()
283
+ } else {
284
+ return this.createArchive().getFinalizedHeight()
285
+ }
286
+ }
287
+
288
+ async getBlockHash(height: number): Promise<Base58Bytes | undefined> {
289
+ await this.assertConsistency()
290
+ if (this.archiveSettings == null) {
291
+ assert(this.rpc)
292
+ return this.rpc.getBlockHash(height)
293
+ } else {
294
+ let archive = this.createArchive()
295
+ let head = await archive.getFinalizedHeight()
296
+ if (head >= height) return archive.getBlockHash(height)
297
+ if (this.rpc) return this.rpc.getBlockHash(height)
298
+ }
299
+ }
300
+
301
+ private async assertConsistency(): Promise<void> {
302
+ if (this.isConsistent || this.archiveSettings == null || this.rpc == null) return
303
+ let blocks = await this.performConsistencyCheck().catch(err => {
304
+ throw addErrorContext(
305
+ new Error(`Failed to check consistency between Subsquid Gateway and RPC endpoints`),
306
+ {reason: err}
307
+ )
308
+ })
309
+ if (blocks == null) {
310
+ this.isConsistent = true
311
+ } else {
312
+ throw addErrorContext(
313
+ new Error(`Provided Subsquid Gateway and RPC endpoints don't agree on slot ${blocks.archiveBlock.slot}`),
314
+ blocks
315
+ )
316
+ }
317
+ }
318
+
319
+ private async performConsistencyCheck(): Promise<{
320
+ archiveBlock: BlockHeader
321
+ rpcBlock: BlockInfo | null
322
+ } | undefined> {
323
+ let archive = this.createArchive()
324
+ let height = await archive.getFinalizedHeight()
325
+ let archiveBlock = await archive.getBlockHeader(height)
326
+ let rpcBlock = await this.rpc!.getBlockInfo(archiveBlock.slot)
327
+ if (rpcBlock?.blockhash === archiveBlock.hash && rpcBlock.blockHeight === archiveBlock.height) return
328
+ return {archiveBlock, rpcBlock: rpcBlock || null}
329
+ }
330
+
331
+ getBlocksCountInRange(range: FiniteRange): number {
332
+ return getSize(this.ranges, range)
333
+ }
334
+
335
+ async *getBlockStream(fromBlockHeight?: number): AsyncIterable<PartialBlock[]> {
336
+ await this.assertConsistency()
337
+
338
+ let requests = fromBlockHeight == null
339
+ ? this.requests
340
+ : applyRangeBound(this.requests, {from: fromBlockHeight})
341
+
342
+ if (requests.length == 0) return
343
+
344
+ if (this.archiveSettings) {
345
+ let agent = new HttpAgent({keepAlive: true})
346
+ try {
347
+ let archive = this.createArchive(agent)
348
+ let height = await archive.getFinalizedHeight()
349
+ let from = requests[0].range.from
350
+ if (height > from || !this.rpc) {
351
+ for await (let batch of archive.getBlockStream(requests, !!this.rpc)) {
352
+ yield batch
353
+ from = last(batch).header.height + 1
354
+ }
355
+ requests = applyRangeBound(requests, {from})
356
+ }
357
+ } finally {
358
+ agent.close()
359
+ }
360
+ }
361
+
362
+ if (requests.length == 0) return
363
+
364
+ assert(this.rpc)
365
+
366
+ yield* this.rpc.getBlockStream(requests)
367
+ }
368
+
369
+ private createArchive(agent?: HttpAgent): SolanaArchive {
370
+ assert(this.archiveSettings)
371
+
372
+ let http = new HttpClient({
373
+ headers: {
374
+ 'x-squid-id': this.getSquidId()
375
+ },
376
+ agent
377
+ })
378
+
379
+ return new SolanaArchive(
380
+ new ArchiveClient({
381
+ http,
382
+ url: this.archiveSettings.url,
383
+ queryTimeout: this.archiveSettings.requestTimeout,
384
+ })
385
+ )
386
+ }
387
+
388
+ @def
389
+ private getSquidId(): string {
390
+ return getOrGenerateSquidId()
391
+ }
392
+ }