@lunatest/core 0.1.0 → 0.1.1
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/browser.d.ts +6 -0
- package/dist/browser.js +5 -0
- package/dist/config/lua-config.js +2 -43
- package/dist/config/read-source.d.ts +3 -0
- package/dist/config/read-source.js +56 -0
- package/dist/coverage/catalog.d.ts +14 -0
- package/dist/coverage/catalog.js +91 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/mocks/provider.js +1 -3
- package/dist/presets/__tests__/registry.test.ts +213 -0
- package/dist/presets/loader.d.ts +6 -0
- package/dist/presets/loader.js +40 -0
- package/dist/presets/loader.ts +58 -0
- package/dist/presets/project-sources.node.d.ts +2 -0
- package/dist/presets/project-sources.node.js +44 -0
- package/dist/presets/project-sources.node.ts +57 -0
- package/dist/presets/protocol/aave.lua +82 -0
- package/dist/presets/protocol/curve.lua +82 -0
- package/dist/presets/protocol/uniswap_v2.lua +92 -0
- package/dist/presets/protocol/uniswap_v3.lua +144 -0
- package/dist/presets/registry.d.ts +59 -0
- package/dist/presets/registry.js +483 -0
- package/dist/presets/registry.ts +784 -0
- package/dist/presets/wallet/demo_sepolia.lua +66 -0
- package/dist/presets/wallet/empty_wallet.lua +54 -0
- package/dist/provider/luna-provider.d.ts +3 -0
- package/dist/provider/luna-provider.js +66 -5
- package/dist/runner/assert.js +23 -1
- package/dist/runtime/bridge.js +10 -2
- package/dist/runtime/engine.js +6 -2
- package/dist/runtime/scenario-runtime.d.ts +8 -0
- package/dist/runtime/scenario-runtime.js +9 -0
- package/dist/scenario/index.d.ts +7 -1
- package/dist/scenario/index.js +8 -0
- package/package.json +13 -3
|
@@ -0,0 +1,784 @@
|
|
|
1
|
+
import {
|
|
2
|
+
asRecord,
|
|
3
|
+
createLunaWalletSession,
|
|
4
|
+
createPresetDiagnostic,
|
|
5
|
+
deepMerge,
|
|
6
|
+
parseProtocolPresetManifest,
|
|
7
|
+
parseWalletPresetManifest,
|
|
8
|
+
qualifyPresetId,
|
|
9
|
+
type PresetDiagnostic,
|
|
10
|
+
type PresetParamDescriptor,
|
|
11
|
+
type PresetSource,
|
|
12
|
+
type ProtocolPresetCatalogEntry,
|
|
13
|
+
type ProtocolPresetManifest,
|
|
14
|
+
type ProtocolPresetMaterialization,
|
|
15
|
+
type WalletPresetCatalogEntry,
|
|
16
|
+
type WalletPresetManifest,
|
|
17
|
+
type WalletPresetMaterialization,
|
|
18
|
+
type WalletPresetReference,
|
|
19
|
+
} from "@lunatest/contracts";
|
|
20
|
+
|
|
21
|
+
import { loadLuaPresetModule } from "./loader.js";
|
|
22
|
+
|
|
23
|
+
export type PresetSourceInput = string | URL;
|
|
24
|
+
|
|
25
|
+
export type ProjectPresetSources = {
|
|
26
|
+
protocol?: Record<string, PresetSourceInput>;
|
|
27
|
+
wallet?: Record<string, PresetSourceInput>;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type PresetRegistry = {
|
|
31
|
+
protocolSources: Record<string, { source: PresetSource; input: PresetSourceInput; localId: string }>;
|
|
32
|
+
walletSources: Record<string, { source: PresetSource; input: PresetSourceInput; localId: string }>;
|
|
33
|
+
protocolCache: Map<string, Promise<LoadedProtocolPreset | null>>;
|
|
34
|
+
walletCache: Map<string, Promise<LoadedWalletPreset | null>>;
|
|
35
|
+
diagnostics: Map<string, PresetDiagnostic>;
|
|
36
|
+
protocolQualifiedOwners: Map<string, string>;
|
|
37
|
+
walletQualifiedOwners: Map<string, string>;
|
|
38
|
+
hasProjectSources: boolean;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type PresetRegistryOptions = {
|
|
42
|
+
projectSources?: ProjectPresetSources;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type ValidatePresetContext = {
|
|
46
|
+
source: PresetSource;
|
|
47
|
+
expectedId: string;
|
|
48
|
+
registry?: PresetRegistry;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
type LoadedProtocolPreset = {
|
|
52
|
+
entry: ProtocolPresetCatalogEntry;
|
|
53
|
+
materialize: (params?: Record<string, unknown>) => Promise<Record<string, unknown>>;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
type LoadedWalletPreset = {
|
|
57
|
+
entry: WalletPresetCatalogEntry;
|
|
58
|
+
materialize: (params?: Record<string, unknown>) => Promise<Record<string, unknown>>;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const BUILTIN_PROTOCOL_SOURCES: Record<string, URL> = {
|
|
62
|
+
uniswap_v2: new URL("./protocol/uniswap_v2.lua", import.meta.url),
|
|
63
|
+
uniswap_v3: new URL("./protocol/uniswap_v3.lua", import.meta.url),
|
|
64
|
+
curve: new URL("./protocol/curve.lua", import.meta.url),
|
|
65
|
+
aave: new URL("./protocol/aave.lua", import.meta.url),
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const BUILTIN_WALLET_SOURCES: Record<string, URL> = {
|
|
69
|
+
empty_wallet: new URL("./wallet/empty_wallet.lua", import.meta.url),
|
|
70
|
+
demo_sepolia: new URL("./wallet/demo_sepolia.lua", import.meta.url),
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
let defaultRegistry: PresetRegistry | null = null;
|
|
74
|
+
|
|
75
|
+
function toChainNumber(value: unknown): number | null {
|
|
76
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (typeof value === "string" && value.startsWith("0x")) {
|
|
81
|
+
return Number.parseInt(value, 16);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (typeof value === "string" && value.length > 0) {
|
|
85
|
+
return Number(value);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function resolveParamDefaults(
|
|
92
|
+
schema: PresetParamDescriptor[],
|
|
93
|
+
input: Record<string, unknown> = {},
|
|
94
|
+
): Record<string, unknown> {
|
|
95
|
+
const resolved: Record<string, unknown> = { ...input };
|
|
96
|
+
|
|
97
|
+
for (const descriptor of schema) {
|
|
98
|
+
if (resolved[descriptor.key] === undefined && descriptor.default !== undefined) {
|
|
99
|
+
resolved[descriptor.key] = descriptor.default;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return resolved;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function getSourcePath(input: PresetSourceInput): string | undefined {
|
|
107
|
+
if (input instanceof URL) {
|
|
108
|
+
return input.toString();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (typeof input === "string" && !input.includes("\n")) {
|
|
112
|
+
return input;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function diagnosticKey(diagnostic: PresetDiagnostic): string {
|
|
119
|
+
return [
|
|
120
|
+
diagnostic.source,
|
|
121
|
+
diagnostic.phase,
|
|
122
|
+
diagnostic.code,
|
|
123
|
+
diagnostic.qualifiedId ?? "none",
|
|
124
|
+
diagnostic.path ?? "none",
|
|
125
|
+
].join(":");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function addDiagnostic(registry: PresetRegistry, diagnostic: PresetDiagnostic): void {
|
|
129
|
+
registry.diagnostics.set(diagnosticKey(diagnostic), diagnostic);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function buildSourceMap(
|
|
133
|
+
builtins: Record<string, URL>,
|
|
134
|
+
project: Record<string, PresetSourceInput> | undefined,
|
|
135
|
+
): Record<string, { source: PresetSource; input: PresetSourceInput; localId: string }> {
|
|
136
|
+
const sources: Record<string, { source: PresetSource; input: PresetSourceInput; localId: string }> = {};
|
|
137
|
+
|
|
138
|
+
for (const [id, input] of Object.entries(builtins)) {
|
|
139
|
+
sources[qualifyPresetId("builtin", id)] = {
|
|
140
|
+
source: "builtin",
|
|
141
|
+
input,
|
|
142
|
+
localId: id,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
for (const [id, input] of Object.entries(project ?? {})) {
|
|
147
|
+
sources[qualifyPresetId("project", id)] = {
|
|
148
|
+
source: "project",
|
|
149
|
+
input,
|
|
150
|
+
localId: id,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return sources;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function toProtocolEntry(
|
|
158
|
+
manifest: ProtocolPresetManifest,
|
|
159
|
+
source: PresetSource,
|
|
160
|
+
): ProtocolPresetCatalogEntry {
|
|
161
|
+
return {
|
|
162
|
+
...manifest,
|
|
163
|
+
qualifiedId: qualifyPresetId(source, manifest.id),
|
|
164
|
+
source,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function toWalletEntry(
|
|
169
|
+
manifest: WalletPresetManifest,
|
|
170
|
+
source: PresetSource,
|
|
171
|
+
): WalletPresetCatalogEntry {
|
|
172
|
+
return {
|
|
173
|
+
...manifest,
|
|
174
|
+
qualifiedId: qualifyPresetId(source, manifest.id),
|
|
175
|
+
source,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function resolveQualifiedId(
|
|
180
|
+
registry: PresetRegistry,
|
|
181
|
+
id: string,
|
|
182
|
+
kind: "protocol" | "wallet",
|
|
183
|
+
): string | null {
|
|
184
|
+
const sources = kind === "protocol" ? registry.protocolSources : registry.walletSources;
|
|
185
|
+
if (sources[id]) {
|
|
186
|
+
return id;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const builtinId = qualifyPresetId("builtin", id);
|
|
190
|
+
if (sources[builtinId]) {
|
|
191
|
+
return builtinId;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const projectId = qualifyPresetId("project", id);
|
|
195
|
+
if (sources[projectId]) {
|
|
196
|
+
return projectId;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function validateRecommendedControls(
|
|
203
|
+
qualifiedId: string,
|
|
204
|
+
source: PresetSource,
|
|
205
|
+
path: string | undefined,
|
|
206
|
+
recommendedControls: string[],
|
|
207
|
+
schema: PresetParamDescriptor[],
|
|
208
|
+
): PresetDiagnostic[] {
|
|
209
|
+
const schemaKeys = new Set(schema.map((item) => item.key));
|
|
210
|
+
const diagnostics: PresetDiagnostic[] = [];
|
|
211
|
+
|
|
212
|
+
for (const control of recommendedControls) {
|
|
213
|
+
if (!schemaKeys.has(control)) {
|
|
214
|
+
diagnostics.push(
|
|
215
|
+
createPresetDiagnostic({
|
|
216
|
+
code: "preset_recommended_control_unknown",
|
|
217
|
+
message: `recommendedControls references missing param: ${control}`,
|
|
218
|
+
severity: "error",
|
|
219
|
+
phase: "manifest",
|
|
220
|
+
source,
|
|
221
|
+
qualifiedId,
|
|
222
|
+
path,
|
|
223
|
+
hint: "Add the key to paramsSchema or remove it from recommendedControls.",
|
|
224
|
+
}),
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return diagnostics;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function validateProtocolEntry(
|
|
233
|
+
registry: PresetRegistry,
|
|
234
|
+
entry: ProtocolPresetCatalogEntry,
|
|
235
|
+
sourcePath: string | undefined,
|
|
236
|
+
expectedId: string,
|
|
237
|
+
): PresetDiagnostic[] {
|
|
238
|
+
const diagnostics = validateRecommendedControls(
|
|
239
|
+
entry.qualifiedId,
|
|
240
|
+
entry.source,
|
|
241
|
+
sourcePath,
|
|
242
|
+
entry.recommendedControls,
|
|
243
|
+
entry.paramsSchema,
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
if (entry.id !== expectedId) {
|
|
247
|
+
diagnostics.push(
|
|
248
|
+
createPresetDiagnostic({
|
|
249
|
+
code: "preset_id_mismatch",
|
|
250
|
+
message: `manifest.id does not match discovered id: expected ${expectedId}, got ${entry.id}`,
|
|
251
|
+
severity: "error",
|
|
252
|
+
phase: "manifest",
|
|
253
|
+
source: entry.source,
|
|
254
|
+
qualifiedId: entry.qualifiedId,
|
|
255
|
+
path: sourcePath,
|
|
256
|
+
hint: "Match manifest.id to the file-based discovery id.",
|
|
257
|
+
}),
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (!resolveQualifiedId(registry, entry.defaultWalletPreset.id, "wallet")) {
|
|
262
|
+
diagnostics.push(
|
|
263
|
+
createPresetDiagnostic({
|
|
264
|
+
code: "preset_wallet_reference_missing",
|
|
265
|
+
message: `defaultWalletPreset.id not found: ${entry.defaultWalletPreset.id}`,
|
|
266
|
+
severity: "error",
|
|
267
|
+
phase: "manifest",
|
|
268
|
+
source: entry.source,
|
|
269
|
+
qualifiedId: entry.qualifiedId,
|
|
270
|
+
path: sourcePath,
|
|
271
|
+
hint: "Create the referenced wallet preset or point to an existing qualified id.",
|
|
272
|
+
}),
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const existingOwner = registry.protocolQualifiedOwners.get(entry.qualifiedId);
|
|
277
|
+
const owner = sourcePath ?? expectedId;
|
|
278
|
+
if (existingOwner && existingOwner !== owner) {
|
|
279
|
+
diagnostics.push(
|
|
280
|
+
createPresetDiagnostic({
|
|
281
|
+
code: "preset_duplicate_qualified_id",
|
|
282
|
+
message: `duplicate protocol preset qualifiedId: ${entry.qualifiedId}`,
|
|
283
|
+
severity: "error",
|
|
284
|
+
phase: "registry",
|
|
285
|
+
source: entry.source,
|
|
286
|
+
qualifiedId: entry.qualifiedId,
|
|
287
|
+
path: sourcePath,
|
|
288
|
+
hint: "Rename manifest.id or change the project-local preset id.",
|
|
289
|
+
}),
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return diagnostics;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function validateWalletEntry(
|
|
297
|
+
registry: PresetRegistry,
|
|
298
|
+
entry: WalletPresetCatalogEntry,
|
|
299
|
+
sourcePath: string | undefined,
|
|
300
|
+
expectedId: string,
|
|
301
|
+
): PresetDiagnostic[] {
|
|
302
|
+
const diagnostics = validateRecommendedControls(
|
|
303
|
+
entry.qualifiedId,
|
|
304
|
+
entry.source,
|
|
305
|
+
sourcePath,
|
|
306
|
+
entry.recommendedControls ?? [],
|
|
307
|
+
entry.paramsSchema ?? [],
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
if (entry.id !== expectedId) {
|
|
311
|
+
diagnostics.push(
|
|
312
|
+
createPresetDiagnostic({
|
|
313
|
+
code: "preset_id_mismatch",
|
|
314
|
+
message: `manifest.id does not match discovered id: expected ${expectedId}, got ${entry.id}`,
|
|
315
|
+
severity: "error",
|
|
316
|
+
phase: "manifest",
|
|
317
|
+
source: entry.source,
|
|
318
|
+
qualifiedId: entry.qualifiedId,
|
|
319
|
+
path: sourcePath,
|
|
320
|
+
hint: "Match manifest.id to the file-based discovery id.",
|
|
321
|
+
}),
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const existingOwner = registry.walletQualifiedOwners.get(entry.qualifiedId);
|
|
326
|
+
const owner = sourcePath ?? expectedId;
|
|
327
|
+
if (existingOwner && existingOwner !== owner) {
|
|
328
|
+
diagnostics.push(
|
|
329
|
+
createPresetDiagnostic({
|
|
330
|
+
code: "preset_duplicate_qualified_id",
|
|
331
|
+
message: `duplicate wallet preset qualifiedId: ${entry.qualifiedId}`,
|
|
332
|
+
severity: "error",
|
|
333
|
+
phase: "registry",
|
|
334
|
+
source: entry.source,
|
|
335
|
+
qualifiedId: entry.qualifiedId,
|
|
336
|
+
path: sourcePath,
|
|
337
|
+
hint: "Rename manifest.id or change the project-local preset id.",
|
|
338
|
+
}),
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return diagnostics;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export async function validateProtocolPresetSource(
|
|
346
|
+
source: PresetSourceInput,
|
|
347
|
+
context: ValidatePresetContext,
|
|
348
|
+
): Promise<{ entry: ProtocolPresetCatalogEntry | null; diagnostics: PresetDiagnostic[]; materialize?: LoadedProtocolPreset["materialize"] }> {
|
|
349
|
+
const diagnostics: PresetDiagnostic[] = [];
|
|
350
|
+
const path = getSourcePath(source);
|
|
351
|
+
|
|
352
|
+
try {
|
|
353
|
+
const module = await loadLuaPresetModule(source);
|
|
354
|
+
const entry = toProtocolEntry(parseProtocolPresetManifest(module.manifest), context.source);
|
|
355
|
+
diagnostics.push(
|
|
356
|
+
...validateProtocolEntry(
|
|
357
|
+
context.registry ?? createPresetRegistry(),
|
|
358
|
+
entry,
|
|
359
|
+
path,
|
|
360
|
+
context.expectedId,
|
|
361
|
+
),
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
if (diagnostics.some((item) => item.severity === "error")) {
|
|
365
|
+
return { entry: null, diagnostics };
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return {
|
|
369
|
+
entry,
|
|
370
|
+
diagnostics,
|
|
371
|
+
materialize: async (params = {}) => (await module.materialize(params)) as Record<string, unknown>,
|
|
372
|
+
};
|
|
373
|
+
} catch (error) {
|
|
374
|
+
diagnostics.push(
|
|
375
|
+
createPresetDiagnostic({
|
|
376
|
+
code: "preset_manifest_invalid",
|
|
377
|
+
message: error instanceof Error ? error.message : String(error),
|
|
378
|
+
severity: "error",
|
|
379
|
+
phase: "manifest",
|
|
380
|
+
source: context.source,
|
|
381
|
+
qualifiedId: qualifyPresetId(context.source, context.expectedId),
|
|
382
|
+
path,
|
|
383
|
+
hint: "Check manifest fields and Lua syntax.",
|
|
384
|
+
}),
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
return {
|
|
388
|
+
entry: null,
|
|
389
|
+
diagnostics,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export async function validateWalletPresetSource(
|
|
395
|
+
source: PresetSourceInput,
|
|
396
|
+
context: ValidatePresetContext,
|
|
397
|
+
): Promise<{ entry: WalletPresetCatalogEntry | null; diagnostics: PresetDiagnostic[]; materialize?: LoadedWalletPreset["materialize"] }> {
|
|
398
|
+
const diagnostics: PresetDiagnostic[] = [];
|
|
399
|
+
const path = getSourcePath(source);
|
|
400
|
+
|
|
401
|
+
try {
|
|
402
|
+
const module = await loadLuaPresetModule(source);
|
|
403
|
+
const entry = toWalletEntry(parseWalletPresetManifest(module.manifest), context.source);
|
|
404
|
+
diagnostics.push(
|
|
405
|
+
...validateWalletEntry(
|
|
406
|
+
context.registry ?? createPresetRegistry(),
|
|
407
|
+
entry,
|
|
408
|
+
path,
|
|
409
|
+
context.expectedId,
|
|
410
|
+
),
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
if (diagnostics.some((item) => item.severity === "error")) {
|
|
414
|
+
return { entry: null, diagnostics };
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return {
|
|
418
|
+
entry,
|
|
419
|
+
diagnostics,
|
|
420
|
+
materialize: async (params = {}) => (await module.materialize(params)) as Record<string, unknown>,
|
|
421
|
+
};
|
|
422
|
+
} catch (error) {
|
|
423
|
+
diagnostics.push(
|
|
424
|
+
createPresetDiagnostic({
|
|
425
|
+
code: "preset_manifest_invalid",
|
|
426
|
+
message: error instanceof Error ? error.message : String(error),
|
|
427
|
+
severity: "error",
|
|
428
|
+
phase: "manifest",
|
|
429
|
+
source: context.source,
|
|
430
|
+
qualifiedId: qualifyPresetId(context.source, context.expectedId),
|
|
431
|
+
path,
|
|
432
|
+
hint: "Check manifest fields and Lua syntax.",
|
|
433
|
+
}),
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
return {
|
|
437
|
+
entry: null,
|
|
438
|
+
diagnostics,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
async function loadProtocolPreset(
|
|
444
|
+
registry: PresetRegistry,
|
|
445
|
+
id: string,
|
|
446
|
+
): Promise<LoadedProtocolPreset | null> {
|
|
447
|
+
const qualifiedId = resolveQualifiedId(registry, id, "protocol");
|
|
448
|
+
if (!qualifiedId) {
|
|
449
|
+
return null;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (!registry.protocolCache.has(qualifiedId)) {
|
|
453
|
+
const sourceInfo = registry.protocolSources[qualifiedId];
|
|
454
|
+
registry.protocolCache.set(
|
|
455
|
+
qualifiedId,
|
|
456
|
+
(async () => {
|
|
457
|
+
const validated = await validateProtocolPresetSource(sourceInfo.input, {
|
|
458
|
+
source: sourceInfo.source,
|
|
459
|
+
expectedId: sourceInfo.localId,
|
|
460
|
+
registry,
|
|
461
|
+
});
|
|
462
|
+
for (const diagnostic of validated.diagnostics) {
|
|
463
|
+
addDiagnostic(registry, diagnostic);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (!validated.entry || !validated.materialize) {
|
|
467
|
+
return null;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
registry.protocolQualifiedOwners.set(validated.entry.qualifiedId, sourceInfo.localId);
|
|
471
|
+
return {
|
|
472
|
+
entry: validated.entry,
|
|
473
|
+
materialize: validated.materialize,
|
|
474
|
+
};
|
|
475
|
+
})(),
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return registry.protocolCache.get(qualifiedId)!;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
async function loadWalletPreset(
|
|
483
|
+
registry: PresetRegistry,
|
|
484
|
+
id: string,
|
|
485
|
+
): Promise<LoadedWalletPreset | null> {
|
|
486
|
+
const qualifiedId = resolveQualifiedId(registry, id, "wallet");
|
|
487
|
+
if (!qualifiedId) {
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (!registry.walletCache.has(qualifiedId)) {
|
|
492
|
+
const sourceInfo = registry.walletSources[qualifiedId];
|
|
493
|
+
registry.walletCache.set(
|
|
494
|
+
qualifiedId,
|
|
495
|
+
(async () => {
|
|
496
|
+
const validated = await validateWalletPresetSource(sourceInfo.input, {
|
|
497
|
+
source: sourceInfo.source,
|
|
498
|
+
expectedId: sourceInfo.localId,
|
|
499
|
+
registry,
|
|
500
|
+
});
|
|
501
|
+
for (const diagnostic of validated.diagnostics) {
|
|
502
|
+
addDiagnostic(registry, diagnostic);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (!validated.entry || !validated.materialize) {
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
registry.walletQualifiedOwners.set(validated.entry.qualifiedId, sourceInfo.localId);
|
|
510
|
+
return {
|
|
511
|
+
entry: validated.entry,
|
|
512
|
+
materialize: validated.materialize,
|
|
513
|
+
};
|
|
514
|
+
})(),
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
return registry.walletCache.get(qualifiedId)!;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function mergeWalletReference(
|
|
522
|
+
walletSession: Record<string, unknown>,
|
|
523
|
+
reference: WalletPresetReference,
|
|
524
|
+
rawOverrides: Record<string, unknown> | null,
|
|
525
|
+
) {
|
|
526
|
+
const mergedReferenceOverrides = asRecord(reference.overrides) ?? {};
|
|
527
|
+
const mergedRawOverrides = rawOverrides ?? {};
|
|
528
|
+
|
|
529
|
+
return createLunaWalletSession(
|
|
530
|
+
deepMerge(
|
|
531
|
+
deepMerge(walletSession, mergedReferenceOverrides),
|
|
532
|
+
mergedRawOverrides,
|
|
533
|
+
),
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
export function createPresetRegistry(
|
|
538
|
+
options: PresetRegistryOptions = {},
|
|
539
|
+
): PresetRegistry {
|
|
540
|
+
const registry: PresetRegistry = {
|
|
541
|
+
protocolSources: buildSourceMap(BUILTIN_PROTOCOL_SOURCES, options.projectSources?.protocol),
|
|
542
|
+
walletSources: buildSourceMap(BUILTIN_WALLET_SOURCES, options.projectSources?.wallet),
|
|
543
|
+
protocolCache: new Map(),
|
|
544
|
+
walletCache: new Map(),
|
|
545
|
+
diagnostics: new Map(),
|
|
546
|
+
protocolQualifiedOwners: new Map(),
|
|
547
|
+
walletQualifiedOwners: new Map(),
|
|
548
|
+
hasProjectSources: Boolean(
|
|
549
|
+
(options.projectSources?.protocol && Object.keys(options.projectSources.protocol).length > 0) ||
|
|
550
|
+
(options.projectSources?.wallet && Object.keys(options.projectSources.wallet).length > 0),
|
|
551
|
+
),
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
if (options.projectSources && !registry.hasProjectSources) {
|
|
555
|
+
addDiagnostic(
|
|
556
|
+
registry,
|
|
557
|
+
createPresetDiagnostic({
|
|
558
|
+
code: "local_preset_sources_empty",
|
|
559
|
+
message: "No project-local preset sources were provided.",
|
|
560
|
+
severity: "info",
|
|
561
|
+
phase: "discovery",
|
|
562
|
+
source: "project",
|
|
563
|
+
hint: "Create ./lunatest/presets/protocol or ./lunatest/presets/wallet, or inject preset sources via bootstrap.",
|
|
564
|
+
}),
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
return registry;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function getRegistry(registry?: PresetRegistry): PresetRegistry {
|
|
572
|
+
if (registry) {
|
|
573
|
+
return registry;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (!defaultRegistry) {
|
|
577
|
+
defaultRegistry = createPresetRegistry();
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
return defaultRegistry;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
async function ensureRegistryLoaded(registry: PresetRegistry): Promise<void> {
|
|
584
|
+
await Promise.all([
|
|
585
|
+
...Object.keys(registry.protocolSources).map((id) => loadProtocolPreset(registry, id)),
|
|
586
|
+
...Object.keys(registry.walletSources).map((id) => loadWalletPreset(registry, id)),
|
|
587
|
+
]);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
export async function getPresetDiagnostics(
|
|
591
|
+
registry?: PresetRegistry,
|
|
592
|
+
): Promise<PresetDiagnostic[]> {
|
|
593
|
+
const activeRegistry = getRegistry(registry);
|
|
594
|
+
await ensureRegistryLoaded(activeRegistry);
|
|
595
|
+
return Array.from(activeRegistry.diagnostics.values());
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
export async function listProtocolPresets(
|
|
599
|
+
registry?: PresetRegistry,
|
|
600
|
+
): Promise<ProtocolPresetCatalogEntry[]> {
|
|
601
|
+
const activeRegistry = getRegistry(registry);
|
|
602
|
+
const presets = await Promise.all(
|
|
603
|
+
Object.keys(activeRegistry.protocolSources).map(async (id) => {
|
|
604
|
+
const preset = await loadProtocolPreset(activeRegistry, id);
|
|
605
|
+
return preset?.entry ?? null;
|
|
606
|
+
}),
|
|
607
|
+
);
|
|
608
|
+
|
|
609
|
+
return presets.filter((item): item is ProtocolPresetCatalogEntry => item !== null);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
export async function getProtocolPreset(
|
|
613
|
+
id: string,
|
|
614
|
+
registry?: PresetRegistry,
|
|
615
|
+
): Promise<ProtocolPresetCatalogEntry | null> {
|
|
616
|
+
const preset = await loadProtocolPreset(getRegistry(registry), id);
|
|
617
|
+
return preset?.entry ?? null;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
export async function listWalletPresets(
|
|
621
|
+
registry?: PresetRegistry,
|
|
622
|
+
): Promise<WalletPresetCatalogEntry[]> {
|
|
623
|
+
const activeRegistry = getRegistry(registry);
|
|
624
|
+
const presets = await Promise.all(
|
|
625
|
+
Object.keys(activeRegistry.walletSources).map(async (id) => {
|
|
626
|
+
const preset = await loadWalletPreset(activeRegistry, id);
|
|
627
|
+
return preset?.entry ?? null;
|
|
628
|
+
}),
|
|
629
|
+
);
|
|
630
|
+
|
|
631
|
+
return presets.filter((item): item is WalletPresetCatalogEntry => item !== null);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
export async function getWalletPreset(
|
|
635
|
+
id: string,
|
|
636
|
+
registry?: PresetRegistry,
|
|
637
|
+
): Promise<WalletPresetCatalogEntry | null> {
|
|
638
|
+
const preset = await loadWalletPreset(getRegistry(registry), id);
|
|
639
|
+
return preset?.entry ?? null;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
export async function materializeWalletPreset(
|
|
643
|
+
id: string,
|
|
644
|
+
params: Record<string, unknown> = {},
|
|
645
|
+
registry?: PresetRegistry,
|
|
646
|
+
): Promise<WalletPresetMaterialization> {
|
|
647
|
+
const activeRegistry = getRegistry(registry);
|
|
648
|
+
const preset = await loadWalletPreset(activeRegistry, id);
|
|
649
|
+
if (!preset) {
|
|
650
|
+
throw new Error(`Wallet preset not found: ${id}`);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const resolvedParams = resolveParamDefaults(preset.entry.paramsSchema ?? [], params);
|
|
654
|
+
const raw = await preset.materialize(resolvedParams);
|
|
655
|
+
const rawSession = asRecord(raw.defaultSession);
|
|
656
|
+
if (raw.defaultSession !== undefined && !rawSession) {
|
|
657
|
+
addDiagnostic(
|
|
658
|
+
activeRegistry,
|
|
659
|
+
createPresetDiagnostic({
|
|
660
|
+
code: "preset_materialize_invalid_default_session",
|
|
661
|
+
message: "wallet materialize() returned invalid defaultSession",
|
|
662
|
+
severity: "error",
|
|
663
|
+
phase: "materialize",
|
|
664
|
+
source: preset.entry.source,
|
|
665
|
+
qualifiedId: preset.entry.qualifiedId,
|
|
666
|
+
hint: "Return a table for defaultSession.",
|
|
667
|
+
}),
|
|
668
|
+
);
|
|
669
|
+
throw new Error(`Wallet preset ${id} returned invalid defaultSession`);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
const walletSession = createLunaWalletSession(
|
|
673
|
+
deepMerge(
|
|
674
|
+
preset.entry.defaultSession as Record<string, unknown>,
|
|
675
|
+
rawSession ?? {},
|
|
676
|
+
),
|
|
677
|
+
);
|
|
678
|
+
|
|
679
|
+
const chainId = toChainNumber(resolvedParams.chainId ?? walletSession.chainId);
|
|
680
|
+
if (chainId !== null && !preset.entry.supportedChains.includes(chainId)) {
|
|
681
|
+
addDiagnostic(
|
|
682
|
+
activeRegistry,
|
|
683
|
+
createPresetDiagnostic({
|
|
684
|
+
code: "preset_unsupported_chain",
|
|
685
|
+
message: `wallet preset does not support chain ${chainId}`,
|
|
686
|
+
severity: "error",
|
|
687
|
+
phase: "materialize",
|
|
688
|
+
source: preset.entry.source,
|
|
689
|
+
qualifiedId: preset.entry.qualifiedId,
|
|
690
|
+
}),
|
|
691
|
+
);
|
|
692
|
+
throw new Error(`Wallet preset ${id} does not support chain ${chainId}`);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
return {
|
|
696
|
+
walletPresetId: preset.entry.qualifiedId,
|
|
697
|
+
resolvedParams,
|
|
698
|
+
walletSession,
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
export async function materializeProtocolPreset(
|
|
703
|
+
id: string,
|
|
704
|
+
params: Record<string, unknown> = {},
|
|
705
|
+
registry?: PresetRegistry,
|
|
706
|
+
): Promise<ProtocolPresetMaterialization> {
|
|
707
|
+
const activeRegistry = getRegistry(registry);
|
|
708
|
+
const preset = await loadProtocolPreset(activeRegistry, id);
|
|
709
|
+
if (!preset) {
|
|
710
|
+
throw new Error(`Protocol preset not found: ${id}`);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
const resolvedParams = resolveParamDefaults(preset.entry.paramsSchema, params);
|
|
714
|
+
const chainId = toChainNumber(resolvedParams.chainId);
|
|
715
|
+
if (chainId !== null && !preset.entry.supportedChains.includes(chainId)) {
|
|
716
|
+
addDiagnostic(
|
|
717
|
+
activeRegistry,
|
|
718
|
+
createPresetDiagnostic({
|
|
719
|
+
code: "preset_unsupported_chain",
|
|
720
|
+
message: `protocol preset does not support chain ${chainId}`,
|
|
721
|
+
severity: "error",
|
|
722
|
+
phase: "materialize",
|
|
723
|
+
source: preset.entry.source,
|
|
724
|
+
qualifiedId: preset.entry.qualifiedId,
|
|
725
|
+
}),
|
|
726
|
+
);
|
|
727
|
+
throw new Error(`Protocol preset ${id} does not support chain ${chainId}`);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
const raw = await preset.materialize(resolvedParams);
|
|
731
|
+
const walletReference =
|
|
732
|
+
(asRecord(raw.walletPreset) as WalletPresetReference | null) ??
|
|
733
|
+
preset.entry.defaultWalletPreset;
|
|
734
|
+
const walletMaterialization = await materializeWalletPreset(
|
|
735
|
+
walletReference.id,
|
|
736
|
+
resolvedParams,
|
|
737
|
+
activeRegistry,
|
|
738
|
+
);
|
|
739
|
+
const walletSession = mergeWalletReference(
|
|
740
|
+
walletMaterialization.walletSession as unknown as Record<string, unknown>,
|
|
741
|
+
walletReference,
|
|
742
|
+
asRecord(raw.walletSessionOverrides),
|
|
743
|
+
);
|
|
744
|
+
|
|
745
|
+
if (
|
|
746
|
+
raw.routeMocks !== undefined &&
|
|
747
|
+
!Array.isArray(raw.routeMocks) &&
|
|
748
|
+
!(asRecord(raw.routeMocks) && Object.keys(asRecord(raw.routeMocks)!).length === 0)
|
|
749
|
+
) {
|
|
750
|
+
addDiagnostic(
|
|
751
|
+
activeRegistry,
|
|
752
|
+
createPresetDiagnostic({
|
|
753
|
+
code: "preset_materialize_invalid_route_mocks",
|
|
754
|
+
message: "protocol materialize() returned invalid routeMocks",
|
|
755
|
+
severity: "error",
|
|
756
|
+
phase: "materialize",
|
|
757
|
+
source: preset.entry.source,
|
|
758
|
+
qualifiedId: preset.entry.qualifiedId,
|
|
759
|
+
hint: "Return an array of route mocks or an empty table.",
|
|
760
|
+
}),
|
|
761
|
+
);
|
|
762
|
+
throw new Error(`Protocol preset ${id} returned invalid routeMocks`);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
return {
|
|
766
|
+
protocolPresetId: preset.entry.qualifiedId,
|
|
767
|
+
walletPresetId: walletMaterialization.walletPresetId,
|
|
768
|
+
resolvedParams: asRecord(raw.resolvedParams) ?? resolvedParams,
|
|
769
|
+
walletSession,
|
|
770
|
+
interceptState: deepMerge(
|
|
771
|
+
preset.entry.defaultInterceptState,
|
|
772
|
+
asRecord(raw.interceptState) ?? {},
|
|
773
|
+
),
|
|
774
|
+
routeMocks: [
|
|
775
|
+
...preset.entry.defaultRouteMocks,
|
|
776
|
+
...(Array.isArray(raw.routeMocks)
|
|
777
|
+
? (raw.routeMocks as ProtocolPresetMaterialization["routeMocks"])
|
|
778
|
+
: []),
|
|
779
|
+
],
|
|
780
|
+
builtinScenarios: Array.isArray(raw.builtinScenarios)
|
|
781
|
+
? (raw.builtinScenarios as ProtocolPresetMaterialization["builtinScenarios"])
|
|
782
|
+
: preset.entry.builtinScenarios,
|
|
783
|
+
};
|
|
784
|
+
}
|