@secondlayer/subgraphs 3.7.2 → 3.7.4
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/dist/src/index.js +7 -11
- package/dist/src/index.js.map +3 -3
- package/dist/src/runtime/block-processor.js +7 -11
- package/dist/src/runtime/block-processor.js.map +3 -3
- package/dist/src/runtime/catchup.js +7 -11
- package/dist/src/runtime/catchup.js.map +3 -3
- package/dist/src/runtime/processor.js +78 -855
- package/dist/src/runtime/processor.js.map +7 -17
- package/dist/src/runtime/reindex.js +7 -11
- package/dist/src/runtime/reindex.js.map +3 -3
- package/dist/src/runtime/reorg.js +7 -11
- package/dist/src/runtime/reorg.js.map +3 -3
- package/dist/src/runtime/replay.js +686 -4
- package/dist/src/runtime/replay.js.map +10 -5
- package/dist/src/service.js +83 -860
- package/dist/src/service.js.map +7 -17
- package/package.json +2 -2
|
@@ -1,16 +1,661 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
3
3
|
|
|
4
|
+
// src/runtime/source-matcher.ts
|
|
5
|
+
var patternCache = new Map;
|
|
6
|
+
function matchPattern(value, pattern) {
|
|
7
|
+
if (!pattern.includes("*"))
|
|
8
|
+
return value === pattern;
|
|
9
|
+
let re = patternCache.get(pattern);
|
|
10
|
+
if (!re) {
|
|
11
|
+
const regex = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
12
|
+
re = new RegExp(`^${regex}$`);
|
|
13
|
+
patternCache.set(pattern, re);
|
|
14
|
+
}
|
|
15
|
+
return re.test(value);
|
|
16
|
+
}
|
|
17
|
+
var EMPTY_SET = new Set;
|
|
18
|
+
function traitAllows(filter, contractId, traitContracts) {
|
|
19
|
+
const trait = filter.trait;
|
|
20
|
+
if (!trait)
|
|
21
|
+
return true;
|
|
22
|
+
if (!contractId)
|
|
23
|
+
return false;
|
|
24
|
+
return (traitContracts.get(trait) ?? EMPTY_SET).has(contractId);
|
|
25
|
+
}
|
|
26
|
+
function assetContract(assetId) {
|
|
27
|
+
return assetId?.split("::")[0];
|
|
28
|
+
}
|
|
29
|
+
function matchFilter(filter, transactions, eventsByTx, traitContracts) {
|
|
30
|
+
const results = [];
|
|
31
|
+
switch (filter.type) {
|
|
32
|
+
case "stx_transfer":
|
|
33
|
+
case "stx_mint":
|
|
34
|
+
case "stx_burn":
|
|
35
|
+
case "stx_lock": {
|
|
36
|
+
const eventType = `${filter.type}_event`;
|
|
37
|
+
for (const tx of transactions) {
|
|
38
|
+
const txEvents = eventsByTx.get(tx.tx_id) ?? [];
|
|
39
|
+
const matched = txEvents.filter((e) => e.type === eventType);
|
|
40
|
+
if (matched.length === 0)
|
|
41
|
+
continue;
|
|
42
|
+
const filtered = matched.filter((e) => {
|
|
43
|
+
const data = e.data;
|
|
44
|
+
if (!data)
|
|
45
|
+
return false;
|
|
46
|
+
if ("sender" in filter && filter.sender) {
|
|
47
|
+
if (!matchPattern(data.sender, filter.sender))
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
if ("recipient" in filter && filter.recipient) {
|
|
51
|
+
if (!matchPattern(data.recipient, filter.recipient))
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
if ("lockedAddress" in filter && filter.lockedAddress) {
|
|
55
|
+
if (!matchPattern(data.locked_address, filter.lockedAddress))
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
if ("minAmount" in filter && filter.minAmount !== undefined) {
|
|
59
|
+
const amount = BigInt(data.amount ?? data.locked_amount ?? "0");
|
|
60
|
+
if (amount < filter.minAmount)
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
if ("maxAmount" in filter && filter.maxAmount !== undefined) {
|
|
64
|
+
const amount = BigInt(data.amount ?? "0");
|
|
65
|
+
if (amount > filter.maxAmount)
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
return true;
|
|
69
|
+
});
|
|
70
|
+
if (filtered.length > 0) {
|
|
71
|
+
results.push({ tx, events: filtered });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
case "ft_transfer":
|
|
77
|
+
case "ft_mint":
|
|
78
|
+
case "ft_burn": {
|
|
79
|
+
const eventType = `${filter.type}_event`;
|
|
80
|
+
for (const tx of transactions) {
|
|
81
|
+
const txEvents = eventsByTx.get(tx.tx_id) ?? [];
|
|
82
|
+
const matched = txEvents.filter((e) => {
|
|
83
|
+
if (e.type !== eventType)
|
|
84
|
+
return false;
|
|
85
|
+
const data = e.data;
|
|
86
|
+
if (!data)
|
|
87
|
+
return false;
|
|
88
|
+
if (filter.assetIdentifier) {
|
|
89
|
+
const assetId = data.asset_identifier;
|
|
90
|
+
if (!assetId || !matchPattern(assetId, filter.assetIdentifier))
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
if (!traitAllows(filter, assetContract(data.asset_identifier), traitContracts))
|
|
94
|
+
return false;
|
|
95
|
+
if ("sender" in filter && filter.sender) {
|
|
96
|
+
if (!matchPattern(data.sender, filter.sender))
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
if ("recipient" in filter && filter.recipient) {
|
|
100
|
+
if (!matchPattern(data.recipient, filter.recipient))
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
if (filter.minAmount !== undefined) {
|
|
104
|
+
const amount = BigInt(data.amount ?? "0");
|
|
105
|
+
if (amount < filter.minAmount)
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
return true;
|
|
109
|
+
});
|
|
110
|
+
if (matched.length > 0) {
|
|
111
|
+
results.push({ tx, events: matched });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
case "nft_transfer":
|
|
117
|
+
case "nft_mint":
|
|
118
|
+
case "nft_burn": {
|
|
119
|
+
const eventType = `${filter.type}_event`;
|
|
120
|
+
for (const tx of transactions) {
|
|
121
|
+
const txEvents = eventsByTx.get(tx.tx_id) ?? [];
|
|
122
|
+
const matched = txEvents.filter((e) => {
|
|
123
|
+
if (e.type !== eventType)
|
|
124
|
+
return false;
|
|
125
|
+
const data = e.data;
|
|
126
|
+
if (!data)
|
|
127
|
+
return false;
|
|
128
|
+
if (filter.assetIdentifier) {
|
|
129
|
+
const assetId = data.asset_identifier;
|
|
130
|
+
if (!assetId || !matchPattern(assetId, filter.assetIdentifier))
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
if (!traitAllows(filter, assetContract(data.asset_identifier), traitContracts))
|
|
134
|
+
return false;
|
|
135
|
+
if ("sender" in filter && filter.sender) {
|
|
136
|
+
if (!matchPattern(data.sender, filter.sender))
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
if ("recipient" in filter && filter.recipient) {
|
|
140
|
+
if (!matchPattern(data.recipient, filter.recipient))
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
return true;
|
|
144
|
+
});
|
|
145
|
+
if (matched.length > 0) {
|
|
146
|
+
results.push({ tx, events: matched });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
case "contract_call": {
|
|
152
|
+
for (const tx of transactions) {
|
|
153
|
+
if (tx.type !== "contract_call")
|
|
154
|
+
continue;
|
|
155
|
+
if (filter.contractId) {
|
|
156
|
+
if (!tx.contract_id || !matchPattern(tx.contract_id, filter.contractId))
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (filter.functionName) {
|
|
160
|
+
if (!tx.function_name || !matchPattern(tx.function_name, filter.functionName))
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (filter.caller) {
|
|
164
|
+
if (!matchPattern(tx.sender, filter.caller))
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (!traitAllows(filter, tx.contract_id, traitContracts))
|
|
168
|
+
continue;
|
|
169
|
+
const txEvents = eventsByTx.get(tx.tx_id) ?? [];
|
|
170
|
+
results.push({ tx, events: txEvents });
|
|
171
|
+
}
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
case "contract_deploy": {
|
|
175
|
+
for (const tx of transactions) {
|
|
176
|
+
if (tx.type !== "smart_contract")
|
|
177
|
+
continue;
|
|
178
|
+
if (filter.deployer) {
|
|
179
|
+
if (!matchPattern(tx.sender, filter.deployer))
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (filter.contractName) {
|
|
183
|
+
const name = tx.contract_id?.split(".")[1] ?? "";
|
|
184
|
+
if (!matchPattern(name, filter.contractName))
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
const txEvents = eventsByTx.get(tx.tx_id) ?? [];
|
|
188
|
+
results.push({ tx, events: txEvents });
|
|
189
|
+
}
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
case "print_event": {
|
|
193
|
+
for (const tx of transactions) {
|
|
194
|
+
const txEvents = eventsByTx.get(tx.tx_id) ?? [];
|
|
195
|
+
const matched = txEvents.filter((e) => {
|
|
196
|
+
if (e.type !== "smart_contract_event" && e.type !== "contract_event")
|
|
197
|
+
return false;
|
|
198
|
+
const data = e.data;
|
|
199
|
+
if (!data)
|
|
200
|
+
return false;
|
|
201
|
+
if (data.topic !== "print")
|
|
202
|
+
return false;
|
|
203
|
+
const printContractId = data.contract_identifier ?? data.contract_id;
|
|
204
|
+
if (filter.contractId) {
|
|
205
|
+
if (!printContractId || !matchPattern(printContractId, filter.contractId))
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
if (!traitAllows(filter, printContractId, traitContracts))
|
|
209
|
+
return false;
|
|
210
|
+
return true;
|
|
211
|
+
});
|
|
212
|
+
if (matched.length > 0) {
|
|
213
|
+
results.push({ tx, events: matched });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return results;
|
|
220
|
+
}
|
|
221
|
+
function matchSources(sources, transactions, events, traitContracts = new Map) {
|
|
222
|
+
const eventsByTx = new Map;
|
|
223
|
+
for (const event of events) {
|
|
224
|
+
const list = eventsByTx.get(event.tx_id) ?? [];
|
|
225
|
+
list.push(event);
|
|
226
|
+
eventsByTx.set(event.tx_id, list);
|
|
227
|
+
}
|
|
228
|
+
const seen = new Set;
|
|
229
|
+
const results = [];
|
|
230
|
+
for (const [sourceName, filter] of Object.entries(sources)) {
|
|
231
|
+
const matches = matchFilter(filter, transactions, eventsByTx, traitContracts);
|
|
232
|
+
for (const match of matches) {
|
|
233
|
+
const dedupeKey = `${match.tx.tx_id}:${sourceName}`;
|
|
234
|
+
if (!seen.has(dedupeKey)) {
|
|
235
|
+
seen.add(dedupeKey);
|
|
236
|
+
results.push({ ...match, sourceName });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return results;
|
|
241
|
+
}
|
|
242
|
+
|
|
4
243
|
// src/runtime/replay.ts
|
|
5
244
|
import { createHash } from "node:crypto";
|
|
6
245
|
import { getTargetDb } from "@secondlayer/shared/db";
|
|
7
246
|
import { getSubscription } from "@secondlayer/shared/db/queries/subscriptions";
|
|
8
|
-
import { logger } from "@secondlayer/shared/logger";
|
|
247
|
+
import { logger as logger2 } from "@secondlayer/shared/logger";
|
|
9
248
|
import { sql } from "kysely";
|
|
10
249
|
|
|
11
250
|
// src/schema/utils.ts
|
|
12
251
|
import { pgSchemaName } from "@secondlayer/shared/db/queries/subgraphs";
|
|
13
252
|
|
|
253
|
+
// src/runtime/block-source.ts
|
|
254
|
+
import { getSourceDb } from "@secondlayer/shared/db";
|
|
255
|
+
import { IndexHttpClient } from "@secondlayer/shared/index-http";
|
|
256
|
+
import { logger } from "@secondlayer/shared/logger";
|
|
257
|
+
|
|
258
|
+
// src/runtime/batch-loader.ts
|
|
259
|
+
async function loadBlockRange(db, fromHeight, toHeight) {
|
|
260
|
+
const [blocks, txs, events] = await Promise.all([
|
|
261
|
+
db.selectFrom("blocks").selectAll().where("height", ">=", fromHeight).where("height", "<=", toHeight).where("canonical", "=", true).execute(),
|
|
262
|
+
db.selectFrom("transactions").selectAll().where("block_height", ">=", fromHeight).where("block_height", "<=", toHeight).execute(),
|
|
263
|
+
db.selectFrom("events").selectAll().where("block_height", ">=", fromHeight).where("block_height", "<=", toHeight).execute()
|
|
264
|
+
]);
|
|
265
|
+
const txsByHeight = new Map;
|
|
266
|
+
for (const tx of txs) {
|
|
267
|
+
const h = Number(tx.block_height);
|
|
268
|
+
const list = txsByHeight.get(h) ?? [];
|
|
269
|
+
list.push(tx);
|
|
270
|
+
txsByHeight.set(h, list);
|
|
271
|
+
}
|
|
272
|
+
const eventsByHeight = new Map;
|
|
273
|
+
for (const evt of events) {
|
|
274
|
+
const h = Number(evt.block_height);
|
|
275
|
+
const list = eventsByHeight.get(h) ?? [];
|
|
276
|
+
list.push(evt);
|
|
277
|
+
eventsByHeight.set(h, list);
|
|
278
|
+
}
|
|
279
|
+
const result = new Map;
|
|
280
|
+
for (const block of blocks) {
|
|
281
|
+
const h = Number(block.height);
|
|
282
|
+
result.set(h, {
|
|
283
|
+
block,
|
|
284
|
+
txs: txsByHeight.get(h) ?? [],
|
|
285
|
+
events: eventsByHeight.get(h) ?? []
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
return result;
|
|
289
|
+
}
|
|
290
|
+
function avgEventsPerBlock(batch) {
|
|
291
|
+
if (batch.size === 0)
|
|
292
|
+
return 0;
|
|
293
|
+
let totalEvents = 0;
|
|
294
|
+
for (const data of batch.values()) {
|
|
295
|
+
totalEvents += data.events.length;
|
|
296
|
+
}
|
|
297
|
+
return totalEvents / batch.size;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// src/runtime/reconstruct.ts
|
|
301
|
+
function isoToUnixSeconds(iso) {
|
|
302
|
+
if (!iso)
|
|
303
|
+
return 0;
|
|
304
|
+
return Math.floor(new Date(iso).getTime() / 1000);
|
|
305
|
+
}
|
|
306
|
+
function reconstructBlock(b) {
|
|
307
|
+
return {
|
|
308
|
+
height: b.block_height,
|
|
309
|
+
hash: b.block_hash,
|
|
310
|
+
parent_hash: b.parent_hash,
|
|
311
|
+
burn_block_height: b.burn_block_height,
|
|
312
|
+
burn_block_hash: b.burn_block_hash,
|
|
313
|
+
timestamp: isoToUnixSeconds(b.block_time),
|
|
314
|
+
canonical: true,
|
|
315
|
+
created_at: new Date(0)
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
function reconstructTransaction(t) {
|
|
319
|
+
return {
|
|
320
|
+
tx_id: t.tx_id,
|
|
321
|
+
block_height: t.block_height,
|
|
322
|
+
tx_index: t.tx_index,
|
|
323
|
+
type: t.tx_type,
|
|
324
|
+
sender: t.sender,
|
|
325
|
+
status: t.status,
|
|
326
|
+
contract_id: t.contract_call?.contract_id ?? t.smart_contract?.contract_id ?? null,
|
|
327
|
+
function_name: t.contract_call?.function_name ?? null,
|
|
328
|
+
function_args: t.contract_call?.function_args_hex ?? [],
|
|
329
|
+
raw_result: t.contract_call?.result_hex ?? null,
|
|
330
|
+
raw_tx: "",
|
|
331
|
+
created_at: new Date(0)
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
function reconstructEvent(e) {
|
|
335
|
+
const base = {
|
|
336
|
+
id: `${e.tx_id}#${e.event_index}`,
|
|
337
|
+
tx_id: e.tx_id,
|
|
338
|
+
block_height: e.block_height,
|
|
339
|
+
event_index: e.event_index,
|
|
340
|
+
created_at: new Date(0)
|
|
341
|
+
};
|
|
342
|
+
switch (e.event_type) {
|
|
343
|
+
case "ft_transfer":
|
|
344
|
+
case "ft_mint":
|
|
345
|
+
case "ft_burn":
|
|
346
|
+
return {
|
|
347
|
+
...base,
|
|
348
|
+
type: `${e.event_type}_event`,
|
|
349
|
+
data: {
|
|
350
|
+
asset_identifier: e.asset_identifier,
|
|
351
|
+
sender: e.sender,
|
|
352
|
+
recipient: e.recipient,
|
|
353
|
+
amount: e.amount
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
case "nft_transfer":
|
|
357
|
+
case "nft_mint":
|
|
358
|
+
case "nft_burn":
|
|
359
|
+
return {
|
|
360
|
+
...base,
|
|
361
|
+
type: `${e.event_type}_event`,
|
|
362
|
+
data: {
|
|
363
|
+
asset_identifier: e.asset_identifier,
|
|
364
|
+
sender: e.sender,
|
|
365
|
+
recipient: e.recipient,
|
|
366
|
+
raw_value: e.value
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
case "stx_transfer":
|
|
370
|
+
case "stx_mint":
|
|
371
|
+
case "stx_burn":
|
|
372
|
+
return {
|
|
373
|
+
...base,
|
|
374
|
+
type: `${e.event_type}_event`,
|
|
375
|
+
data: {
|
|
376
|
+
sender: e.sender,
|
|
377
|
+
recipient: e.recipient,
|
|
378
|
+
amount: e.amount,
|
|
379
|
+
...e.event_type === "stx_transfer" ? { memo: e.memo ?? "" } : {}
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
case "stx_lock":
|
|
383
|
+
return {
|
|
384
|
+
...base,
|
|
385
|
+
type: "stx_lock_event",
|
|
386
|
+
data: {
|
|
387
|
+
locked_address: e.sender,
|
|
388
|
+
locked_amount: e.amount,
|
|
389
|
+
unlock_height: e.payload.unlock_height
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
case "print":
|
|
393
|
+
return {
|
|
394
|
+
...base,
|
|
395
|
+
type: "contract_event",
|
|
396
|
+
data: {
|
|
397
|
+
topic: e.payload.topic,
|
|
398
|
+
contract_identifier: e.contract_id,
|
|
399
|
+
value: e.payload.value,
|
|
400
|
+
raw_value: e.payload.raw_value
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// src/runtime/block-source.ts
|
|
407
|
+
class PostgresBlockSource {
|
|
408
|
+
async getTip() {
|
|
409
|
+
const progress = await getSourceDb().selectFrom("index_progress").selectAll().where("network", "=", process.env.NETWORK ?? "mainnet").executeTakeFirst();
|
|
410
|
+
return progress ? Number(progress.highest_seen_block) : 0;
|
|
411
|
+
}
|
|
412
|
+
loadBlockRange(fromHeight, toHeight) {
|
|
413
|
+
return loadBlockRange(getSourceDb(), fromHeight, toHeight);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
var EVENT_FILTER_TO_INDEX_TYPE = {
|
|
417
|
+
stx_transfer: "stx_transfer",
|
|
418
|
+
stx_mint: "stx_mint",
|
|
419
|
+
stx_burn: "stx_burn",
|
|
420
|
+
stx_lock: "stx_lock",
|
|
421
|
+
ft_transfer: "ft_transfer",
|
|
422
|
+
ft_mint: "ft_mint",
|
|
423
|
+
ft_burn: "ft_burn",
|
|
424
|
+
nft_transfer: "nft_transfer",
|
|
425
|
+
nft_mint: "nft_mint",
|
|
426
|
+
nft_burn: "nft_burn",
|
|
427
|
+
print_event: "print"
|
|
428
|
+
};
|
|
429
|
+
var TX_SOURCE_TYPES = new Set(["contract_call", "contract_deploy"]);
|
|
430
|
+
var ALL_INDEX_EVENT_TYPES = [
|
|
431
|
+
...new Set(Object.values(EVENT_FILTER_TO_INDEX_TYPE))
|
|
432
|
+
];
|
|
433
|
+
function sourceFilters(subgraph) {
|
|
434
|
+
const sources = subgraph.sources;
|
|
435
|
+
return Array.isArray(sources) ? sources : Object.values(sources);
|
|
436
|
+
}
|
|
437
|
+
function indexEventTypesForFilterTypes(filterTypes) {
|
|
438
|
+
if (filterTypes.some((t) => TX_SOURCE_TYPES.has(t))) {
|
|
439
|
+
return ALL_INDEX_EVENT_TYPES;
|
|
440
|
+
}
|
|
441
|
+
const types = new Set;
|
|
442
|
+
for (const t of filterTypes) {
|
|
443
|
+
const indexType = EVENT_FILTER_TO_INDEX_TYPE[t];
|
|
444
|
+
if (indexType)
|
|
445
|
+
types.add(indexType);
|
|
446
|
+
}
|
|
447
|
+
return [...types];
|
|
448
|
+
}
|
|
449
|
+
function referencedIndexEventTypes(subgraph) {
|
|
450
|
+
return indexEventTypesForFilterTypes(sourceFilters(subgraph).map((f) => f.type));
|
|
451
|
+
}
|
|
452
|
+
function isStreamsIndexEligible(subgraph) {
|
|
453
|
+
if (Array.isArray(subgraph.sources))
|
|
454
|
+
return false;
|
|
455
|
+
const filters = sourceFilters(subgraph);
|
|
456
|
+
if (filters.length === 0)
|
|
457
|
+
return false;
|
|
458
|
+
for (const f of filters) {
|
|
459
|
+
const known = EVENT_FILTER_TO_INDEX_TYPE[f.type] || TX_SOURCE_TYPES.has(f.type);
|
|
460
|
+
if (!known)
|
|
461
|
+
return false;
|
|
462
|
+
}
|
|
463
|
+
return true;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
class PublicApiBlockSource {
|
|
467
|
+
http;
|
|
468
|
+
eventTypes;
|
|
469
|
+
constructor(http, eventTypes) {
|
|
470
|
+
this.http = http;
|
|
471
|
+
this.eventTypes = eventTypes;
|
|
472
|
+
}
|
|
473
|
+
getTip() {
|
|
474
|
+
return this.http.getIndexTip();
|
|
475
|
+
}
|
|
476
|
+
async loadBlockRange(fromHeight, toHeight) {
|
|
477
|
+
const [blocks, txs, eventLists] = await Promise.all([
|
|
478
|
+
this.http.walkBlocks(fromHeight, toHeight),
|
|
479
|
+
this.http.walkTransactions(fromHeight, toHeight),
|
|
480
|
+
Promise.all(this.eventTypes.map((t) => this.http.walkEvents(t, fromHeight, toHeight)))
|
|
481
|
+
]);
|
|
482
|
+
const map = new Map;
|
|
483
|
+
for (const b of blocks) {
|
|
484
|
+
map.set(b.block_height, {
|
|
485
|
+
block: reconstructBlock(b),
|
|
486
|
+
txs: [],
|
|
487
|
+
events: []
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
for (const t of txs) {
|
|
491
|
+
map.get(t.block_height)?.txs.push(reconstructTransaction(t));
|
|
492
|
+
}
|
|
493
|
+
for (const list of eventLists) {
|
|
494
|
+
for (const e of list) {
|
|
495
|
+
map.get(e.block_height)?.events.push(reconstructEvent(e));
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
for (const bd of map.values()) {
|
|
499
|
+
bd.txs.sort((a, b) => a.tx_index - b.tx_index);
|
|
500
|
+
bd.events.sort((a, b) => a.event_index - b.event_index);
|
|
501
|
+
}
|
|
502
|
+
return map;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
var postgresBlockSource = new PostgresBlockSource;
|
|
506
|
+
function buildHttpClient() {
|
|
507
|
+
const baseUrl = process.env.SUBGRAPH_INDEX_API_URL ?? process.env.STREAMS_API_URL ?? "http://api:3800";
|
|
508
|
+
return new IndexHttpClient({
|
|
509
|
+
indexBaseUrl: baseUrl,
|
|
510
|
+
streamsBaseUrl: baseUrl,
|
|
511
|
+
streamsApiKey: process.env.STREAMS_INTERNAL_API_KEY ?? "sk-sl_streams_l2_internal"
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
function resolveBlockSource(subgraph) {
|
|
515
|
+
if (process.env.SUBGRAPH_SOURCE === "streams-index" && subgraph && isStreamsIndexEligible(subgraph)) {
|
|
516
|
+
return new PublicApiBlockSource(buildHttpClient(), referencedIndexEventTypes(subgraph));
|
|
517
|
+
}
|
|
518
|
+
if (process.env.SUBGRAPH_SOURCE === "streams-index" && subgraph) {
|
|
519
|
+
logger.debug("Subgraph not streams-index eligible, using DB tap", {
|
|
520
|
+
subgraph: subgraph.name
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
return postgresBlockSource;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// src/runtime/trigger-evaluator.ts
|
|
527
|
+
import { resolveTraitContractIds } from "@secondlayer/shared/db/queries/contracts";
|
|
528
|
+
var TX_LEVEL_TRIGGER_TYPES = new Set(["contract_call", "contract_deploy"]);
|
|
529
|
+
function sourceKey(subscriptionId, triggerIndex) {
|
|
530
|
+
return `${subscriptionId}#${triggerIndex}`;
|
|
531
|
+
}
|
|
532
|
+
function toAmount(v) {
|
|
533
|
+
return v === undefined ? undefined : BigInt(v);
|
|
534
|
+
}
|
|
535
|
+
function chainTriggerToFilter(trigger) {
|
|
536
|
+
const t = trigger;
|
|
537
|
+
const filter = { ...trigger };
|
|
538
|
+
const minAmount = toAmount(t.minAmount);
|
|
539
|
+
const maxAmount = toAmount(t.maxAmount);
|
|
540
|
+
if (minAmount !== undefined)
|
|
541
|
+
filter.minAmount = minAmount;
|
|
542
|
+
if (maxAmount !== undefined)
|
|
543
|
+
filter.maxAmount = maxAmount;
|
|
544
|
+
return filter;
|
|
545
|
+
}
|
|
546
|
+
function triggersOf(sub) {
|
|
547
|
+
return sub.triggers ?? [];
|
|
548
|
+
}
|
|
549
|
+
function buildSourcesMap(chainSubs) {
|
|
550
|
+
const sources = {};
|
|
551
|
+
const keyMeta = new Map;
|
|
552
|
+
for (const sub of chainSubs) {
|
|
553
|
+
triggersOf(sub).forEach((trigger, triggerIndex) => {
|
|
554
|
+
const key = sourceKey(sub.id, triggerIndex);
|
|
555
|
+
sources[key] = chainTriggerToFilter(trigger);
|
|
556
|
+
keyMeta.set(key, {
|
|
557
|
+
subscriptionId: sub.id,
|
|
558
|
+
triggerIndex,
|
|
559
|
+
triggerType: trigger.type
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
return { sources, keyMeta };
|
|
564
|
+
}
|
|
565
|
+
function referencedEventTypes(chainSubs) {
|
|
566
|
+
const filterTypes = new Set;
|
|
567
|
+
for (const sub of chainSubs) {
|
|
568
|
+
for (const trigger of triggersOf(sub))
|
|
569
|
+
filterTypes.add(trigger.type);
|
|
570
|
+
}
|
|
571
|
+
return indexEventTypesForFilterTypes([...filterTypes]);
|
|
572
|
+
}
|
|
573
|
+
function referencedTraits(chainSubs) {
|
|
574
|
+
const traits = new Set;
|
|
575
|
+
for (const sub of chainSubs) {
|
|
576
|
+
for (const trigger of triggersOf(sub)) {
|
|
577
|
+
const trait = trigger.trait;
|
|
578
|
+
if (trait)
|
|
579
|
+
traits.add(trait);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
return [...traits];
|
|
583
|
+
}
|
|
584
|
+
async function buildTraitContracts(db, chainSubs, asOfBlock) {
|
|
585
|
+
const resolved = new Map;
|
|
586
|
+
for (const trait of referencedTraits(chainSubs)) {
|
|
587
|
+
const ids = await resolveTraitContractIds(db, trait, asOfBlock);
|
|
588
|
+
resolved.set(trait, new Set(ids));
|
|
589
|
+
}
|
|
590
|
+
return resolved;
|
|
591
|
+
}
|
|
592
|
+
function evaluateBlock(block, sources, traitContracts) {
|
|
593
|
+
return matchSources(sources, block.txs, block.events, traitContracts);
|
|
594
|
+
}
|
|
595
|
+
function chainDedupKey(subscriptionId, txId, eventIndex, blockHash, replayId) {
|
|
596
|
+
const base = `chain:${subscriptionId}:${txId}:${eventIndex}:${blockHash}`;
|
|
597
|
+
return replayId ? `replay:${replayId}:${base}` : base;
|
|
598
|
+
}
|
|
599
|
+
function applyRow(meta, blockHeight, blockHash, txId, eventIndex, event, replayId) {
|
|
600
|
+
const payload = {
|
|
601
|
+
action: "apply",
|
|
602
|
+
block_hash: blockHash,
|
|
603
|
+
block_height: blockHeight,
|
|
604
|
+
tx_id: txId,
|
|
605
|
+
canonical: true,
|
|
606
|
+
trigger: meta.triggerType,
|
|
607
|
+
event
|
|
608
|
+
};
|
|
609
|
+
return {
|
|
610
|
+
subscription_id: meta.subscriptionId,
|
|
611
|
+
kind: "chain",
|
|
612
|
+
subgraph_name: null,
|
|
613
|
+
table_name: null,
|
|
614
|
+
block_height: blockHeight,
|
|
615
|
+
tx_id: txId,
|
|
616
|
+
row_pk: { tx_id: txId, event_index: eventIndex },
|
|
617
|
+
event_type: `chain.${meta.triggerType}.apply`,
|
|
618
|
+
payload,
|
|
619
|
+
dedup_key: chainDedupKey(meta.subscriptionId, txId, eventIndex, blockHash, replayId),
|
|
620
|
+
...replayId ? { is_replay: true } : {}
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
async function emitChainOutbox(db, matches, keyMeta, blockHeight, blockHash, opts) {
|
|
624
|
+
const replayId = opts?.replayId;
|
|
625
|
+
const rows = [];
|
|
626
|
+
for (const match of matches) {
|
|
627
|
+
const meta = keyMeta.get(match.sourceName);
|
|
628
|
+
if (!meta)
|
|
629
|
+
continue;
|
|
630
|
+
const txId = match.tx.tx_id;
|
|
631
|
+
if (TX_LEVEL_TRIGGER_TYPES.has(meta.triggerType)) {
|
|
632
|
+
rows.push(applyRow(meta, blockHeight, blockHash, txId, -1, {
|
|
633
|
+
tx_id: txId,
|
|
634
|
+
type: match.tx.type,
|
|
635
|
+
sender: match.tx.sender,
|
|
636
|
+
status: match.tx.status,
|
|
637
|
+
contract_id: match.tx.contract_id ?? null,
|
|
638
|
+
function_name: match.tx.function_name ?? null,
|
|
639
|
+
function_args: match.tx.function_args ?? null,
|
|
640
|
+
result_hex: match.tx.raw_result ?? null
|
|
641
|
+
}, replayId));
|
|
642
|
+
} else {
|
|
643
|
+
for (const event of match.events) {
|
|
644
|
+
rows.push(applyRow(meta, blockHeight, blockHash, txId, event.event_index, {
|
|
645
|
+
tx_id: txId,
|
|
646
|
+
type: event.type,
|
|
647
|
+
event_index: event.event_index,
|
|
648
|
+
data: event.data
|
|
649
|
+
}, replayId));
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
if (rows.length === 0)
|
|
654
|
+
return 0;
|
|
655
|
+
const result = await db.insertInto("subscription_outbox").values(rows).onConflict((oc) => oc.columns(["subscription_id", "dedup_key"]).doNothing()).executeTakeFirst();
|
|
656
|
+
return Number(result.numInsertedOrUpdatedRows ?? 0);
|
|
657
|
+
}
|
|
658
|
+
|
|
14
659
|
// src/runtime/replay.ts
|
|
15
660
|
var BATCH_SIZE = 500;
|
|
16
661
|
function replayDedupKey(subgraphName, tableName, row, replayId) {
|
|
@@ -46,10 +691,13 @@ async function replaySubscription(input) {
|
|
|
46
691
|
const sub = await getSubscription(db, input.accountId, input.subscriptionId);
|
|
47
692
|
if (!sub)
|
|
48
693
|
throw new Error("Subscription not found");
|
|
694
|
+
if (sub.kind === "chain") {
|
|
695
|
+
return replayChainSubscription(db, sub, input);
|
|
696
|
+
}
|
|
49
697
|
const subgraphName = sub.subgraph_name;
|
|
50
698
|
const tableName = sub.table_name;
|
|
51
699
|
if (sub.kind !== "subgraph" || !subgraphName || !tableName) {
|
|
52
|
-
throw new Error("replay is only supported for subgraph subscriptions");
|
|
700
|
+
throw new Error("replay is only supported for subgraph or chain subscriptions");
|
|
53
701
|
}
|
|
54
702
|
const schema = await resolveSchemaName(db, subgraphName);
|
|
55
703
|
const replayId = deterministicReplayId(sub.id, input.fromBlock, input.toBlock, input.replayIdSuffix);
|
|
@@ -87,7 +735,41 @@ async function replaySubscription(input) {
|
|
|
87
735
|
break;
|
|
88
736
|
offset += BATCH_SIZE;
|
|
89
737
|
}
|
|
90
|
-
|
|
738
|
+
logger2.info("Replay enqueued", {
|
|
739
|
+
subscription: sub.name,
|
|
740
|
+
replayId,
|
|
741
|
+
scanned,
|
|
742
|
+
enqueued,
|
|
743
|
+
fromBlock: input.fromBlock,
|
|
744
|
+
toBlock: input.toBlock
|
|
745
|
+
});
|
|
746
|
+
return { replayId, enqueuedCount: enqueued, scannedCount: scanned };
|
|
747
|
+
}
|
|
748
|
+
var CHAIN_REPLAY_BATCH = 200;
|
|
749
|
+
async function replayChainSubscription(db, sub, input) {
|
|
750
|
+
const replayId = deterministicReplayId(sub.id, input.fromBlock, input.toBlock, input.replayIdSuffix);
|
|
751
|
+
const { sources, keyMeta } = buildSourcesMap([sub]);
|
|
752
|
+
const source = new PublicApiBlockSource(buildHttpClient(), referencedEventTypes([sub]));
|
|
753
|
+
let scanned = 0;
|
|
754
|
+
let enqueued = 0;
|
|
755
|
+
for (let from = input.fromBlock;from <= input.toBlock; from += CHAIN_REPLAY_BATCH) {
|
|
756
|
+
const to = Math.min(from + CHAIN_REPLAY_BATCH - 1, input.toBlock);
|
|
757
|
+
const blocks = await source.loadBlockRange(from, to);
|
|
758
|
+
const traitContracts = await buildTraitContracts(db, [sub], to);
|
|
759
|
+
for (let h = from;h <= to; h++) {
|
|
760
|
+
const bd = blocks.get(h);
|
|
761
|
+
if (!bd)
|
|
762
|
+
continue;
|
|
763
|
+
scanned++;
|
|
764
|
+
const matches = evaluateBlock(bd, sources, traitContracts);
|
|
765
|
+
if (matches.length === 0)
|
|
766
|
+
continue;
|
|
767
|
+
enqueued += await emitChainOutbox(db, matches, keyMeta, h, bd.block.hash, {
|
|
768
|
+
replayId
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
logger2.info("Chain replay enqueued", {
|
|
91
773
|
subscription: sub.name,
|
|
92
774
|
replayId,
|
|
93
775
|
scanned,
|
|
@@ -101,5 +783,5 @@ export {
|
|
|
101
783
|
replaySubscription
|
|
102
784
|
};
|
|
103
785
|
|
|
104
|
-
//# debugId=
|
|
786
|
+
//# debugId=5038D6E43C1A22BA64756E2164756E21
|
|
105
787
|
//# sourceMappingURL=replay.js.map
|