@secondlayer/subgraphs 0.5.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 (46) hide show
  1. package/dist/src/index.d.ts +219 -0
  2. package/dist/src/index.js +956 -0
  3. package/dist/src/index.js.map +22 -0
  4. package/dist/src/runtime/block-processor.d.ts +100 -0
  5. package/dist/src/runtime/block-processor.js +501 -0
  6. package/dist/src/runtime/block-processor.js.map +16 -0
  7. package/dist/src/runtime/catchup.d.ts +78 -0
  8. package/dist/src/runtime/catchup.js +630 -0
  9. package/dist/src/runtime/catchup.js.map +18 -0
  10. package/dist/src/runtime/clarity.d.ts +15 -0
  11. package/dist/src/runtime/clarity.js +41 -0
  12. package/dist/src/runtime/clarity.js.map +10 -0
  13. package/dist/src/runtime/context.d.ts +69 -0
  14. package/dist/src/runtime/context.js +177 -0
  15. package/dist/src/runtime/context.js.map +10 -0
  16. package/dist/src/runtime/processor.d.ts +8 -0
  17. package/dist/src/runtime/processor.js +770 -0
  18. package/dist/src/runtime/processor.js.map +20 -0
  19. package/dist/src/runtime/reindex.d.ts +84 -0
  20. package/dist/src/runtime/reindex.js +738 -0
  21. package/dist/src/runtime/reindex.js.map +19 -0
  22. package/dist/src/runtime/reorg.d.ts +78 -0
  23. package/dist/src/runtime/reorg.js +536 -0
  24. package/dist/src/runtime/reorg.js.map +17 -0
  25. package/dist/src/runtime/runner.d.ts +147 -0
  26. package/dist/src/runtime/runner.js +130 -0
  27. package/dist/src/runtime/runner.js.map +11 -0
  28. package/dist/src/runtime/source-matcher.d.ts +53 -0
  29. package/dist/src/runtime/source-matcher.js +111 -0
  30. package/dist/src/runtime/source-matcher.js.map +11 -0
  31. package/dist/src/runtime/stats.d.ts +24 -0
  32. package/dist/src/runtime/stats.js +75 -0
  33. package/dist/src/runtime/stats.js.map +10 -0
  34. package/dist/src/schema/index.d.ts +117 -0
  35. package/dist/src/schema/index.js +305 -0
  36. package/dist/src/schema/index.js.map +13 -0
  37. package/dist/src/service.d.ts +0 -0
  38. package/dist/src/service.js +780 -0
  39. package/dist/src/service.js.map +21 -0
  40. package/dist/src/types.d.ts +80 -0
  41. package/dist/src/types.js +22 -0
  42. package/dist/src/types.js.map +10 -0
  43. package/dist/src/validate.d.ts +84 -0
  44. package/dist/src/validate.js +59 -0
  45. package/dist/src/validate.js.map +10 -0
  46. package/package.json +49 -0
@@ -0,0 +1,147 @@
1
+ /** Supported column types for subgraph schemas */
2
+ type ColumnType = "text" | "uint" | "int" | "principal" | "boolean" | "timestamp" | "jsonb";
3
+ /** Column definition in a subgraph table */
4
+ interface SubgraphColumn {
5
+ type: ColumnType;
6
+ nullable?: boolean;
7
+ indexed?: boolean;
8
+ search?: boolean;
9
+ default?: string | number | boolean;
10
+ }
11
+ /** Table definition within a subgraph schema */
12
+ interface SubgraphTable {
13
+ columns: Record<string, SubgraphColumn>;
14
+ /** Composite indexes (each entry is an array of column names) */
15
+ indexes?: string[][];
16
+ /** Unique key constraints (each entry is an array of column names). Required for upsert. */
17
+ uniqueKeys?: string[][];
18
+ }
19
+ /** Subgraph schema — maps table names to table definitions */
20
+ type SubgraphSchema = Record<string, SubgraphTable>;
21
+ /** Source filter for what blockchain data this subgraph processes */
22
+ interface SubgraphSource {
23
+ /** Contract principal (e.g., SP000...::contract-name). Supports * wildcards. */
24
+ contract?: string;
25
+ /** Event name/topic to filter on */
26
+ event?: string;
27
+ /** Function name to filter on */
28
+ function?: string;
29
+ /** Transaction type filter (e.g., "stx_transfer", "contract_call") */
30
+ type?: string;
31
+ /** Minimum amount filter (for stx_transfer sources) */
32
+ minAmount?: bigint;
33
+ }
34
+ /** Context passed to subgraph handlers during event processing */
35
+ interface SubgraphContext {
36
+ block: {
37
+ height: number
38
+ hash: string
39
+ timestamp: number
40
+ burnBlockHeight: number
41
+ };
42
+ tx: {
43
+ txId: string
44
+ sender: string
45
+ type: string
46
+ status: string
47
+ };
48
+ insert(table: string, row: Record<string, unknown>): void;
49
+ update(table: string, where: Record<string, unknown>, set: Record<string, unknown>): void;
50
+ upsert(table: string, key: Record<string, unknown>, row: Record<string, unknown>): void;
51
+ delete(table: string, where: Record<string, unknown>): void;
52
+ findOne(table: string, where: Record<string, unknown>): Promise<Record<string, unknown> | null>;
53
+ findMany(table: string, where: Record<string, unknown>): Promise<Record<string, unknown>[]>;
54
+ }
55
+ /** Handler function that processes events and writes to the subgraph */
56
+ type SubgraphHandler = (event: Record<string, unknown>, ctx: SubgraphContext) => Promise<void> | void;
57
+ /** Complete subgraph definition */
58
+ interface SubgraphDefinition {
59
+ /** Unique subgraph name (lowercase, alphanumeric + hyphens) */
60
+ name: string;
61
+ /** Semantic version */
62
+ version?: string;
63
+ /** Human description */
64
+ description?: string;
65
+ /** What blockchain data to process — one or more source filters */
66
+ sources: SubgraphSource[];
67
+ /** Tables in this subgraph */
68
+ schema: SubgraphSchema;
69
+ /** Keyed handler functions — keys match sourceKey() output, "*" is catch-all */
70
+ handlers: Record<string, SubgraphHandler>;
71
+ }
72
+ import { Kysely, Transaction } from "kysely";
73
+ import { Database } from "@secondlayer/shared/db";
74
+ type AnyDb = Kysely<Database> | Transaction<Database>;
75
+ interface BlockMeta {
76
+ height: number;
77
+ hash: string;
78
+ timestamp: number;
79
+ burnBlockHeight: number;
80
+ }
81
+ interface TxMeta {
82
+ txId: string;
83
+ sender: string;
84
+ type: string;
85
+ status: string;
86
+ }
87
+ /**
88
+ * Runtime context passed to subgraph handlers.
89
+ * Batches writes and flushes them atomically at the end of a block.
90
+ * Reads execute immediately against the DB (pre-flush state).
91
+ */
92
+ declare class SubgraphContext2 {
93
+ readonly block: BlockMeta;
94
+ private _tx;
95
+ private readonly db;
96
+ private readonly pgSchemaName;
97
+ private readonly subgraphSchema;
98
+ private readonly ops;
99
+ constructor(db: AnyDb, pgSchemaName: string, subgraphSchema: SubgraphSchema, block: BlockMeta, tx: TxMeta);
100
+ get tx(): TxMeta;
101
+ /** Update the current transaction context (used by runner between events) */
102
+ setTx(tx: TxMeta): void;
103
+ insert(table: string, row: Record<string, unknown>): void;
104
+ update(table: string, where: Record<string, unknown>, set: Record<string, unknown>): void;
105
+ upsert(table: string, key: Record<string, unknown>, row: Record<string, unknown>): void;
106
+ delete(table: string, where: Record<string, unknown>): void;
107
+ findOne(table: string, where: Record<string, unknown>): Promise<Record<string, unknown> | null>;
108
+ findMany(table: string, where: Record<string, unknown>): Promise<Record<string, unknown>[]>;
109
+ /** Number of pending write operations */
110
+ get pendingOps(): number;
111
+ /**
112
+ * Execute all batched writes in a single transaction.
113
+ * Auto-populates _block_height, _tx_id, _created_at on inserts.
114
+ */
115
+ flush(): Promise<number>;
116
+ /** Build SQL statements from write ops */
117
+ private buildStatements;
118
+ private validateTable;
119
+ }
120
+ interface MatchedTx {
121
+ tx: {
122
+ tx_id: string
123
+ type: string
124
+ sender: string
125
+ status: string
126
+ contract_id?: string | null
127
+ function_name?: string | null
128
+ };
129
+ events: {
130
+ id: string
131
+ tx_id: string
132
+ type: string
133
+ event_index: number
134
+ data: unknown
135
+ }[];
136
+ /** Which source produced this match — used for handler dispatch */
137
+ sourceKey: string;
138
+ }
139
+ interface RunResult {
140
+ processed: number;
141
+ errors: number;
142
+ }
143
+ /**\\n* Run a subgraph's keyed handlers against all matched transactions/events.\\n*\\n* Each MatchedTx carries a sourceKey from the matcher. The runner looks up\\n* the corresponding handler in subgraph.handlers, falling back to "*".\\n*\\n* Does NOT flush — caller is responsible for flushing ctx after run.\\n*/
144
+ declare function runHandlers(subgraph: SubgraphDefinition, matched: MatchedTx[], ctx: SubgraphContext2, opts?: {
145
+ errorThreshold?: number
146
+ }): Promise<RunResult>;
147
+ export { runHandlers, RunResult };
@@ -0,0 +1,130 @@
1
+ import { createRequire } from "node:module";
2
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
3
+
4
+ // src/runtime/clarity.ts
5
+ import { cvToJSON, deserializeCV } from "@secondlayer/stacks/clarity";
6
+ function decodeClarityValue(hex) {
7
+ try {
8
+ const cleanHex = hex.startsWith("0x") ? hex.slice(2) : hex;
9
+ const cv = deserializeCV(cleanHex);
10
+ return cvToJSON(cv);
11
+ } catch {
12
+ return hex;
13
+ }
14
+ }
15
+ function decodeEventData(data) {
16
+ if (typeof data === "string" && data.startsWith("0x") && data.length > 10) {
17
+ return decodeClarityValue(data);
18
+ }
19
+ if (Array.isArray(data)) {
20
+ return data.map(decodeEventData);
21
+ }
22
+ if (typeof data === "object" && data !== null) {
23
+ const decoded = {};
24
+ for (const [key, value] of Object.entries(data)) {
25
+ decoded[key] = decodeEventData(value);
26
+ }
27
+ return decoded;
28
+ }
29
+ return data;
30
+ }
31
+ function decodeFunctionArgs(args) {
32
+ return args.map(decodeClarityValue);
33
+ }
34
+
35
+ // src/runtime/runner.ts
36
+ import { logger } from "@secondlayer/shared/logger";
37
+ import { getErrorMessage } from "@secondlayer/shared";
38
+ var DEFAULT_ERROR_THRESHOLD = 50;
39
+ function resolveHandler(handlers, key) {
40
+ return handlers[key] ?? handlers["*"] ?? null;
41
+ }
42
+ async function runHandlers(subgraph, matched, ctx, opts) {
43
+ let processed = 0;
44
+ let errors = 0;
45
+ const threshold = opts?.errorThreshold ?? DEFAULT_ERROR_THRESHOLD;
46
+ for (const { tx, events, sourceKey } of matched) {
47
+ const handler = resolveHandler(subgraph.handlers, sourceKey);
48
+ if (!handler) {
49
+ logger.warn("No handler found for source key", { subgraph: subgraph.name, sourceKey, txId: tx.tx_id });
50
+ continue;
51
+ }
52
+ ctx.setTx({
53
+ txId: tx.tx_id,
54
+ sender: tx.sender,
55
+ type: tx.type,
56
+ status: tx.status
57
+ });
58
+ if (events.length === 0) {
59
+ try {
60
+ const txPayload = {
61
+ tx: {
62
+ txId: tx.tx_id,
63
+ sender: tx.sender,
64
+ type: tx.type,
65
+ status: tx.status,
66
+ contractId: tx.contract_id,
67
+ functionName: tx.function_name
68
+ }
69
+ };
70
+ await handler(txPayload, ctx);
71
+ processed++;
72
+ } catch (err) {
73
+ errors++;
74
+ logger.error("Subgraph handler error", {
75
+ subgraph: subgraph.name,
76
+ sourceKey,
77
+ txId: tx.tx_id,
78
+ error: getErrorMessage(err)
79
+ });
80
+ }
81
+ continue;
82
+ }
83
+ for (const event of events) {
84
+ if (errors >= threshold) {
85
+ logger.error("Subgraph error threshold reached, skipping remaining events", {
86
+ subgraph: subgraph.name,
87
+ errors,
88
+ threshold
89
+ });
90
+ return { processed, errors };
91
+ }
92
+ try {
93
+ const decoded = decodeEventData(event.data);
94
+ const eventPayload = {
95
+ ...decoded,
96
+ _eventId: event.id,
97
+ _eventType: event.type,
98
+ _eventIndex: event.event_index,
99
+ tx: {
100
+ txId: tx.tx_id,
101
+ sender: tx.sender,
102
+ type: tx.type,
103
+ status: tx.status,
104
+ contractId: tx.contract_id,
105
+ functionName: tx.function_name
106
+ }
107
+ };
108
+ await handler(eventPayload, ctx);
109
+ processed++;
110
+ } catch (err) {
111
+ errors++;
112
+ logger.error("Subgraph handler error", {
113
+ subgraph: subgraph.name,
114
+ sourceKey,
115
+ txId: tx.tx_id,
116
+ eventId: event.id,
117
+ eventType: event.type,
118
+ error: getErrorMessage(err)
119
+ });
120
+ }
121
+ }
122
+ }
123
+ return { processed, errors };
124
+ }
125
+ export {
126
+ runHandlers
127
+ };
128
+
129
+ //# debugId=68B81C2CF799988B64756E2164756E21
130
+ //# sourceMappingURL=runner.js.map
@@ -0,0 +1,11 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/runtime/clarity.ts", "../src/runtime/runner.ts"],
4
+ "sourcesContent": [
5
+ "import { cvToJSON, deserializeCV } from \"@secondlayer/stacks/clarity\";\n\n/**\n * Decode a hex-encoded Clarity value to a JS object.\n * Returns original value on failure.\n */\nexport function decodeClarityValue(hex: string): unknown {\n try {\n const cleanHex = hex.startsWith(\"0x\") ? hex.slice(2) : hex;\n const cv = deserializeCV(cleanHex);\n return cvToJSON(cv);\n } catch {\n return hex;\n }\n}\n\n/**\n * Recursively decode all hex-encoded Clarity values in an object.\n * Any string starting with \"0x\" and longer than 10 chars is attempted.\n */\nexport function decodeEventData(data: unknown): unknown {\n if (typeof data === \"string\" && data.startsWith(\"0x\") && data.length > 10) {\n return decodeClarityValue(data);\n }\n\n if (Array.isArray(data)) {\n return data.map(decodeEventData);\n }\n\n if (typeof data === \"object\" && data !== null) {\n const decoded: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(data)) {\n decoded[key] = decodeEventData(value);\n }\n return decoded;\n }\n\n return data;\n}\n\n/**\n * Decode function args array (hex-encoded Clarity values).\n */\nexport function decodeFunctionArgs(args: string[]): unknown[] {\n return args.map(decodeClarityValue);\n}\n",
6
+ "import { logger } from \"@secondlayer/shared/logger\";\nimport { getErrorMessage } from \"@secondlayer/shared\";\nimport type { SubgraphDefinition } from \"../types.ts\";\nimport type { SubgraphContext } from \"./context.ts\";\nimport type { MatchedTx } from \"./source-matcher.ts\";\nimport { decodeEventData } from \"./clarity.ts\";\n\n/** Max consecutive handler errors before marking subgraph as error */\nconst DEFAULT_ERROR_THRESHOLD = 50;\n\nexport interface RunResult {\n processed: number;\n errors: number;\n}\n\n/**\n * Resolve the handler for a matched tx.\n * Looks up by sourceKey first, then falls back to \"*\" catch-all.\n */\nfunction resolveHandler(handlers: SubgraphDefinition[\"handlers\"], key: string) {\n return handlers[key] ?? handlers[\"*\"] ?? null;\n}\n\n/**\n * Run a subgraph's keyed handlers against all matched transactions/events.\n *\n * Each MatchedTx carries a sourceKey from the matcher. The runner looks up\n * the corresponding handler in subgraph.handlers, falling back to \"*\".\n *\n * Does NOT flush — caller is responsible for flushing ctx after run.\n */\nexport async function runHandlers(\n subgraph: SubgraphDefinition,\n matched: MatchedTx[],\n ctx: SubgraphContext,\n opts?: { errorThreshold?: number },\n): Promise<RunResult> {\n let processed = 0;\n let errors = 0;\n const threshold = opts?.errorThreshold ?? DEFAULT_ERROR_THRESHOLD;\n\n for (const { tx, events, sourceKey } of matched) {\n const handler = resolveHandler(subgraph.handlers, sourceKey);\n if (!handler) {\n logger.warn(\"No handler found for source key\", { subgraph: subgraph.name, sourceKey, txId: tx.tx_id });\n continue;\n }\n\n ctx.setTx({\n txId: tx.tx_id,\n sender: tx.sender,\n type: tx.type,\n status: tx.status,\n });\n\n // If no events but tx matched, call handler with tx-level data\n if (events.length === 0) {\n try {\n const txPayload: Record<string, unknown> = {\n tx: {\n txId: tx.tx_id,\n sender: tx.sender,\n type: tx.type,\n status: tx.status,\n contractId: tx.contract_id,\n functionName: tx.function_name,\n },\n };\n await handler(txPayload, ctx);\n processed++;\n } catch (err) {\n errors++;\n logger.error(\"Subgraph handler error\", {\n subgraph: subgraph.name,\n sourceKey,\n txId: tx.tx_id,\n error: getErrorMessage(err),\n });\n }\n continue;\n }\n\n for (const event of events) {\n if (errors >= threshold) {\n logger.error(\"Subgraph error threshold reached, skipping remaining events\", {\n subgraph: subgraph.name,\n errors,\n threshold,\n });\n return { processed, errors };\n }\n\n try {\n const decoded = decodeEventData(event.data) as Record<string, unknown>;\n const eventPayload: Record<string, unknown> = {\n ...decoded,\n _eventId: event.id,\n _eventType: event.type,\n _eventIndex: event.event_index,\n tx: {\n txId: tx.tx_id,\n sender: tx.sender,\n type: tx.type,\n status: tx.status,\n contractId: tx.contract_id,\n functionName: tx.function_name,\n },\n };\n\n await handler(eventPayload, ctx);\n processed++;\n } catch (err) {\n errors++;\n logger.error(\"Subgraph handler error\", {\n subgraph: subgraph.name,\n sourceKey,\n txId: tx.tx_id,\n eventId: event.id,\n eventType: event.type,\n error: getErrorMessage(err),\n });\n }\n }\n }\n\n return { processed, errors };\n}\n"
7
+ ],
8
+ "mappings": "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",
9
+ "debugId": "68B81C2CF799988B64756E2164756E21",
10
+ "names": []
11
+ }
@@ -0,0 +1,53 @@
1
+ /** Source filter for what blockchain data this subgraph processes */
2
+ interface SubgraphSource {
3
+ /** Contract principal (e.g., SP000...::contract-name). Supports * wildcards. */
4
+ contract?: string;
5
+ /** Event name/topic to filter on */
6
+ event?: string;
7
+ /** Function name to filter on */
8
+ function?: string;
9
+ /** Transaction type filter (e.g., "stx_transfer", "contract_call") */
10
+ type?: string;
11
+ /** Minimum amount filter (for stx_transfer sources) */
12
+ minAmount?: bigint;
13
+ }
14
+ interface MatchedTx {
15
+ tx: {
16
+ tx_id: string
17
+ type: string
18
+ sender: string
19
+ status: string
20
+ contract_id?: string | null
21
+ function_name?: string | null
22
+ };
23
+ events: {
24
+ id: string
25
+ tx_id: string
26
+ type: string
27
+ event_index: number
28
+ data: unknown
29
+ }[];
30
+ /** Which source produced this match — used for handler dispatch */
31
+ sourceKey: string;
32
+ }
33
+ type TxRecord = {
34
+ tx_id: string
35
+ type: string
36
+ sender: string
37
+ status: string
38
+ contract_id?: string | null
39
+ function_name?: string | null
40
+ };
41
+ type EventRecord = {
42
+ id: string
43
+ tx_id: string
44
+ type: string
45
+ event_index: number
46
+ data: unknown
47
+ };
48
+ /**
49
+ * Match all sources against a block's transactions and events.
50
+ * Deduplicates by (txId, sourceKey) — each handler key fires at most once per tx.
51
+ */
52
+ declare function matchSources(sources: SubgraphSource[], transactions: TxRecord[], events: EventRecord[]): MatchedTx[];
53
+ export { matchSources, MatchedTx };
@@ -0,0 +1,111 @@
1
+ import { createRequire } from "node:module";
2
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
3
+
4
+ // src/types.ts
5
+ function sourceKey(source) {
6
+ if (source.contract) {
7
+ if (source.function)
8
+ return `${source.contract}::${source.function}`;
9
+ if (source.event)
10
+ return `${source.contract}::${source.event}`;
11
+ return source.contract;
12
+ }
13
+ if (source.type)
14
+ return source.type;
15
+ return "*";
16
+ }
17
+
18
+ // src/runtime/source-matcher.ts
19
+ function matchPattern(value, pattern) {
20
+ if (!pattern.includes("*"))
21
+ return value === pattern;
22
+ const regex = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
23
+ return new RegExp(`^${regex}$`).test(value);
24
+ }
25
+ function matchSource(source, transactions, eventsByTx) {
26
+ const results = [];
27
+ const key = sourceKey(source);
28
+ for (const tx of transactions) {
29
+ if (source.type) {
30
+ if (!matchPattern(tx.type, source.type))
31
+ continue;
32
+ const txEvents = eventsByTx.get(tx.tx_id) ?? [];
33
+ let matchedEvents = txEvents;
34
+ if (source.minAmount !== undefined) {
35
+ const amountEvents = matchedEvents.filter((e) => {
36
+ const data = e.data;
37
+ const rawAmount = data?.amount;
38
+ if (rawAmount === undefined)
39
+ return false;
40
+ const amount = BigInt(rawAmount);
41
+ return amount >= source.minAmount;
42
+ });
43
+ if (amountEvents.length === 0)
44
+ continue;
45
+ matchedEvents = amountEvents;
46
+ }
47
+ results.push({ tx, events: matchedEvents, sourceKey: key });
48
+ continue;
49
+ }
50
+ if (source.contract) {
51
+ const txContractMatch = tx.contract_id && matchPattern(tx.contract_id, source.contract);
52
+ if (source.function && tx.function_name) {
53
+ if (!matchPattern(tx.function_name, source.function))
54
+ continue;
55
+ } else if (source.function && !tx.function_name) {
56
+ continue;
57
+ }
58
+ const txEvents = eventsByTx.get(tx.tx_id) ?? [];
59
+ let matchedEvents = txEvents;
60
+ if (!txContractMatch) {
61
+ matchedEvents = txEvents.filter((e) => {
62
+ const data = e.data;
63
+ const contractIdentifier = data?.contract_identifier;
64
+ return contractIdentifier && matchPattern(contractIdentifier, source.contract);
65
+ });
66
+ if (matchedEvents.length === 0)
67
+ continue;
68
+ }
69
+ if (source.event) {
70
+ matchedEvents = matchedEvents.filter((e) => {
71
+ if (matchPattern(e.type, source.event))
72
+ return true;
73
+ const data = e.data;
74
+ const topic = data?.topic;
75
+ return topic ? matchPattern(topic, source.event) : false;
76
+ });
77
+ }
78
+ if (txContractMatch || matchedEvents.length > 0) {
79
+ results.push({ tx, events: matchedEvents, sourceKey: key });
80
+ }
81
+ }
82
+ }
83
+ return results;
84
+ }
85
+ function matchSources(sources, transactions, events) {
86
+ const eventsByTx = new Map;
87
+ for (const event of events) {
88
+ const list = eventsByTx.get(event.tx_id) ?? [];
89
+ list.push(event);
90
+ eventsByTx.set(event.tx_id, list);
91
+ }
92
+ const seen = new Set;
93
+ const results = [];
94
+ for (const source of sources) {
95
+ const matches = matchSource(source, transactions, eventsByTx);
96
+ for (const match of matches) {
97
+ const dedupeKey = `${match.tx.tx_id}:${match.sourceKey}`;
98
+ if (!seen.has(dedupeKey)) {
99
+ seen.add(dedupeKey);
100
+ results.push(match);
101
+ }
102
+ }
103
+ }
104
+ return results;
105
+ }
106
+ export {
107
+ matchSources
108
+ };
109
+
110
+ //# debugId=A86478F2FF92B95F64756E2164756E21
111
+ //# sourceMappingURL=source-matcher.js.map
@@ -0,0 +1,11 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/types.ts", "../src/runtime/source-matcher.ts"],
4
+ "sourcesContent": [
5
+ "/** Supported column types for subgraph schemas */\nexport type ColumnType =\n | \"text\"\n | \"uint\"\n | \"int\"\n | \"principal\"\n | \"boolean\"\n | \"timestamp\"\n | \"jsonb\";\n\n/** Column definition in a subgraph table */\nexport interface SubgraphColumn {\n type: ColumnType;\n nullable?: boolean;\n indexed?: boolean;\n search?: boolean;\n default?: string | number | boolean;\n}\n\n/** Table definition within a subgraph schema */\nexport interface SubgraphTable {\n columns: Record<string, SubgraphColumn>;\n /** Composite indexes (each entry is an array of column names) */\n indexes?: string[][];\n /** Unique key constraints (each entry is an array of column names). Required for upsert. */\n uniqueKeys?: string[][];\n}\n\n/** Subgraph schema — maps table names to table definitions */\nexport type SubgraphSchema = Record<string, SubgraphTable>;\n\n/** Source filter for what blockchain data this subgraph processes */\nexport interface SubgraphSource {\n /** Contract principal (e.g., SP000...::contract-name). Supports * wildcards. */\n contract?: string;\n /** Event name/topic to filter on */\n event?: string;\n /** Function name to filter on */\n function?: string;\n /** Transaction type filter (e.g., \"stx_transfer\", \"contract_call\") */\n type?: string;\n /** Minimum amount filter (for stx_transfer sources) */\n minAmount?: bigint;\n}\n\n/** Context passed to subgraph handlers during event processing */\nexport interface SubgraphContext {\n block: { height: number; hash: string; timestamp: number; burnBlockHeight: number };\n tx: { txId: string; sender: string; type: string; status: string };\n insert(table: string, row: Record<string, unknown>): void;\n update(table: string, where: Record<string, unknown>, set: Record<string, unknown>): void;\n upsert(table: string, key: Record<string, unknown>, row: Record<string, unknown>): void;\n delete(table: string, where: Record<string, unknown>): void;\n findOne(table: string, where: Record<string, unknown>): Promise<Record<string, unknown> | null>;\n findMany(table: string, where: Record<string, unknown>): Promise<Record<string, unknown>[]>;\n}\n\n/** Handler function that processes events and writes to the subgraph */\nexport type SubgraphHandler = (\n event: Record<string, unknown>,\n ctx: SubgraphContext,\n) => Promise<void> | void;\n\n/**\n * Derive the source key used to look up handlers.\n * - { contract: \"SP123.market\", function: \"list\" } → \"SP123.market::list\"\n * - { contract: \"SP123.market\", event: \"sale\" } → \"SP123.market::sale\"\n * - { contract: \"SP123.market\" } → \"SP123.market\"\n * - { type: \"stx_transfer\" } → \"stx_transfer\"\n */\nexport function sourceKey(source: SubgraphSource): string {\n if (source.contract) {\n if (source.function) return `${source.contract}::${source.function}`;\n if (source.event) return `${source.contract}::${source.event}`;\n return source.contract;\n }\n if (source.type) return source.type;\n return \"*\";\n}\n\n/** Complete subgraph definition */\nexport interface SubgraphDefinition {\n /** Unique subgraph name (lowercase, alphanumeric + hyphens) */\n name: string;\n /** Semantic version */\n version?: string;\n /** Human description */\n description?: string;\n /** What blockchain data to process — one or more source filters */\n sources: SubgraphSource[];\n /** Tables in this subgraph */\n schema: SubgraphSchema;\n /** Keyed handler functions — keys match sourceKey() output, \"*\" is catch-all */\n handlers: Record<string, SubgraphHandler>;\n}\n",
6
+ "import { type SubgraphSource, sourceKey } from \"../types.ts\";\n\nexport interface MatchedTx {\n tx: { tx_id: string; type: string; sender: string; status: string; contract_id?: string | null; function_name?: string | null };\n events: { id: string; tx_id: string; type: string; event_index: number; data: unknown }[];\n /** Which source produced this match — used for handler dispatch */\n sourceKey: string;\n}\n\n/**\n * Check if a string matches a pattern with `*` wildcard support.\n */\nfunction matchPattern(value: string, pattern: string): boolean {\n if (!pattern.includes(\"*\")) return value === pattern;\n const regex = pattern\n .replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\")\n .replace(/\\*/g, \".*\");\n return new RegExp(`^${regex}$`).test(value);\n}\n\ntype TxRecord = { tx_id: string; type: string; sender: string; status: string; contract_id?: string | null; function_name?: string | null };\ntype EventRecord = { id: string; tx_id: string; type: string; event_index: number; data: unknown };\n\n/**\n * Match a single source against transactions and events.\n */\nfunction matchSource(\n source: SubgraphSource,\n transactions: TxRecord[],\n eventsByTx: Map<string, EventRecord[]>,\n): MatchedTx[] {\n const results: MatchedTx[] = [];\n const key = sourceKey(source);\n\n for (const tx of transactions) {\n // Type-based matching (e.g., token_transfer → matches tx.type)\n if (source.type) {\n if (!matchPattern(tx.type, source.type)) continue;\n\n const txEvents = eventsByTx.get(tx.tx_id) ?? [];\n\n // Collect all events for this tx (type-based sources match the whole tx)\n let matchedEvents = txEvents;\n\n // minAmount filter — check events with an amount field\n if (source.minAmount !== undefined) {\n const amountEvents = matchedEvents.filter((e) => {\n const data = e.data as Record<string, unknown> | null;\n const rawAmount = data?.amount as string | number | undefined;\n if (rawAmount === undefined) return false;\n const amount = BigInt(rawAmount);\n return amount >= source.minAmount!;\n });\n if (amountEvents.length === 0) continue;\n matchedEvents = amountEvents;\n }\n\n results.push({ tx, events: matchedEvents, sourceKey: key });\n continue;\n }\n\n // Contract-based matching\n if (source.contract) {\n const txContractMatch = tx.contract_id && matchPattern(tx.contract_id, source.contract);\n\n // Function filter\n if (source.function && tx.function_name) {\n if (!matchPattern(tx.function_name, source.function)) continue;\n } else if (source.function && !tx.function_name) {\n continue;\n }\n\n const txEvents = eventsByTx.get(tx.tx_id) ?? [];\n let matchedEvents = txEvents;\n\n if (!txContractMatch) {\n // Check if any events match the contract\n matchedEvents = txEvents.filter((e) => {\n const data = e.data as Record<string, unknown> | null;\n const contractIdentifier = data?.contract_identifier as string | undefined;\n return contractIdentifier && matchPattern(contractIdentifier, source.contract!);\n });\n if (matchedEvents.length === 0) continue;\n }\n\n // Event type / topic filter\n if (source.event) {\n matchedEvents = matchedEvents.filter((e) => {\n if (matchPattern(e.type, source.event!)) return true;\n const data = e.data as Record<string, unknown> | null;\n const topic = data?.topic as string | undefined;\n return topic ? matchPattern(topic, source.event!) : false;\n });\n }\n\n if (txContractMatch || matchedEvents.length > 0) {\n results.push({ tx, events: matchedEvents, sourceKey: key });\n }\n }\n }\n\n return results;\n}\n\n/**\n * Match all sources against a block's transactions and events.\n * Deduplicates by (txId, sourceKey) — each handler key fires at most once per tx.\n */\nexport function matchSources(\n sources: SubgraphSource[],\n transactions: TxRecord[],\n events: EventRecord[],\n): MatchedTx[] {\n // Index events by txId\n const eventsByTx = new Map<string, EventRecord[]>();\n for (const event of events) {\n const list = eventsByTx.get(event.tx_id) ?? [];\n list.push(event);\n eventsByTx.set(event.tx_id, list);\n }\n\n const seen = new Set<string>();\n const results: MatchedTx[] = [];\n\n for (const source of sources) {\n const matches = matchSource(source, transactions, eventsByTx);\n for (const match of matches) {\n const dedupeKey = `${match.tx.tx_id}:${match.sourceKey}`;\n if (!seen.has(dedupeKey)) {\n seen.add(dedupeKey);\n results.push(match);\n }\n }\n }\n\n return results;\n}\n"
7
+ ],
8
+ "mappings": ";;;;AAsEO,SAAS,SAAS,CAAC,QAAgC;AAAA,EACxD,IAAI,OAAO,UAAU;AAAA,IACnB,IAAI,OAAO;AAAA,MAAU,OAAO,GAAG,OAAO,aAAa,OAAO;AAAA,IAC1D,IAAI,OAAO;AAAA,MAAO,OAAO,GAAG,OAAO,aAAa,OAAO;AAAA,IACvD,OAAO,OAAO;AAAA,EAChB;AAAA,EACA,IAAI,OAAO;AAAA,IAAM,OAAO,OAAO;AAAA,EAC/B,OAAO;AAAA;;;ACjET,SAAS,YAAY,CAAC,OAAe,SAA0B;AAAA,EAC7D,IAAI,CAAC,QAAQ,SAAS,GAAG;AAAA,IAAG,OAAO,UAAU;AAAA,EAC7C,MAAM,QAAQ,QACX,QAAQ,qBAAqB,MAAM,EACnC,QAAQ,OAAO,IAAI;AAAA,EACtB,OAAO,IAAI,OAAO,IAAI,QAAQ,EAAE,KAAK,KAAK;AAAA;AAS5C,SAAS,WAAW,CAClB,QACA,cACA,YACa;AAAA,EACb,MAAM,UAAuB,CAAC;AAAA,EAC9B,MAAM,MAAM,UAAU,MAAM;AAAA,EAE5B,WAAW,MAAM,cAAc;AAAA,IAE7B,IAAI,OAAO,MAAM;AAAA,MACf,IAAI,CAAC,aAAa,GAAG,MAAM,OAAO,IAAI;AAAA,QAAG;AAAA,MAEzC,MAAM,WAAW,WAAW,IAAI,GAAG,KAAK,KAAK,CAAC;AAAA,MAG9C,IAAI,gBAAgB;AAAA,MAGpB,IAAI,OAAO,cAAc,WAAW;AAAA,QAClC,MAAM,eAAe,cAAc,OAAO,CAAC,MAAM;AAAA,UAC/C,MAAM,OAAO,EAAE;AAAA,UACf,MAAM,YAAY,MAAM;AAAA,UACxB,IAAI,cAAc;AAAA,YAAW,OAAO;AAAA,UACpC,MAAM,SAAS,OAAO,SAAS;AAAA,UAC/B,OAAO,UAAU,OAAO;AAAA,SACzB;AAAA,QACD,IAAI,aAAa,WAAW;AAAA,UAAG;AAAA,QAC/B,gBAAgB;AAAA,MAClB;AAAA,MAEA,QAAQ,KAAK,EAAE,IAAI,QAAQ,eAAe,WAAW,IAAI,CAAC;AAAA,MAC1D;AAAA,IACF;AAAA,IAGA,IAAI,OAAO,UAAU;AAAA,MACnB,MAAM,kBAAkB,GAAG,eAAe,aAAa,GAAG,aAAa,OAAO,QAAQ;AAAA,MAGtF,IAAI,OAAO,YAAY,GAAG,eAAe;AAAA,QACvC,IAAI,CAAC,aAAa,GAAG,eAAe,OAAO,QAAQ;AAAA,UAAG;AAAA,MACxD,EAAO,SAAI,OAAO,YAAY,CAAC,GAAG,eAAe;AAAA,QAC/C;AAAA,MACF;AAAA,MAEA,MAAM,WAAW,WAAW,IAAI,GAAG,KAAK,KAAK,CAAC;AAAA,MAC9C,IAAI,gBAAgB;AAAA,MAEpB,IAAI,CAAC,iBAAiB;AAAA,QAEpB,gBAAgB,SAAS,OAAO,CAAC,MAAM;AAAA,UACrC,MAAM,OAAO,EAAE;AAAA,UACf,MAAM,qBAAqB,MAAM;AAAA,UACjC,OAAO,sBAAsB,aAAa,oBAAoB,OAAO,QAAS;AAAA,SAC/E;AAAA,QACD,IAAI,cAAc,WAAW;AAAA,UAAG;AAAA,MAClC;AAAA,MAGA,IAAI,OAAO,OAAO;AAAA,QAChB,gBAAgB,cAAc,OAAO,CAAC,MAAM;AAAA,UAC1C,IAAI,aAAa,EAAE,MAAM,OAAO,KAAM;AAAA,YAAG,OAAO;AAAA,UAChD,MAAM,OAAO,EAAE;AAAA,UACf,MAAM,QAAQ,MAAM;AAAA,UACpB,OAAO,QAAQ,aAAa,OAAO,OAAO,KAAM,IAAI;AAAA,SACrD;AAAA,MACH;AAAA,MAEA,IAAI,mBAAmB,cAAc,SAAS,GAAG;AAAA,QAC/C,QAAQ,KAAK,EAAE,IAAI,QAAQ,eAAe,WAAW,IAAI,CAAC;AAAA,MAC5D;AAAA,IACF;AAAA,EACF;AAAA,EAEA,OAAO;AAAA;AAOF,SAAS,YAAY,CAC1B,SACA,cACA,QACa;AAAA,EAEb,MAAM,aAAa,IAAI;AAAA,EACvB,WAAW,SAAS,QAAQ;AAAA,IAC1B,MAAM,OAAO,WAAW,IAAI,MAAM,KAAK,KAAK,CAAC;AAAA,IAC7C,KAAK,KAAK,KAAK;AAAA,IACf,WAAW,IAAI,MAAM,OAAO,IAAI;AAAA,EAClC;AAAA,EAEA,MAAM,OAAO,IAAI;AAAA,EACjB,MAAM,UAAuB,CAAC;AAAA,EAE9B,WAAW,UAAU,SAAS;AAAA,IAC5B,MAAM,UAAU,YAAY,QAAQ,cAAc,UAAU;AAAA,IAC5D,WAAW,SAAS,SAAS;AAAA,MAC3B,MAAM,YAAY,GAAG,MAAM,GAAG,SAAS,MAAM;AAAA,MAC7C,IAAI,CAAC,KAAK,IAAI,SAAS,GAAG;AAAA,QACxB,KAAK,IAAI,SAAS;AAAA,QAClB,QAAQ,KAAK,KAAK;AAAA,MACpB;AAAA,IACF;AAAA,EACF;AAAA,EAEA,OAAO;AAAA;",
9
+ "debugId": "A86478F2FF92B95F64756E2164756E21",
10
+ "names": []
11
+ }
@@ -0,0 +1,24 @@
1
+ interface ProcessBlockTiming {
2
+ totalMs: number;
3
+ handlerMs: number;
4
+ flushMs: number;
5
+ }
6
+ import { Kysely } from "kysely";
7
+ import { Database } from "@secondlayer/shared/db";
8
+ /**
9
+ * Accumulates per-block timing stats and flushes to DB periodically.
10
+ * One instance per subgraph during catchup/reindex/live processing.
11
+ */
12
+ declare class StatsAccumulator {
13
+ private subgraphName;
14
+ private apiKeyId;
15
+ private isCatchup;
16
+ private current;
17
+ private lastFlush;
18
+ constructor(subgraphName: string, apiKeyId: string | null, isCatchup: boolean);
19
+ record(timing: ProcessBlockTiming, opsCount: number): void;
20
+ shouldFlush(): boolean;
21
+ flush(db: Kysely<Database>): Promise<void>;
22
+ private newEntry;
23
+ }
24
+ export { StatsAccumulator };
@@ -0,0 +1,75 @@
1
+ import { createRequire } from "node:module";
2
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
3
+
4
+ // src/runtime/stats.ts
5
+ var FLUSH_INTERVAL_BLOCKS = 100;
6
+ var FLUSH_INTERVAL_MS = 60000;
7
+
8
+ class StatsAccumulator {
9
+ subgraphName;
10
+ apiKeyId;
11
+ isCatchup;
12
+ current;
13
+ lastFlush = Date.now();
14
+ constructor(subgraphName, apiKeyId, isCatchup) {
15
+ this.subgraphName = subgraphName;
16
+ this.apiKeyId = apiKeyId;
17
+ this.isCatchup = isCatchup;
18
+ this.current = this.newEntry();
19
+ }
20
+ record(timing, opsCount) {
21
+ this.current.blocksProcessed++;
22
+ this.current.totalTimeMs += timing.totalMs;
23
+ this.current.handlerTimeMs += timing.handlerMs;
24
+ this.current.flushTimeMs += timing.flushMs;
25
+ this.current.maxBlockTimeMs = Math.max(this.current.maxBlockTimeMs, timing.totalMs);
26
+ this.current.maxHandlerTimeMs = Math.max(this.current.maxHandlerTimeMs, timing.handlerMs);
27
+ this.current.totalOps += opsCount;
28
+ }
29
+ shouldFlush() {
30
+ return this.current.blocksProcessed >= FLUSH_INTERVAL_BLOCKS || Date.now() - this.lastFlush >= FLUSH_INTERVAL_MS;
31
+ }
32
+ async flush(db) {
33
+ if (this.current.blocksProcessed === 0)
34
+ return;
35
+ const entry = this.current;
36
+ this.current = this.newEntry();
37
+ this.lastFlush = Date.now();
38
+ const avgOpsPerBlock = entry.blocksProcessed > 0 ? entry.totalOps / entry.blocksProcessed : 0;
39
+ await db.insertInto("subgraph_processing_stats").values({
40
+ subgraph_name: entry.subgraphName,
41
+ api_key_id: entry.apiKeyId,
42
+ bucket_start: entry.bucketStart,
43
+ bucket_end: new Date,
44
+ blocks_processed: entry.blocksProcessed,
45
+ total_time_ms: Math.round(entry.totalTimeMs),
46
+ handler_time_ms: Math.round(entry.handlerTimeMs),
47
+ flush_time_ms: Math.round(entry.flushTimeMs),
48
+ max_block_time_ms: Math.round(entry.maxBlockTimeMs),
49
+ max_handler_time_ms: Math.round(entry.maxHandlerTimeMs),
50
+ avg_ops_per_block: parseFloat(avgOpsPerBlock.toFixed(2)),
51
+ is_catchup: entry.isCatchup
52
+ }).execute();
53
+ }
54
+ newEntry() {
55
+ return {
56
+ subgraphName: this.subgraphName,
57
+ apiKeyId: this.apiKeyId,
58
+ bucketStart: new Date,
59
+ blocksProcessed: 0,
60
+ totalTimeMs: 0,
61
+ handlerTimeMs: 0,
62
+ flushTimeMs: 0,
63
+ maxBlockTimeMs: 0,
64
+ maxHandlerTimeMs: 0,
65
+ totalOps: 0,
66
+ isCatchup: this.isCatchup
67
+ };
68
+ }
69
+ }
70
+ export {
71
+ StatsAccumulator
72
+ };
73
+
74
+ //# debugId=2992B5CA15D3B67064756E2164756E21
75
+ //# sourceMappingURL=stats.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/runtime/stats.ts"],
4
+ "sourcesContent": [
5
+ "import type { Kysely } from \"kysely\";\nimport type { Database } from \"@secondlayer/shared/db\";\nimport type { ProcessBlockTiming } from \"./block-processor.ts\";\n\ninterface StatsEntry {\n subgraphName: string;\n apiKeyId: string | null;\n bucketStart: Date;\n blocksProcessed: number;\n totalTimeMs: number;\n handlerTimeMs: number;\n flushTimeMs: number;\n maxBlockTimeMs: number;\n maxHandlerTimeMs: number;\n totalOps: number;\n isCatchup: boolean;\n}\n\nconst FLUSH_INTERVAL_BLOCKS = 100;\nconst FLUSH_INTERVAL_MS = 60_000;\n\n/**\n * Accumulates per-block timing stats and flushes to DB periodically.\n * One instance per subgraph during catchup/reindex/live processing.\n */\nexport class StatsAccumulator {\n private current: StatsEntry;\n private lastFlush = Date.now();\n\n constructor(\n private subgraphName: string,\n private apiKeyId: string | null,\n private isCatchup: boolean,\n ) {\n this.current = this.newEntry();\n }\n\n record(timing: ProcessBlockTiming, opsCount: number): void {\n this.current.blocksProcessed++;\n this.current.totalTimeMs += timing.totalMs;\n this.current.handlerTimeMs += timing.handlerMs;\n this.current.flushTimeMs += timing.flushMs;\n this.current.maxBlockTimeMs = Math.max(this.current.maxBlockTimeMs, timing.totalMs);\n this.current.maxHandlerTimeMs = Math.max(this.current.maxHandlerTimeMs, timing.handlerMs);\n this.current.totalOps += opsCount;\n }\n\n shouldFlush(): boolean {\n return (\n this.current.blocksProcessed >= FLUSH_INTERVAL_BLOCKS ||\n Date.now() - this.lastFlush >= FLUSH_INTERVAL_MS\n );\n }\n\n async flush(db: Kysely<Database>): Promise<void> {\n if (this.current.blocksProcessed === 0) return;\n\n const entry = this.current;\n this.current = this.newEntry();\n this.lastFlush = Date.now();\n\n const avgOpsPerBlock = entry.blocksProcessed > 0\n ? entry.totalOps / entry.blocksProcessed\n : 0;\n\n await db\n .insertInto(\"subgraph_processing_stats\")\n .values({\n subgraph_name: entry.subgraphName,\n api_key_id: entry.apiKeyId,\n bucket_start: entry.bucketStart,\n bucket_end: new Date(),\n blocks_processed: entry.blocksProcessed,\n total_time_ms: Math.round(entry.totalTimeMs),\n handler_time_ms: Math.round(entry.handlerTimeMs),\n flush_time_ms: Math.round(entry.flushTimeMs),\n max_block_time_ms: Math.round(entry.maxBlockTimeMs),\n max_handler_time_ms: Math.round(entry.maxHandlerTimeMs),\n avg_ops_per_block: parseFloat(avgOpsPerBlock.toFixed(2)),\n is_catchup: entry.isCatchup,\n })\n .execute();\n }\n\n private newEntry(): StatsEntry {\n return {\n subgraphName: this.subgraphName,\n apiKeyId: this.apiKeyId,\n bucketStart: new Date(),\n blocksProcessed: 0,\n totalTimeMs: 0,\n handlerTimeMs: 0,\n flushTimeMs: 0,\n maxBlockTimeMs: 0,\n maxHandlerTimeMs: 0,\n totalOps: 0,\n isCatchup: this.isCatchup,\n };\n }\n}\n"
6
+ ],
7
+ "mappings": ";;;;AAkBA,IAAM,wBAAwB;AAC9B,IAAM,oBAAoB;AAAA;AAMnB,MAAM,iBAAiB;AAAA,EAKlB;AAAA,EACA;AAAA,EACA;AAAA,EANF;AAAA,EACA,YAAY,KAAK,IAAI;AAAA,EAE7B,WAAW,CACD,cACA,UACA,WACR;AAAA,IAHQ;AAAA,IACA;AAAA,IACA;AAAA,IAER,KAAK,UAAU,KAAK,SAAS;AAAA;AAAA,EAG/B,MAAM,CAAC,QAA4B,UAAwB;AAAA,IACzD,KAAK,QAAQ;AAAA,IACb,KAAK,QAAQ,eAAe,OAAO;AAAA,IACnC,KAAK,QAAQ,iBAAiB,OAAO;AAAA,IACrC,KAAK,QAAQ,eAAe,OAAO;AAAA,IACnC,KAAK,QAAQ,iBAAiB,KAAK,IAAI,KAAK,QAAQ,gBAAgB,OAAO,OAAO;AAAA,IAClF,KAAK,QAAQ,mBAAmB,KAAK,IAAI,KAAK,QAAQ,kBAAkB,OAAO,SAAS;AAAA,IACxF,KAAK,QAAQ,YAAY;AAAA;AAAA,EAG3B,WAAW,GAAY;AAAA,IACrB,OACE,KAAK,QAAQ,mBAAmB,yBAChC,KAAK,IAAI,IAAI,KAAK,aAAa;AAAA;AAAA,OAI7B,MAAK,CAAC,IAAqC;AAAA,IAC/C,IAAI,KAAK,QAAQ,oBAAoB;AAAA,MAAG;AAAA,IAExC,MAAM,QAAQ,KAAK;AAAA,IACnB,KAAK,UAAU,KAAK,SAAS;AAAA,IAC7B,KAAK,YAAY,KAAK,IAAI;AAAA,IAE1B,MAAM,iBAAiB,MAAM,kBAAkB,IAC3C,MAAM,WAAW,MAAM,kBACvB;AAAA,IAEJ,MAAM,GACH,WAAW,2BAA2B,EACtC,OAAO;AAAA,MACN,eAAe,MAAM;AAAA,MACrB,YAAY,MAAM;AAAA,MAClB,cAAc,MAAM;AAAA,MACpB,YAAY,IAAI;AAAA,MAChB,kBAAkB,MAAM;AAAA,MACxB,eAAe,KAAK,MAAM,MAAM,WAAW;AAAA,MAC3C,iBAAiB,KAAK,MAAM,MAAM,aAAa;AAAA,MAC/C,eAAe,KAAK,MAAM,MAAM,WAAW;AAAA,MAC3C,mBAAmB,KAAK,MAAM,MAAM,cAAc;AAAA,MAClD,qBAAqB,KAAK,MAAM,MAAM,gBAAgB;AAAA,MACtD,mBAAmB,WAAW,eAAe,QAAQ,CAAC,CAAC;AAAA,MACvD,YAAY,MAAM;AAAA,IACpB,CAAC,EACA,QAAQ;AAAA;AAAA,EAGL,QAAQ,GAAe;AAAA,IAC7B,OAAO;AAAA,MACL,cAAc,KAAK;AAAA,MACnB,UAAU,KAAK;AAAA,MACf,aAAa,IAAI;AAAA,MACjB,iBAAiB;AAAA,MACjB,aAAa;AAAA,MACb,eAAe;AAAA,MACf,aAAa;AAAA,MACb,gBAAgB;AAAA,MAChB,kBAAkB;AAAA,MAClB,UAAU;AAAA,MACV,WAAW,KAAK;AAAA,IAClB;AAAA;AAEJ;",
8
+ "debugId": "2992B5CA15D3B67064756E2164756E21",
9
+ "names": []
10
+ }