@5ive-tech/sdk 1.1.13 → 1.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +16 -0
  2. package/dist/FiveSDK.d.ts +43 -1
  3. package/dist/FiveSDK.js +6 -0
  4. package/dist/accounts/index.d.ts +10 -28
  5. package/dist/accounts/index.js +33 -61
  6. package/dist/assets/vm/five_vm_wasm.d.ts +8 -0
  7. package/dist/assets/vm/five_vm_wasm.js +25 -0
  8. package/dist/assets/vm/five_vm_wasm_bg.wasm +0 -0
  9. package/dist/assets/vm/five_vm_wasm_bg.wasm.d.ts +3 -0
  10. package/dist/bin/gen-types.js +0 -0
  11. package/dist/compiler/BytecodeCompiler.js +10 -6
  12. package/dist/compiler/source-normalization.d.ts +1 -0
  13. package/dist/compiler/source-normalization.js +67 -0
  14. package/dist/constants/headers.d.ts +2 -0
  15. package/dist/constants/headers.js +2 -0
  16. package/dist/crypto/index.d.ts +8 -1
  17. package/dist/crypto/index.js +27 -14
  18. package/dist/index.d.ts +1 -0
  19. package/dist/index.js +1 -0
  20. package/dist/modules/accounts.js +1 -1
  21. package/dist/modules/deploy.js +165 -97
  22. package/dist/modules/execute.d.ts +5 -0
  23. package/dist/modules/execute.js +107 -49
  24. package/dist/modules/fees.js +2 -2
  25. package/dist/modules/namespaces.d.ts +11 -0
  26. package/dist/modules/namespaces.js +64 -0
  27. package/dist/program/FiveProgram.js +4 -3
  28. package/dist/program/TypeGenerator.js +8 -1
  29. package/dist/project/config.js +113 -1
  30. package/dist/project/workspace.d.ts +5 -0
  31. package/dist/testing/TestDiscovery.d.ts +1 -0
  32. package/dist/testing/TestDiscovery.js +18 -2
  33. package/dist/testing/TestRunner.js +4 -1
  34. package/dist/types.d.ts +16 -5
  35. package/dist/types.js +1 -0
  36. package/dist/utils/abi.js +33 -10
  37. package/dist/utils/transaction.d.ts +16 -0
  38. package/dist/utils/transaction.js +81 -5
  39. package/dist/wasm/compiler/CompilationLogic.js +3 -3
  40. package/dist/wasm/vm.d.ts +2 -2
  41. package/dist/wasm/vm.js +10 -11
  42. package/package.json +1 -1
@@ -277,56 +277,100 @@ export async function generateExecuteInstruction(scriptAccount, functionName, pa
277
277
  : selectFeeShard(shardCount);
278
278
  const feeVault = await deriveProgramFeeVault(programId, feeShardIndex);
279
279
  const instructionAccounts = [
280
+ // Execute runtime reads script/vm_state; keep them readonly.
280
281
  { pubkey: scriptAccount, isSigner: false, isWritable: false },
281
282
  { pubkey: vmState, isSigner: false, isWritable: false },
282
283
  ];
283
284
  const abiAccountMetadata = new Map();
284
- const hasFullParameterList = !!funcDef &&
285
- Array.isArray(funcDef.parameters) &&
286
- parameters.length === funcDef.parameters.length;
287
- if (funcDef && funcDef.parameters && hasFullParameterList) {
288
- // First pass: detect if there's an @init constraint and find the payer
285
+ const normalizeAccountPubkey = (value) => {
286
+ if (!value)
287
+ return undefined;
288
+ if (typeof value === "string")
289
+ return value;
290
+ if (typeof value.toBase58 === "function")
291
+ return value.toBase58();
292
+ if (typeof value.toString === "function") {
293
+ const str = value.toString();
294
+ if (str && str !== "[object Object]")
295
+ return str;
296
+ }
297
+ return undefined;
298
+ };
299
+ const isAccountParam = (param) => {
300
+ if (!param)
301
+ return false;
302
+ if (param.is_account || param.isAccount)
303
+ return true;
304
+ const type = (param.type || param.param_type || "").toString().trim().toLowerCase();
305
+ return type === "account" || type === "mint" || type === "tokenaccount";
306
+ };
307
+ const allParams = funcDef && Array.isArray(funcDef.parameters) ? funcDef.parameters : [];
308
+ const accountParams = allParams.filter((param) => isAccountParam(param));
309
+ const hasFullParameterList = allParams.length > 0 && parameters.length === allParams.length;
310
+ if (accountParams.length > 0) {
311
+ // First pass: detect if there's an @init constraint and identify payer param.
289
312
  let hasInit = false;
290
- let payerPubkey;
291
- for (let i = 0; i < funcDef.parameters.length; i++) {
292
- const param = funcDef.parameters[i];
293
- if (param.is_account || param.isAccount) {
294
- const attributes = param.attributes || [];
295
- if (attributes.includes('init')) {
296
- hasInit = true;
297
- for (let j = 0; j < funcDef.parameters.length; j++) {
298
- const payerParam = funcDef.parameters[j];
299
- if (i !== j &&
300
- (payerParam.is_account || payerParam.isAccount) &&
301
- (payerParam.attributes || []).includes('signer')) {
302
- const payerValue = parameters[j];
303
- payerPubkey = payerValue?.toString();
304
- break;
305
- }
306
- }
313
+ let initParamName;
314
+ let payerParamName;
315
+ for (const param of accountParams) {
316
+ const attributes = param.attributes || [];
317
+ if (attributes.includes("init")) {
318
+ hasInit = true;
319
+ initParamName = param.name;
320
+ break;
321
+ }
322
+ }
323
+ if (hasInit) {
324
+ for (const param of accountParams) {
325
+ if (param.name !== initParamName &&
326
+ (param.attributes || []).includes("signer")) {
327
+ payerParamName = param.name;
307
328
  break;
308
329
  }
309
330
  }
310
331
  }
311
- funcDef.parameters.forEach((param, paramIndex) => {
312
- if (param.is_account || param.isAccount) {
313
- const value = parameters[paramIndex];
314
- const pubkey = value?.toString();
332
+ // Resolve pubkeys for account params in either full-param mode or args-only mode.
333
+ const accountPubkeysByParamName = new Map();
334
+ if (hasFullParameterList) {
335
+ allParams.forEach((param, paramIndex) => {
336
+ if (!isAccountParam(param))
337
+ return;
338
+ const pubkey = normalizeAccountPubkey(parameters[paramIndex]);
315
339
  if (pubkey) {
316
- const attributes = param.attributes || [];
317
- const isSigner = attributes.includes('signer');
318
- const isWritable = attributes.includes('mut') ||
319
- attributes.includes('init') ||
320
- (hasInit && pubkey === payerPubkey);
321
- const existing = abiAccountMetadata.get(pubkey) || { isSigner: false, isWritable: false };
322
- abiAccountMetadata.set(pubkey, {
323
- isSigner: existing.isSigner || isSigner,
324
- isWritable: existing.isWritable || isWritable
325
- });
340
+ accountPubkeysByParamName.set(param.name, pubkey);
341
+ }
342
+ });
343
+ }
344
+ else {
345
+ const count = Math.min(accountParams.length, accounts.length);
346
+ for (let i = 0; i < count; i++) {
347
+ const paramName = accountParams[i]?.name;
348
+ const pubkey = accounts[i];
349
+ if (paramName && pubkey) {
350
+ accountPubkeysByParamName.set(paramName, pubkey);
326
351
  }
327
352
  }
328
- });
353
+ }
354
+ for (const param of accountParams) {
355
+ const pubkey = accountPubkeysByParamName.get(param.name);
356
+ if (!pubkey)
357
+ continue;
358
+ const attributes = param.attributes || [];
359
+ const isSigner = attributes.includes("signer");
360
+ const isWritable = attributes.includes("mut") ||
361
+ attributes.includes("init") ||
362
+ (hasInit && payerParamName === param.name);
363
+ const existing = abiAccountMetadata.get(pubkey) || {
364
+ isSigner: false,
365
+ isWritable: false,
366
+ };
367
+ abiAccountMetadata.set(pubkey, {
368
+ isSigner: existing.isSigner || isSigner,
369
+ isWritable: existing.isWritable || isWritable,
370
+ });
371
+ }
329
372
  }
373
+ const unknownAccounts = [];
330
374
  const userInstructionAccounts = accounts.map((acc) => {
331
375
  // Check both derived ABI metadata and passed-in metadata (from FunctionBuilder)
332
376
  const abiMetadata = abiAccountMetadata.get(acc);
@@ -335,25 +379,40 @@ export async function generateExecuteInstruction(scriptAccount, functionName, pa
335
379
  const isSigner = metadata ? metadata.isSigner : false;
336
380
  const isWritable = metadata
337
381
  ? (metadata.isSystemAccount ? false : metadata.isWritable)
338
- : true;
382
+ : false;
383
+ if (!metadata) {
384
+ unknownAccounts.push(acc);
385
+ }
339
386
  return {
340
387
  pubkey: acc,
341
388
  isSigner,
342
389
  isWritable
343
390
  };
344
391
  });
392
+ if (unknownAccounts.length > 0) {
393
+ const deduped = Array.from(new Set(unknownAccounts));
394
+ console.warn(`[FiveSDK] Missing account metadata for ${deduped.join(", ")}; defaulting readonly. Pass accountMetadata or use FiveProgram.function(...).instruction().`);
395
+ }
345
396
  instructionAccounts.push(...userInstructionAccounts);
346
397
  const instructionData = encodeExecuteInstruction(functionIndex, encodedParams, actualParamCount, feeShardIndex, options.debug === true);
347
398
  // Runtime requires strict tail: [payer, fee_vault, system_program].
399
+ // Prefer explicit payer, otherwise infer from signer+writable account metadata only.
348
400
  const signerCandidates = instructionAccounts
349
401
  .filter((acc) => acc.isSigner)
350
402
  .map((acc) => acc.pubkey);
351
- const inferredPayer = options.payerAccount ||
352
- (signerCandidates.length > 0
353
- ? signerCandidates[signerCandidates.length - 1]
354
- : accounts[0]);
403
+ const metadataFor = (pubkey) => options.accountMetadata?.get(pubkey) || abiAccountMetadata.get(pubkey);
404
+ const writableSignerCandidates = signerCandidates.filter((pubkey) => {
405
+ const metadata = metadataFor(pubkey);
406
+ if (!metadata)
407
+ return false;
408
+ return !metadata.isSystemAccount && metadata.isWritable;
409
+ });
410
+ const inferredPayer = options.payerAccount || writableSignerCandidates[0];
355
411
  if (!inferredPayer) {
356
- throw new Error("Could not infer execute fee payer account. Provide a signer account or set options.payerAccount.");
412
+ if (signerCandidates.length > 0) {
413
+ throw new Error("Could not infer execute fee payer account from writable signer metadata. Pass options.payerAccount or use FiveProgram.function(...).payer(...).instruction().");
414
+ }
415
+ throw new Error("Could not infer execute fee payer account. Provide options.payerAccount or include a writable signer account.");
357
416
  }
358
417
  const feeTailAccounts = [
359
418
  { pubkey: inferredPayer, isSigner: true, isWritable: true },
@@ -404,6 +463,7 @@ export async function executeOnSolana(scriptAccount, connection, signerKeypair,
404
463
  vmStateAccount: options.vmStateAccount,
405
464
  fiveVMProgramId: options.fiveVMProgramId,
406
465
  abi: options.abi,
466
+ accountMetadata: options.accountMetadata,
407
467
  feeShardIndex: options.feeShardIndex,
408
468
  payerAccount: options.payerAccount || signerKeypair.publicKey.toString(),
409
469
  });
@@ -430,7 +490,6 @@ export async function executeOnSolana(scriptAccount, connection, signerKeypair,
430
490
  for (const meta of accountKeys) {
431
491
  if (meta.pubkey === signerPubkey) {
432
492
  meta.isSigner = true;
433
- meta.isWritable = true;
434
493
  signerFound = true;
435
494
  }
436
495
  }
@@ -438,7 +497,7 @@ export async function executeOnSolana(scriptAccount, connection, signerKeypair,
438
497
  accountKeys.push({
439
498
  pubkey: signerPubkey,
440
499
  isSigner: true,
441
- isWritable: true,
500
+ isWritable: false,
442
501
  });
443
502
  }
444
503
  const executeInstruction = new TransactionInstruction({
@@ -452,8 +511,8 @@ export async function executeOnSolana(scriptAccount, connection, signerKeypair,
452
511
  });
453
512
  transaction.add(executeInstruction);
454
513
  transaction.feePayer = signerKeypair.publicKey;
455
- const { blockhash } = await connection.getLatestBlockhash("confirmed");
456
- transaction.recentBlockhash = blockhash;
514
+ const latestBlockhash = await connection.getLatestBlockhash("confirmed");
515
+ transaction.recentBlockhash = latestBlockhash.blockhash;
457
516
  transaction.partialSign(signerKeypair);
458
517
  const firstSig = transaction.signatures[0]?.signature;
459
518
  if (firstSig) {
@@ -465,12 +524,11 @@ export async function executeOnSolana(scriptAccount, connection, signerKeypair,
465
524
  maxRetries: options.maxRetries || 3,
466
525
  });
467
526
  lastSignature = signature;
468
- const latestBlockhash = await connection.getLatestBlockhash("confirmed");
469
527
  const confirmation = await confirmTransactionRobust(connection, signature, {
470
528
  commitment: "confirmed",
471
529
  timeoutMs: 120000,
472
530
  debug: options.debug,
473
- blockhash,
531
+ blockhash: latestBlockhash.blockhash,
474
532
  lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
475
533
  });
476
534
  if (!confirmation.success) {
@@ -24,7 +24,7 @@ export async function getFees(connection, fiveVMProgramId) {
24
24
  export async function calculateDeployFee(bytecodeSize, connection, fiveVMProgramId) {
25
25
  try {
26
26
  const accountSize = 64 + bytecodeSize;
27
- const rentLamports = await RentCalculator.calculateRentExemption(accountSize);
27
+ const rentLamports = await RentCalculator.calculateRentExemptionWithConnection(accountSize, connection);
28
28
  const vmState = await getVMState(connection, fiveVMProgramId);
29
29
  const deployFeeLamports = vmState.deployFeeLamports;
30
30
  return {
@@ -41,7 +41,7 @@ export async function calculateDeployFee(bytecodeSize, connection, fiveVMProgram
41
41
  }
42
42
  catch (error) {
43
43
  const accountSize = 64 + bytecodeSize;
44
- const rentLamports = await RentCalculator.calculateRentExemption(accountSize);
44
+ const rentLamports = await RentCalculator.calculateRentExemptionWithConnection(accountSize, connection);
45
45
  return {
46
46
  feeBps: 0,
47
47
  basisLamports: rentLamports,
@@ -36,4 +36,15 @@ export declare function resolveNamespaceOnChain(namespaceValue: string, options:
36
36
  resolvedScript?: string;
37
37
  bindingAddress: string;
38
38
  }>;
39
+ export declare function setNamespaceSymbolPriceOnChain(symbol: string, priceLamports: number, options: NamespaceOnChainOptions): Promise<{
40
+ transactionId?: string;
41
+ symbol: ScopedNamespace["symbol"];
42
+ priceLamports: number;
43
+ }>;
44
+ export declare function getNamespaceSymbolPriceOnChain(symbol: string, options: NamespaceOnChainOptions): Promise<{
45
+ transactionId?: string;
46
+ symbol: ScopedNamespace["symbol"];
47
+ priceLamports: number;
48
+ priceSol: number;
49
+ }>;
39
50
  export {};
@@ -2,6 +2,7 @@ const SYMBOLS = new Set(["!", "@", "#", "$", "%"]);
2
2
  const NAMESPACE_CONFIG_SEED = "5ns_config";
3
3
  const NAMESPACE_TLD_SEED = "5ns_tld";
4
4
  const NAMESPACE_BINDING_SEED = "5ns_binding";
5
+ const LAMPORTS_PER_SOL = 1000000000;
5
6
  export function canonicalizeScopedNamespace(input) {
6
7
  const value = input.trim();
7
8
  if (value.length < 2) {
@@ -52,6 +53,17 @@ export function resolveNamespaceFromLockfile(namespaceValue, lockfile) {
52
53
  function asBuffer(value) {
53
54
  return Buffer.from(value, "utf8");
54
55
  }
56
+ function validateSymbol(symbol) {
57
+ if (!SYMBOLS.has(symbol)) {
58
+ throw new Error("namespace symbol must be one of ! @ # $ %");
59
+ }
60
+ return symbol;
61
+ }
62
+ async function deriveNamespaceConfigAccount(fiveVMProgramId) {
63
+ const { PDAUtils } = await import("../crypto/index.js");
64
+ const cfg = await PDAUtils.findProgramAddress([asBuffer(NAMESPACE_CONFIG_SEED)], fiveVMProgramId);
65
+ return cfg.address;
66
+ }
55
67
  export async function deriveNamespaceAccounts(namespaceValue, fiveVMProgramId) {
56
68
  const { PDAUtils } = await import("../crypto/index.js");
57
69
  const parsed = canonicalizeScopedNamespace(namespaceValue);
@@ -188,3 +200,55 @@ export async function resolveNamespaceOnChain(namespaceValue, options) {
188
200
  bindingAddress: addresses.binding,
189
201
  };
190
202
  }
203
+ export async function setNamespaceSymbolPriceOnChain(symbol, priceLamports, options) {
204
+ const { ProgramIdResolver } = await import("../config/ProgramIdResolver.js");
205
+ const { executeOnSolana } = await import("./execute.js");
206
+ const validatedSymbol = validateSymbol(symbol);
207
+ if (!Number.isFinite(priceLamports) || priceLamports <= 0 || !Number.isInteger(priceLamports)) {
208
+ throw new Error("priceLamports must be a positive integer");
209
+ }
210
+ const vmProgramId = ProgramIdResolver.resolve(options.fiveVMProgramId);
211
+ const configAddress = await deriveNamespaceConfigAccount(vmProgramId);
212
+ const admin = options.signerKeypair.publicKey.toBase58();
213
+ const result = await executeOnSolana(options.managerScriptAccount, options.connection, options.signerKeypair, "set_symbol_price", [configAddress, admin, validatedSymbol, priceLamports], [configAddress, admin], {
214
+ debug: options.debug,
215
+ fiveVMProgramId: vmProgramId,
216
+ computeUnitLimit: 300000,
217
+ });
218
+ if (!result.success) {
219
+ throw new Error(result.error || "set_symbol_price failed");
220
+ }
221
+ return {
222
+ transactionId: result.transactionId,
223
+ symbol: validatedSymbol,
224
+ priceLamports,
225
+ };
226
+ }
227
+ export async function getNamespaceSymbolPriceOnChain(symbol, options) {
228
+ const { ProgramIdResolver } = await import("../config/ProgramIdResolver.js");
229
+ const { executeOnSolana } = await import("./execute.js");
230
+ const validatedSymbol = validateSymbol(symbol);
231
+ const vmProgramId = ProgramIdResolver.resolve(options.fiveVMProgramId);
232
+ const configAddress = await deriveNamespaceConfigAccount(vmProgramId);
233
+ const result = await executeOnSolana(options.managerScriptAccount, options.connection, options.signerKeypair, "get_symbol_price", [configAddress, validatedSymbol], [configAddress], {
234
+ debug: options.debug,
235
+ fiveVMProgramId: vmProgramId,
236
+ computeUnitLimit: 300000,
237
+ });
238
+ if (!result.success) {
239
+ throw new Error(result.error || "get_symbol_price failed");
240
+ }
241
+ const priceLamportsRaw = typeof result.result === "number"
242
+ ? result.result
243
+ : Number(result.result);
244
+ if (!Number.isFinite(priceLamportsRaw) || priceLamportsRaw < 0) {
245
+ throw new Error(`invalid get_symbol_price result: ${String(result.result)}`);
246
+ }
247
+ const priceLamports = Math.trunc(priceLamportsRaw);
248
+ return {
249
+ transactionId: result.transactionId,
250
+ symbol: validatedSymbol,
251
+ priceLamports,
252
+ priceSol: priceLamports / LAMPORTS_PER_SOL,
253
+ };
254
+ }
@@ -48,12 +48,13 @@ export class FiveProgram {
48
48
  if (typeof prop === 'string') {
49
49
  return (...args) => {
50
50
  const builder = target.function(prop);
51
- // TODO: Robust argument mapping based on ABI types
52
- // Simple map: if args[0] is object, assume named params
53
51
  if (args.length === 1 && typeof args[0] === 'object' && !Array.isArray(args[0])) {
54
52
  builder.args(args[0]);
55
53
  }
56
- // Logic for positional args would go here
54
+ else if (args.length > 0) {
55
+ throw new Error(`FiveProgram.methods.${prop} only supports a single named-arguments object. ` +
56
+ `Use program.function("${prop}").args({...}) for now.`);
57
+ }
57
58
  return builder;
58
59
  };
59
60
  }
@@ -20,12 +20,19 @@
20
20
  * }
21
21
  * ```
22
22
  */
23
+ import { normalizeAbiFunctions } from '../utils/abi.js';
23
24
  /**
24
25
  * TypeGenerator creates TypeScript interfaces from ABI
25
26
  */
26
27
  export class TypeGenerator {
27
28
  constructor(abi, options) {
28
- this.abi = abi;
29
+ this.abi = {
30
+ ...abi,
31
+ functions: normalizeAbiFunctions(abi.functions ?? abi).map((func) => ({
32
+ ...func,
33
+ visibility: func.visibility ?? 'public',
34
+ })),
35
+ };
29
36
  this.options = {
30
37
  scriptName: abi.name || 'Program',
31
38
  debug: false,
@@ -1,14 +1,28 @@
1
+ const SUPPORTED_SCHEMA_VERSION = 1;
1
2
  /**
2
3
  * Parses a raw TOML object into a strict ProjectConfig.
3
4
  */
4
5
  export function parseProjectConfig(parsedToml) {
6
+ const schemaVersionRaw = parsedToml.schema_version;
7
+ if (schemaVersionRaw === undefined) {
8
+ throw new Error(`Missing required top-level 'schema_version'. Add:\nschema_version = ${SUPPORTED_SCHEMA_VERSION}`);
9
+ }
10
+ if (!Number.isInteger(schemaVersionRaw) || Number(schemaVersionRaw) <= 0) {
11
+ throw new Error('schema_version must be a positive integer');
12
+ }
13
+ const schemaVersion = Number(schemaVersionRaw);
14
+ if (schemaVersion !== SUPPORTED_SCHEMA_VERSION) {
15
+ throw new Error(`Unsupported schema_version=${schemaVersion}. Supported schema_version: ${SUPPORTED_SCHEMA_VERSION}`);
16
+ }
5
17
  const project = parsedToml.project ?? {};
6
18
  const build = parsedToml.build ?? {};
7
19
  const optimizations = parsedToml.optimizations ?? {};
20
+ const dependencies = parsedToml.dependencies ?? {};
8
21
  const deploy = parsedToml.deploy ?? {};
9
22
  const name = project.name ?? 'five-project';
10
23
  const target = (project.target ?? 'vm');
11
24
  return {
25
+ schemaVersion,
12
26
  name,
13
27
  version: project.version ?? '0.1.0',
14
28
  description: project.description,
@@ -25,8 +39,106 @@ export function parseProjectConfig(parsedToml) {
25
39
  optimizations: {
26
40
  enableCompression: optimizations.enable_compression ?? true,
27
41
  enableConstraintOptimization: optimizations.enable_constraint_optimization ?? true,
42
+ // Public SDK config is locked to the canonical production mode.
28
43
  optimizationLevel: 'production'
29
44
  },
30
- dependencies: []
45
+ dependencies: parseDependencies(dependencies)
31
46
  };
32
47
  }
48
+ function parseDependencies(rawDeps) {
49
+ const out = [];
50
+ const seenNamespaces = new Set();
51
+ const seenAddresses = new Set();
52
+ const seenMoatTargets = new Set();
53
+ for (const [alias, rawValue] of Object.entries(rawDeps ?? {})) {
54
+ const value = (rawValue ?? {});
55
+ const dependency = {
56
+ alias,
57
+ package: String(value.package ?? ''),
58
+ version: value.version !== undefined ? String(value.version) : undefined,
59
+ source: String(value.source ?? ''),
60
+ link: String(value.link ?? ''),
61
+ path: value.path !== undefined ? String(value.path) : undefined,
62
+ namespace: value.namespace !== undefined ? String(value.namespace) : undefined,
63
+ address: value.address !== undefined ? String(value.address) : undefined,
64
+ moatAccount: value.moat_account !== undefined ? String(value.moat_account) : undefined,
65
+ module: value.module !== undefined ? String(value.module) : undefined,
66
+ pin: value.pin !== undefined ? String(value.pin) : undefined,
67
+ cluster: value.cluster !== undefined ? String(value.cluster) : undefined,
68
+ };
69
+ if (!dependency.package) {
70
+ throw new Error(`Invalid dependency '${alias}': missing required field 'package'`);
71
+ }
72
+ if (dependency.source !== 'bundled' &&
73
+ dependency.source !== 'path' &&
74
+ dependency.source !== 'namespace' &&
75
+ dependency.source !== 'address' &&
76
+ dependency.source !== 'moat') {
77
+ throw new Error(`Invalid dependency '${alias}': source must be one of bundled|path|namespace|address|moat`);
78
+ }
79
+ if (dependency.link !== 'inline' && dependency.link !== 'external') {
80
+ throw new Error(`Invalid dependency '${alias}': link must be one of inline|external`);
81
+ }
82
+ const hasPath = Boolean(dependency.path);
83
+ const hasNamespace = Boolean(dependency.namespace);
84
+ const hasAddress = Boolean(dependency.address);
85
+ const hasMoatAccount = Boolean(dependency.moatAccount);
86
+ const hasModule = Boolean(dependency.module);
87
+ if (dependency.source === 'path' && !hasPath) {
88
+ throw new Error(`Invalid dependency '${alias}': source=path requires 'path'`);
89
+ }
90
+ if (dependency.source === 'namespace' && !hasNamespace) {
91
+ throw new Error(`Invalid dependency '${alias}': source=namespace requires 'namespace'`);
92
+ }
93
+ if (dependency.source === 'address' && !hasAddress) {
94
+ throw new Error(`Invalid dependency '${alias}': source=address requires 'address'`);
95
+ }
96
+ if (dependency.source === 'moat' && (!hasMoatAccount || !hasModule)) {
97
+ throw new Error(`Invalid dependency '${alias}': source=moat requires 'moat_account' and 'module'`);
98
+ }
99
+ if (dependency.source !== 'path' && hasPath) {
100
+ throw new Error(`Invalid dependency '${alias}': 'path' is only valid for source=path`);
101
+ }
102
+ if (dependency.source !== 'namespace' && hasNamespace) {
103
+ throw new Error(`Invalid dependency '${alias}': 'namespace' is only valid for source=namespace`);
104
+ }
105
+ if (dependency.source !== 'address' && hasAddress) {
106
+ throw new Error(`Invalid dependency '${alias}': 'address' is only valid for source=address`);
107
+ }
108
+ if (dependency.source !== 'moat' && hasMoatAccount) {
109
+ throw new Error(`Invalid dependency '${alias}': 'moat_account' is only valid for source=moat`);
110
+ }
111
+ if (dependency.source !== 'moat' && hasModule) {
112
+ throw new Error(`Invalid dependency '${alias}': 'module' is only valid for source=moat`);
113
+ }
114
+ if (dependency.source === 'bundled' || dependency.source === 'path') {
115
+ if (dependency.link !== 'inline') {
116
+ throw new Error(`Invalid dependency '${alias}': source=${dependency.source} currently requires link=inline`);
117
+ }
118
+ }
119
+ else if (dependency.link !== 'external') {
120
+ throw new Error(`Invalid dependency '${alias}': source=${dependency.source} currently requires link=external`);
121
+ }
122
+ if (dependency.namespace) {
123
+ if (seenNamespaces.has(dependency.namespace)) {
124
+ throw new Error(`Invalid dependencies: duplicate namespace '${dependency.namespace}'`);
125
+ }
126
+ seenNamespaces.add(dependency.namespace);
127
+ }
128
+ if (dependency.address) {
129
+ if (seenAddresses.has(dependency.address)) {
130
+ throw new Error(`Invalid dependencies: duplicate address '${dependency.address}'`);
131
+ }
132
+ seenAddresses.add(dependency.address);
133
+ }
134
+ if (dependency.moatAccount && dependency.module) {
135
+ const moatTarget = `${dependency.moatAccount}::${dependency.module}`;
136
+ if (seenMoatTargets.has(moatTarget)) {
137
+ throw new Error(`Invalid dependencies: duplicate moat target '${moatTarget}'`);
138
+ }
139
+ seenMoatTargets.add(moatTarget);
140
+ }
141
+ out.push(dependency);
142
+ }
143
+ return out;
144
+ }
@@ -106,6 +106,11 @@ export interface LockEntry {
106
106
  address: string;
107
107
  bytecode_hash: string;
108
108
  deployed_at?: string;
109
+ package?: string;
110
+ source?: 'bundled' | 'path' | 'namespace' | 'address';
111
+ link?: LinkType;
112
+ resolved_namespace?: string;
113
+ resolved_script_account?: string;
109
114
  }
110
115
  /**
111
116
  * Resolved workspace state for IDE
@@ -45,6 +45,7 @@ export interface DiscoveredTest {
45
45
  * Discover tests from directory
46
46
  */
47
47
  export declare class TestDiscovery {
48
+ private static normalizeJsonTestCases;
48
49
  /**
49
50
  * Discover all tests in a directory
50
51
  */
@@ -12,6 +12,16 @@ import { FiveSDK } from '../FiveSDK.js';
12
12
  * Discover tests from directory
13
13
  */
14
14
  export class TestDiscovery {
15
+ static normalizeJsonTestCases(data) {
16
+ const testCases = data.tests || data.testCases || [];
17
+ if (Array.isArray(testCases)) {
18
+ return testCases;
19
+ }
20
+ if (testCases && typeof testCases === 'object') {
21
+ return null;
22
+ }
23
+ return [];
24
+ }
15
25
  /**
16
26
  * Discover all tests in a directory
17
27
  */
@@ -66,7 +76,10 @@ export class TestDiscovery {
66
76
  try {
67
77
  const content = await readFile(file, 'utf8');
68
78
  const data = JSON.parse(content);
69
- const testCases = data.tests || data.testCases || [];
79
+ const testCases = this.normalizeJsonTestCases(data);
80
+ if (testCases === null) {
81
+ continue;
82
+ }
70
83
  for (const testCase of testCases) {
71
84
  tests.push({
72
85
  name: testCase.name,
@@ -98,7 +111,10 @@ export class TestDiscovery {
98
111
  try {
99
112
  const content = await readFile(file, 'utf8');
100
113
  const data = JSON.parse(content);
101
- const testCases = data.tests || data.testCases || [];
114
+ const testCases = this.normalizeJsonTestCases(data);
115
+ if (testCases === null) {
116
+ return [];
117
+ }
102
118
  return testCases.map((testCase) => ({
103
119
  name: testCase.name,
104
120
  path: file,
@@ -211,10 +211,13 @@ export class FiveTestRunner {
211
211
  try {
212
212
  const content = await readFile(test.path, 'utf8');
213
213
  const data = JSON.parse(content);
214
+ const testCases = Array.isArray(data.tests || data.testCases)
215
+ ? (data.tests || data.testCases)
216
+ : [];
214
217
  suites.push({
215
218
  name: data.name || basename(test.path, '.test.json'),
216
219
  description: data.description,
217
- testCases: data.tests || data.testCases || []
220
+ testCases
218
221
  });
219
222
  loadedJsonSuites.add(test.path);
220
223
  }