@secondlayer/subgraphs 4.1.4 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/index.d.ts +91 -31
- package/dist/src/index.js +3319 -3067
- package/dist/src/index.js.map +11 -9
- package/dist/src/runtime/block-processor.d.ts +72 -23
- package/dist/src/runtime/block-processor.js +867 -400
- package/dist/src/runtime/block-processor.js.map +9 -6
- package/dist/src/runtime/catchup.d.ts +72 -23
- package/dist/src/runtime/catchup.js +878 -411
- package/dist/src/runtime/catchup.js.map +9 -6
- package/dist/src/runtime/processor.js +943 -475
- package/dist/src/runtime/processor.js.map +10 -7
- package/dist/src/runtime/reindex.d.ts +72 -23
- package/dist/src/runtime/reindex.js +911 -444
- package/dist/src/runtime/reindex.js.map +9 -6
- package/dist/src/runtime/reorg.d.ts +72 -23
- package/dist/src/runtime/reorg.js +890 -422
- package/dist/src/runtime/reorg.js.map +10 -7
- package/dist/src/runtime/replay.js +12 -6
- package/dist/src/runtime/replay.js.map +4 -4
- package/dist/src/runtime/runner.d.ts +72 -23
- package/dist/src/runtime/runner.js +757 -28
- package/dist/src/runtime/runner.js.map +9 -5
- package/dist/src/runtime/source-matcher.d.ts +80 -24
- package/dist/src/runtime/source-matcher.js +12 -5
- package/dist/src/runtime/source-matcher.js.map +3 -3
- package/dist/src/schema/index.d.ts +72 -23
- package/dist/src/schema/index.js +115 -1
- package/dist/src/schema/index.js.map +3 -3
- package/dist/src/service.js +983 -515
- package/dist/src/service.js.map +10 -7
- package/dist/src/testing/index.d.ts +195 -24
- package/dist/src/testing/index.js +1049 -30
- package/dist/src/testing/index.js.map +10 -5
- package/dist/src/triggers/index.d.ts +83 -24
- package/dist/src/triggers/index.js +5 -2
- package/dist/src/triggers/index.js.map +3 -3
- package/dist/src/types.d.ts +73 -24
- package/dist/src/validate.d.ts +72 -23
- package/dist/src/validate.js +115 -1
- package/dist/src/validate.js.map +3 -3
- package/package.json +2 -2
|
@@ -32,15 +32,679 @@ function decodeFunctionArgs(args) {
|
|
|
32
32
|
return args.map(decodeClarityValue);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
// src/runtime/source-matcher.ts
|
|
36
|
+
var patternCache = new Map;
|
|
37
|
+
function matchAny(value, pattern) {
|
|
38
|
+
return Array.isArray(pattern) ? pattern.some((p) => matchPattern(value, p)) : matchPattern(value, pattern);
|
|
39
|
+
}
|
|
40
|
+
function matchPattern(value, pattern) {
|
|
41
|
+
if (!pattern.includes("*"))
|
|
42
|
+
return value === pattern;
|
|
43
|
+
let re = patternCache.get(pattern);
|
|
44
|
+
if (!re) {
|
|
45
|
+
const regex = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
46
|
+
re = new RegExp(`^${regex}$`);
|
|
47
|
+
patternCache.set(pattern, re);
|
|
48
|
+
}
|
|
49
|
+
return re.test(value);
|
|
50
|
+
}
|
|
51
|
+
var EMPTY_SET = new Set;
|
|
52
|
+
function factoryAllows(filter, contractId, factoryContracts) {
|
|
53
|
+
if (!filter.factory)
|
|
54
|
+
return true;
|
|
55
|
+
if (!contractId)
|
|
56
|
+
return false;
|
|
57
|
+
return (factoryContracts.get(filter.factory.from) ?? EMPTY_SET).has(contractId);
|
|
58
|
+
}
|
|
59
|
+
function readPath(payload, path) {
|
|
60
|
+
let cur = payload;
|
|
61
|
+
for (const key of path.split(".")) {
|
|
62
|
+
if (cur === null || typeof cur !== "object")
|
|
63
|
+
return;
|
|
64
|
+
cur = cur[key];
|
|
65
|
+
}
|
|
66
|
+
return cur;
|
|
67
|
+
}
|
|
68
|
+
function traitAllows(filter, contractId, traitContracts) {
|
|
69
|
+
const trait = filter.trait;
|
|
70
|
+
if (!trait)
|
|
71
|
+
return true;
|
|
72
|
+
if (!contractId)
|
|
73
|
+
return false;
|
|
74
|
+
return (traitContracts.get(trait) ?? EMPTY_SET).has(contractId);
|
|
75
|
+
}
|
|
76
|
+
function assetContract(assetId) {
|
|
77
|
+
return assetId?.split("::")[0];
|
|
78
|
+
}
|
|
79
|
+
function printContractId(data) {
|
|
80
|
+
if (!data)
|
|
81
|
+
return;
|
|
82
|
+
const id = (typeof data.contract_identifier === "string" ? data.contract_identifier : undefined) ?? (typeof data.contract_id === "string" ? data.contract_id : undefined);
|
|
83
|
+
return id || undefined;
|
|
84
|
+
}
|
|
85
|
+
function matchFilter(filter, transactions, eventsByTx, traitContracts, factoryContracts = new Map) {
|
|
86
|
+
const results = [];
|
|
87
|
+
switch (filter.type) {
|
|
88
|
+
case "stx_transfer":
|
|
89
|
+
case "stx_mint":
|
|
90
|
+
case "stx_burn":
|
|
91
|
+
case "stx_lock": {
|
|
92
|
+
const eventType = `${filter.type}_event`;
|
|
93
|
+
for (const tx of transactions) {
|
|
94
|
+
const txEvents = eventsByTx.get(tx.tx_id) ?? [];
|
|
95
|
+
const matched = txEvents.filter((e) => e.type === eventType);
|
|
96
|
+
if (matched.length === 0)
|
|
97
|
+
continue;
|
|
98
|
+
const filtered = matched.filter((e) => {
|
|
99
|
+
const data = e.data;
|
|
100
|
+
if (!data)
|
|
101
|
+
return false;
|
|
102
|
+
if ("sender" in filter && filter.sender) {
|
|
103
|
+
if (!matchPattern(data.sender, filter.sender))
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
if ("recipient" in filter && filter.recipient) {
|
|
107
|
+
if (!matchPattern(data.recipient, filter.recipient))
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
if ("lockedAddress" in filter && filter.lockedAddress) {
|
|
111
|
+
if (!matchPattern(data.locked_address, filter.lockedAddress))
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
if ("minAmount" in filter && filter.minAmount !== undefined) {
|
|
115
|
+
const amount = BigInt(data.amount ?? data.locked_amount ?? "0");
|
|
116
|
+
if (amount < filter.minAmount)
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
if ("maxAmount" in filter && filter.maxAmount !== undefined) {
|
|
120
|
+
const amount = BigInt(data.amount ?? "0");
|
|
121
|
+
if (amount > filter.maxAmount)
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
return true;
|
|
125
|
+
});
|
|
126
|
+
if (filtered.length > 0) {
|
|
127
|
+
results.push({ tx, events: filtered });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
case "ft_transfer":
|
|
133
|
+
case "ft_mint":
|
|
134
|
+
case "ft_burn": {
|
|
135
|
+
const eventType = `${filter.type}_event`;
|
|
136
|
+
for (const tx of transactions) {
|
|
137
|
+
const txEvents = eventsByTx.get(tx.tx_id) ?? [];
|
|
138
|
+
const matched = txEvents.filter((e) => {
|
|
139
|
+
if (e.type !== eventType)
|
|
140
|
+
return false;
|
|
141
|
+
const data = e.data;
|
|
142
|
+
if (!data)
|
|
143
|
+
return false;
|
|
144
|
+
if (filter.assetIdentifier) {
|
|
145
|
+
const assetId = data.asset_identifier;
|
|
146
|
+
if (!assetId || !matchPattern(assetId, filter.assetIdentifier))
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
if (!traitAllows(filter, assetContract(data.asset_identifier), traitContracts))
|
|
150
|
+
return false;
|
|
151
|
+
if ("sender" in filter && filter.sender) {
|
|
152
|
+
if (!matchPattern(data.sender, filter.sender))
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
if ("recipient" in filter && filter.recipient) {
|
|
156
|
+
if (!matchPattern(data.recipient, filter.recipient))
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
if (filter.minAmount !== undefined) {
|
|
160
|
+
const amount = BigInt(data.amount ?? "0");
|
|
161
|
+
if (amount < filter.minAmount)
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
return true;
|
|
165
|
+
});
|
|
166
|
+
if (matched.length > 0) {
|
|
167
|
+
results.push({ tx, events: matched });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
case "nft_transfer":
|
|
173
|
+
case "nft_mint":
|
|
174
|
+
case "nft_burn": {
|
|
175
|
+
const eventType = `${filter.type}_event`;
|
|
176
|
+
for (const tx of transactions) {
|
|
177
|
+
const txEvents = eventsByTx.get(tx.tx_id) ?? [];
|
|
178
|
+
const matched = txEvents.filter((e) => {
|
|
179
|
+
if (e.type !== eventType)
|
|
180
|
+
return false;
|
|
181
|
+
const data = e.data;
|
|
182
|
+
if (!data)
|
|
183
|
+
return false;
|
|
184
|
+
if (filter.assetIdentifier) {
|
|
185
|
+
const assetId = data.asset_identifier;
|
|
186
|
+
if (!assetId || !matchPattern(assetId, filter.assetIdentifier))
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
if (!traitAllows(filter, assetContract(data.asset_identifier), traitContracts))
|
|
190
|
+
return false;
|
|
191
|
+
if ("sender" in filter && filter.sender) {
|
|
192
|
+
if (!matchPattern(data.sender, filter.sender))
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
if ("recipient" in filter && filter.recipient) {
|
|
196
|
+
if (!matchPattern(data.recipient, filter.recipient))
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
return true;
|
|
200
|
+
});
|
|
201
|
+
if (matched.length > 0) {
|
|
202
|
+
results.push({ tx, events: matched });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
case "contract_call": {
|
|
208
|
+
for (const tx of transactions) {
|
|
209
|
+
if (tx.type !== "contract_call")
|
|
210
|
+
continue;
|
|
211
|
+
if (filter.contractId) {
|
|
212
|
+
if (!tx.contract_id || !matchAny(tx.contract_id, filter.contractId))
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (!factoryAllows(filter, tx.contract_id, factoryContracts))
|
|
216
|
+
continue;
|
|
217
|
+
if (filter.functionName) {
|
|
218
|
+
if (!tx.function_name || !matchPattern(tx.function_name, filter.functionName))
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (filter.caller) {
|
|
222
|
+
if (!matchPattern(tx.sender, filter.caller))
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (!traitAllows(filter, tx.contract_id, traitContracts))
|
|
226
|
+
continue;
|
|
227
|
+
const txEvents = eventsByTx.get(tx.tx_id) ?? [];
|
|
228
|
+
results.push({ tx, events: txEvents });
|
|
229
|
+
}
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
case "contract_deploy": {
|
|
233
|
+
for (const tx of transactions) {
|
|
234
|
+
if (tx.type !== "smart_contract")
|
|
235
|
+
continue;
|
|
236
|
+
if (filter.deployer) {
|
|
237
|
+
if (!matchPattern(tx.sender, filter.deployer))
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
if (filter.contractName) {
|
|
241
|
+
const name = tx.contract_id?.split(".")[1] ?? "";
|
|
242
|
+
if (!matchPattern(name, filter.contractName))
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
const txEvents = eventsByTx.get(tx.tx_id) ?? [];
|
|
246
|
+
results.push({ tx, events: txEvents });
|
|
247
|
+
}
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
case "print_event": {
|
|
251
|
+
for (const tx of transactions) {
|
|
252
|
+
const txEvents = eventsByTx.get(tx.tx_id) ?? [];
|
|
253
|
+
const matched = txEvents.filter((e) => {
|
|
254
|
+
if (e.type !== "smart_contract_event" && e.type !== "contract_event")
|
|
255
|
+
return false;
|
|
256
|
+
const data = e.data;
|
|
257
|
+
if (!data)
|
|
258
|
+
return false;
|
|
259
|
+
if (data.topic !== "print")
|
|
260
|
+
return false;
|
|
261
|
+
const contractId = printContractId(data);
|
|
262
|
+
if (filter.contractId) {
|
|
263
|
+
if (!contractId || !matchAny(contractId, filter.contractId))
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
if (!traitAllows(filter, contractId, traitContracts))
|
|
267
|
+
return false;
|
|
268
|
+
if (!factoryAllows(filter, contractId, factoryContracts))
|
|
269
|
+
return false;
|
|
270
|
+
return true;
|
|
271
|
+
});
|
|
272
|
+
if (matched.length > 0) {
|
|
273
|
+
results.push({ tx, events: matched });
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return results;
|
|
280
|
+
}
|
|
281
|
+
function matchSources(sources, transactions, events, traitContracts = new Map, factoryContracts = new Map) {
|
|
282
|
+
const eventsByTx = new Map;
|
|
283
|
+
for (const event of events) {
|
|
284
|
+
const list = eventsByTx.get(event.tx_id) ?? [];
|
|
285
|
+
list.push(event);
|
|
286
|
+
eventsByTx.set(event.tx_id, list);
|
|
287
|
+
}
|
|
288
|
+
const seen = new Set;
|
|
289
|
+
const results = [];
|
|
290
|
+
for (const [sourceName, filter] of Object.entries(sources)) {
|
|
291
|
+
const matches = matchFilter(filter, transactions, eventsByTx, traitContracts, factoryContracts);
|
|
292
|
+
for (const match of matches) {
|
|
293
|
+
const dedupeKey = `${match.tx.tx_id}:${sourceName}`;
|
|
294
|
+
if (!seen.has(dedupeKey)) {
|
|
295
|
+
seen.add(dedupeKey);
|
|
296
|
+
results.push({ ...match, sourceName });
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return results;
|
|
301
|
+
}
|
|
302
|
+
|
|
35
303
|
// src/runtime/runner.ts
|
|
36
304
|
import { getErrorMessage } from "@secondlayer/shared";
|
|
37
|
-
import { logger } from "@secondlayer/shared/logger";
|
|
305
|
+
import { logger as logger2 } from "@secondlayer/shared/logger";
|
|
38
306
|
import {
|
|
39
307
|
clarityValueToJS,
|
|
40
|
-
deserializeCV as
|
|
308
|
+
deserializeCV as deserializeCV3,
|
|
41
309
|
toCamelCase
|
|
42
310
|
} from "@secondlayer/stacks/clarity";
|
|
43
311
|
|
|
312
|
+
// src/print-schema.ts
|
|
313
|
+
import {
|
|
314
|
+
deserializeCV as deserializeCV2
|
|
315
|
+
} from "@secondlayer/stacks/clarity";
|
|
316
|
+
function camelizeDataKey(str) {
|
|
317
|
+
return str.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
|
|
318
|
+
}
|
|
319
|
+
function camelizeKeys(obj) {
|
|
320
|
+
if (obj === null || obj === undefined)
|
|
321
|
+
return obj;
|
|
322
|
+
if (typeof obj !== "object")
|
|
323
|
+
return obj;
|
|
324
|
+
if (Array.isArray(obj))
|
|
325
|
+
return obj.map(camelizeKeys);
|
|
326
|
+
const result = {};
|
|
327
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
328
|
+
result[camelizeDataKey(k)] = camelizeKeys(v);
|
|
329
|
+
}
|
|
330
|
+
return result;
|
|
331
|
+
}
|
|
332
|
+
function cvToTree(cv) {
|
|
333
|
+
switch (cv.type) {
|
|
334
|
+
case "uint":
|
|
335
|
+
case "int":
|
|
336
|
+
return { kind: cv.type };
|
|
337
|
+
case "true":
|
|
338
|
+
case "false":
|
|
339
|
+
return { kind: "bool" };
|
|
340
|
+
case "address":
|
|
341
|
+
case "contract":
|
|
342
|
+
return { kind: "principal" };
|
|
343
|
+
case "buffer":
|
|
344
|
+
return { kind: "buffer", len: cv.value.length / 2 };
|
|
345
|
+
case "ascii":
|
|
346
|
+
return { kind: "ascii", len: cv.value.length };
|
|
347
|
+
case "utf8":
|
|
348
|
+
return { kind: "utf8", len: new TextEncoder().encode(cv.value).length };
|
|
349
|
+
case "none":
|
|
350
|
+
return { kind: "optional", inner: null };
|
|
351
|
+
case "some":
|
|
352
|
+
return { kind: "optional", inner: cvToTree(cv.value) };
|
|
353
|
+
case "ok":
|
|
354
|
+
return { kind: "response", ok: cvToTree(cv.value), err: null };
|
|
355
|
+
case "err":
|
|
356
|
+
return { kind: "response", ok: null, err: cvToTree(cv.value) };
|
|
357
|
+
case "list": {
|
|
358
|
+
let inner = null;
|
|
359
|
+
for (const el of cv.value) {
|
|
360
|
+
const t = cvToTree(el);
|
|
361
|
+
inner = inner ? unify(inner, t) : t;
|
|
362
|
+
}
|
|
363
|
+
return { kind: "list", inner };
|
|
364
|
+
}
|
|
365
|
+
case "tuple": {
|
|
366
|
+
const entries = new Map;
|
|
367
|
+
for (const [k, v] of Object.entries(cv.value)) {
|
|
368
|
+
entries.set(k, { tree: cvToTree(v), present: 1 });
|
|
369
|
+
}
|
|
370
|
+
return { kind: "tuple", count: 1, entries };
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
function unifyNullable(a, b) {
|
|
375
|
+
if (!a)
|
|
376
|
+
return b;
|
|
377
|
+
if (!b)
|
|
378
|
+
return a;
|
|
379
|
+
return unify(a, b);
|
|
380
|
+
}
|
|
381
|
+
var UNION_KIND_ORDER = {
|
|
382
|
+
uint: 0,
|
|
383
|
+
int: 1,
|
|
384
|
+
bool: 2,
|
|
385
|
+
principal: 3,
|
|
386
|
+
buffer: 4,
|
|
387
|
+
ascii: 5,
|
|
388
|
+
utf8: 6,
|
|
389
|
+
list: 7,
|
|
390
|
+
tuple: 8,
|
|
391
|
+
response: 9
|
|
392
|
+
};
|
|
393
|
+
function makeUnion(members) {
|
|
394
|
+
const sorted = [...members].sort((a, b) => (UNION_KIND_ORDER[a.kind] ?? 99) - (UNION_KIND_ORDER[b.kind] ?? 99));
|
|
395
|
+
return { kind: "union", members: sorted };
|
|
396
|
+
}
|
|
397
|
+
function unify(a, b) {
|
|
398
|
+
if (a.kind === "optional" || b.kind === "optional") {
|
|
399
|
+
const ai = a.kind === "optional" ? a.inner : a;
|
|
400
|
+
const bi = b.kind === "optional" ? b.inner : b;
|
|
401
|
+
return { kind: "optional", inner: unifyNullable(ai, bi) };
|
|
402
|
+
}
|
|
403
|
+
if (a.kind === "union")
|
|
404
|
+
return unionAdd(a.members, b);
|
|
405
|
+
if (b.kind === "union")
|
|
406
|
+
return unionAdd(b.members, a);
|
|
407
|
+
if (a.kind !== b.kind)
|
|
408
|
+
return makeUnion([a, b]);
|
|
409
|
+
switch (a.kind) {
|
|
410
|
+
case "uint":
|
|
411
|
+
case "int":
|
|
412
|
+
case "bool":
|
|
413
|
+
case "principal":
|
|
414
|
+
return a;
|
|
415
|
+
case "buffer":
|
|
416
|
+
case "ascii":
|
|
417
|
+
case "utf8":
|
|
418
|
+
return { kind: a.kind, len: Math.max(a.len, b.len) };
|
|
419
|
+
case "list":
|
|
420
|
+
return {
|
|
421
|
+
kind: "list",
|
|
422
|
+
inner: unifyNullable(a.inner, b.inner)
|
|
423
|
+
};
|
|
424
|
+
case "response": {
|
|
425
|
+
const rb = b;
|
|
426
|
+
return {
|
|
427
|
+
kind: "response",
|
|
428
|
+
ok: unifyNullable(a.ok, rb.ok),
|
|
429
|
+
err: unifyNullable(a.err, rb.err)
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
case "tuple": {
|
|
433
|
+
const tb = b;
|
|
434
|
+
const entries = new Map([...a.entries].map(([k, e]) => [k, { ...e }]));
|
|
435
|
+
for (const [k, e] of tb.entries) {
|
|
436
|
+
const existing = entries.get(k);
|
|
437
|
+
entries.set(k, existing ? {
|
|
438
|
+
tree: unify(existing.tree, e.tree),
|
|
439
|
+
present: existing.present + e.present
|
|
440
|
+
} : { ...e });
|
|
441
|
+
}
|
|
442
|
+
return { kind: "tuple", count: a.count + tb.count, entries };
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
function unionAdd(members, t) {
|
|
447
|
+
if (t.kind === "union") {
|
|
448
|
+
let acc = { kind: "union", members };
|
|
449
|
+
for (const m of t.members)
|
|
450
|
+
acc = unify(acc, m);
|
|
451
|
+
return acc;
|
|
452
|
+
}
|
|
453
|
+
const next = [...members];
|
|
454
|
+
for (let i = 0;i < next.length; i++) {
|
|
455
|
+
const member = next[i];
|
|
456
|
+
if (!member)
|
|
457
|
+
continue;
|
|
458
|
+
const merged = unify(member, t);
|
|
459
|
+
if (merged.kind !== "union") {
|
|
460
|
+
next[i] = merged;
|
|
461
|
+
return makeUnion(next);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
next.push(t);
|
|
465
|
+
return makeUnion(next);
|
|
466
|
+
}
|
|
467
|
+
function wrapOptional(t) {
|
|
468
|
+
return t.kind === "optional" ? t : { kind: "optional", inner: t };
|
|
469
|
+
}
|
|
470
|
+
function renderClarity(t) {
|
|
471
|
+
if (!t)
|
|
472
|
+
return "?";
|
|
473
|
+
switch (t.kind) {
|
|
474
|
+
case "uint":
|
|
475
|
+
case "int":
|
|
476
|
+
case "bool":
|
|
477
|
+
case "principal":
|
|
478
|
+
return t.kind;
|
|
479
|
+
case "buffer":
|
|
480
|
+
return `(buff ${t.len})`;
|
|
481
|
+
case "ascii":
|
|
482
|
+
return `(string-ascii ${t.len})`;
|
|
483
|
+
case "utf8":
|
|
484
|
+
return `(string-utf8 ${t.len})`;
|
|
485
|
+
case "optional":
|
|
486
|
+
return `(optional ${renderClarity(t.inner)})`;
|
|
487
|
+
case "list":
|
|
488
|
+
return `(list ${renderClarity(t.inner)})`;
|
|
489
|
+
case "response":
|
|
490
|
+
return `(response ${renderClarity(t.ok)} ${renderClarity(t.err)})`;
|
|
491
|
+
case "tuple": {
|
|
492
|
+
const parts = [...t.entries].map(([k, e]) => {
|
|
493
|
+
const tree = e.present < t.count ? wrapOptional(e.tree) : e.tree;
|
|
494
|
+
return `(${k} ${renderClarity(tree)})`;
|
|
495
|
+
});
|
|
496
|
+
return `(tuple ${parts.join(" ")})`;
|
|
497
|
+
}
|
|
498
|
+
case "union":
|
|
499
|
+
return t.members.map(renderClarity).join(" | ");
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
function renderTs(t) {
|
|
503
|
+
if (!t)
|
|
504
|
+
return "unknown";
|
|
505
|
+
switch (t.kind) {
|
|
506
|
+
case "uint":
|
|
507
|
+
case "int":
|
|
508
|
+
return "bigint";
|
|
509
|
+
case "bool":
|
|
510
|
+
return "boolean";
|
|
511
|
+
case "principal":
|
|
512
|
+
case "buffer":
|
|
513
|
+
case "ascii":
|
|
514
|
+
case "utf8":
|
|
515
|
+
return "string";
|
|
516
|
+
case "optional":
|
|
517
|
+
return t.inner ? `${renderTs(t.inner)} | null` : "unknown | null";
|
|
518
|
+
case "list": {
|
|
519
|
+
const inner = renderTs(t.inner);
|
|
520
|
+
return inner.includes(" | ") ? `(${inner})[]` : `${inner}[]`;
|
|
521
|
+
}
|
|
522
|
+
case "response": {
|
|
523
|
+
const sides = [...new Set([renderTs(t.ok), renderTs(t.err)])];
|
|
524
|
+
return sides.join(" | ");
|
|
525
|
+
}
|
|
526
|
+
case "tuple": {
|
|
527
|
+
const parts = [...t.entries].map(([k, e]) => {
|
|
528
|
+
const opt = e.present < t.count ? "?" : "";
|
|
529
|
+
return `${camelizeDataKey(k)}${opt}: ${renderTs(e.tree)}`;
|
|
530
|
+
});
|
|
531
|
+
return `{ ${parts.join("; ")} }`;
|
|
532
|
+
}
|
|
533
|
+
case "union":
|
|
534
|
+
return [...new Set(t.members.map((m) => renderTs(m)))].join(" | ");
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
function toColumnType(t) {
|
|
538
|
+
if (!t)
|
|
539
|
+
return "jsonb";
|
|
540
|
+
switch (t.kind) {
|
|
541
|
+
case "uint":
|
|
542
|
+
return "uint";
|
|
543
|
+
case "int":
|
|
544
|
+
return "int";
|
|
545
|
+
case "bool":
|
|
546
|
+
return "boolean";
|
|
547
|
+
case "principal":
|
|
548
|
+
return "principal";
|
|
549
|
+
case "buffer":
|
|
550
|
+
case "ascii":
|
|
551
|
+
case "utf8":
|
|
552
|
+
return "text";
|
|
553
|
+
case "list":
|
|
554
|
+
case "tuple":
|
|
555
|
+
return "jsonb";
|
|
556
|
+
case "optional":
|
|
557
|
+
return toColumnType(t.inner);
|
|
558
|
+
case "response":
|
|
559
|
+
return t.ok ? toColumnType(t.ok) : "jsonb";
|
|
560
|
+
case "union":
|
|
561
|
+
return "jsonb";
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
var MAX_DECODED_PER_TOPIC_NEWEST = 75;
|
|
565
|
+
var MAX_DECODED_PER_TOPIC_OLDEST = 25;
|
|
566
|
+
function inferPrintTopics(samples) {
|
|
567
|
+
const groups = new Map;
|
|
568
|
+
for (const s of samples) {
|
|
569
|
+
const group = groups.get(s.topic);
|
|
570
|
+
if (group)
|
|
571
|
+
group.push(s);
|
|
572
|
+
else
|
|
573
|
+
groups.set(s.topic, [s]);
|
|
574
|
+
}
|
|
575
|
+
const out = [];
|
|
576
|
+
for (const [topic, rows] of groups) {
|
|
577
|
+
let first = Number.POSITIVE_INFINITY;
|
|
578
|
+
let last = Number.NEGATIVE_INFINITY;
|
|
579
|
+
for (const r of rows) {
|
|
580
|
+
if (r.blockHeight < first)
|
|
581
|
+
first = r.blockHeight;
|
|
582
|
+
if (r.blockHeight > last)
|
|
583
|
+
last = r.blockHeight;
|
|
584
|
+
}
|
|
585
|
+
const withHex = [...rows].filter((r) => r.rawHex !== null).sort((a, b) => b.blockHeight - a.blockHeight);
|
|
586
|
+
const budget = MAX_DECODED_PER_TOPIC_NEWEST + MAX_DECODED_PER_TOPIC_OLDEST;
|
|
587
|
+
const picked = withHex.length <= budget ? withHex : [
|
|
588
|
+
...withHex.slice(0, MAX_DECODED_PER_TOPIC_NEWEST),
|
|
589
|
+
...withHex.slice(-MAX_DECODED_PER_TOPIC_OLDEST)
|
|
590
|
+
];
|
|
591
|
+
const tuples = [];
|
|
592
|
+
let decoded = 0;
|
|
593
|
+
for (const p of picked) {
|
|
594
|
+
try {
|
|
595
|
+
const cv = deserializeCV2(p.rawHex);
|
|
596
|
+
decoded++;
|
|
597
|
+
if (cv.type === "tuple")
|
|
598
|
+
tuples.push(cv);
|
|
599
|
+
} catch {}
|
|
600
|
+
}
|
|
601
|
+
const nonTuple = decoded > 0 && tuples.length === 0;
|
|
602
|
+
const fields = [];
|
|
603
|
+
if (!nonTuple) {
|
|
604
|
+
const stats = new Map;
|
|
605
|
+
for (const t of tuples) {
|
|
606
|
+
for (const [key, value] of Object.entries(t.value)) {
|
|
607
|
+
if (key === "topic")
|
|
608
|
+
continue;
|
|
609
|
+
const tree = cvToTree(value);
|
|
610
|
+
const existing = stats.get(key);
|
|
611
|
+
if (existing) {
|
|
612
|
+
existing.present++;
|
|
613
|
+
existing.tree = unify(existing.tree, tree);
|
|
614
|
+
if (value.type === "none")
|
|
615
|
+
existing.noneCount++;
|
|
616
|
+
if (value.type === "none" || value.type === "some") {
|
|
617
|
+
existing.optionalSeen = true;
|
|
618
|
+
}
|
|
619
|
+
} else {
|
|
620
|
+
stats.set(key, {
|
|
621
|
+
tree,
|
|
622
|
+
present: 1,
|
|
623
|
+
noneCount: value.type === "none" ? 1 : 0,
|
|
624
|
+
optionalSeen: value.type === "none" || value.type === "some"
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
for (const [name, st] of [...stats].sort(([a], [b]) => a.localeCompare(b))) {
|
|
630
|
+
const field = {
|
|
631
|
+
name,
|
|
632
|
+
camel_name: camelizeDataKey(name),
|
|
633
|
+
clarity_type: renderClarity(st.tree),
|
|
634
|
+
ts_type: renderTs(st.tree),
|
|
635
|
+
column_type: toColumnType(st.tree),
|
|
636
|
+
always_present: st.present === tuples.length
|
|
637
|
+
};
|
|
638
|
+
if (st.optionalSeen) {
|
|
639
|
+
field.optional_some_rate = (st.present - st.noneCount) / st.present;
|
|
640
|
+
}
|
|
641
|
+
fields.push(field);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
out.push({
|
|
645
|
+
topic,
|
|
646
|
+
count: rows.length,
|
|
647
|
+
first_height: first,
|
|
648
|
+
last_height: last,
|
|
649
|
+
non_tuple: nonTuple,
|
|
650
|
+
fields
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
return out.sort((a, b) => b.count - a.count);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// src/runtime/materialize.ts
|
|
657
|
+
function resolveColumn(col, event, tx, block) {
|
|
658
|
+
if ("fromTx" in col) {
|
|
659
|
+
return { ok: true, value: tx[col.fromTx] };
|
|
660
|
+
}
|
|
661
|
+
if ("fromBlock" in col) {
|
|
662
|
+
return { ok: true, value: block[col.fromBlock] };
|
|
663
|
+
}
|
|
664
|
+
const data = event.data;
|
|
665
|
+
const dataObj = data !== null && typeof data === "object" && !Array.isArray(data) ? data : undefined;
|
|
666
|
+
if (dataObj && Object.prototype.hasOwnProperty.call(dataObj, col.from)) {
|
|
667
|
+
return { ok: true, value: dataObj[col.from] };
|
|
668
|
+
}
|
|
669
|
+
if (col.from === "topic" && "topic" in event) {
|
|
670
|
+
return { ok: true, value: event.topic };
|
|
671
|
+
}
|
|
672
|
+
if (Object.prototype.hasOwnProperty.call(event, col.from)) {
|
|
673
|
+
return { ok: true, value: event[col.from] };
|
|
674
|
+
}
|
|
675
|
+
return { ok: false, missing: col.from };
|
|
676
|
+
}
|
|
677
|
+
function buildMaterializeRow(spec, event, ctx, tableColumns) {
|
|
678
|
+
const row = {};
|
|
679
|
+
for (const [colName, mapping] of Object.entries(spec.columns)) {
|
|
680
|
+
const resolved = resolveColumn(mapping, event, ctx.tx, ctx.block);
|
|
681
|
+
if (!resolved.ok) {
|
|
682
|
+
const colDef = tableColumns[colName];
|
|
683
|
+
if (colDef?.nullable) {
|
|
684
|
+
row[colName] = null;
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
return {
|
|
688
|
+
ok: false,
|
|
689
|
+
reason: `required materialize field "${resolved.missing}" missing for column "${colName}"`
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
row[colName] = resolved.value ?? null;
|
|
693
|
+
}
|
|
694
|
+
return { ok: true, row };
|
|
695
|
+
}
|
|
696
|
+
function applyMaterializeInsert(spec, event, ctx, schema) {
|
|
697
|
+
const table = schema[spec.table];
|
|
698
|
+
if (!table) {
|
|
699
|
+
return { ok: false, reason: `materialize table "${spec.table}" missing` };
|
|
700
|
+
}
|
|
701
|
+
const built = buildMaterializeRow(spec, event, ctx, table.columns);
|
|
702
|
+
if (!built.ok)
|
|
703
|
+
return built;
|
|
704
|
+
ctx.insert(spec.table, built.row);
|
|
705
|
+
return { ok: true };
|
|
706
|
+
}
|
|
707
|
+
|
|
44
708
|
// src/runtime/print-validate.ts
|
|
45
709
|
var COLUMN_TYPES = new Set([
|
|
46
710
|
"text",
|
|
@@ -120,24 +784,62 @@ function validatePrintPayload(prints, topic, data) {
|
|
|
120
784
|
return matchesTopic(declared, data);
|
|
121
785
|
}
|
|
122
786
|
|
|
123
|
-
// src/runtime/
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
787
|
+
// src/runtime/violations.ts
|
|
788
|
+
import { getTargetDb } from "@secondlayer/shared/db";
|
|
789
|
+
import { logger } from "@secondlayer/shared/logger";
|
|
790
|
+
import { sql } from "kysely";
|
|
791
|
+
var VIOLATIONS_CAP_PER_SUBGRAPH = 100;
|
|
792
|
+
function jsonSafe(value) {
|
|
793
|
+
if (typeof value === "bigint")
|
|
794
|
+
return value.toString();
|
|
795
|
+
if (value === null || value === undefined)
|
|
796
|
+
return value;
|
|
797
|
+
if (Array.isArray(value))
|
|
798
|
+
return value.map(jsonSafe);
|
|
799
|
+
if (typeof value === "object") {
|
|
800
|
+
const out = {};
|
|
801
|
+
for (const [k, v] of Object.entries(value)) {
|
|
802
|
+
out[k] = jsonSafe(v);
|
|
803
|
+
}
|
|
804
|
+
return out;
|
|
805
|
+
}
|
|
806
|
+
return value;
|
|
127
807
|
}
|
|
128
|
-
function
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
808
|
+
async function defaultRecordPrintViolation(input) {
|
|
809
|
+
try {
|
|
810
|
+
const db = getTargetDb();
|
|
811
|
+
const sample = jsonSafe(input.samplePayload);
|
|
812
|
+
await db.insertInto("subgraph_violations").values({
|
|
813
|
+
subgraph_name: input.subgraphName,
|
|
814
|
+
source_name: input.sourceName,
|
|
815
|
+
block_height: input.blockHeight,
|
|
816
|
+
tx_id: input.txId,
|
|
817
|
+
reason: input.reason,
|
|
818
|
+
sample_payload: sample
|
|
819
|
+
}).execute();
|
|
820
|
+
await sql`
|
|
821
|
+
DELETE FROM subgraph_violations
|
|
822
|
+
WHERE id IN (
|
|
823
|
+
SELECT id FROM subgraph_violations
|
|
824
|
+
WHERE subgraph_name = ${input.subgraphName}
|
|
825
|
+
ORDER BY seen_at DESC
|
|
826
|
+
OFFSET ${VIOLATIONS_CAP_PER_SUBGRAPH}
|
|
827
|
+
)
|
|
828
|
+
`.execute(db);
|
|
829
|
+
} catch (err) {
|
|
830
|
+
logger.warn("Failed to record print violation", {
|
|
831
|
+
subgraph: input.subgraphName,
|
|
832
|
+
error: err instanceof Error ? err.message : String(err)
|
|
833
|
+
});
|
|
138
834
|
}
|
|
139
|
-
return result;
|
|
140
835
|
}
|
|
836
|
+
var recorder = defaultRecordPrintViolation;
|
|
837
|
+
async function recordPrintViolation(input) {
|
|
838
|
+
await recorder(input);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// src/runtime/runner.ts
|
|
842
|
+
var DEFAULT_ERROR_THRESHOLD = 50;
|
|
141
843
|
function decodeFunctionArgs2(args) {
|
|
142
844
|
let parsed = args;
|
|
143
845
|
if (typeof parsed === "string") {
|
|
@@ -201,7 +903,7 @@ function buildContractCallInput(filter, tx) {
|
|
|
201
903
|
return;
|
|
202
904
|
try {
|
|
203
905
|
const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
|
|
204
|
-
input[toCamelCase(arg.name)] = decodeArg(arg.type,
|
|
906
|
+
input[toCamelCase(arg.name)] = decodeArg(arg.type, deserializeCV3(clean));
|
|
205
907
|
} catch {}
|
|
206
908
|
});
|
|
207
909
|
return input;
|
|
@@ -324,7 +1026,7 @@ function buildEventPayload(filter, tx, event) {
|
|
|
324
1026
|
const { topic: _, ...rest } = clarityObj ?? {};
|
|
325
1027
|
const data = Object.keys(rest).length > 0 ? camelizeKeys(rest) : rawValue && typeof rawValue !== "object" ? rawValue : {};
|
|
326
1028
|
return {
|
|
327
|
-
contractId: decoded
|
|
1029
|
+
contractId: printContractId(decoded) ?? tx.contract_id ?? "",
|
|
328
1030
|
topic,
|
|
329
1031
|
data: data ?? {},
|
|
330
1032
|
tx: txMeta
|
|
@@ -385,16 +1087,18 @@ async function runHandlers(subgraph, matched, ctx, opts) {
|
|
|
385
1087
|
units.sort((a, b) => (a.tx.tx_index ?? 0) - (b.tx.tx_index ?? 0) || (a.event?.event_index ?? -1) - (b.event?.event_index ?? -1));
|
|
386
1088
|
for (const { tx, event, sourceName } of units) {
|
|
387
1089
|
if (errors >= threshold) {
|
|
388
|
-
|
|
1090
|
+
logger2.error("Subgraph error threshold reached, skipping remaining events", {
|
|
389
1091
|
subgraph: subgraph.name,
|
|
390
1092
|
errors,
|
|
391
1093
|
threshold
|
|
392
1094
|
});
|
|
393
1095
|
return { processed, errors, ...skipped > 0 ? { skipped } : {} };
|
|
394
1096
|
}
|
|
1097
|
+
const filter = filterLookup.get(sourceName);
|
|
1098
|
+
const materialize = filter && "materialize" in filter && filter.materialize !== undefined ? filter.materialize : undefined;
|
|
395
1099
|
const handler = subgraph.handlers[sourceName] ?? subgraph.handlers["*"] ?? null;
|
|
396
|
-
if (!handler) {
|
|
397
|
-
|
|
1100
|
+
if (!handler && !materialize) {
|
|
1101
|
+
logger2.warn("No handler found for source", {
|
|
398
1102
|
subgraph: subgraph.name,
|
|
399
1103
|
sourceName,
|
|
400
1104
|
txId: tx.tx_id
|
|
@@ -409,7 +1113,6 @@ async function runHandlers(subgraph, matched, ctx, opts) {
|
|
|
409
1113
|
contractId: tx.contract_id ?? null,
|
|
410
1114
|
functionName: tx.function_name ?? null
|
|
411
1115
|
});
|
|
412
|
-
const filter = filterLookup.get(sourceName);
|
|
413
1116
|
const checkpoint = ctx.opsCheckpoint();
|
|
414
1117
|
try {
|
|
415
1118
|
let payload;
|
|
@@ -451,22 +1154,48 @@ async function runHandlers(subgraph, matched, ctx, opts) {
|
|
|
451
1154
|
const verdict = validatePrintPayload(filter.prints, printPayload.topic ?? "", printPayload.data ?? {});
|
|
452
1155
|
if (!verdict.ok) {
|
|
453
1156
|
skipped++;
|
|
454
|
-
|
|
1157
|
+
logger2.warn("Print payload does not match declared prints schema", {
|
|
455
1158
|
subgraph: subgraph.name,
|
|
456
1159
|
sourceName,
|
|
457
1160
|
topic: printPayload.topic,
|
|
458
1161
|
txId: tx.tx_id,
|
|
459
1162
|
reason: verdict.reason
|
|
460
1163
|
});
|
|
1164
|
+
recordPrintViolation({
|
|
1165
|
+
subgraphName: subgraph.name,
|
|
1166
|
+
sourceName,
|
|
1167
|
+
blockHeight: ctx.block.height,
|
|
1168
|
+
txId: tx.tx_id,
|
|
1169
|
+
reason: verdict.reason,
|
|
1170
|
+
samplePayload: {
|
|
1171
|
+
topic: printPayload.topic ?? "",
|
|
1172
|
+
data: printPayload.data ?? {}
|
|
1173
|
+
}
|
|
1174
|
+
});
|
|
1175
|
+
continue;
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
if (materialize) {
|
|
1179
|
+
const result = applyMaterializeInsert(materialize, payload, ctx, subgraph.schema);
|
|
1180
|
+
if (!result.ok) {
|
|
1181
|
+
skipped++;
|
|
1182
|
+
logger2.warn("Materialize skipped: required field missing", {
|
|
1183
|
+
subgraph: subgraph.name,
|
|
1184
|
+
sourceName,
|
|
1185
|
+
txId: tx.tx_id,
|
|
1186
|
+
reason: result.reason
|
|
1187
|
+
});
|
|
461
1188
|
continue;
|
|
462
1189
|
}
|
|
1190
|
+
processed++;
|
|
1191
|
+
} else if (handler) {
|
|
1192
|
+
await handler(payload, ctx);
|
|
1193
|
+
processed++;
|
|
463
1194
|
}
|
|
464
|
-
await handler(payload, ctx);
|
|
465
|
-
processed++;
|
|
466
1195
|
} catch (err) {
|
|
467
1196
|
ctx.rollbackTo(checkpoint);
|
|
468
1197
|
errors++;
|
|
469
|
-
|
|
1198
|
+
logger2.error("Subgraph handler error", {
|
|
470
1199
|
subgraph: subgraph.name,
|
|
471
1200
|
sourceName,
|
|
472
1201
|
txId: tx.tx_id,
|
|
@@ -483,5 +1212,5 @@ export {
|
|
|
483
1212
|
buildContractCallInput
|
|
484
1213
|
};
|
|
485
1214
|
|
|
486
|
-
//# debugId=
|
|
1215
|
+
//# debugId=B6BA5F58B8903CEC64756E2164756E21
|
|
487
1216
|
//# sourceMappingURL=runner.js.map
|