@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.
- package/lib/builder.d.ts +92 -0
- package/lib/builder.d.ts.map +1 -0
- package/lib/builder.js +182 -0
- package/lib/builder.js.map +1 -0
- package/lib/data/fields.d.ts.map +1 -1
- package/lib/data/fields.js +0 -14
- package/lib/data/fields.js.map +1 -1
- package/lib/data/model.d.ts +16 -6
- package/lib/data/model.d.ts.map +1 -1
- package/lib/data/model.js +5 -0
- package/lib/data/model.js.map +1 -1
- package/lib/data/request.d.ts +2 -3
- package/lib/data/request.d.ts.map +1 -1
- package/lib/data/type-util.d.ts +35 -13
- package/lib/data/type-util.d.ts.map +1 -1
- package/lib/index.d.ts +3 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +3 -0
- package/lib/index.js.map +1 -1
- package/lib/{merge.test.d.ts.map → portal/merge.test.d.ts.map} +1 -1
- package/lib/{merge.test.js → portal/merge.test.js} +34 -34
- package/lib/portal/merge.test.js.map +1 -0
- package/lib/portal/source.d.ts +14 -11
- package/lib/portal/source.d.ts.map +1 -1
- package/lib/portal/source.js +75 -56
- package/lib/portal/source.js.map +1 -1
- package/lib/query.d.ts +53 -0
- package/lib/query.d.ts.map +1 -0
- package/lib/query.js +102 -0
- package/lib/query.js.map +1 -0
- package/lib/source.d.ts +2 -69
- package/lib/source.d.ts.map +1 -1
- package/lib/source.js +0 -265
- package/lib/source.js.map +1 -1
- package/package.json +18 -16
- package/src/builder.ts +205 -0
- package/src/data/fields.ts +0 -20
- package/src/data/model.ts +23 -27
- package/src/data/request.ts +2 -3
- package/src/data/type-util.ts +39 -37
- package/src/index.ts +3 -0
- package/src/{merge.test.ts → portal/merge.test.ts} +2 -2
- package/src/portal/source.ts +115 -98
- package/src/query.ts +112 -0
- package/src/source.ts +3 -314
- package/lib/merge.test.js.map +0 -1
- package/lib/portal/schema.d.ts +0 -167
- package/lib/portal/schema.d.ts.map +0 -1
- package/lib/portal/schema.js +0 -117
- package/lib/portal/schema.js.map +0 -1
- package/src/portal/schema.ts +0 -136
- /package/lib/{merge.test.d.ts → portal/merge.test.d.ts} +0 -0
package/src/portal/source.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
26
|
-
TX_FILTER_KEYS,
|
|
20
|
+
BALANCE_FILTER_KEYS,
|
|
27
21
|
INSTRUCTION_FILTER_KEYS,
|
|
28
22
|
LOG_FILTER_KEYS,
|
|
29
|
-
|
|
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
|
|
28
|
+
import type {SolanaDataSource} from '../source'
|
|
34
29
|
|
|
35
|
-
export type RangeRequestResolver
|
|
36
|
-
| RangeRequestList<DataRequest
|
|
37
|
-
| ((
|
|
30
|
+
export type RangeRequestResolver =
|
|
31
|
+
| RangeRequestList<DataRequest>
|
|
32
|
+
| (() => RangeRequestList<DataRequest>)
|
|
38
33
|
|
|
39
|
-
export class
|
|
34
|
+
export class PortalSolanaDataSource<F extends FieldSelection> implements SolanaDataSource<F> {
|
|
40
35
|
constructor(
|
|
41
36
|
private client: PortalClient,
|
|
42
|
-
private
|
|
43
|
-
private
|
|
37
|
+
private fields: FieldSelection,
|
|
38
|
+
private requests: RangeRequestResolver,
|
|
39
|
+
private opts?: {squidId?: string},
|
|
44
40
|
) {}
|
|
45
41
|
|
|
46
|
-
getHead(): Promise<BlockRef
|
|
47
|
-
|
|
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
|
|
51
|
-
|
|
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?:
|
|
54
|
+
getFinalizedStream(opts?: StreamRequest): AsyncIterable<BlockBatch<Block<F>>> {
|
|
55
55
|
return this._getStream(opts, true)
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
getStream(opts?:
|
|
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(
|
|
63
|
+
return getSize(
|
|
64
|
+
this.resolveRequests().map((r) => r.range),
|
|
65
|
+
range,
|
|
66
|
+
)
|
|
64
67
|
}
|
|
65
68
|
|
|
66
|
-
private resolveRequests(
|
|
67
|
-
return typeof this.requests === 'function' ? 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
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
}
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
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
|
|
144
|
-
req: RangeRequest<DataRequest
|
|
145
|
-
|
|
146
|
-
|
|
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(
|
|
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
|
|
186
|
+
function mapFieldSelection(fields: FieldSelection) {
|
|
170
187
|
return {
|
|
171
|
-
block: fields
|
|
172
|
-
transaction: {...fields
|
|
173
|
-
instruction: {...fields
|
|
174
|
-
log: {...fields
|
|
175
|
-
balance: {...fields
|
|
176
|
-
tokenBalance: {...fields
|
|
177
|
-
reward: {...fields
|
|
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
|
|
198
|
+
type MapFieldSelection = ReturnType<typeof mapFieldSelection>
|
|
182
199
|
|
|
183
|
-
export function mapBlock<F extends FieldSelection>(rawBlock: solana.Block<MapFieldSelection
|
|
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
|
+
}
|