@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
|
@@ -942,15 +942,679 @@ function decodeFunctionArgs(args) {
|
|
|
942
942
|
return args.map(decodeClarityValue);
|
|
943
943
|
}
|
|
944
944
|
|
|
945
|
+
// src/runtime/source-matcher.ts
|
|
946
|
+
var patternCache = new Map;
|
|
947
|
+
function matchAny(value, pattern) {
|
|
948
|
+
return Array.isArray(pattern) ? pattern.some((p) => matchPattern(value, p)) : matchPattern(value, pattern);
|
|
949
|
+
}
|
|
950
|
+
function matchPattern(value, pattern) {
|
|
951
|
+
if (!pattern.includes("*"))
|
|
952
|
+
return value === pattern;
|
|
953
|
+
let re = patternCache.get(pattern);
|
|
954
|
+
if (!re) {
|
|
955
|
+
const regex = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
956
|
+
re = new RegExp(`^${regex}$`);
|
|
957
|
+
patternCache.set(pattern, re);
|
|
958
|
+
}
|
|
959
|
+
return re.test(value);
|
|
960
|
+
}
|
|
961
|
+
var EMPTY_SET = new Set;
|
|
962
|
+
function factoryAllows(filter, contractId, factoryContracts) {
|
|
963
|
+
if (!filter.factory)
|
|
964
|
+
return true;
|
|
965
|
+
if (!contractId)
|
|
966
|
+
return false;
|
|
967
|
+
return (factoryContracts.get(filter.factory.from) ?? EMPTY_SET).has(contractId);
|
|
968
|
+
}
|
|
969
|
+
function readPath(payload, path) {
|
|
970
|
+
let cur = payload;
|
|
971
|
+
for (const key of path.split(".")) {
|
|
972
|
+
if (cur === null || typeof cur !== "object")
|
|
973
|
+
return;
|
|
974
|
+
cur = cur[key];
|
|
975
|
+
}
|
|
976
|
+
return cur;
|
|
977
|
+
}
|
|
978
|
+
function traitAllows(filter, contractId, traitContracts) {
|
|
979
|
+
const trait = filter.trait;
|
|
980
|
+
if (!trait)
|
|
981
|
+
return true;
|
|
982
|
+
if (!contractId)
|
|
983
|
+
return false;
|
|
984
|
+
return (traitContracts.get(trait) ?? EMPTY_SET).has(contractId);
|
|
985
|
+
}
|
|
986
|
+
function assetContract(assetId) {
|
|
987
|
+
return assetId?.split("::")[0];
|
|
988
|
+
}
|
|
989
|
+
function printContractId(data) {
|
|
990
|
+
if (!data)
|
|
991
|
+
return;
|
|
992
|
+
const id = (typeof data.contract_identifier === "string" ? data.contract_identifier : undefined) ?? (typeof data.contract_id === "string" ? data.contract_id : undefined);
|
|
993
|
+
return id || undefined;
|
|
994
|
+
}
|
|
995
|
+
function matchFilter(filter, transactions, eventsByTx, traitContracts, factoryContracts = new Map) {
|
|
996
|
+
const results = [];
|
|
997
|
+
switch (filter.type) {
|
|
998
|
+
case "stx_transfer":
|
|
999
|
+
case "stx_mint":
|
|
1000
|
+
case "stx_burn":
|
|
1001
|
+
case "stx_lock": {
|
|
1002
|
+
const eventType = `${filter.type}_event`;
|
|
1003
|
+
for (const tx of transactions) {
|
|
1004
|
+
const txEvents = eventsByTx.get(tx.tx_id) ?? [];
|
|
1005
|
+
const matched = txEvents.filter((e) => e.type === eventType);
|
|
1006
|
+
if (matched.length === 0)
|
|
1007
|
+
continue;
|
|
1008
|
+
const filtered = matched.filter((e) => {
|
|
1009
|
+
const data = e.data;
|
|
1010
|
+
if (!data)
|
|
1011
|
+
return false;
|
|
1012
|
+
if ("sender" in filter && filter.sender) {
|
|
1013
|
+
if (!matchPattern(data.sender, filter.sender))
|
|
1014
|
+
return false;
|
|
1015
|
+
}
|
|
1016
|
+
if ("recipient" in filter && filter.recipient) {
|
|
1017
|
+
if (!matchPattern(data.recipient, filter.recipient))
|
|
1018
|
+
return false;
|
|
1019
|
+
}
|
|
1020
|
+
if ("lockedAddress" in filter && filter.lockedAddress) {
|
|
1021
|
+
if (!matchPattern(data.locked_address, filter.lockedAddress))
|
|
1022
|
+
return false;
|
|
1023
|
+
}
|
|
1024
|
+
if ("minAmount" in filter && filter.minAmount !== undefined) {
|
|
1025
|
+
const amount = BigInt(data.amount ?? data.locked_amount ?? "0");
|
|
1026
|
+
if (amount < filter.minAmount)
|
|
1027
|
+
return false;
|
|
1028
|
+
}
|
|
1029
|
+
if ("maxAmount" in filter && filter.maxAmount !== undefined) {
|
|
1030
|
+
const amount = BigInt(data.amount ?? "0");
|
|
1031
|
+
if (amount > filter.maxAmount)
|
|
1032
|
+
return false;
|
|
1033
|
+
}
|
|
1034
|
+
return true;
|
|
1035
|
+
});
|
|
1036
|
+
if (filtered.length > 0) {
|
|
1037
|
+
results.push({ tx, events: filtered });
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
break;
|
|
1041
|
+
}
|
|
1042
|
+
case "ft_transfer":
|
|
1043
|
+
case "ft_mint":
|
|
1044
|
+
case "ft_burn": {
|
|
1045
|
+
const eventType = `${filter.type}_event`;
|
|
1046
|
+
for (const tx of transactions) {
|
|
1047
|
+
const txEvents = eventsByTx.get(tx.tx_id) ?? [];
|
|
1048
|
+
const matched = txEvents.filter((e) => {
|
|
1049
|
+
if (e.type !== eventType)
|
|
1050
|
+
return false;
|
|
1051
|
+
const data = e.data;
|
|
1052
|
+
if (!data)
|
|
1053
|
+
return false;
|
|
1054
|
+
if (filter.assetIdentifier) {
|
|
1055
|
+
const assetId = data.asset_identifier;
|
|
1056
|
+
if (!assetId || !matchPattern(assetId, filter.assetIdentifier))
|
|
1057
|
+
return false;
|
|
1058
|
+
}
|
|
1059
|
+
if (!traitAllows(filter, assetContract(data.asset_identifier), traitContracts))
|
|
1060
|
+
return false;
|
|
1061
|
+
if ("sender" in filter && filter.sender) {
|
|
1062
|
+
if (!matchPattern(data.sender, filter.sender))
|
|
1063
|
+
return false;
|
|
1064
|
+
}
|
|
1065
|
+
if ("recipient" in filter && filter.recipient) {
|
|
1066
|
+
if (!matchPattern(data.recipient, filter.recipient))
|
|
1067
|
+
return false;
|
|
1068
|
+
}
|
|
1069
|
+
if (filter.minAmount !== undefined) {
|
|
1070
|
+
const amount = BigInt(data.amount ?? "0");
|
|
1071
|
+
if (amount < filter.minAmount)
|
|
1072
|
+
return false;
|
|
1073
|
+
}
|
|
1074
|
+
return true;
|
|
1075
|
+
});
|
|
1076
|
+
if (matched.length > 0) {
|
|
1077
|
+
results.push({ tx, events: matched });
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
break;
|
|
1081
|
+
}
|
|
1082
|
+
case "nft_transfer":
|
|
1083
|
+
case "nft_mint":
|
|
1084
|
+
case "nft_burn": {
|
|
1085
|
+
const eventType = `${filter.type}_event`;
|
|
1086
|
+
for (const tx of transactions) {
|
|
1087
|
+
const txEvents = eventsByTx.get(tx.tx_id) ?? [];
|
|
1088
|
+
const matched = txEvents.filter((e) => {
|
|
1089
|
+
if (e.type !== eventType)
|
|
1090
|
+
return false;
|
|
1091
|
+
const data = e.data;
|
|
1092
|
+
if (!data)
|
|
1093
|
+
return false;
|
|
1094
|
+
if (filter.assetIdentifier) {
|
|
1095
|
+
const assetId = data.asset_identifier;
|
|
1096
|
+
if (!assetId || !matchPattern(assetId, filter.assetIdentifier))
|
|
1097
|
+
return false;
|
|
1098
|
+
}
|
|
1099
|
+
if (!traitAllows(filter, assetContract(data.asset_identifier), traitContracts))
|
|
1100
|
+
return false;
|
|
1101
|
+
if ("sender" in filter && filter.sender) {
|
|
1102
|
+
if (!matchPattern(data.sender, filter.sender))
|
|
1103
|
+
return false;
|
|
1104
|
+
}
|
|
1105
|
+
if ("recipient" in filter && filter.recipient) {
|
|
1106
|
+
if (!matchPattern(data.recipient, filter.recipient))
|
|
1107
|
+
return false;
|
|
1108
|
+
}
|
|
1109
|
+
return true;
|
|
1110
|
+
});
|
|
1111
|
+
if (matched.length > 0) {
|
|
1112
|
+
results.push({ tx, events: matched });
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
break;
|
|
1116
|
+
}
|
|
1117
|
+
case "contract_call": {
|
|
1118
|
+
for (const tx of transactions) {
|
|
1119
|
+
if (tx.type !== "contract_call")
|
|
1120
|
+
continue;
|
|
1121
|
+
if (filter.contractId) {
|
|
1122
|
+
if (!tx.contract_id || !matchAny(tx.contract_id, filter.contractId))
|
|
1123
|
+
continue;
|
|
1124
|
+
}
|
|
1125
|
+
if (!factoryAllows(filter, tx.contract_id, factoryContracts))
|
|
1126
|
+
continue;
|
|
1127
|
+
if (filter.functionName) {
|
|
1128
|
+
if (!tx.function_name || !matchPattern(tx.function_name, filter.functionName))
|
|
1129
|
+
continue;
|
|
1130
|
+
}
|
|
1131
|
+
if (filter.caller) {
|
|
1132
|
+
if (!matchPattern(tx.sender, filter.caller))
|
|
1133
|
+
continue;
|
|
1134
|
+
}
|
|
1135
|
+
if (!traitAllows(filter, tx.contract_id, traitContracts))
|
|
1136
|
+
continue;
|
|
1137
|
+
const txEvents = eventsByTx.get(tx.tx_id) ?? [];
|
|
1138
|
+
results.push({ tx, events: txEvents });
|
|
1139
|
+
}
|
|
1140
|
+
break;
|
|
1141
|
+
}
|
|
1142
|
+
case "contract_deploy": {
|
|
1143
|
+
for (const tx of transactions) {
|
|
1144
|
+
if (tx.type !== "smart_contract")
|
|
1145
|
+
continue;
|
|
1146
|
+
if (filter.deployer) {
|
|
1147
|
+
if (!matchPattern(tx.sender, filter.deployer))
|
|
1148
|
+
continue;
|
|
1149
|
+
}
|
|
1150
|
+
if (filter.contractName) {
|
|
1151
|
+
const name = tx.contract_id?.split(".")[1] ?? "";
|
|
1152
|
+
if (!matchPattern(name, filter.contractName))
|
|
1153
|
+
continue;
|
|
1154
|
+
}
|
|
1155
|
+
const txEvents = eventsByTx.get(tx.tx_id) ?? [];
|
|
1156
|
+
results.push({ tx, events: txEvents });
|
|
1157
|
+
}
|
|
1158
|
+
break;
|
|
1159
|
+
}
|
|
1160
|
+
case "print_event": {
|
|
1161
|
+
for (const tx of transactions) {
|
|
1162
|
+
const txEvents = eventsByTx.get(tx.tx_id) ?? [];
|
|
1163
|
+
const matched = txEvents.filter((e) => {
|
|
1164
|
+
if (e.type !== "smart_contract_event" && e.type !== "contract_event")
|
|
1165
|
+
return false;
|
|
1166
|
+
const data = e.data;
|
|
1167
|
+
if (!data)
|
|
1168
|
+
return false;
|
|
1169
|
+
if (data.topic !== "print")
|
|
1170
|
+
return false;
|
|
1171
|
+
const contractId = printContractId(data);
|
|
1172
|
+
if (filter.contractId) {
|
|
1173
|
+
if (!contractId || !matchAny(contractId, filter.contractId))
|
|
1174
|
+
return false;
|
|
1175
|
+
}
|
|
1176
|
+
if (!traitAllows(filter, contractId, traitContracts))
|
|
1177
|
+
return false;
|
|
1178
|
+
if (!factoryAllows(filter, contractId, factoryContracts))
|
|
1179
|
+
return false;
|
|
1180
|
+
return true;
|
|
1181
|
+
});
|
|
1182
|
+
if (matched.length > 0) {
|
|
1183
|
+
results.push({ tx, events: matched });
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
break;
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
return results;
|
|
1190
|
+
}
|
|
1191
|
+
function matchSources(sources, transactions, events, traitContracts = new Map, factoryContracts = new Map) {
|
|
1192
|
+
const eventsByTx = new Map;
|
|
1193
|
+
for (const event of events) {
|
|
1194
|
+
const list = eventsByTx.get(event.tx_id) ?? [];
|
|
1195
|
+
list.push(event);
|
|
1196
|
+
eventsByTx.set(event.tx_id, list);
|
|
1197
|
+
}
|
|
1198
|
+
const seen = new Set;
|
|
1199
|
+
const results = [];
|
|
1200
|
+
for (const [sourceName, filter] of Object.entries(sources)) {
|
|
1201
|
+
const matches = matchFilter(filter, transactions, eventsByTx, traitContracts, factoryContracts);
|
|
1202
|
+
for (const match of matches) {
|
|
1203
|
+
const dedupeKey = `${match.tx.tx_id}:${sourceName}`;
|
|
1204
|
+
if (!seen.has(dedupeKey)) {
|
|
1205
|
+
seen.add(dedupeKey);
|
|
1206
|
+
results.push({ ...match, sourceName });
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
return results;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
945
1213
|
// src/runtime/runner.ts
|
|
946
1214
|
import { getErrorMessage } from "@secondlayer/shared";
|
|
947
|
-
import { logger as
|
|
1215
|
+
import { logger as logger4 } from "@secondlayer/shared/logger";
|
|
948
1216
|
import {
|
|
949
1217
|
clarityValueToJS,
|
|
950
|
-
deserializeCV as
|
|
1218
|
+
deserializeCV as deserializeCV4,
|
|
951
1219
|
toCamelCase as toCamelCase2
|
|
952
1220
|
} from "@secondlayer/stacks/clarity";
|
|
953
1221
|
|
|
1222
|
+
// src/print-schema.ts
|
|
1223
|
+
import {
|
|
1224
|
+
deserializeCV as deserializeCV3
|
|
1225
|
+
} from "@secondlayer/stacks/clarity";
|
|
1226
|
+
function camelizeDataKey(str) {
|
|
1227
|
+
return str.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
|
|
1228
|
+
}
|
|
1229
|
+
function camelizeKeys(obj) {
|
|
1230
|
+
if (obj === null || obj === undefined)
|
|
1231
|
+
return obj;
|
|
1232
|
+
if (typeof obj !== "object")
|
|
1233
|
+
return obj;
|
|
1234
|
+
if (Array.isArray(obj))
|
|
1235
|
+
return obj.map(camelizeKeys);
|
|
1236
|
+
const result = {};
|
|
1237
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
1238
|
+
result[camelizeDataKey(k)] = camelizeKeys(v);
|
|
1239
|
+
}
|
|
1240
|
+
return result;
|
|
1241
|
+
}
|
|
1242
|
+
function cvToTree(cv) {
|
|
1243
|
+
switch (cv.type) {
|
|
1244
|
+
case "uint":
|
|
1245
|
+
case "int":
|
|
1246
|
+
return { kind: cv.type };
|
|
1247
|
+
case "true":
|
|
1248
|
+
case "false":
|
|
1249
|
+
return { kind: "bool" };
|
|
1250
|
+
case "address":
|
|
1251
|
+
case "contract":
|
|
1252
|
+
return { kind: "principal" };
|
|
1253
|
+
case "buffer":
|
|
1254
|
+
return { kind: "buffer", len: cv.value.length / 2 };
|
|
1255
|
+
case "ascii":
|
|
1256
|
+
return { kind: "ascii", len: cv.value.length };
|
|
1257
|
+
case "utf8":
|
|
1258
|
+
return { kind: "utf8", len: new TextEncoder().encode(cv.value).length };
|
|
1259
|
+
case "none":
|
|
1260
|
+
return { kind: "optional", inner: null };
|
|
1261
|
+
case "some":
|
|
1262
|
+
return { kind: "optional", inner: cvToTree(cv.value) };
|
|
1263
|
+
case "ok":
|
|
1264
|
+
return { kind: "response", ok: cvToTree(cv.value), err: null };
|
|
1265
|
+
case "err":
|
|
1266
|
+
return { kind: "response", ok: null, err: cvToTree(cv.value) };
|
|
1267
|
+
case "list": {
|
|
1268
|
+
let inner = null;
|
|
1269
|
+
for (const el of cv.value) {
|
|
1270
|
+
const t = cvToTree(el);
|
|
1271
|
+
inner = inner ? unify(inner, t) : t;
|
|
1272
|
+
}
|
|
1273
|
+
return { kind: "list", inner };
|
|
1274
|
+
}
|
|
1275
|
+
case "tuple": {
|
|
1276
|
+
const entries = new Map;
|
|
1277
|
+
for (const [k, v] of Object.entries(cv.value)) {
|
|
1278
|
+
entries.set(k, { tree: cvToTree(v), present: 1 });
|
|
1279
|
+
}
|
|
1280
|
+
return { kind: "tuple", count: 1, entries };
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
function unifyNullable(a, b) {
|
|
1285
|
+
if (!a)
|
|
1286
|
+
return b;
|
|
1287
|
+
if (!b)
|
|
1288
|
+
return a;
|
|
1289
|
+
return unify(a, b);
|
|
1290
|
+
}
|
|
1291
|
+
var UNION_KIND_ORDER = {
|
|
1292
|
+
uint: 0,
|
|
1293
|
+
int: 1,
|
|
1294
|
+
bool: 2,
|
|
1295
|
+
principal: 3,
|
|
1296
|
+
buffer: 4,
|
|
1297
|
+
ascii: 5,
|
|
1298
|
+
utf8: 6,
|
|
1299
|
+
list: 7,
|
|
1300
|
+
tuple: 8,
|
|
1301
|
+
response: 9
|
|
1302
|
+
};
|
|
1303
|
+
function makeUnion(members) {
|
|
1304
|
+
const sorted = [...members].sort((a, b) => (UNION_KIND_ORDER[a.kind] ?? 99) - (UNION_KIND_ORDER[b.kind] ?? 99));
|
|
1305
|
+
return { kind: "union", members: sorted };
|
|
1306
|
+
}
|
|
1307
|
+
function unify(a, b) {
|
|
1308
|
+
if (a.kind === "optional" || b.kind === "optional") {
|
|
1309
|
+
const ai = a.kind === "optional" ? a.inner : a;
|
|
1310
|
+
const bi = b.kind === "optional" ? b.inner : b;
|
|
1311
|
+
return { kind: "optional", inner: unifyNullable(ai, bi) };
|
|
1312
|
+
}
|
|
1313
|
+
if (a.kind === "union")
|
|
1314
|
+
return unionAdd(a.members, b);
|
|
1315
|
+
if (b.kind === "union")
|
|
1316
|
+
return unionAdd(b.members, a);
|
|
1317
|
+
if (a.kind !== b.kind)
|
|
1318
|
+
return makeUnion([a, b]);
|
|
1319
|
+
switch (a.kind) {
|
|
1320
|
+
case "uint":
|
|
1321
|
+
case "int":
|
|
1322
|
+
case "bool":
|
|
1323
|
+
case "principal":
|
|
1324
|
+
return a;
|
|
1325
|
+
case "buffer":
|
|
1326
|
+
case "ascii":
|
|
1327
|
+
case "utf8":
|
|
1328
|
+
return { kind: a.kind, len: Math.max(a.len, b.len) };
|
|
1329
|
+
case "list":
|
|
1330
|
+
return {
|
|
1331
|
+
kind: "list",
|
|
1332
|
+
inner: unifyNullable(a.inner, b.inner)
|
|
1333
|
+
};
|
|
1334
|
+
case "response": {
|
|
1335
|
+
const rb = b;
|
|
1336
|
+
return {
|
|
1337
|
+
kind: "response",
|
|
1338
|
+
ok: unifyNullable(a.ok, rb.ok),
|
|
1339
|
+
err: unifyNullable(a.err, rb.err)
|
|
1340
|
+
};
|
|
1341
|
+
}
|
|
1342
|
+
case "tuple": {
|
|
1343
|
+
const tb = b;
|
|
1344
|
+
const entries = new Map([...a.entries].map(([k, e]) => [k, { ...e }]));
|
|
1345
|
+
for (const [k, e] of tb.entries) {
|
|
1346
|
+
const existing = entries.get(k);
|
|
1347
|
+
entries.set(k, existing ? {
|
|
1348
|
+
tree: unify(existing.tree, e.tree),
|
|
1349
|
+
present: existing.present + e.present
|
|
1350
|
+
} : { ...e });
|
|
1351
|
+
}
|
|
1352
|
+
return { kind: "tuple", count: a.count + tb.count, entries };
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
function unionAdd(members, t) {
|
|
1357
|
+
if (t.kind === "union") {
|
|
1358
|
+
let acc = { kind: "union", members };
|
|
1359
|
+
for (const m of t.members)
|
|
1360
|
+
acc = unify(acc, m);
|
|
1361
|
+
return acc;
|
|
1362
|
+
}
|
|
1363
|
+
const next = [...members];
|
|
1364
|
+
for (let i = 0;i < next.length; i++) {
|
|
1365
|
+
const member = next[i];
|
|
1366
|
+
if (!member)
|
|
1367
|
+
continue;
|
|
1368
|
+
const merged = unify(member, t);
|
|
1369
|
+
if (merged.kind !== "union") {
|
|
1370
|
+
next[i] = merged;
|
|
1371
|
+
return makeUnion(next);
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
next.push(t);
|
|
1375
|
+
return makeUnion(next);
|
|
1376
|
+
}
|
|
1377
|
+
function wrapOptional(t) {
|
|
1378
|
+
return t.kind === "optional" ? t : { kind: "optional", inner: t };
|
|
1379
|
+
}
|
|
1380
|
+
function renderClarity(t) {
|
|
1381
|
+
if (!t)
|
|
1382
|
+
return "?";
|
|
1383
|
+
switch (t.kind) {
|
|
1384
|
+
case "uint":
|
|
1385
|
+
case "int":
|
|
1386
|
+
case "bool":
|
|
1387
|
+
case "principal":
|
|
1388
|
+
return t.kind;
|
|
1389
|
+
case "buffer":
|
|
1390
|
+
return `(buff ${t.len})`;
|
|
1391
|
+
case "ascii":
|
|
1392
|
+
return `(string-ascii ${t.len})`;
|
|
1393
|
+
case "utf8":
|
|
1394
|
+
return `(string-utf8 ${t.len})`;
|
|
1395
|
+
case "optional":
|
|
1396
|
+
return `(optional ${renderClarity(t.inner)})`;
|
|
1397
|
+
case "list":
|
|
1398
|
+
return `(list ${renderClarity(t.inner)})`;
|
|
1399
|
+
case "response":
|
|
1400
|
+
return `(response ${renderClarity(t.ok)} ${renderClarity(t.err)})`;
|
|
1401
|
+
case "tuple": {
|
|
1402
|
+
const parts = [...t.entries].map(([k, e]) => {
|
|
1403
|
+
const tree = e.present < t.count ? wrapOptional(e.tree) : e.tree;
|
|
1404
|
+
return `(${k} ${renderClarity(tree)})`;
|
|
1405
|
+
});
|
|
1406
|
+
return `(tuple ${parts.join(" ")})`;
|
|
1407
|
+
}
|
|
1408
|
+
case "union":
|
|
1409
|
+
return t.members.map(renderClarity).join(" | ");
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
function renderTs(t) {
|
|
1413
|
+
if (!t)
|
|
1414
|
+
return "unknown";
|
|
1415
|
+
switch (t.kind) {
|
|
1416
|
+
case "uint":
|
|
1417
|
+
case "int":
|
|
1418
|
+
return "bigint";
|
|
1419
|
+
case "bool":
|
|
1420
|
+
return "boolean";
|
|
1421
|
+
case "principal":
|
|
1422
|
+
case "buffer":
|
|
1423
|
+
case "ascii":
|
|
1424
|
+
case "utf8":
|
|
1425
|
+
return "string";
|
|
1426
|
+
case "optional":
|
|
1427
|
+
return t.inner ? `${renderTs(t.inner)} | null` : "unknown | null";
|
|
1428
|
+
case "list": {
|
|
1429
|
+
const inner = renderTs(t.inner);
|
|
1430
|
+
return inner.includes(" | ") ? `(${inner})[]` : `${inner}[]`;
|
|
1431
|
+
}
|
|
1432
|
+
case "response": {
|
|
1433
|
+
const sides = [...new Set([renderTs(t.ok), renderTs(t.err)])];
|
|
1434
|
+
return sides.join(" | ");
|
|
1435
|
+
}
|
|
1436
|
+
case "tuple": {
|
|
1437
|
+
const parts = [...t.entries].map(([k, e]) => {
|
|
1438
|
+
const opt = e.present < t.count ? "?" : "";
|
|
1439
|
+
return `${camelizeDataKey(k)}${opt}: ${renderTs(e.tree)}`;
|
|
1440
|
+
});
|
|
1441
|
+
return `{ ${parts.join("; ")} }`;
|
|
1442
|
+
}
|
|
1443
|
+
case "union":
|
|
1444
|
+
return [...new Set(t.members.map((m) => renderTs(m)))].join(" | ");
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
function toColumnType(t) {
|
|
1448
|
+
if (!t)
|
|
1449
|
+
return "jsonb";
|
|
1450
|
+
switch (t.kind) {
|
|
1451
|
+
case "uint":
|
|
1452
|
+
return "uint";
|
|
1453
|
+
case "int":
|
|
1454
|
+
return "int";
|
|
1455
|
+
case "bool":
|
|
1456
|
+
return "boolean";
|
|
1457
|
+
case "principal":
|
|
1458
|
+
return "principal";
|
|
1459
|
+
case "buffer":
|
|
1460
|
+
case "ascii":
|
|
1461
|
+
case "utf8":
|
|
1462
|
+
return "text";
|
|
1463
|
+
case "list":
|
|
1464
|
+
case "tuple":
|
|
1465
|
+
return "jsonb";
|
|
1466
|
+
case "optional":
|
|
1467
|
+
return toColumnType(t.inner);
|
|
1468
|
+
case "response":
|
|
1469
|
+
return t.ok ? toColumnType(t.ok) : "jsonb";
|
|
1470
|
+
case "union":
|
|
1471
|
+
return "jsonb";
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
var MAX_DECODED_PER_TOPIC_NEWEST = 75;
|
|
1475
|
+
var MAX_DECODED_PER_TOPIC_OLDEST = 25;
|
|
1476
|
+
function inferPrintTopics(samples) {
|
|
1477
|
+
const groups = new Map;
|
|
1478
|
+
for (const s of samples) {
|
|
1479
|
+
const group = groups.get(s.topic);
|
|
1480
|
+
if (group)
|
|
1481
|
+
group.push(s);
|
|
1482
|
+
else
|
|
1483
|
+
groups.set(s.topic, [s]);
|
|
1484
|
+
}
|
|
1485
|
+
const out = [];
|
|
1486
|
+
for (const [topic, rows] of groups) {
|
|
1487
|
+
let first = Number.POSITIVE_INFINITY;
|
|
1488
|
+
let last = Number.NEGATIVE_INFINITY;
|
|
1489
|
+
for (const r of rows) {
|
|
1490
|
+
if (r.blockHeight < first)
|
|
1491
|
+
first = r.blockHeight;
|
|
1492
|
+
if (r.blockHeight > last)
|
|
1493
|
+
last = r.blockHeight;
|
|
1494
|
+
}
|
|
1495
|
+
const withHex = [...rows].filter((r) => r.rawHex !== null).sort((a, b) => b.blockHeight - a.blockHeight);
|
|
1496
|
+
const budget = MAX_DECODED_PER_TOPIC_NEWEST + MAX_DECODED_PER_TOPIC_OLDEST;
|
|
1497
|
+
const picked = withHex.length <= budget ? withHex : [
|
|
1498
|
+
...withHex.slice(0, MAX_DECODED_PER_TOPIC_NEWEST),
|
|
1499
|
+
...withHex.slice(-MAX_DECODED_PER_TOPIC_OLDEST)
|
|
1500
|
+
];
|
|
1501
|
+
const tuples = [];
|
|
1502
|
+
let decoded = 0;
|
|
1503
|
+
for (const p of picked) {
|
|
1504
|
+
try {
|
|
1505
|
+
const cv = deserializeCV3(p.rawHex);
|
|
1506
|
+
decoded++;
|
|
1507
|
+
if (cv.type === "tuple")
|
|
1508
|
+
tuples.push(cv);
|
|
1509
|
+
} catch {}
|
|
1510
|
+
}
|
|
1511
|
+
const nonTuple = decoded > 0 && tuples.length === 0;
|
|
1512
|
+
const fields = [];
|
|
1513
|
+
if (!nonTuple) {
|
|
1514
|
+
const stats = new Map;
|
|
1515
|
+
for (const t of tuples) {
|
|
1516
|
+
for (const [key, value] of Object.entries(t.value)) {
|
|
1517
|
+
if (key === "topic")
|
|
1518
|
+
continue;
|
|
1519
|
+
const tree = cvToTree(value);
|
|
1520
|
+
const existing = stats.get(key);
|
|
1521
|
+
if (existing) {
|
|
1522
|
+
existing.present++;
|
|
1523
|
+
existing.tree = unify(existing.tree, tree);
|
|
1524
|
+
if (value.type === "none")
|
|
1525
|
+
existing.noneCount++;
|
|
1526
|
+
if (value.type === "none" || value.type === "some") {
|
|
1527
|
+
existing.optionalSeen = true;
|
|
1528
|
+
}
|
|
1529
|
+
} else {
|
|
1530
|
+
stats.set(key, {
|
|
1531
|
+
tree,
|
|
1532
|
+
present: 1,
|
|
1533
|
+
noneCount: value.type === "none" ? 1 : 0,
|
|
1534
|
+
optionalSeen: value.type === "none" || value.type === "some"
|
|
1535
|
+
});
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
for (const [name, st] of [...stats].sort(([a], [b]) => a.localeCompare(b))) {
|
|
1540
|
+
const field = {
|
|
1541
|
+
name,
|
|
1542
|
+
camel_name: camelizeDataKey(name),
|
|
1543
|
+
clarity_type: renderClarity(st.tree),
|
|
1544
|
+
ts_type: renderTs(st.tree),
|
|
1545
|
+
column_type: toColumnType(st.tree),
|
|
1546
|
+
always_present: st.present === tuples.length
|
|
1547
|
+
};
|
|
1548
|
+
if (st.optionalSeen) {
|
|
1549
|
+
field.optional_some_rate = (st.present - st.noneCount) / st.present;
|
|
1550
|
+
}
|
|
1551
|
+
fields.push(field);
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
out.push({
|
|
1555
|
+
topic,
|
|
1556
|
+
count: rows.length,
|
|
1557
|
+
first_height: first,
|
|
1558
|
+
last_height: last,
|
|
1559
|
+
non_tuple: nonTuple,
|
|
1560
|
+
fields
|
|
1561
|
+
});
|
|
1562
|
+
}
|
|
1563
|
+
return out.sort((a, b) => b.count - a.count);
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
// src/runtime/materialize.ts
|
|
1567
|
+
function resolveColumn(col, event, tx, block) {
|
|
1568
|
+
if ("fromTx" in col) {
|
|
1569
|
+
return { ok: true, value: tx[col.fromTx] };
|
|
1570
|
+
}
|
|
1571
|
+
if ("fromBlock" in col) {
|
|
1572
|
+
return { ok: true, value: block[col.fromBlock] };
|
|
1573
|
+
}
|
|
1574
|
+
const data = event.data;
|
|
1575
|
+
const dataObj = data !== null && typeof data === "object" && !Array.isArray(data) ? data : undefined;
|
|
1576
|
+
if (dataObj && Object.prototype.hasOwnProperty.call(dataObj, col.from)) {
|
|
1577
|
+
return { ok: true, value: dataObj[col.from] };
|
|
1578
|
+
}
|
|
1579
|
+
if (col.from === "topic" && "topic" in event) {
|
|
1580
|
+
return { ok: true, value: event.topic };
|
|
1581
|
+
}
|
|
1582
|
+
if (Object.prototype.hasOwnProperty.call(event, col.from)) {
|
|
1583
|
+
return { ok: true, value: event[col.from] };
|
|
1584
|
+
}
|
|
1585
|
+
return { ok: false, missing: col.from };
|
|
1586
|
+
}
|
|
1587
|
+
function buildMaterializeRow(spec, event, ctx, tableColumns) {
|
|
1588
|
+
const row = {};
|
|
1589
|
+
for (const [colName, mapping] of Object.entries(spec.columns)) {
|
|
1590
|
+
const resolved = resolveColumn(mapping, event, ctx.tx, ctx.block);
|
|
1591
|
+
if (!resolved.ok) {
|
|
1592
|
+
const colDef = tableColumns[colName];
|
|
1593
|
+
if (colDef?.nullable) {
|
|
1594
|
+
row[colName] = null;
|
|
1595
|
+
continue;
|
|
1596
|
+
}
|
|
1597
|
+
return {
|
|
1598
|
+
ok: false,
|
|
1599
|
+
reason: `required materialize field "${resolved.missing}" missing for column "${colName}"`
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
row[colName] = resolved.value ?? null;
|
|
1603
|
+
}
|
|
1604
|
+
return { ok: true, row };
|
|
1605
|
+
}
|
|
1606
|
+
function applyMaterializeInsert(spec, event, ctx, schema) {
|
|
1607
|
+
const table = schema[spec.table];
|
|
1608
|
+
if (!table) {
|
|
1609
|
+
return { ok: false, reason: `materialize table "${spec.table}" missing` };
|
|
1610
|
+
}
|
|
1611
|
+
const built = buildMaterializeRow(spec, event, ctx, table.columns);
|
|
1612
|
+
if (!built.ok)
|
|
1613
|
+
return built;
|
|
1614
|
+
ctx.insert(spec.table, built.row);
|
|
1615
|
+
return { ok: true };
|
|
1616
|
+
}
|
|
1617
|
+
|
|
954
1618
|
// src/runtime/print-validate.ts
|
|
955
1619
|
var COLUMN_TYPES = new Set([
|
|
956
1620
|
"text",
|
|
@@ -1030,24 +1694,62 @@ function validatePrintPayload(prints, topic, data) {
|
|
|
1030
1694
|
return matchesTopic(declared, data);
|
|
1031
1695
|
}
|
|
1032
1696
|
|
|
1033
|
-
// src/runtime/
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1697
|
+
// src/runtime/violations.ts
|
|
1698
|
+
import { getTargetDb } from "@secondlayer/shared/db";
|
|
1699
|
+
import { logger as logger3 } from "@secondlayer/shared/logger";
|
|
1700
|
+
import { sql as sql2 } from "kysely";
|
|
1701
|
+
var VIOLATIONS_CAP_PER_SUBGRAPH = 100;
|
|
1702
|
+
function jsonSafe2(value) {
|
|
1703
|
+
if (typeof value === "bigint")
|
|
1704
|
+
return value.toString();
|
|
1705
|
+
if (value === null || value === undefined)
|
|
1706
|
+
return value;
|
|
1707
|
+
if (Array.isArray(value))
|
|
1708
|
+
return value.map(jsonSafe2);
|
|
1709
|
+
if (typeof value === "object") {
|
|
1710
|
+
const out = {};
|
|
1711
|
+
for (const [k, v] of Object.entries(value)) {
|
|
1712
|
+
out[k] = jsonSafe2(v);
|
|
1713
|
+
}
|
|
1714
|
+
return out;
|
|
1715
|
+
}
|
|
1716
|
+
return value;
|
|
1037
1717
|
}
|
|
1038
|
-
function
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1718
|
+
async function defaultRecordPrintViolation(input) {
|
|
1719
|
+
try {
|
|
1720
|
+
const db = getTargetDb();
|
|
1721
|
+
const sample = jsonSafe2(input.samplePayload);
|
|
1722
|
+
await db.insertInto("subgraph_violations").values({
|
|
1723
|
+
subgraph_name: input.subgraphName,
|
|
1724
|
+
source_name: input.sourceName,
|
|
1725
|
+
block_height: input.blockHeight,
|
|
1726
|
+
tx_id: input.txId,
|
|
1727
|
+
reason: input.reason,
|
|
1728
|
+
sample_payload: sample
|
|
1729
|
+
}).execute();
|
|
1730
|
+
await sql2`
|
|
1731
|
+
DELETE FROM subgraph_violations
|
|
1732
|
+
WHERE id IN (
|
|
1733
|
+
SELECT id FROM subgraph_violations
|
|
1734
|
+
WHERE subgraph_name = ${input.subgraphName}
|
|
1735
|
+
ORDER BY seen_at DESC
|
|
1736
|
+
OFFSET ${VIOLATIONS_CAP_PER_SUBGRAPH}
|
|
1737
|
+
)
|
|
1738
|
+
`.execute(db);
|
|
1739
|
+
} catch (err) {
|
|
1740
|
+
logger3.warn("Failed to record print violation", {
|
|
1741
|
+
subgraph: input.subgraphName,
|
|
1742
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1743
|
+
});
|
|
1048
1744
|
}
|
|
1049
|
-
return result;
|
|
1050
1745
|
}
|
|
1746
|
+
var recorder = defaultRecordPrintViolation;
|
|
1747
|
+
async function recordPrintViolation(input) {
|
|
1748
|
+
await recorder(input);
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
// src/runtime/runner.ts
|
|
1752
|
+
var DEFAULT_ERROR_THRESHOLD = 50;
|
|
1051
1753
|
function decodeFunctionArgs2(args) {
|
|
1052
1754
|
let parsed = args;
|
|
1053
1755
|
if (typeof parsed === "string") {
|
|
@@ -1111,7 +1813,7 @@ function buildContractCallInput(filter, tx) {
|
|
|
1111
1813
|
return;
|
|
1112
1814
|
try {
|
|
1113
1815
|
const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
|
|
1114
|
-
input[toCamelCase2(arg.name)] = decodeArg(arg.type,
|
|
1816
|
+
input[toCamelCase2(arg.name)] = decodeArg(arg.type, deserializeCV4(clean));
|
|
1115
1817
|
} catch {}
|
|
1116
1818
|
});
|
|
1117
1819
|
return input;
|
|
@@ -1234,7 +1936,7 @@ function buildEventPayload(filter, tx, event) {
|
|
|
1234
1936
|
const { topic: _, ...rest } = clarityObj ?? {};
|
|
1235
1937
|
const data = Object.keys(rest).length > 0 ? camelizeKeys(rest) : rawValue && typeof rawValue !== "object" ? rawValue : {};
|
|
1236
1938
|
return {
|
|
1237
|
-
contractId: decoded
|
|
1939
|
+
contractId: printContractId(decoded) ?? tx.contract_id ?? "",
|
|
1238
1940
|
topic,
|
|
1239
1941
|
data: data ?? {},
|
|
1240
1942
|
tx: txMeta
|
|
@@ -1295,16 +1997,18 @@ async function runHandlers(subgraph, matched, ctx, opts) {
|
|
|
1295
1997
|
units.sort((a, b) => (a.tx.tx_index ?? 0) - (b.tx.tx_index ?? 0) || (a.event?.event_index ?? -1) - (b.event?.event_index ?? -1));
|
|
1296
1998
|
for (const { tx, event, sourceName } of units) {
|
|
1297
1999
|
if (errors >= threshold) {
|
|
1298
|
-
|
|
2000
|
+
logger4.error("Subgraph error threshold reached, skipping remaining events", {
|
|
1299
2001
|
subgraph: subgraph.name,
|
|
1300
2002
|
errors,
|
|
1301
2003
|
threshold
|
|
1302
2004
|
});
|
|
1303
2005
|
return { processed, errors, ...skipped > 0 ? { skipped } : {} };
|
|
1304
2006
|
}
|
|
2007
|
+
const filter = filterLookup.get(sourceName);
|
|
2008
|
+
const materialize = filter && "materialize" in filter && filter.materialize !== undefined ? filter.materialize : undefined;
|
|
1305
2009
|
const handler = subgraph.handlers[sourceName] ?? subgraph.handlers["*"] ?? null;
|
|
1306
|
-
if (!handler) {
|
|
1307
|
-
|
|
2010
|
+
if (!handler && !materialize) {
|
|
2011
|
+
logger4.warn("No handler found for source", {
|
|
1308
2012
|
subgraph: subgraph.name,
|
|
1309
2013
|
sourceName,
|
|
1310
2014
|
txId: tx.tx_id
|
|
@@ -1319,7 +2023,6 @@ async function runHandlers(subgraph, matched, ctx, opts) {
|
|
|
1319
2023
|
contractId: tx.contract_id ?? null,
|
|
1320
2024
|
functionName: tx.function_name ?? null
|
|
1321
2025
|
});
|
|
1322
|
-
const filter = filterLookup.get(sourceName);
|
|
1323
2026
|
const checkpoint = ctx.opsCheckpoint();
|
|
1324
2027
|
try {
|
|
1325
2028
|
let payload;
|
|
@@ -1361,22 +2064,48 @@ async function runHandlers(subgraph, matched, ctx, opts) {
|
|
|
1361
2064
|
const verdict = validatePrintPayload(filter.prints, printPayload.topic ?? "", printPayload.data ?? {});
|
|
1362
2065
|
if (!verdict.ok) {
|
|
1363
2066
|
skipped++;
|
|
1364
|
-
|
|
2067
|
+
logger4.warn("Print payload does not match declared prints schema", {
|
|
1365
2068
|
subgraph: subgraph.name,
|
|
1366
2069
|
sourceName,
|
|
1367
2070
|
topic: printPayload.topic,
|
|
1368
2071
|
txId: tx.tx_id,
|
|
1369
2072
|
reason: verdict.reason
|
|
1370
2073
|
});
|
|
2074
|
+
recordPrintViolation({
|
|
2075
|
+
subgraphName: subgraph.name,
|
|
2076
|
+
sourceName,
|
|
2077
|
+
blockHeight: ctx.block.height,
|
|
2078
|
+
txId: tx.tx_id,
|
|
2079
|
+
reason: verdict.reason,
|
|
2080
|
+
samplePayload: {
|
|
2081
|
+
topic: printPayload.topic ?? "",
|
|
2082
|
+
data: printPayload.data ?? {}
|
|
2083
|
+
}
|
|
2084
|
+
});
|
|
2085
|
+
continue;
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
if (materialize) {
|
|
2089
|
+
const result = applyMaterializeInsert(materialize, payload, ctx, subgraph.schema);
|
|
2090
|
+
if (!result.ok) {
|
|
2091
|
+
skipped++;
|
|
2092
|
+
logger4.warn("Materialize skipped: required field missing", {
|
|
2093
|
+
subgraph: subgraph.name,
|
|
2094
|
+
sourceName,
|
|
2095
|
+
txId: tx.tx_id,
|
|
2096
|
+
reason: result.reason
|
|
2097
|
+
});
|
|
1371
2098
|
continue;
|
|
1372
2099
|
}
|
|
2100
|
+
processed++;
|
|
2101
|
+
} else if (handler) {
|
|
2102
|
+
await handler(payload, ctx);
|
|
2103
|
+
processed++;
|
|
1373
2104
|
}
|
|
1374
|
-
await handler(payload, ctx);
|
|
1375
|
-
processed++;
|
|
1376
2105
|
} catch (err) {
|
|
1377
2106
|
ctx.rollbackTo(checkpoint);
|
|
1378
2107
|
errors++;
|
|
1379
|
-
|
|
2108
|
+
logger4.error("Subgraph handler error", {
|
|
1380
2109
|
subgraph: subgraph.name,
|
|
1381
2110
|
sourceName,
|
|
1382
2111
|
txId: tx.tx_id,
|
|
@@ -1388,7 +2117,7 @@ async function runHandlers(subgraph, matched, ctx, opts) {
|
|
|
1388
2117
|
return { processed, errors, ...skipped > 0 ? { skipped } : {} };
|
|
1389
2118
|
}
|
|
1390
2119
|
|
|
1391
|
-
// src/testing/
|
|
2120
|
+
// src/testing/harness.ts
|
|
1392
2121
|
import { normalizeAbi as normalizeAbi2, toCamelCase as toCamelCase3 } from "@secondlayer/stacks/clarity";
|
|
1393
2122
|
var TEST_SCHEMA_NAME = "subgraph_test";
|
|
1394
2123
|
function defaultBlock(overrides = {}) {
|
|
@@ -1456,6 +2185,20 @@ class InMemorySubgraphContext extends SubgraphContext {
|
|
|
1456
2185
|
}
|
|
1457
2186
|
this.ops.length = 0;
|
|
1458
2187
|
}
|
|
2188
|
+
insertsSince(checkpoint) {
|
|
2189
|
+
const out = [];
|
|
2190
|
+
for (const op of this.ops.slice(Math.max(0, checkpoint))) {
|
|
2191
|
+
if (op.kind !== "insert")
|
|
2192
|
+
continue;
|
|
2193
|
+
const row = { ...op.data };
|
|
2194
|
+
out.push({
|
|
2195
|
+
table: op.table,
|
|
2196
|
+
keys: Object.keys(row).filter((k) => !k.startsWith("_")),
|
|
2197
|
+
row
|
|
2198
|
+
});
|
|
2199
|
+
}
|
|
2200
|
+
return out;
|
|
2201
|
+
}
|
|
1459
2202
|
}
|
|
1460
2203
|
function sameValue(a, b) {
|
|
1461
2204
|
if (a === b)
|
|
@@ -1471,7 +2214,10 @@ function createTestContext(schema, options = {}) {
|
|
|
1471
2214
|
const ctx = impl;
|
|
1472
2215
|
ctx.rows = (table) => impl.rowsOf(table);
|
|
1473
2216
|
ctx.commit = () => impl.commitOps();
|
|
1474
|
-
ctx.setTx = (tx) =>
|
|
2217
|
+
ctx.setTx = (tx) => {
|
|
2218
|
+
SubgraphContext.prototype.setTx.call(impl, defaultTx(tx));
|
|
2219
|
+
};
|
|
2220
|
+
ctx.pendingInsertsSince = (checkpoint) => impl.insertsSince(checkpoint);
|
|
1475
2221
|
return ctx;
|
|
1476
2222
|
}
|
|
1477
2223
|
function buildEvent(source, payload, options = {}) {
|
|
@@ -1491,11 +2237,284 @@ function buildEvent(source, payload, options = {}) {
|
|
|
1491
2237
|
}
|
|
1492
2238
|
return { ...base, ...payload };
|
|
1493
2239
|
}
|
|
2240
|
+
// src/testing/run.ts
|
|
2241
|
+
function toHandlerPayload(_filter, row) {
|
|
2242
|
+
if (row.event_type === "print") {
|
|
2243
|
+
const payload = row.payload;
|
|
2244
|
+
return {
|
|
2245
|
+
contractId: row.contract_id ?? "",
|
|
2246
|
+
topic: payload?.topic ?? "",
|
|
2247
|
+
data: camelizeKeys(payload?.value) ?? {}
|
|
2248
|
+
};
|
|
2249
|
+
}
|
|
2250
|
+
const r = row;
|
|
2251
|
+
return {
|
|
2252
|
+
...r.sender !== undefined ? { sender: r.sender } : {},
|
|
2253
|
+
...r.recipient !== undefined ? { recipient: r.recipient } : {},
|
|
2254
|
+
...r.amount !== undefined ? { amount: BigInt(String(r.amount)) } : {},
|
|
2255
|
+
...r.asset_identifier !== undefined ? { assetIdentifier: r.asset_identifier } : {},
|
|
2256
|
+
...r.value !== undefined ? { tokenId: r.value } : {}
|
|
2257
|
+
};
|
|
2258
|
+
}
|
|
2259
|
+
function toContractCallPayload(row) {
|
|
2260
|
+
return {
|
|
2261
|
+
type: "contract_call",
|
|
2262
|
+
sender: row.sender,
|
|
2263
|
+
contractId: row.contract_id ?? "",
|
|
2264
|
+
functionName: row.function_name ?? "",
|
|
2265
|
+
args: Array.isArray(row.args) ? row.args : [],
|
|
2266
|
+
result: row.result ?? null,
|
|
2267
|
+
resultHex: row.result_hex ?? null,
|
|
2268
|
+
tx: {
|
|
2269
|
+
txId: row.tx_id,
|
|
2270
|
+
sender: row.sender,
|
|
2271
|
+
type: "contract_call",
|
|
2272
|
+
status: row.status,
|
|
2273
|
+
contractId: row.contract_id ?? null,
|
|
2274
|
+
functionName: row.function_name ?? null
|
|
2275
|
+
}
|
|
2276
|
+
};
|
|
2277
|
+
}
|
|
2278
|
+
function jsonSafe3(value) {
|
|
2279
|
+
if (typeof value === "bigint")
|
|
2280
|
+
return value.toString();
|
|
2281
|
+
if (value === null || value === undefined)
|
|
2282
|
+
return value;
|
|
2283
|
+
if (Array.isArray(value))
|
|
2284
|
+
return value.map(jsonSafe3);
|
|
2285
|
+
if (typeof value === "object") {
|
|
2286
|
+
const out = {};
|
|
2287
|
+
for (const [k, v] of Object.entries(value)) {
|
|
2288
|
+
out[k] = jsonSafe3(v);
|
|
2289
|
+
}
|
|
2290
|
+
return out;
|
|
2291
|
+
}
|
|
2292
|
+
return value;
|
|
2293
|
+
}
|
|
2294
|
+
function dataKeysOf(data) {
|
|
2295
|
+
if (data && typeof data === "object" && !Array.isArray(data)) {
|
|
2296
|
+
return Object.keys(data);
|
|
2297
|
+
}
|
|
2298
|
+
return [];
|
|
2299
|
+
}
|
|
2300
|
+
function rowMeta(row) {
|
|
2301
|
+
const r = row;
|
|
2302
|
+
return {
|
|
2303
|
+
...typeof r.block_height === "number" ? { blockHeight: r.block_height } : {},
|
|
2304
|
+
...typeof r.tx_id === "string" ? { txId: r.tx_id } : {}
|
|
2305
|
+
};
|
|
2306
|
+
}
|
|
2307
|
+
function inKeysOf(filter, payload) {
|
|
2308
|
+
if (filter?.type === "contract_call") {
|
|
2309
|
+
return ["functionName", "args", "sender", "contractId"].filter((k) => payload[k] !== undefined);
|
|
2310
|
+
}
|
|
2311
|
+
return dataKeysOf(payload.data);
|
|
2312
|
+
}
|
|
2313
|
+
function unusedInKeysAcross(traces) {
|
|
2314
|
+
if (traces.length === 0)
|
|
2315
|
+
return;
|
|
2316
|
+
const alwaysIn = new Set(traces[0]?.inKeys ?? []);
|
|
2317
|
+
for (const t of traces.slice(1)) {
|
|
2318
|
+
const keys = new Set(t.inKeys);
|
|
2319
|
+
for (const k of [...alwaysIn]) {
|
|
2320
|
+
if (!keys.has(k))
|
|
2321
|
+
alwaysIn.delete(k);
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
const outKeys = new Set;
|
|
2325
|
+
for (const t of traces) {
|
|
2326
|
+
for (const o of t.outs) {
|
|
2327
|
+
for (const k of o.keys)
|
|
2328
|
+
outKeys.add(k);
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
const outNormalized = new Set([...outKeys].map((k) => k.replace(/_/g, "").toLowerCase()));
|
|
2332
|
+
const unused = [...alwaysIn].filter((k) => !outNormalized.has(k.replace(/_/g, "").toLowerCase()));
|
|
2333
|
+
return unused.length > 0 ? unused : undefined;
|
|
2334
|
+
}
|
|
2335
|
+
async function runSubgraphTest(input) {
|
|
2336
|
+
const { schema, handlers, sources, events, trace } = input;
|
|
2337
|
+
const sourceNames = Object.keys(sources);
|
|
2338
|
+
if (sourceNames.length === 0) {
|
|
2339
|
+
return {
|
|
2340
|
+
ok: false,
|
|
2341
|
+
code: "NO_SOURCES",
|
|
2342
|
+
matched: 0,
|
|
2343
|
+
written: 0,
|
|
2344
|
+
tables: [],
|
|
2345
|
+
hint: "No sources to test."
|
|
2346
|
+
};
|
|
2347
|
+
}
|
|
2348
|
+
const ctx = createTestContext(schema);
|
|
2349
|
+
let matched = 0;
|
|
2350
|
+
let firstEvent;
|
|
2351
|
+
const traces = [];
|
|
2352
|
+
for (const [name, rows] of Object.entries(events)) {
|
|
2353
|
+
const filter = sources[name];
|
|
2354
|
+
const handler = handlers[name] ?? handlers["*"];
|
|
2355
|
+
const materialize = filter && "materialize" in filter && filter.materialize !== undefined ? filter.materialize : undefined;
|
|
2356
|
+
if (typeof handler !== "function" && !materialize)
|
|
2357
|
+
continue;
|
|
2358
|
+
for (const row of rows) {
|
|
2359
|
+
const payload = filter?.type === "contract_call" ? toContractCallPayload(row) : toHandlerPayload(filter, row);
|
|
2360
|
+
if (filter?.type === "print_event" && filter.topic && payload.topic !== filter.topic) {
|
|
2361
|
+
continue;
|
|
2362
|
+
}
|
|
2363
|
+
if (!firstEvent) {
|
|
2364
|
+
firstEvent = {
|
|
2365
|
+
source: name,
|
|
2366
|
+
data: filter?.type === "contract_call" ? {
|
|
2367
|
+
functionName: payload.functionName,
|
|
2368
|
+
args: payload.args
|
|
2369
|
+
} : payload.data
|
|
2370
|
+
};
|
|
2371
|
+
}
|
|
2372
|
+
matched++;
|
|
2373
|
+
const checkpoint = ctx.opsCheckpoint();
|
|
2374
|
+
try {
|
|
2375
|
+
const event = buildEvent(filter, payload);
|
|
2376
|
+
if (materialize) {
|
|
2377
|
+
applyMaterializeInsert(materialize, event, {
|
|
2378
|
+
tx: ctx.tx,
|
|
2379
|
+
block: ctx.block,
|
|
2380
|
+
insert: (table, insertRow) => {
|
|
2381
|
+
ctx.insert(table, insertRow);
|
|
2382
|
+
}
|
|
2383
|
+
}, schema);
|
|
2384
|
+
} else {
|
|
2385
|
+
await handler(event, ctx);
|
|
2386
|
+
}
|
|
2387
|
+
} catch {}
|
|
2388
|
+
if (trace) {
|
|
2389
|
+
const inserts = ctx.pendingInsertsSince(checkpoint);
|
|
2390
|
+
traces.push({
|
|
2391
|
+
source: name,
|
|
2392
|
+
...rowMeta(row),
|
|
2393
|
+
inKeys: inKeysOf(filter, payload),
|
|
2394
|
+
outs: inserts.map((i) => ({ table: i.table, keys: i.keys }))
|
|
2395
|
+
});
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
const tableNames = Object.keys(schema);
|
|
2400
|
+
const tables = [];
|
|
2401
|
+
let written = 0;
|
|
2402
|
+
for (const table of tableNames) {
|
|
2403
|
+
const tableRows = await ctx.rows(table);
|
|
2404
|
+
written += tableRows.length;
|
|
2405
|
+
const sample = tableRows[0];
|
|
2406
|
+
tables.push({
|
|
2407
|
+
name: table,
|
|
2408
|
+
rows: tableRows.length,
|
|
2409
|
+
...sample ? { sampleRow: jsonSafe3(sample) } : {}
|
|
2410
|
+
});
|
|
2411
|
+
}
|
|
2412
|
+
const fetched = Object.values(events).reduce((n, r) => n + r.length, 0);
|
|
2413
|
+
const unusedInKeys = trace ? unusedInKeysAcross(traces) : undefined;
|
|
2414
|
+
const traceFields = {
|
|
2415
|
+
...trace ? { traces } : {},
|
|
2416
|
+
...unusedInKeys ? { unusedInKeys } : {}
|
|
2417
|
+
};
|
|
2418
|
+
if (fetched === 0) {
|
|
2419
|
+
return {
|
|
2420
|
+
ok: false,
|
|
2421
|
+
code: "NO_EVENTS",
|
|
2422
|
+
matched: 0,
|
|
2423
|
+
written,
|
|
2424
|
+
tables,
|
|
2425
|
+
hint: "No events matched these sources in the given range. Widen the range or check the source filters.",
|
|
2426
|
+
...traceFields
|
|
2427
|
+
};
|
|
2428
|
+
}
|
|
2429
|
+
if (written === 0) {
|
|
2430
|
+
const keys = dataKeysOf(firstEvent?.data);
|
|
2431
|
+
const keyPart = keys.length > 0 ? ` Observed event.data keys on first event (${firstEvent?.source}): ${keys.join(", ")}.` : firstEvent ? ` First event source: ${firstEvent.source}.` : "";
|
|
2432
|
+
return {
|
|
2433
|
+
ok: false,
|
|
2434
|
+
code: "EMPTY_MAPPING",
|
|
2435
|
+
matched,
|
|
2436
|
+
written: 0,
|
|
2437
|
+
tables,
|
|
2438
|
+
firstEvent: firstEvent ? { ...firstEvent, data: jsonSafe3(firstEvent.data) } : undefined,
|
|
2439
|
+
hint: `${matched} event${matched === 1 ? "" : "s"} matched, but NO rows were written — the shape of the field-mapping bug that ships a 0-row subgraph.${keyPart} Map only observed keys; do not invent fields.`,
|
|
2440
|
+
...traceFields
|
|
2441
|
+
};
|
|
2442
|
+
}
|
|
2443
|
+
if (trace && traces.some((t) => t.outs.length === 0)) {
|
|
2444
|
+
const empty = traces.find((t) => t.outs.length === 0);
|
|
2445
|
+
const keyPart = empty && empty.inKeys.length > 0 ? ` Empty event (${empty.source}) IN keys: ${empty.inKeys.join(", ")}.` : "";
|
|
2446
|
+
return {
|
|
2447
|
+
ok: false,
|
|
2448
|
+
code: "EMPTY_MAPPING",
|
|
2449
|
+
matched,
|
|
2450
|
+
written,
|
|
2451
|
+
tables,
|
|
2452
|
+
firstEvent: firstEvent ? { ...firstEvent, data: jsonSafe3(firstEvent.data) } : undefined,
|
|
2453
|
+
hint: `At least one matched event wrote 0 rows.${keyPart}`,
|
|
2454
|
+
...traceFields
|
|
2455
|
+
};
|
|
2456
|
+
}
|
|
2457
|
+
return {
|
|
2458
|
+
ok: true,
|
|
2459
|
+
matched,
|
|
2460
|
+
written,
|
|
2461
|
+
tables,
|
|
2462
|
+
firstEvent: firstEvent ? { ...firstEvent, data: jsonSafe3(firstEvent.data) } : undefined,
|
|
2463
|
+
...traceFields
|
|
2464
|
+
};
|
|
2465
|
+
}
|
|
2466
|
+
async function probeHandlers(def, samples) {
|
|
2467
|
+
const ctx = createTestContext(def.schema);
|
|
2468
|
+
let matched = 0;
|
|
2469
|
+
let firstEventKeys;
|
|
2470
|
+
for (const sample of samples) {
|
|
2471
|
+
const filter = def.sources[sample.source];
|
|
2472
|
+
if (!filter)
|
|
2473
|
+
continue;
|
|
2474
|
+
const handler = def.handlers[sample.source] ?? def.handlers["*"];
|
|
2475
|
+
const materialize = "materialize" in filter && filter.materialize !== undefined ? filter.materialize : undefined;
|
|
2476
|
+
if (typeof handler !== "function" && !materialize)
|
|
2477
|
+
continue;
|
|
2478
|
+
if (firstEventKeys === undefined) {
|
|
2479
|
+
firstEventKeys = dataKeysOf(sample.event.data);
|
|
2480
|
+
}
|
|
2481
|
+
matched++;
|
|
2482
|
+
try {
|
|
2483
|
+
const event = buildEvent(filter, sample.event);
|
|
2484
|
+
if (materialize) {
|
|
2485
|
+
applyMaterializeInsert(materialize, event, {
|
|
2486
|
+
tx: ctx.tx,
|
|
2487
|
+
block: ctx.block,
|
|
2488
|
+
insert: (table, row) => {
|
|
2489
|
+
ctx.insert(table, row);
|
|
2490
|
+
}
|
|
2491
|
+
}, def.schema);
|
|
2492
|
+
} else {
|
|
2493
|
+
await handler(event, ctx);
|
|
2494
|
+
}
|
|
2495
|
+
} catch {}
|
|
2496
|
+
}
|
|
2497
|
+
const tables = Object.keys(def.schema);
|
|
2498
|
+
let written = 0;
|
|
2499
|
+
for (const table of tables) {
|
|
2500
|
+
written += (await ctx.rows(table)).length;
|
|
2501
|
+
}
|
|
2502
|
+
return {
|
|
2503
|
+
matched,
|
|
2504
|
+
written,
|
|
2505
|
+
tables,
|
|
2506
|
+
...firstEventKeys !== undefined ? { firstEventKeys } : {}
|
|
2507
|
+
};
|
|
2508
|
+
}
|
|
1494
2509
|
export {
|
|
2510
|
+
toHandlerPayload,
|
|
2511
|
+
toContractCallPayload,
|
|
2512
|
+
runSubgraphTest,
|
|
2513
|
+
probeHandlers,
|
|
1495
2514
|
createTestContext,
|
|
1496
2515
|
buildEventPayload,
|
|
1497
2516
|
buildEvent
|
|
1498
2517
|
};
|
|
1499
2518
|
|
|
1500
|
-
//# debugId=
|
|
2519
|
+
//# debugId=ABB2AE2A4DA0D8AC64756E2164756E21
|
|
1501
2520
|
//# sourceMappingURL=index.js.map
|