@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.
- package/README.md +4 -0
- package/lib/archive/schema.d.ts +167 -0
- package/lib/archive/schema.d.ts.map +1 -0
- package/lib/archive/schema.js +117 -0
- package/lib/archive/schema.js.map +1 -0
- package/lib/archive/source.d.ts +15 -0
- package/lib/archive/source.d.ts.map +1 -0
- package/lib/archive/source.js +98 -0
- package/lib/archive/source.js.map +1 -0
- package/lib/data/fields.d.ts +8 -0
- package/lib/data/fields.d.ts.map +1 -0
- package/lib/data/fields.js +47 -0
- package/lib/data/fields.js.map +1 -0
- package/lib/data/model.d.ts +79 -0
- package/lib/data/model.d.ts.map +1 -0
- package/lib/data/model.js +44 -0
- package/lib/data/model.js.map +1 -0
- package/lib/data/partial.d.ts +26 -0
- package/lib/data/partial.d.ts.map +1 -0
- package/lib/data/partial.js +3 -0
- package/lib/data/partial.js.map +1 -0
- package/lib/data/request.d.ts +104 -0
- package/lib/data/request.d.ts.map +1 -0
- package/lib/data/request.js +3 -0
- package/lib/data/request.js.map +1 -0
- package/lib/data/type-util.d.ts +20 -0
- package/lib/data/type-util.d.ts.map +1 -0
- package/lib/data/type-util.js +3 -0
- package/lib/data/type-util.js.map +1 -0
- package/lib/index.d.ts +5 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +21 -0
- package/lib/index.js.map +1 -0
- package/lib/instruction.d.ts +13 -0
- package/lib/instruction.d.ts.map +1 -0
- package/lib/instruction.js +24 -0
- package/lib/instruction.js.map +1 -0
- package/lib/rpc/client.d.ts +17 -0
- package/lib/rpc/client.d.ts.map +1 -0
- package/lib/rpc/client.js +15 -0
- package/lib/rpc/client.js.map +1 -0
- package/lib/rpc/filter.d.ts +4 -0
- package/lib/rpc/filter.d.ts.map +1 -0
- package/lib/rpc/filter.js +337 -0
- package/lib/rpc/filter.js.map +1 -0
- package/lib/rpc/mapping.d.ts +5 -0
- package/lib/rpc/mapping.d.ts.map +1 -0
- package/lib/rpc/mapping.js +13 -0
- package/lib/rpc/mapping.js.map +1 -0
- package/lib/rpc/project.d.ts +5 -0
- package/lib/rpc/project.d.ts.map +1 -0
- package/lib/rpc/project.js +61 -0
- package/lib/rpc/project.js.map +1 -0
- package/lib/rpc/source.d.ts +15 -0
- package/lib/rpc/source.d.ts.map +1 -0
- package/lib/rpc/source.js +82 -0
- package/lib/rpc/source.js.map +1 -0
- package/lib/source.d.ts +107 -0
- package/lib/source.d.ts.map +1 -0
- package/lib/source.js +280 -0
- package/lib/source.js.map +1 -0
- package/package.json +38 -0
- package/src/archive/schema.ts +137 -0
- package/src/archive/source.ts +105 -0
- package/src/data/fields.ts +50 -0
- package/src/data/model.ts +154 -0
- package/src/data/partial.ts +31 -0
- package/src/data/request.ts +138 -0
- package/src/data/type-util.ts +42 -0
- package/src/index.ts +4 -0
- package/src/instruction.ts +28 -0
- package/src/rpc/client.ts +26 -0
- package/src/rpc/filter.ts +351 -0
- package/src/rpc/mapping.ts +13 -0
- package/src/rpc/project.ts +61 -0
- package/src/rpc/source.ts +90 -0
- 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
|
+
}
|