@secondlayer/subgraphs 3.7.2 → 3.7.3

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.
@@ -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
- logger.info("Replay enqueued", {
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=757891FB3BFD957264756E2164756E21
786
+ //# debugId=5038D6E43C1A22BA64756E2164756E21
105
787
  //# sourceMappingURL=replay.js.map