@subsquid/solana-stream 1.0.0-portal-api.d5861e → 1.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 (52) hide show
  1. package/lib/builder.d.ts +92 -0
  2. package/lib/builder.d.ts.map +1 -0
  3. package/lib/builder.js +182 -0
  4. package/lib/builder.js.map +1 -0
  5. package/lib/data/fields.d.ts.map +1 -1
  6. package/lib/data/fields.js +0 -14
  7. package/lib/data/fields.js.map +1 -1
  8. package/lib/data/model.d.ts +16 -6
  9. package/lib/data/model.d.ts.map +1 -1
  10. package/lib/data/model.js +5 -0
  11. package/lib/data/model.js.map +1 -1
  12. package/lib/data/request.d.ts +2 -3
  13. package/lib/data/request.d.ts.map +1 -1
  14. package/lib/data/type-util.d.ts +35 -13
  15. package/lib/data/type-util.d.ts.map +1 -1
  16. package/lib/index.d.ts +3 -0
  17. package/lib/index.d.ts.map +1 -1
  18. package/lib/index.js +3 -0
  19. package/lib/index.js.map +1 -1
  20. package/lib/{merge.test.d.ts.map → portal/merge.test.d.ts.map} +1 -1
  21. package/lib/{merge.test.js → portal/merge.test.js} +34 -34
  22. package/lib/portal/merge.test.js.map +1 -0
  23. package/lib/portal/source.d.ts +14 -11
  24. package/lib/portal/source.d.ts.map +1 -1
  25. package/lib/portal/source.js +75 -56
  26. package/lib/portal/source.js.map +1 -1
  27. package/lib/query.d.ts +53 -0
  28. package/lib/query.d.ts.map +1 -0
  29. package/lib/query.js +102 -0
  30. package/lib/query.js.map +1 -0
  31. package/lib/source.d.ts +2 -69
  32. package/lib/source.d.ts.map +1 -1
  33. package/lib/source.js +0 -265
  34. package/lib/source.js.map +1 -1
  35. package/package.json +18 -16
  36. package/src/builder.ts +205 -0
  37. package/src/data/fields.ts +0 -20
  38. package/src/data/model.ts +23 -27
  39. package/src/data/request.ts +2 -3
  40. package/src/data/type-util.ts +39 -37
  41. package/src/index.ts +3 -0
  42. package/src/{merge.test.ts → portal/merge.test.ts} +2 -2
  43. package/src/portal/source.ts +115 -98
  44. package/src/query.ts +112 -0
  45. package/src/source.ts +3 -314
  46. package/lib/merge.test.js.map +0 -1
  47. package/lib/portal/schema.d.ts +0 -167
  48. package/lib/portal/schema.d.ts.map +0 -1
  49. package/lib/portal/schema.js +0 -117
  50. package/lib/portal/schema.js.map +0 -1
  51. package/src/portal/schema.ts +0 -136
  52. /package/lib/{merge.test.d.ts → portal/merge.test.d.ts} +0 -0
@@ -1,130 +1,146 @@
1
1
  import {isForkException as isPortalForkException, PortalClient, solana} from '@subsquid/portal-client'
2
2
  import {maybeLast} from '@subsquid/util-internal'
3
3
  import {
4
+ BlockBatch,
4
5
  BlockRef,
5
- DataSource,
6
- DataSourceStreamOptions,
7
6
  ForkException,
8
- type BlockBatch,
9
- TemplateRegistry,
7
+ StreamRequest,
10
8
  } from '@subsquid/util-internal-data-source'
11
- import {applyRangeBound, FiniteRange, getSize, RangeRequestList, type RangeRequest} from '@subsquid/util-internal-range'
12
9
  import {
13
- Block,
14
- FieldSelection,
15
- type Balance,
16
- type BlockHeader,
17
- type Instruction,
18
- type LogMessage,
19
- type Reward,
20
- type TokenBalance,
21
- type Transaction,
22
- } from '../data/model'
10
+ applyRangeBound,
11
+ FiniteRange,
12
+ getSize,
13
+ RangeRequest,
14
+ RangeRequestList,
15
+ } from '@subsquid/util-internal-range'
16
+ import assert from 'assert'
17
+ import {Block, FieldSelection} from '../data/model'
23
18
  import {DataRequest} from '../data/request'
24
19
  import {
25
- mergeItems,
26
- TX_FILTER_KEYS,
20
+ BALANCE_FILTER_KEYS,
27
21
  INSTRUCTION_FILTER_KEYS,
28
22
  LOG_FILTER_KEYS,
29
- BALANCE_FILTER_KEYS,
30
- TOKEN_BALANCE_FILTER_KEYS,
23
+ mergeItems,
31
24
  REWARD_FILTER_KEYS,
25
+ TOKEN_BALANCE_FILTER_KEYS,
26
+ TX_FILTER_KEYS,
32
27
  } from './merge'
33
- import assert from 'assert'
28
+ import type {SolanaDataSource} from '../source'
34
29
 
35
- export type RangeRequestResolver<F extends FieldSelection> =
36
- | RangeRequestList<DataRequest<F>>
37
- | ((registry?: TemplateRegistry) => RangeRequestList<DataRequest<F>>)
30
+ export type RangeRequestResolver =
31
+ | RangeRequestList<DataRequest>
32
+ | (() => RangeRequestList<DataRequest>)
38
33
 
39
- export class PortalDataSource<F extends FieldSelection> implements DataSource<Block<F>> {
34
+ export class PortalSolanaDataSource<F extends FieldSelection> implements SolanaDataSource<F> {
40
35
  constructor(
41
36
  private client: PortalClient,
42
- private requests: RangeRequestResolver<F>,
43
- private opts?: {squidId?: string}
37
+ private fields: FieldSelection,
38
+ private requests: RangeRequestResolver,
39
+ private opts?: {squidId?: string},
44
40
  ) {}
45
41
 
46
- getHead(): Promise<BlockRef | undefined> {
47
- return this.client.getHead({headers: this.getHeaders()})
42
+ async getHead(): Promise<BlockRef> {
43
+ let head = await this.client.getHead({headers: this.getHeaders()})
44
+ assert(head, 'portal has no chain head')
45
+ return head
48
46
  }
49
47
 
50
- getFinalizedHead(): Promise<BlockRef | undefined> {
51
- return this.client.getFinalizedHead({headers: this.getHeaders()})
48
+ async getFinalizedHead(): Promise<BlockRef> {
49
+ let head = await this.client.getFinalizedHead({headers: this.getHeaders()})
50
+ assert(head, 'portal has no finalized head')
51
+ return head
52
52
  }
53
53
 
54
- getFinalizedStream(opts?: DataSourceStreamOptions): AsyncIterable<BlockBatch<Block<F>>> {
54
+ getFinalizedStream(opts?: StreamRequest): AsyncIterable<BlockBatch<Block<F>>> {
55
55
  return this._getStream(opts, true)
56
56
  }
57
57
 
58
- getStream(opts?: DataSourceStreamOptions): AsyncIterable<BlockBatch<Block<F>>> {
58
+ getStream(opts?: StreamRequest): AsyncIterable<BlockBatch<Block<F>>> {
59
59
  return this._getStream(opts, false)
60
60
  }
61
61
 
62
62
  getBlocksCountInRange(range: FiniteRange): number {
63
- return getSize(this.resolveRequests().map(r => r.range), range)
63
+ return getSize(
64
+ this.resolveRequests().map((r) => r.range),
65
+ range,
66
+ )
64
67
  }
65
68
 
66
- private resolveRequests(registry?: TemplateRegistry): RangeRequestList<DataRequest<F>> {
67
- return typeof this.requests === 'function' ? this.requests(registry) : this.requests
69
+ private resolveRequests(): RangeRequestList<DataRequest> {
70
+ return typeof this.requests === 'function' ? this.requests() : this.requests
68
71
  }
69
72
 
70
- private async *_getStream(
71
- opts?: DataSourceStreamOptions,
72
- finalized?: boolean
73
- ): AsyncIterable<BlockBatch<Block<F>>> {
74
- let requests = applyRangeBound(this.resolveRequests(opts?.templateRegistry), opts?.from != null ? {from: opts.from} : undefined)
73
+ private async *_getStream(opts?: StreamRequest, finalized?: boolean): AsyncIterable<BlockBatch<Block<F>>> {
74
+ let requests = applyRangeBound(this.resolveRequests(), opts?.from != null ? {from: opts.from} : undefined)
75
75
  if (requests.length === 0) return
76
76
 
77
77
  let streamOptions = {request: {headers: this.getHeaders()}}
78
78
  let parentHash = opts?.parentHash
79
-
80
- for (let i = 0; i < requests.length; i++) {
81
- let req = requests[i]
82
- let query = mapRequest(req, parentHash)
83
- let stream = this.client.getStream(query, streamOptions, finalized)
84
-
85
- try {
86
- for await (let {blocks, meta} of stream) {
87
- yield {
88
- blocks: blocks.map((block) => mapBlock(block)),
89
- finalizedHead: getHead(meta.finalizedHeadNumber, meta.finalizedHeadHash),
90
- }
91
-
92
- let lastBlock = maybeLast(blocks)?.header
93
- if (lastBlock != null) {
94
- parentHash = lastBlock.hash
95
- }
79
+ let nextBlock = opts?.from
80
+
81
+ // Stream a request from the portal and keep (parentHash, nextBlock)
82
+ // in sync with the last block of every batch.
83
+ let drain = async function* (
84
+ this: PortalSolanaDataSource<F>,
85
+ req: RangeRequest<DataRequest>,
86
+ ): AsyncIterable<BlockBatch<Block<F>>> {
87
+ for await (let {blocks, meta} of this.client.getStream(
88
+ mapRequest(req, this.fields, parentHash),
89
+ streamOptions,
90
+ finalized,
91
+ )) {
92
+ yield {
93
+ blocks: blocks.map((block) => mapBlock(block)),
94
+ finalizedHead: getHead(meta.finalizedHeadNumber, meta.finalizedHeadHash),
96
95
  }
97
96
 
98
- // finalize range
99
- assert(req.range.to != null)
100
-
101
- let nextReq = requests[i + 1]
102
- let gapRange = {from: req.range.to + 1, to: nextReq ? nextReq.range.from - 1 : undefined}
103
- if (gapRange.from === gapRange.to) continue
104
-
105
- for await (let {blocks, meta} of this.client.getStream(
106
- mapRequest({range: gapRange, request: {fields: req.request.fields}}, parentHash),
107
- streamOptions,
108
- finalized
109
- )) {
110
- let finalizedHead = getHead(meta.finalizedHeadNumber, meta.finalizedHeadHash)
111
- if (finalizedHead && req.range.to <= finalizedHead.number) {
112
- parentHash = undefined
113
- break
114
- }
115
-
116
- let lastBlock = maybeLast(blocks)?.header
117
- if (lastBlock != null) {
118
- parentHash = lastBlock.hash
119
- }
97
+ let lastBlock = maybeLast(blocks)?.header
98
+ if (lastBlock != null) {
99
+ parentHash = lastBlock.hash
100
+ nextBlock = lastBlock.number + 1
101
+ }
102
+ }
103
+ }.bind(this)
104
+
105
+ // Advance the stream until the given block becomes finalized, after
106
+ // which chain-continuity info is no longer needed and is dropped.
107
+ // When `to` is undefined the stream is open-ended and we wait on the
108
+ // current tail (nextBlock - 1).
109
+ let finalize = async (to?: number) => {
110
+ if (nextBlock == null) return
111
+ if (to != null && nextBlock > to) return
112
+ let target = to ?? nextBlock - 1
113
+ for await (let {finalizedHead} of drain({range: {from: nextBlock, to}, request: {}})) {
114
+ if (finalizedHead && target <= finalizedHead.number) {
115
+ parentHash = undefined
116
+ nextBlock = undefined
117
+ return
120
118
  }
121
- } catch (e: unknown) {
122
- if (isPortalForkException(e)) {
123
- throw new ForkException(e.blockNumber, e.parentBlockHash, e.previousBlocks)
119
+ }
120
+ }
121
+
122
+ try {
123
+ for (let req of requests) {
124
+ // Close any gap preceding this query so parentHash lands on
125
+ // req.range.from (or is safely dropped).
126
+ await finalize(req.range.from - 1)
127
+
128
+ if (nextBlock !== req.range.from) {
129
+ parentHash = undefined
124
130
  }
125
131
 
126
- throw e
132
+ yield* drain(req)
127
133
  }
134
+
135
+ // After the last query, wait for its range to finalize before
136
+ // ending the stream.
137
+ await finalize()
138
+ } catch (e: unknown) {
139
+ if (isPortalForkException(e)) {
140
+ throw new ForkException(e.blockNumber, e.parentBlockHash, e.previousBlocks)
141
+ }
142
+
143
+ throw e
128
144
  }
129
145
  }
130
146
 
@@ -140,10 +156,11 @@ function getHead(number?: number | undefined, hash?: string | undefined): BlockR
140
156
  return {number, hash}
141
157
  }
142
158
 
143
- function mapRequest<F extends FieldSelection>(
144
- req: RangeRequest<DataRequest<F>>,
145
- parentBlockHash?: string
146
- ): solana.Query<MapFieldSelection<F>> {
159
+ function mapRequest(
160
+ req: RangeRequest<DataRequest>,
161
+ fields: FieldSelection,
162
+ parentBlockHash?: string,
163
+ ): solana.Query<MapFieldSelection> {
147
164
  let transactions = req.request.transactions?.map((tx) => ({...tx.where, ...tx.include}))
148
165
  let instructions = req.request.instructions?.map((ix) => ({...ix.where, ...ix.include}))
149
166
  let logs = req.request.logs?.map((log) => ({...log.where, ...log.include}))
@@ -155,7 +172,7 @@ function mapRequest<F extends FieldSelection>(
155
172
  fromBlock: req.range.from,
156
173
  toBlock: req.range.to === Infinity ? undefined : req.range.to,
157
174
  parentBlockHash: parentBlockHash,
158
- fields: mapFieldSelection(req.request.fields),
175
+ fields: mapFieldSelection(fields),
159
176
  includeAllBlocks: req.request.includeAllBlocks,
160
177
  transactions: transactions && mergeItems(transactions, TX_FILTER_KEYS),
161
178
  instructions: instructions && mergeItems(instructions, INSTRUCTION_FILTER_KEYS),
@@ -166,21 +183,21 @@ function mapRequest<F extends FieldSelection>(
166
183
  }
167
184
  }
168
185
 
169
- function mapFieldSelection<F extends FieldSelection>(fields?: F) {
186
+ function mapFieldSelection(fields: FieldSelection) {
170
187
  return {
171
- block: fields?.block,
172
- transaction: {...fields?.transaction, transactionIndex: true},
173
- instruction: {...fields?.instruction, transactionIndex: true, instructionAddress: true},
174
- log: {...fields?.log, logIndex: true, transactionIndex: true, instructionAddress: true},
175
- balance: {...fields?.balance, transactionIndex: true, account: true},
176
- tokenBalance: {...fields?.tokenBalance, transactionIndex: true, account: true},
177
- reward: {...fields?.reward, pubkey: true},
188
+ block: fields.block,
189
+ transaction: {...fields.transaction, transactionIndex: true},
190
+ instruction: {...fields.instruction, transactionIndex: true, instructionAddress: true},
191
+ log: {...fields.log, logIndex: true, transactionIndex: true, instructionAddress: true},
192
+ balance: {...fields.balance, transactionIndex: true, account: true},
193
+ tokenBalance: {...fields.tokenBalance, transactionIndex: true, account: true},
194
+ reward: {...fields.reward, pubkey: true},
178
195
  } satisfies solana.FieldSelection
179
196
  }
180
197
 
181
- type MapFieldSelection<F extends FieldSelection> = ReturnType<typeof mapFieldSelection<F>>
198
+ type MapFieldSelection = ReturnType<typeof mapFieldSelection>
182
199
 
183
- export function mapBlock<F extends FieldSelection>(rawBlock: solana.Block<MapFieldSelection<F>>): Block<F> {
200
+ export function mapBlock<F extends FieldSelection>(rawBlock: solana.Block<MapFieldSelection>): Block<F> {
184
201
  let {number, hash, ...hdr} = rawBlock.header
185
202
  let header = {
186
203
  number,
package/src/query.ts ADDED
@@ -0,0 +1,112 @@
1
+ import type {Range, RangeRequest} from '@subsquid/util-internal-range'
2
+ import type {
3
+ BalanceRequest,
4
+ DataRequest,
5
+ InstructionRequest,
6
+ LogRequest,
7
+ RewardRequest,
8
+ TokenBalanceRequest,
9
+ TransactionRequest,
10
+ } from './data/request'
11
+
12
+ /**
13
+ * A single portal query - a set of item filters that share a block range.
14
+ *
15
+ * Produced by {@link QueryBuilder#build} and consumed by
16
+ * {@link DataSourceBuilder#addQuery}.
17
+ */
18
+ export type Query = RangeRequest<DataRequest>
19
+
20
+ /**
21
+ * Builder for a single portal query - a set of item filters that share a block range.
22
+ *
23
+ * Pass a {@link QueryBuilder} or the result of {@link QueryBuilder#build}
24
+ * to {@link DataSourceBuilder#addQuery} to register the query.
25
+ *
26
+ * @example
27
+ * ```ts
28
+ * let query = new QueryBuilder()
29
+ * .setRange({from: 250_000_000})
30
+ * .addInstruction({where: {programId: [PROGRAM_ID]}, include: {logs: true}})
31
+ * .addTransaction({where: {feePayer: [FEE_PAYER]}})
32
+ *
33
+ * dataSource.addQuery(query)
34
+ * ```
35
+ */
36
+ export class QueryBuilder {
37
+ private _range: Range = {from: 0}
38
+ private _request: DataRequest = {}
39
+
40
+ /**
41
+ * Restrict this query to the given block range.
42
+ *
43
+ * When omitted, the query applies from block 0 onwards.
44
+ */
45
+ setRange(range: Range): this {
46
+ this._range = range
47
+ return this
48
+ }
49
+
50
+ /**
51
+ * Fetch every block in the query's range, even blocks without any
52
+ * matching items. Without this flag such blocks may be omitted.
53
+ */
54
+ includeAllBlocks(): this {
55
+ this._request.includeAllBlocks = true
56
+ return this
57
+ }
58
+
59
+ addTransaction(options: TransactionRequest): this {
60
+ ;(this._request.transactions ??= []).push(options)
61
+ return this
62
+ }
63
+
64
+ addInstruction(options: InstructionRequest): this {
65
+ ;(this._request.instructions ??= []).push(options)
66
+ return this
67
+ }
68
+
69
+ addLog(options: LogRequest): this {
70
+ ;(this._request.logs ??= []).push(options)
71
+ return this
72
+ }
73
+
74
+ addBalance(options: BalanceRequest): this {
75
+ ;(this._request.balances ??= []).push(options)
76
+ return this
77
+ }
78
+
79
+ addTokenBalance(options: TokenBalanceRequest): this {
80
+ ;(this._request.tokenBalances ??= []).push(options)
81
+ return this
82
+ }
83
+
84
+ addReward(options: RewardRequest): this {
85
+ ;(this._request.rewards ??= []).push(options)
86
+ return this
87
+ }
88
+
89
+ /**
90
+ * Produce an immutable {@link Query} describing the configured range
91
+ * and filters. Subsequent mutations of this builder do not affect
92
+ * the returned object.
93
+ */
94
+ build(): Query {
95
+ return {
96
+ range: {...this._range},
97
+ request: cloneRequest(this._request),
98
+ }
99
+ }
100
+ }
101
+
102
+ function cloneRequest(request: DataRequest): DataRequest {
103
+ return {
104
+ includeAllBlocks: request.includeAllBlocks,
105
+ transactions: request.transactions?.slice(),
106
+ instructions: request.instructions?.slice(),
107
+ logs: request.logs?.slice(),
108
+ balances: request.balances?.slice(),
109
+ tokenBalances: request.tokenBalances?.slice(),
110
+ rewards: request.rewards?.slice(),
111
+ }
112
+ }