@5ive-tech/sdk 1.1.13 → 1.1.15

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 (44) hide show
  1. package/README.md +22 -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/config/ProgramIdResolver.js +6 -2
  15. package/dist/constants/headers.d.ts +2 -0
  16. package/dist/constants/headers.js +2 -0
  17. package/dist/crypto/index.d.ts +8 -1
  18. package/dist/crypto/index.js +27 -14
  19. package/dist/index.d.ts +1 -0
  20. package/dist/index.js +1 -0
  21. package/dist/modules/accounts.js +1 -1
  22. package/dist/modules/deploy.js +167 -98
  23. package/dist/modules/execute.d.ts +5 -0
  24. package/dist/modules/execute.js +109 -50
  25. package/dist/modules/fees.js +2 -2
  26. package/dist/modules/namespaces.d.ts +11 -0
  27. package/dist/modules/namespaces.js +64 -0
  28. package/dist/program/FiveProgram.js +4 -3
  29. package/dist/program/TypeGenerator.js +8 -1
  30. package/dist/project/config.js +113 -1
  31. package/dist/project/workspace.d.ts +5 -0
  32. package/dist/testing/AccountTestFixture.js +3 -3
  33. package/dist/testing/TestDiscovery.d.ts +1 -0
  34. package/dist/testing/TestDiscovery.js +18 -2
  35. package/dist/testing/TestRunner.js +4 -1
  36. package/dist/types.d.ts +17 -6
  37. package/dist/types.js +2 -1
  38. package/dist/utils/abi.js +33 -10
  39. package/dist/utils/transaction.d.ts +16 -0
  40. package/dist/utils/transaction.js +81 -5
  41. package/dist/wasm/compiler/CompilationLogic.js +3 -3
  42. package/dist/wasm/vm.d.ts +2 -2
  43. package/dist/wasm/vm.js +10 -11
  44. package/package.json +1 -1
@@ -17,6 +17,7 @@ const DEFAULT_FEE_VAULT_SHARD_COUNT = (() => {
17
17
  return 2;
18
18
  }
19
19
  })();
20
+ const MAX_FEE_VAULT_SHARD_COUNT = 8;
20
21
  const FEE_VAULT_NAMESPACE_SEED = Buffer.from([
21
22
  0xff, 0x66, 0x69, 0x76, 0x65, 0x5f, 0x76, 0x6d, 0x5f, 0x66, 0x65, 0x65,
22
23
  0x5f, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x76, 0x31,
@@ -25,7 +26,7 @@ const EXECUTE_FEE_HEADER_A = 0xff;
25
26
  const EXECUTE_FEE_HEADER_B = 0x53;
26
27
  function clampShardCount(rawCount) {
27
28
  const normalized = rawCount > 0 ? rawCount : DEFAULT_FEE_VAULT_SHARD_COUNT;
28
- return Math.max(1, Math.min(DEFAULT_FEE_VAULT_SHARD_COUNT, normalized));
29
+ return Math.max(1, Math.min(MAX_FEE_VAULT_SHARD_COUNT, normalized));
29
30
  }
30
31
  async function deriveProgramFeeVault(programId, shardIndex) {
31
32
  const { PublicKey } = await import("@solana/web3.js");
@@ -277,56 +278,100 @@ export async function generateExecuteInstruction(scriptAccount, functionName, pa
277
278
  : selectFeeShard(shardCount);
278
279
  const feeVault = await deriveProgramFeeVault(programId, feeShardIndex);
279
280
  const instructionAccounts = [
281
+ // Execute runtime reads script/vm_state; keep them readonly.
280
282
  { pubkey: scriptAccount, isSigner: false, isWritable: false },
281
283
  { pubkey: vmState, isSigner: false, isWritable: false },
282
284
  ];
283
285
  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
286
+ const normalizeAccountPubkey = (value) => {
287
+ if (!value)
288
+ return undefined;
289
+ if (typeof value === "string")
290
+ return value;
291
+ if (typeof value.toBase58 === "function")
292
+ return value.toBase58();
293
+ if (typeof value.toString === "function") {
294
+ const str = value.toString();
295
+ if (str && str !== "[object Object]")
296
+ return str;
297
+ }
298
+ return undefined;
299
+ };
300
+ const isAccountParam = (param) => {
301
+ if (!param)
302
+ return false;
303
+ if (param.is_account || param.isAccount)
304
+ return true;
305
+ const type = (param.type || param.param_type || "").toString().trim().toLowerCase();
306
+ return type === "account" || type === "mint" || type === "tokenaccount";
307
+ };
308
+ const allParams = funcDef && Array.isArray(funcDef.parameters) ? funcDef.parameters : [];
309
+ const accountParams = allParams.filter((param) => isAccountParam(param));
310
+ const hasFullParameterList = allParams.length > 0 && parameters.length === allParams.length;
311
+ if (accountParams.length > 0) {
312
+ // First pass: detect if there's an @init constraint and identify payer param.
289
313
  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
- }
314
+ let initParamName;
315
+ let payerParamName;
316
+ for (const param of accountParams) {
317
+ const attributes = param.attributes || [];
318
+ if (attributes.includes("init")) {
319
+ hasInit = true;
320
+ initParamName = param.name;
321
+ break;
322
+ }
323
+ }
324
+ if (hasInit) {
325
+ for (const param of accountParams) {
326
+ if (param.name !== initParamName &&
327
+ (param.attributes || []).includes("signer")) {
328
+ payerParamName = param.name;
307
329
  break;
308
330
  }
309
331
  }
310
332
  }
311
- funcDef.parameters.forEach((param, paramIndex) => {
312
- if (param.is_account || param.isAccount) {
313
- const value = parameters[paramIndex];
314
- const pubkey = value?.toString();
333
+ // Resolve pubkeys for account params in either full-param mode or args-only mode.
334
+ const accountPubkeysByParamName = new Map();
335
+ if (hasFullParameterList) {
336
+ allParams.forEach((param, paramIndex) => {
337
+ if (!isAccountParam(param))
338
+ return;
339
+ const pubkey = normalizeAccountPubkey(parameters[paramIndex]);
315
340
  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
- });
341
+ accountPubkeysByParamName.set(param.name, pubkey);
342
+ }
343
+ });
344
+ }
345
+ else {
346
+ const count = Math.min(accountParams.length, accounts.length);
347
+ for (let i = 0; i < count; i++) {
348
+ const paramName = accountParams[i]?.name;
349
+ const pubkey = accounts[i];
350
+ if (paramName && pubkey) {
351
+ accountPubkeysByParamName.set(paramName, pubkey);
326
352
  }
327
353
  }
328
- });
354
+ }
355
+ for (const param of accountParams) {
356
+ const pubkey = accountPubkeysByParamName.get(param.name);
357
+ if (!pubkey)
358
+ continue;
359
+ const attributes = param.attributes || [];
360
+ const isSigner = attributes.includes("signer");
361
+ const isWritable = attributes.includes("mut") ||
362
+ attributes.includes("init") ||
363
+ (hasInit && payerParamName === param.name);
364
+ const existing = abiAccountMetadata.get(pubkey) || {
365
+ isSigner: false,
366
+ isWritable: false,
367
+ };
368
+ abiAccountMetadata.set(pubkey, {
369
+ isSigner: existing.isSigner || isSigner,
370
+ isWritable: existing.isWritable || isWritable,
371
+ });
372
+ }
329
373
  }
374
+ const unknownAccounts = [];
330
375
  const userInstructionAccounts = accounts.map((acc) => {
331
376
  // Check both derived ABI metadata and passed-in metadata (from FunctionBuilder)
332
377
  const abiMetadata = abiAccountMetadata.get(acc);
@@ -335,25 +380,40 @@ export async function generateExecuteInstruction(scriptAccount, functionName, pa
335
380
  const isSigner = metadata ? metadata.isSigner : false;
336
381
  const isWritable = metadata
337
382
  ? (metadata.isSystemAccount ? false : metadata.isWritable)
338
- : true;
383
+ : false;
384
+ if (!metadata) {
385
+ unknownAccounts.push(acc);
386
+ }
339
387
  return {
340
388
  pubkey: acc,
341
389
  isSigner,
342
390
  isWritable
343
391
  };
344
392
  });
393
+ if (unknownAccounts.length > 0) {
394
+ const deduped = Array.from(new Set(unknownAccounts));
395
+ console.warn(`[FiveSDK] Missing account metadata for ${deduped.join(", ")}; defaulting readonly. Pass accountMetadata or use FiveProgram.function(...).instruction().`);
396
+ }
345
397
  instructionAccounts.push(...userInstructionAccounts);
346
398
  const instructionData = encodeExecuteInstruction(functionIndex, encodedParams, actualParamCount, feeShardIndex, options.debug === true);
347
399
  // Runtime requires strict tail: [payer, fee_vault, system_program].
400
+ // Prefer explicit payer, otherwise infer from signer+writable account metadata only.
348
401
  const signerCandidates = instructionAccounts
349
402
  .filter((acc) => acc.isSigner)
350
403
  .map((acc) => acc.pubkey);
351
- const inferredPayer = options.payerAccount ||
352
- (signerCandidates.length > 0
353
- ? signerCandidates[signerCandidates.length - 1]
354
- : accounts[0]);
404
+ const metadataFor = (pubkey) => options.accountMetadata?.get(pubkey) || abiAccountMetadata.get(pubkey);
405
+ const writableSignerCandidates = signerCandidates.filter((pubkey) => {
406
+ const metadata = metadataFor(pubkey);
407
+ if (!metadata)
408
+ return false;
409
+ return !metadata.isSystemAccount && metadata.isWritable;
410
+ });
411
+ const inferredPayer = options.payerAccount || writableSignerCandidates[0];
355
412
  if (!inferredPayer) {
356
- throw new Error("Could not infer execute fee payer account. Provide a signer account or set options.payerAccount.");
413
+ if (signerCandidates.length > 0) {
414
+ throw new Error("Could not infer execute fee payer account from writable signer metadata. Pass options.payerAccount or use FiveProgram.function(...).payer(...).instruction().");
415
+ }
416
+ throw new Error("Could not infer execute fee payer account. Provide options.payerAccount or include a writable signer account.");
357
417
  }
358
418
  const feeTailAccounts = [
359
419
  { pubkey: inferredPayer, isSigner: true, isWritable: true },
@@ -404,6 +464,7 @@ export async function executeOnSolana(scriptAccount, connection, signerKeypair,
404
464
  vmStateAccount: options.vmStateAccount,
405
465
  fiveVMProgramId: options.fiveVMProgramId,
406
466
  abi: options.abi,
467
+ accountMetadata: options.accountMetadata,
407
468
  feeShardIndex: options.feeShardIndex,
408
469
  payerAccount: options.payerAccount || signerKeypair.publicKey.toString(),
409
470
  });
@@ -430,7 +491,6 @@ export async function executeOnSolana(scriptAccount, connection, signerKeypair,
430
491
  for (const meta of accountKeys) {
431
492
  if (meta.pubkey === signerPubkey) {
432
493
  meta.isSigner = true;
433
- meta.isWritable = true;
434
494
  signerFound = true;
435
495
  }
436
496
  }
@@ -438,7 +498,7 @@ export async function executeOnSolana(scriptAccount, connection, signerKeypair,
438
498
  accountKeys.push({
439
499
  pubkey: signerPubkey,
440
500
  isSigner: true,
441
- isWritable: true,
501
+ isWritable: false,
442
502
  });
443
503
  }
444
504
  const executeInstruction = new TransactionInstruction({
@@ -452,8 +512,8 @@ export async function executeOnSolana(scriptAccount, connection, signerKeypair,
452
512
  });
453
513
  transaction.add(executeInstruction);
454
514
  transaction.feePayer = signerKeypair.publicKey;
455
- const { blockhash } = await connection.getLatestBlockhash("confirmed");
456
- transaction.recentBlockhash = blockhash;
515
+ const latestBlockhash = await connection.getLatestBlockhash("confirmed");
516
+ transaction.recentBlockhash = latestBlockhash.blockhash;
457
517
  transaction.partialSign(signerKeypair);
458
518
  const firstSig = transaction.signatures[0]?.signature;
459
519
  if (firstSig) {
@@ -465,12 +525,11 @@ export async function executeOnSolana(scriptAccount, connection, signerKeypair,
465
525
  maxRetries: options.maxRetries || 3,
466
526
  });
467
527
  lastSignature = signature;
468
- const latestBlockhash = await connection.getLatestBlockhash("confirmed");
469
528
  const confirmation = await confirmTransactionRobust(connection, signature, {
470
529
  commitment: "confirmed",
471
530
  timeoutMs: 120000,
472
531
  debug: options.debug,
473
- blockhash,
532
+ blockhash: latestBlockhash.blockhash,
474
533
  lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
475
534
  });
476
535
  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
@@ -219,7 +219,7 @@ export class AccountTestFixture {
219
219
  else if (spec.type === 'state' || spec.type === 'mutable') {
220
220
  // Create state/mutable account with initial data
221
221
  const space = 1024; // Default space
222
- const owner = options.fiveVMProgramId || new PublicKey('4Qxf3pbCse2veUgZVMiAm3nWqJrYo2pT4suxHKMJdK1d');
222
+ const owner = options.fiveVMProgramId || new PublicKey('5ive58PJUPaTyAe7tvU1bvBi25o7oieLLTRsJDoQNJst');
223
223
  // Serialize state data if provided
224
224
  let initialData;
225
225
  if (spec.state && spec.type === 'state') {
@@ -242,7 +242,7 @@ export class AccountTestFixture {
242
242
  else if (spec.type === 'init') {
243
243
  // Create init account (will be initialized by script)
244
244
  const space = 1024;
245
- const owner = options.fiveVMProgramId || new PublicKey('4Qxf3pbCse2veUgZVMiAm3nWqJrYo2pT4suxHKMJdK1d');
245
+ const owner = options.fiveVMProgramId || new PublicKey('5ive58PJUPaTyAe7tvU1bvBi25o7oieLLTRsJDoQNJst');
246
246
  publicKey = await manager.createAccount(space, owner);
247
247
  if (options.debug) {
248
248
  console.log(` ${spec.name} (init): ${publicKey.toString()}`);
@@ -256,7 +256,7 @@ export class AccountTestFixture {
256
256
  else {
257
257
  // Create readonly account
258
258
  const space = 0;
259
- const owner = options.fiveVMProgramId || new PublicKey('4Qxf3pbCse2veUgZVMiAm3nWqJrYo2pT4suxHKMJdK1d');
259
+ const owner = options.fiveVMProgramId || new PublicKey('5ive58PJUPaTyAe7tvU1bvBi25o7oieLLTRsJDoQNJst');
260
260
  publicKey = await manager.createAccount(space, owner);
261
261
  if (options.debug) {
262
262
  console.log(` ${spec.name} (readonly): ${publicKey.toString()}`);
@@ -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
  }