@subsquid/evm-stream 0.0.1-2-0.3887d2

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/src/schema.ts ADDED
@@ -0,0 +1,263 @@
1
+ import {FieldSelection} from './query'
2
+ import {
3
+ array,
4
+ BYTES,
5
+ NAT,
6
+ object,
7
+ oneOf,
8
+ option,
9
+ QTY,
10
+ SMALL_QTY,
11
+ STRING,
12
+ STRING_FLOAT,
13
+ taggedUnion,
14
+ withDefault,
15
+ withSentinel,
16
+ } from '@subsquid/util-internal-validation'
17
+
18
+ export function getBlockHeaderProps(fields: FieldSelection['block'], forArchive: boolean) {
19
+ let natural = forArchive ? NAT : SMALL_QTY
20
+ return {
21
+ number: natural,
22
+ hash: BYTES,
23
+ parentHash: BYTES,
24
+ ...project(fields, {
25
+ nonce: withSentinel('BlockHeader.nonce', '0x', BYTES),
26
+ sha3Uncles: withSentinel('BlockHeader.sha3Uncles', '0x', BYTES),
27
+ logsBloom: withSentinel('BlockHeader.logsBloom', '0x', BYTES),
28
+ transactionsRoot: withSentinel('BlockHeader.transactionsRoot', '0x', BYTES),
29
+ stateRoot: withSentinel('BlockHeader.stateRoot', '0x', BYTES),
30
+ receiptsRoot: withSentinel('BlockHeader.receiptsRoot', '0x', BYTES),
31
+ mixHash: withSentinel('BlockHeader.mixHash', '0x', BYTES),
32
+ miner: withSentinel('BlockHeader.miner', '0x', BYTES),
33
+ difficulty: withSentinel('BlockHeader.difficulty', -1n, QTY),
34
+ totalDifficulty: withSentinel('BlockHeader.totalDifficulty', -1n, QTY),
35
+ extraData: withSentinel('BlockHeader.extraData', '0x', BYTES),
36
+ size: withSentinel('BlockHeader.size', -1, natural),
37
+ gasLimit: withSentinel('BlockHeader.gasLimit', -1n, QTY),
38
+ gasUsed: withSentinel('BlockHeader.gasUsed', -1n, QTY),
39
+ baseFeePerGas: withSentinel('BlockHeader.baseFeePerGas', -1n, QTY),
40
+ timestamp: withSentinel('BlockHeader.timestamp', 0, natural),
41
+ l1BlockNumber: withDefault(0, natural),
42
+ }),
43
+ }
44
+ }
45
+
46
+ export function getTxProps(fields: FieldSelection['transaction'], forArchive: boolean) {
47
+ let natural = forArchive ? NAT : SMALL_QTY
48
+
49
+ let Authorization = object({
50
+ chainId: natural,
51
+ nonce: natural,
52
+ address: BYTES,
53
+ yParity: natural,
54
+ r: BYTES,
55
+ s: BYTES,
56
+ })
57
+
58
+ return {
59
+ transactionIndex: natural,
60
+ ...project(fields, {
61
+ hash: BYTES,
62
+ from: BYTES,
63
+ to: option(BYTES),
64
+ gas: withSentinel('Transaction.gas', -1n, QTY),
65
+ gasPrice: withSentinel('Transaction.gasPrice', -1n, QTY),
66
+ maxFeePerGas: option(QTY),
67
+ maxPriorityFeePerGas: option(QTY),
68
+ input: BYTES,
69
+ nonce: withSentinel('Transaction.nonce', -1, natural),
70
+ value: withSentinel('Transaction.value', -1n, QTY),
71
+ v: withSentinel('Transaction.v', -1n, QTY),
72
+ r: withSentinel('Transaction.r', '0x', BYTES),
73
+ s: withSentinel('Transaction.s', '0x', BYTES),
74
+ yParity: option(natural),
75
+ chainId: option(natural),
76
+ authorizationList: option(array(Authorization)),
77
+ }),
78
+ }
79
+ }
80
+
81
+ export function getTxReceiptProps(fields: FieldSelection['transaction'], forArchive: boolean) {
82
+ let natural = forArchive ? NAT : SMALL_QTY
83
+ return project(fields, {
84
+ gasUsed: withSentinel('Receipt.gasUsed', -1n, QTY),
85
+ cumulativeGasUsed: withSentinel('Receipt.cumulativeGasUsed', -1n, QTY),
86
+ effectiveGasPrice: withSentinel('Receipt.effectiveGasPrice', -1n, QTY),
87
+ contractAddress: option(BYTES),
88
+ type: withSentinel('Receipt.type', -1, natural),
89
+ status: withSentinel('Receipt.status', -1, natural),
90
+ l1Fee: option(QTY),
91
+ l1FeeScalar: option(STRING_FLOAT),
92
+ l1GasPrice: option(QTY),
93
+ l1GasUsed: option(QTY),
94
+ l1BlobBaseFee: option(QTY),
95
+ l1BlobBaseFeeScalar: option(natural),
96
+ l1BaseFeeScalar: option(natural),
97
+ })
98
+ }
99
+
100
+ export function getLogProps(fields: FieldSelection['log'], forArchive: boolean) {
101
+ let natural = forArchive ? NAT : SMALL_QTY
102
+ return {
103
+ logIndex: natural,
104
+ transactionIndex: natural,
105
+ ...project(fields, {
106
+ transactionHash: BYTES,
107
+ address: BYTES,
108
+ data: BYTES,
109
+ topics: array(BYTES),
110
+ }),
111
+ }
112
+ }
113
+
114
+ export function getTraceFrameValidator(fields: FieldSelection['trace'], forArchive: boolean) {
115
+ let traceBase = {
116
+ transactionIndex: forArchive ? NAT : undefined,
117
+ traceAddress: array(NAT),
118
+ ...project(fields, {
119
+ subtraces: NAT,
120
+ error: option(STRING),
121
+ revertReason: option(STRING),
122
+ }),
123
+ }
124
+
125
+ let traceCreateAction = project(
126
+ {
127
+ from: fields?.createFrom || !forArchive,
128
+ value: fields?.createValue,
129
+ gas: fields?.createGas,
130
+ init: fields?.createInit,
131
+ },
132
+ {
133
+ from: BYTES,
134
+ value: QTY,
135
+ gas: QTY,
136
+ init: withDefault('0x', BYTES),
137
+ }
138
+ )
139
+
140
+ let traceCreateResult = project(
141
+ {
142
+ gasUsed: fields?.createResultGasUsed,
143
+ code: fields?.createResultCode,
144
+ address: fields?.createResultAddress,
145
+ },
146
+ {
147
+ gasUsed: QTY,
148
+ code: withDefault('0x', BYTES),
149
+ address: withDefault('0x0000000000000000000000000000000000000000', BYTES),
150
+ }
151
+ )
152
+
153
+ let TraceCreate = object({
154
+ ...traceBase,
155
+ action: isEmpty(traceCreateAction) ? undefined : object(traceCreateAction),
156
+ result: isEmpty(traceCreateResult) ? undefined : option(object(traceCreateResult)),
157
+ })
158
+
159
+ let traceCallAction = project(
160
+ {
161
+ callType: fields?.callCallType,
162
+ from: forArchive ? fields?.callFrom : true,
163
+ to: forArchive ? fields?.callTo : true,
164
+ value: fields?.callValue,
165
+ gas: fields?.callGas,
166
+ input: forArchive ? fields?.callInput : true,
167
+ sighash: forArchive ? fields?.callSighash : false,
168
+ },
169
+ {
170
+ callType: STRING,
171
+ from: BYTES,
172
+ to: BYTES,
173
+ value: option(QTY),
174
+ gas: QTY,
175
+ input: BYTES,
176
+ sighash: withDefault('0x', BYTES),
177
+ }
178
+ )
179
+
180
+ let traceCallResult = project(
181
+ {
182
+ gasUsed: fields?.callResultGasUsed,
183
+ output: fields?.callResultOutput,
184
+ },
185
+ {
186
+ gasUsed: QTY,
187
+ output: withDefault('0x', BYTES),
188
+ }
189
+ )
190
+
191
+ let TraceCall = object({
192
+ ...traceBase,
193
+ action: isEmpty(traceCallAction) ? undefined : object(traceCallAction),
194
+ result: isEmpty(traceCallResult) ? undefined : option(object(traceCallResult)),
195
+ })
196
+
197
+ let traceSuicideAction = project(
198
+ {
199
+ address: fields?.suicideAddress,
200
+ refundAddress: forArchive ? fields?.suicideRefundAddress : true,
201
+ balance: fields?.suicideBalance,
202
+ },
203
+ {
204
+ address: BYTES,
205
+ refundAddress: BYTES,
206
+ balance: QTY,
207
+ }
208
+ )
209
+
210
+ let TraceSuicide = object({
211
+ ...traceBase,
212
+ action: isEmpty(traceSuicideAction) ? undefined : object(traceSuicideAction),
213
+ })
214
+
215
+ let traceRewardAction = project(
216
+ {
217
+ author: forArchive ? fields?.rewardAuthor : true,
218
+ value: fields?.rewardValue,
219
+ type: fields?.rewardType,
220
+ },
221
+ {
222
+ author: BYTES,
223
+ value: QTY,
224
+ type: STRING,
225
+ }
226
+ )
227
+
228
+ let TraceReward = object({
229
+ ...traceBase,
230
+ action: isEmpty(traceRewardAction) ? undefined : object(traceRewardAction),
231
+ })
232
+
233
+ return taggedUnion('type', {
234
+ create: TraceCreate,
235
+ call: TraceCall,
236
+ suicide: TraceSuicide,
237
+ reward: TraceReward,
238
+ })
239
+ }
240
+
241
+ export function project<T extends object, F extends {[K in keyof T]?: boolean}>(
242
+ fields: F | undefined,
243
+ obj: T
244
+ ): Partial<T> {
245
+ if (fields == null) return {}
246
+ let result: Partial<T> = {}
247
+ let key: keyof T
248
+ for (key in obj) {
249
+ if (fields[key]) {
250
+ result[key] = obj[key]
251
+ }
252
+ }
253
+ return result
254
+ }
255
+
256
+ export function isEmpty(obj: object): boolean {
257
+ for (let _ in obj) {
258
+ return false
259
+ }
260
+ return true
261
+ }
262
+
263
+ export function assertAssignable<A, B extends A>(): void {}
package/src/source.ts ADDED
@@ -0,0 +1,261 @@
1
+ import {applyRangeBound, mergeRangeRequests, Range, RangeRequest} from '@subsquid/util-internal-range'
2
+ import {PortalClient, PortalStreamData} from '@subsquid/portal-client'
3
+ import {weakMemo} from '@subsquid/util-internal'
4
+ import {array, BYTES, cast, NAT, object, STRING, taggedUnion, withDefault} from '@subsquid/util-internal-validation'
5
+ import {
6
+ getBlockHeaderProps,
7
+ getTxProps,
8
+ getTxReceiptProps,
9
+ getLogProps,
10
+ getTraceFrameValidator,
11
+ project,
12
+ } from './schema'
13
+ import {
14
+ BlockData,
15
+ DataRequest,
16
+ EvmQueryOptions,
17
+ FieldSelection,
18
+ mergeDataRequests,
19
+ MergeFieldSelection,
20
+ mergeSelection,
21
+ Response,
22
+ } from './query'
23
+ import {DataSource, DataSourceStream, DataSourceStreamData} from '@subsquid/data-source'
24
+
25
+ export interface EvmPortalDataSourceOptions<Q extends EvmQueryOptions> {
26
+ portal: string | PortalClient
27
+ query: Q
28
+ }
29
+
30
+ export class EvmPortalDataSource<
31
+ Q extends EvmQueryOptions,
32
+ B extends BlockData<GetFields<Q['fields']>> = BlockData<GetFields<Q['fields']>>
33
+ > implements DataSource<B>
34
+ {
35
+ private portal: PortalClient
36
+ private fields: Q['fields']
37
+ private requests: RangeRequest<DataRequest>[]
38
+
39
+ constructor(options: EvmPortalDataSourceOptions<Q>) {
40
+ this.portal = typeof options.portal === 'string' ? new PortalClient({url: options.portal}) : options.portal
41
+ this.fields = options.query.fields
42
+ this.requests = mergeRangeRequests(options.query.requests, mergeDataRequests)
43
+ }
44
+
45
+ getHeight(): Promise<number> {
46
+ return this.portal.getFinalizedHeight()
47
+ }
48
+
49
+ getFinalizedHeight(): Promise<number> {
50
+ return this.portal.getFinalizedHeight()
51
+ }
52
+
53
+ getBlockStream(range?: Range, stopOnHead?: boolean): DataSourceStream<B> {
54
+ let fields = getFields(this.fields)
55
+ let requests = applyRangeBound(this.requests, range)
56
+
57
+ let {writable, readable} = new TransformStream<
58
+ PortalStreamData<BlockData<typeof fields>>,
59
+ DataSourceStreamData<B>
60
+ >({
61
+ transform: async (data, controller) => {
62
+ let blocks = data.map((b) => {
63
+ let block = mapBlock(b, fields)
64
+ Object.defineProperty(block, DataSource.blockRef, {
65
+ value: {hash: block.header.hash, number: block.header.number},
66
+ })
67
+ return block
68
+ })
69
+
70
+ Object.defineProperty(blocks, DataSource.finalizedHead, {
71
+ value: data[PortalClient.finalizedHead],
72
+ })
73
+
74
+ controller.enqueue(blocks as DataSourceStreamData<B>)
75
+ },
76
+ })
77
+
78
+ const ingest = async () => {
79
+ for (let request of requests) {
80
+ let query = {
81
+ type: 'evm',
82
+ fromBlock: request.range.from,
83
+ toBlock: request.range.to,
84
+ fields,
85
+ ...request.request,
86
+ }
87
+
88
+ await this.portal.getFinalizedStream(query, {stopOnHead}).pipeTo(writable, {
89
+ preventClose: true,
90
+ })
91
+ }
92
+ }
93
+
94
+ ingest()
95
+ .then(
96
+ () => writable.close(),
97
+ (reason) => writable.abort(reason)
98
+ )
99
+ .catch(() => {})
100
+
101
+ return readable
102
+ }
103
+ }
104
+
105
+ export const getBlockValidator = weakMemo(<F extends FieldSelection>(fields: F) => {
106
+ let BlockHeader = object(getBlockHeaderProps(fields.block, true))
107
+
108
+ let Transaction = object({
109
+ hash: fields.transaction?.hash ? BYTES : undefined,
110
+ ...getTxProps(fields.transaction, true),
111
+ sighash: fields.transaction?.sighash ? withDefault('0x', BYTES) : undefined,
112
+ ...getTxReceiptProps(fields.transaction, true),
113
+ })
114
+
115
+ let Log = object(getLogProps(fields.log, true))
116
+
117
+ let Trace = getTraceFrameValidator(fields.trace, true)
118
+
119
+ let stateDiffBase = {
120
+ transactionIndex: NAT,
121
+ address: BYTES,
122
+ key: STRING,
123
+ }
124
+
125
+ let StateDiff = taggedUnion('kind', {
126
+ ['=']: object({...stateDiffBase}),
127
+ ['+']: object({...stateDiffBase, ...project(fields.stateDiff, {next: BYTES})}),
128
+ ['*']: object({...stateDiffBase, ...project(fields.stateDiff, {prev: BYTES, next: BYTES})}),
129
+ ['-']: object({...stateDiffBase, ...project(fields.stateDiff, {prev: BYTES})}),
130
+ })
131
+
132
+ return object({
133
+ header: BlockHeader,
134
+ transactions: withDefault([], array(Transaction)),
135
+ logs: withDefault([], array(Log)),
136
+ traces: withDefault([], array(Trace)),
137
+ stateDiffs: withDefault([], array(StateDiff)),
138
+ })
139
+ })
140
+
141
+ export function mapBlock<F extends FieldSelection, B extends BlockData<F> = BlockData<F>>(
142
+ rawBlock: unknown,
143
+ fields: F
144
+ ): B {
145
+ let validator = getBlockValidator(fields)
146
+ let block = cast(validator, rawBlock)
147
+
148
+ // let {number, hash, parentHash, ...hdr} = src.header
149
+ // if (hdr.timestamp) {
150
+ // hdr.timestamp = hdr.timestamp * 1000 // convert to ms
151
+ // }
152
+
153
+ // let header = new BlockHeader(number, hash, parentHash)
154
+ // Object.assign(header, hdr)
155
+
156
+ // let block = new Block(header)
157
+
158
+ // if (src.transactions) {
159
+ // for (let {transactionIndex, ...props} of src.transactions) {
160
+ // let tx = new Transaction(header, transactionIndex)
161
+ // Object.assign(tx, props)
162
+ // block.transactions.push(tx)
163
+ // }
164
+ // }
165
+
166
+ // if (src.logs) {
167
+ // for (let {logIndex, transactionIndex, ...props} of src.logs) {
168
+ // let log = new Log(header, logIndex, transactionIndex)
169
+ // Object.assign(log, props)
170
+ // block.logs.push(log)
171
+ // }
172
+ // }
173
+
174
+ // if (src.traces) {
175
+ // for (let {transactionIndex, traceAddress, type, ...props} of src.traces) {
176
+ // transactionIndex = assertNotNull(transactionIndex)
177
+ // let trace: Trace
178
+ // switch (type) {
179
+ // case 'create':
180
+ // trace = new TraceCreate(header, transactionIndex, traceAddress)
181
+ // break
182
+ // case 'call':
183
+ // trace = new TraceCall(header, transactionIndex, traceAddress)
184
+ // break
185
+ // case 'suicide':
186
+ // trace = new TraceSuicide(header, transactionIndex, traceAddress)
187
+ // break
188
+ // case 'reward':
189
+ // trace = new TraceReward(header, transactionIndex, traceAddress)
190
+ // break
191
+ // default:
192
+ // throw unexpectedCase()
193
+ // }
194
+ // Object.assign(trace, props)
195
+ // block.traces.push(trace)
196
+ // }
197
+ // }
198
+
199
+ // if (src.stateDiffs) {
200
+ // for (let {transactionIndex, address, key, kind, ...props} of src.stateDiffs) {
201
+ // let diff: StateDiff
202
+ // switch (kind) {
203
+ // case '=':
204
+ // diff = new StateDiffNoChange(header, transactionIndex, address, key)
205
+ // break
206
+ // case '+':
207
+ // diff = new StateDiffAdd(header, transactionIndex, address, key)
208
+ // break
209
+ // case '*':
210
+ // diff = new StateDiffChange(header, transactionIndex, address, key)
211
+ // break
212
+ // case '-':
213
+ // diff = new StateDiffDelete(header, transactionIndex, address, key)
214
+ // break
215
+ // default:
216
+ // throw unexpectedCase()
217
+ // }
218
+ // Object.assign(diff, props)
219
+ // block.stateDiffs.push(diff)
220
+ // }
221
+ // }
222
+
223
+ // setUpRelations(block)
224
+
225
+ return block as unknown as B
226
+ }
227
+
228
+ function getFields<T extends FieldSelection>(fields: T): GetFields<T> {
229
+ return mergeSelection(REQUIRED_FIELDS, fields)
230
+ }
231
+
232
+ type GetFields<F extends FieldSelection> = MergeFieldSelection<ReqiredFieldSelection, F>
233
+
234
+ // type A = BlockData<GetFields<{}>>['header']
235
+
236
+ type ReqiredFieldSelection = typeof REQUIRED_FIELDS
237
+
238
+ const REQUIRED_FIELDS = {
239
+ block: {
240
+ number: true,
241
+ hash: true,
242
+ parentHash: true,
243
+ },
244
+ // transaction: {
245
+ // transactionIndex: true,
246
+ // },
247
+ // log: {
248
+ // logIndex: true,
249
+ // transactionIndex: true,
250
+ // },
251
+ // trace: {
252
+ // transactionIndex: true,
253
+ // traceAddress: true,
254
+ // type: true,
255
+ // },
256
+ // stateDiff: {
257
+ // transactionIndex: true,
258
+ // address: true,
259
+ // key: true,
260
+ // },
261
+ } as const satisfies FieldSelection