@5ive-tech/sdk 1.1.7 → 1.1.9

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.
@@ -52,6 +52,8 @@ export declare function generateExecuteInstruction(scriptAccount: string, functi
52
52
  isWritable: boolean;
53
53
  isSystemAccount?: boolean;
54
54
  }>;
55
+ feeShardIndex?: number;
56
+ payerAccount?: string;
55
57
  }): Promise<SerializedExecution>;
56
58
  export declare function executeOnSolana(scriptAccount: string, connection: any, signerKeypair: any, functionName: string | number, parameters?: any[], accounts?: string[], options?: {
57
59
  debug?: boolean;
@@ -63,6 +65,8 @@ export declare function executeOnSolana(scriptAccount: string, connection: any,
63
65
  vmStateAccount?: string;
64
66
  fiveVMProgramId?: string;
65
67
  abi?: any;
68
+ feeShardIndex?: number;
69
+ payerAccount?: string;
66
70
  }): Promise<{
67
71
  success: boolean;
68
72
  result?: any;
@@ -7,6 +7,45 @@ import { validator, Validators } from "../validation/index.js";
7
7
  import { calculateExecuteFee } from "./fees.js";
8
8
  import { loadWasmVM } from "../wasm/instance.js";
9
9
  import { ProgramIdResolver } from "../config/ProgramIdResolver.js";
10
+ const DEFAULT_FEE_VAULT_SHARD_COUNT = 10;
11
+ const FEE_VAULT_NAMESPACE_SEED = Buffer.from([
12
+ 0xff, 0x66, 0x69, 0x76, 0x65, 0x5f, 0x76, 0x6d, 0x5f, 0x66, 0x65, 0x65,
13
+ 0x5f, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x76, 0x31,
14
+ ]);
15
+ const EXECUTE_FEE_HEADER_A = 0xff;
16
+ const EXECUTE_FEE_HEADER_B = 0x53;
17
+ async function deriveProgramFeeVault(programId, shardIndex) {
18
+ const { PublicKey } = await import("@solana/web3.js");
19
+ const [pda, bump] = PublicKey.findProgramAddressSync([FEE_VAULT_NAMESPACE_SEED, Buffer.from([shardIndex])], new PublicKey(programId));
20
+ return { address: pda.toBase58(), bump };
21
+ }
22
+ async function readVMStateShardCount(connection, vmStateAddress) {
23
+ if (!connection)
24
+ return DEFAULT_FEE_VAULT_SHARD_COUNT;
25
+ try {
26
+ const { PublicKey } = await import("@solana/web3.js");
27
+ const info = await connection.getAccountInfo(new PublicKey(vmStateAddress), "confirmed");
28
+ if (!info)
29
+ return DEFAULT_FEE_VAULT_SHARD_COUNT;
30
+ const data = new Uint8Array(info.data);
31
+ if (data.length <= 50)
32
+ return DEFAULT_FEE_VAULT_SHARD_COUNT;
33
+ const shardCount = data[50];
34
+ return shardCount > 0 ? shardCount : DEFAULT_FEE_VAULT_SHARD_COUNT;
35
+ }
36
+ catch {
37
+ return DEFAULT_FEE_VAULT_SHARD_COUNT;
38
+ }
39
+ }
40
+ function selectFeeShard(shardCount) {
41
+ const totalShards = Math.max(1, shardCount | 0);
42
+ if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
43
+ const bytes = new Uint32Array(1);
44
+ crypto.getRandomValues(bytes);
45
+ return bytes[0] % totalShards;
46
+ }
47
+ return Math.floor(Math.random() * totalShards);
48
+ }
10
49
  // Helper function to initialize ParameterEncoder if needed (though BytecodeEncoder is preferred)
11
50
  // Assume BytecodeEncoder handles it or call it if needed.
12
51
  // BytecodeEncoder uses WASM module directly via loader.
@@ -188,9 +227,9 @@ export async function generateExecuteInstruction(scriptAccount, functionName, pa
188
227
  funcDef = Array.isArray(scriptMetadata.functions)
189
228
  ? scriptMetadata.functions.find((f) => f.index === functionIndex)
190
229
  : scriptMetadata.functions[functionIndex];
191
- const paramDefs = (funcDef.parameters || []);
192
- actualParamCount = paramDefs.length;
193
- encodedParams = await encodeParametersWithABI(parameters, funcDef, functionIndex, accounts, options);
230
+ const encoded = await encodeParametersWithABI(parameters, funcDef, functionIndex, accounts, options);
231
+ actualParamCount = encoded.paramCount;
232
+ encodedParams = encoded.encoded;
194
233
  }
195
234
  catch (metadataError) {
196
235
  if (options.debug) {
@@ -212,34 +251,18 @@ export async function generateExecuteInstruction(scriptAccount, functionName, pa
212
251
  // Resolve program ID with consistent precedence
213
252
  const programId = ProgramIdResolver.resolve(options.fiveVMProgramId);
214
253
  const vmStatePDA = await PDAUtils.deriveVMStatePDA(programId);
215
- const vmState = options.vmStateAccount || vmStatePDA.address;
216
- let adminAccount = options.adminAccount;
217
- if (!adminAccount && connection) {
218
- try {
219
- let vmStateAddress = options.vmStateAccount;
220
- if (!vmStateAddress) {
221
- const pda = await PDAUtils.deriveVMStatePDA(programId);
222
- vmStateAddress = pda.address;
223
- }
224
- const { PublicKey } = await import("@solana/web3.js");
225
- const info = await connection.getAccountInfo(new PublicKey(vmStateAddress));
226
- if (info) {
227
- const data = new Uint8Array(info.data);
228
- if (data.length >= 32) {
229
- const authorityPubkey = new PublicKey(data.slice(0, 32));
230
- adminAccount = authorityPubkey.toBase58();
231
- }
232
- }
233
- }
234
- catch (error) {
235
- if (options.debug) {
236
- console.warn(`[FiveSDK] Failed to resolve admin account from VM state:`, error);
237
- }
238
- }
254
+ if (options.vmStateAccount && options.vmStateAccount !== vmStatePDA.address) {
255
+ throw new Error(`vmStateAccount must be canonical PDA ${vmStatePDA.address}; got ${options.vmStateAccount}`);
239
256
  }
257
+ const vmState = vmStatePDA.address;
258
+ const shardCount = await readVMStateShardCount(connection, vmState);
259
+ const feeShardIndex = options.feeShardIndex !== undefined
260
+ ? ((options.feeShardIndex % shardCount) + shardCount) % shardCount
261
+ : selectFeeShard(shardCount);
262
+ const feeVault = await deriveProgramFeeVault(programId, feeShardIndex);
240
263
  const instructionAccounts = [
241
264
  { pubkey: scriptAccount, isSigner: false, isWritable: false },
242
- { pubkey: vmState, isSigner: false, isWritable: true },
265
+ { pubkey: vmState, isSigner: false, isWritable: false },
243
266
  ];
244
267
  const abiAccountMetadata = new Map();
245
268
  if (funcDef && funcDef.parameters) {
@@ -285,12 +308,12 @@ export async function generateExecuteInstruction(scriptAccount, functionName, pa
285
308
  }
286
309
  });
287
310
  }
288
- const userInstructionAccounts = accounts.map((acc, index) => {
311
+ const userInstructionAccounts = accounts.map((acc) => {
289
312
  // Check both derived ABI metadata and passed-in metadata (from FunctionBuilder)
290
313
  const abiMetadata = abiAccountMetadata.get(acc);
291
314
  const passedMetadata = options.accountMetadata?.get(acc);
292
315
  const metadata = abiMetadata || passedMetadata;
293
- const isSigner = metadata ? metadata.isSigner : (index === 0 && adminAccount ? true : false);
316
+ const isSigner = metadata ? metadata.isSigner : false;
294
317
  const isWritable = metadata ? metadata.isWritable : true;
295
318
  return {
296
319
  pubkey: acc,
@@ -299,20 +322,24 @@ export async function generateExecuteInstruction(scriptAccount, functionName, pa
299
322
  };
300
323
  });
301
324
  instructionAccounts.push(...userInstructionAccounts);
302
- if (adminAccount) {
303
- const existingAdminIdx = instructionAccounts.findIndex(a => a.pubkey === adminAccount);
304
- if (existingAdminIdx === -1) {
305
- instructionAccounts.push({
306
- pubkey: adminAccount,
307
- isSigner: false,
308
- isWritable: true,
309
- });
310
- }
311
- else {
312
- instructionAccounts[existingAdminIdx].isWritable = true;
313
- }
325
+ const instructionData = encodeExecuteInstruction(functionIndex, encodedParams, actualParamCount, feeShardIndex, feeVault.bump);
326
+ // Runtime requires strict tail: [payer, fee_vault, system_program].
327
+ const signerCandidates = instructionAccounts
328
+ .filter((acc) => acc.isSigner)
329
+ .map((acc) => acc.pubkey);
330
+ const inferredPayer = options.payerAccount ||
331
+ (signerCandidates.length > 0
332
+ ? signerCandidates[signerCandidates.length - 1]
333
+ : accounts[0]);
334
+ if (!inferredPayer) {
335
+ throw new Error("Could not infer execute fee payer account. Provide a signer account or set options.payerAccount.");
314
336
  }
315
- const instructionData = encodeExecuteInstruction(functionIndex, encodedParams, actualParamCount);
337
+ const feeTailAccounts = [
338
+ { pubkey: inferredPayer, isSigner: true, isWritable: true },
339
+ { pubkey: feeVault.address, isSigner: false, isWritable: true },
340
+ { pubkey: "11111111111111111111111111111111", isSigner: false, isWritable: false },
341
+ ];
342
+ instructionAccounts.push(...feeTailAccounts);
316
343
  const result = {
317
344
  instruction: {
318
345
  programId: programId,
@@ -328,7 +355,7 @@ export async function generateExecuteInstruction(scriptAccount, functionName, pa
328
355
  requiredSigners: [],
329
356
  estimatedComputeUnits: options.computeUnitLimit ||
330
357
  estimateComputeUnits(functionIndex, parameters.length),
331
- adminAccount: adminAccount,
358
+ adminAccount: feeVault.address,
332
359
  };
333
360
  const shouldEstimateFees = options.estimateFees !== false && connection;
334
361
  if (shouldEstimateFees) {
@@ -356,6 +383,8 @@ export async function executeOnSolana(scriptAccount, connection, signerKeypair,
356
383
  vmStateAccount: options.vmStateAccount,
357
384
  fiveVMProgramId: options.fiveVMProgramId,
358
385
  abi: options.abi,
386
+ feeShardIndex: options.feeShardIndex,
387
+ payerAccount: options.payerAccount || signerKeypair.publicKey.toString(),
359
388
  });
360
389
  }
361
390
  catch (metadataError) {
@@ -375,19 +404,12 @@ export async function executeOnSolana(scriptAccount, connection, signerKeypair,
375
404
  transaction.add(computePriceIx);
376
405
  }
377
406
  const accountKeys = [...executionData.instruction.accounts];
378
- if (options.vmStateAccount && accountKeys.length >= 2) {
379
- for (let i = 0; i < accountKeys.length; i++) {
380
- if (i === 1) {
381
- accountKeys[i].pubkey = options.vmStateAccount;
382
- break;
383
- }
384
- }
385
- }
386
407
  const signerPubkey = signerKeypair.publicKey.toString();
387
408
  let signerFound = false;
388
409
  for (const meta of accountKeys) {
389
410
  if (meta.pubkey === signerPubkey) {
390
411
  meta.isSigner = true;
412
+ meta.isWritable = true;
391
413
  signerFound = true;
392
414
  }
393
415
  }
@@ -537,9 +559,15 @@ export async function executeScriptAccount(scriptAccount, functionIndex = 0, par
537
559
  });
538
560
  }
539
561
  // Helpers
540
- function encodeExecuteInstruction(functionIndex, encodedParams, paramCount) {
562
+ function encodeExecuteInstruction(functionIndex, encodedParams, paramCount, feeShardIndex, feeVaultBump) {
541
563
  const parts = [];
542
564
  parts.push(new Uint8Array([9]));
565
+ parts.push(new Uint8Array([
566
+ EXECUTE_FEE_HEADER_A,
567
+ EXECUTE_FEE_HEADER_B,
568
+ feeShardIndex & 0xff,
569
+ feeVaultBump & 0xff,
570
+ ]));
543
571
  // Function index as fixed u32
544
572
  parts.push(encodeU32(functionIndex));
545
573
  // Param count as fixed u32
@@ -582,7 +610,7 @@ function inferParameterType(value) {
582
610
  return "string";
583
611
  }
584
612
  }
585
- async function encodeParametersWithABI(parameters, functionDef, functionIndex, accounts = [], options = {}) {
613
+ async function encodeParametersWithABI(parameters, functionDef, functionIndex, _accounts = [], options = {}) {
586
614
  const isAccountParam = (param) => {
587
615
  if (!param)
588
616
  return false;
@@ -598,51 +626,33 @@ async function encodeParametersWithABI(parameters, functionDef, functionIndex, a
598
626
  return type === 'pubkey';
599
627
  };
600
628
  const paramDefs = (functionDef.parameters || []);
601
- if (parameters.length !== paramDefs.length) {
629
+ const nonAccountParamDefs = paramDefs.filter((param) => !isAccountParam(param));
630
+ const fullParameterListProvided = parameters.length >= paramDefs.length;
631
+ if (fullParameterListProvided && parameters.length !== paramDefs.length) {
602
632
  console.warn(`[FiveSDK] Parameter validation warning: Function '${functionDef.name}' expects ${paramDefs.length} parameters, but received ${parameters.length}.`);
603
633
  }
604
634
  const paramValues = {};
605
- paramDefs.forEach((param, index) => {
606
- if (index < parameters.length) {
607
- let value = parameters[index];
608
- if (isAccountParam(param)) {
609
- let accountPubkey = null;
610
- if (value && typeof value === 'object' && typeof value.toBase58 === 'function') {
611
- accountPubkey = value.toBase58();
612
- }
613
- else if (typeof value === 'string') {
614
- accountPubkey = value;
615
- }
616
- else if (typeof value === 'number') {
617
- if (value >= 0 && value < accounts.length) {
618
- accountPubkey = accounts[value];
619
- }
620
- else {
621
- throw new Error(`Account index ${value} out of bounds`);
622
- }
623
- }
624
- if (accountPubkey) {
625
- const accountIndex = accounts.indexOf(accountPubkey);
626
- if (accountIndex >= 0) {
627
- // MitoVM receives accounts excluding the script account.
628
- // Account index 0 is the VM state account.
629
- value = accountIndex + 1;
630
- }
631
- else {
632
- throw new Error(`Account ${accountPubkey} not found in accounts array`);
633
- }
634
- }
635
- }
636
- else if (isPubkeyParam(param)) {
637
- if (value && typeof value === 'object' && typeof value.toBase58 === 'function') {
638
- value = value.toBase58();
639
- }
635
+ let argCursor = 0;
636
+ for (let index = 0; index < paramDefs.length; index++) {
637
+ const param = paramDefs[index];
638
+ if (isAccountParam(param)) {
639
+ continue;
640
+ }
641
+ const sourceIndex = fullParameterListProvided ? index : argCursor;
642
+ if (sourceIndex >= parameters.length) {
643
+ throw new Error(`Missing value for parameter: ${param.name}`);
644
+ }
645
+ let value = parameters[sourceIndex];
646
+ if (isPubkeyParam(param)) {
647
+ if (value && typeof value === 'object' && typeof value.toBase58 === 'function') {
648
+ value = value.toBase58();
640
649
  }
641
- paramValues[param.name] = value;
642
650
  }
643
- });
644
- const encoded = await BytecodeEncoder.encodeExecute(functionIndex, paramDefs, paramValues, true, options);
645
- return encoded;
651
+ paramValues[param.name] = value;
652
+ argCursor += 1;
653
+ }
654
+ const encoded = await BytecodeEncoder.encodeExecute(functionIndex, nonAccountParamDefs, paramValues, true, options);
655
+ return { encoded, paramCount: nonAccountParamDefs.length };
646
656
  }
647
657
  function estimateComputeUnits(functionIndex, parameterCount) {
648
658
  return Math.max(5000, 1000 + parameterCount * 500 + functionIndex * 100);
@@ -1,5 +1,7 @@
1
1
  import { FeeInformation } from "../types.js";
2
2
  export declare function getFees(connection: any, fiveVMProgramId?: string): Promise<{
3
+ deployFeeLamports: number;
4
+ executeFeeLamports: number;
3
5
  deployFeeBps: number;
4
6
  executeFeeBps: number;
5
7
  adminAccount: string | null;
@@ -4,16 +4,20 @@ export async function getFees(connection, fiveVMProgramId) {
4
4
  try {
5
5
  const state = await getVMState(connection, fiveVMProgramId);
6
6
  return {
7
- deployFeeBps: state.deployFeeBps,
8
- executeFeeBps: state.executeFeeBps,
9
- adminAccount: state.authority
7
+ deployFeeLamports: state.deployFeeLamports,
8
+ executeFeeLamports: state.executeFeeLamports,
9
+ deployFeeBps: state.deployFeeLamports,
10
+ executeFeeBps: state.executeFeeLamports,
11
+ adminAccount: state.authority,
10
12
  };
11
13
  }
12
14
  catch (error) {
13
15
  return {
16
+ deployFeeLamports: 0,
17
+ executeFeeLamports: 0,
14
18
  deployFeeBps: 0,
15
19
  executeFeeBps: 0,
16
- adminAccount: null
20
+ adminAccount: null,
17
21
  };
18
22
  }
19
23
  }
@@ -22,17 +26,16 @@ export async function calculateDeployFee(bytecodeSize, connection, fiveVMProgram
22
26
  const accountSize = 64 + bytecodeSize;
23
27
  const rentLamports = await RentCalculator.calculateRentExemption(accountSize);
24
28
  const vmState = await getVMState(connection, fiveVMProgramId);
25
- const deployFeeBps = vmState.deployFeeBps;
26
- const feeLamports = Math.floor((rentLamports * deployFeeBps) / 10000);
29
+ const deployFeeLamports = vmState.deployFeeLamports;
27
30
  return {
28
- feeBps: deployFeeBps,
31
+ feeBps: 0,
29
32
  basisLamports: rentLamports,
30
- feeLamports,
31
- totalEstimatedCost: rentLamports + feeLamports,
33
+ feeLamports: deployFeeLamports,
34
+ totalEstimatedCost: rentLamports + deployFeeLamports,
32
35
  costBreakdown: {
33
36
  basis: RentCalculator.formatSOL(rentLamports),
34
- fee: RentCalculator.formatSOL(feeLamports),
35
- total: RentCalculator.formatSOL(rentLamports + feeLamports),
37
+ fee: RentCalculator.formatSOL(deployFeeLamports),
38
+ total: RentCalculator.formatSOL(rentLamports + deployFeeLamports),
36
39
  },
37
40
  };
38
41
  }
@@ -56,17 +59,16 @@ export async function calculateExecuteFee(connection, fiveVMProgramId) {
56
59
  const STANDARD_TX_FEE = 5000;
57
60
  try {
58
61
  const vmState = await getVMState(connection, fiveVMProgramId);
59
- const executeFeeBps = vmState.executeFeeBps;
60
- const feeLamports = Math.floor((STANDARD_TX_FEE * executeFeeBps) / 10000);
62
+ const executeFeeLamports = vmState.executeFeeLamports;
61
63
  return {
62
- feeBps: executeFeeBps,
64
+ feeBps: 0,
63
65
  basisLamports: STANDARD_TX_FEE,
64
- feeLamports,
65
- totalEstimatedCost: STANDARD_TX_FEE + feeLamports,
66
+ feeLamports: executeFeeLamports,
67
+ totalEstimatedCost: STANDARD_TX_FEE + executeFeeLamports,
66
68
  costBreakdown: {
67
69
  basis: RentCalculator.formatSOL(STANDARD_TX_FEE),
68
- fee: RentCalculator.formatSOL(feeLamports),
69
- total: RentCalculator.formatSOL(STANDARD_TX_FEE + feeLamports),
70
+ fee: RentCalculator.formatSOL(executeFeeLamports),
71
+ total: RentCalculator.formatSOL(STANDARD_TX_FEE + executeFeeLamports),
70
72
  },
71
73
  };
72
74
  }
@@ -91,7 +93,7 @@ export async function getFeeInformation(bytecodeSize, connection, fiveVMProgramI
91
93
  calculateExecuteFee(connection, fiveVMProgramId),
92
94
  getVMState(connection, fiveVMProgramId),
93
95
  ]);
94
- const feesEnabled = vmState.deployFeeBps > 0 || vmState.executeFeeBps > 0;
96
+ const feesEnabled = vmState.deployFeeLamports > 0 || vmState.executeFeeLamports > 0;
95
97
  return {
96
98
  deploy: deployFee,
97
99
  execute: executeFee,
@@ -1,7 +1,11 @@
1
1
  export declare function getVMState(connection: any, fiveVMProgramId?: string): Promise<{
2
2
  authority: string;
3
3
  scriptCount: number;
4
+ deployFeeLamports: number;
5
+ executeFeeLamports: number;
4
6
  deployFeeBps: number;
5
7
  executeFeeBps: number;
8
+ feeVaultShardCount: number;
9
+ vmStateBump: number;
6
10
  isInitialized: boolean;
7
11
  }>;
@@ -30,11 +30,18 @@ export async function getVMState(connection, fiveVMProgramId) {
30
30
  throw new Error(`VM State account data too small: expected 56, got ${accountData.length}`);
31
31
  const authority = Base58Utils.encode(accountData.slice(0, 32));
32
32
  const view = new DataView(accountData.buffer, accountData.byteOffset, accountData.byteLength);
33
+ const deployFeeLamports = view.getUint32(40, true);
34
+ const executeFeeLamports = view.getUint32(44, true);
33
35
  return {
34
36
  authority,
35
37
  scriptCount: Number(view.getBigUint64(32, true)),
36
- deployFeeBps: view.getUint32(40, true),
37
- executeFeeBps: view.getUint32(44, true),
38
+ deployFeeLamports,
39
+ executeFeeLamports,
40
+ // Backward-compatible aliases; deprecated.
41
+ deployFeeBps: deployFeeLamports,
42
+ executeFeeBps: executeFeeLamports,
43
+ feeVaultShardCount: accountData[50] || 10,
44
+ vmStateBump: accountData[51] || 0,
38
45
  isInitialized: accountData[48] === 1
39
46
  };
40
47
  }
@@ -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('9MHGM73eszNUtmJS6ypDCESguxWhCBnkUPpTMyLGqURH');
222
+ const owner = options.fiveVMProgramId || new PublicKey('4Qxf3pbCse2veUgZVMiAm3nWqJrYo2pT4suxHKMJdK1d');
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('9MHGM73eszNUtmJS6ypDCESguxWhCBnkUPpTMyLGqURH');
245
+ const owner = options.fiveVMProgramId || new PublicKey('4Qxf3pbCse2veUgZVMiAm3nWqJrYo2pT4suxHKMJdK1d');
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('9MHGM73eszNUtmJS6ypDCESguxWhCBnkUPpTMyLGqURH');
259
+ const owner = options.fiveVMProgramId || new PublicKey('4Qxf3pbCse2veUgZVMiAm3nWqJrYo2pT4suxHKMJdK1d');
260
260
  publicKey = await manager.createAccount(space, owner);
261
261
  if (options.debug) {
262
262
  console.log(` ${spec.name} (readonly): ${publicKey.toString()}`);
@@ -13,6 +13,8 @@ export interface VSourceTest {
13
13
  file: string;
14
14
  functionName: string;
15
15
  parameters?: any[];
16
+ expectedResult?: any;
17
+ expectsResult?: boolean;
16
18
  description?: string;
17
19
  }
18
20
  /**
@@ -36,6 +38,8 @@ export interface DiscoveredTest {
36
38
  source?: VSourceTest;
37
39
  description?: string;
38
40
  parameters?: any[];
41
+ expectedResult?: any;
42
+ expectsResult?: boolean;
39
43
  }
40
44
  /**
41
45
  * Discover tests from directory
@@ -64,6 +68,8 @@ export declare class TestDiscovery {
64
68
  * Parse .v source file for test functions and parameters
65
69
  */
66
70
  private static parseVFile;
71
+ private static splitParamsAndExpectation;
72
+ private static parseTokenValue;
67
73
  /**
68
74
  * Compile a .v test source file
69
75
  */
@@ -6,7 +6,7 @@
6
6
  * extracts parameters from @test-params comments, and compiles source files.
7
7
  */
8
8
  import { readFile, readdir, stat } from 'fs/promises';
9
- import { join, basename } from 'path';
9
+ import { basename, join } from 'path';
10
10
  import { FiveSDK } from '../FiveSDK.js';
11
11
  /**
12
12
  * Discover tests from directory
@@ -121,80 +121,62 @@ export class TestDiscovery {
121
121
  const tests = [];
122
122
  try {
123
123
  const content = await readFile(file, 'utf8');
124
- // Find all function definitions with potential test annotations
125
- // Pattern 1: pub function with #[test] annotation
126
- // Pattern 2: function with specific naming convention (test_*, *_test)
127
124
  const lines = content.split('\n');
128
- let currentFunction = null;
129
- let currentParams = null;
130
- let currentDescription = null;
125
+ let pendingParams;
131
126
  for (let i = 0; i < lines.length; i++) {
132
127
  const line = lines[i].trim();
133
128
  // Check for @test-params comment
134
- const paramsMatch = line.match(/@test-params\s+(.*)/);
129
+ const paramsMatch = line.match(/@test-params(?:\s+(.*))?$/);
135
130
  if (paramsMatch) {
136
131
  try {
137
- const paramsStr = paramsMatch[1].trim();
138
- // Try to parse as JSON array first
139
- if (paramsStr.startsWith('[')) {
140
- currentParams = JSON.parse(paramsStr);
132
+ const paramsStr = (paramsMatch[1] || '').trim();
133
+ if (paramsStr.length === 0) {
134
+ pendingParams = [];
135
+ }
136
+ else if (paramsStr.startsWith('[')) {
137
+ const parsed = JSON.parse(paramsStr);
138
+ pendingParams = Array.isArray(parsed) ? parsed : [];
141
139
  }
142
140
  else {
143
- // Parse space-separated values
144
- currentParams = paramsStr.split(/\s+/).map(p => {
145
- // Try to parse as number
146
- if (!isNaN(Number(p))) {
147
- return Number(p);
148
- }
149
- return p;
150
- });
141
+ pendingParams = paramsStr
142
+ .split(/\s+/)
143
+ .filter(Boolean)
144
+ .map((token) => this.parseTokenValue(token));
151
145
  }
152
146
  }
153
147
  catch (error) {
154
148
  console.warn(`Failed to parse @test-params in ${file}:${i + 1}: ${line}`);
149
+ pendingParams = undefined;
155
150
  }
156
151
  continue;
157
152
  }
158
- // Check for test annotation
159
- if (line.startsWith('#[test]') || line.includes('#[test]')) {
160
- // Next non-empty line should be the function definition
161
- for (let j = i + 1; j < lines.length; j++) {
162
- const nextLine = lines[j].trim();
163
- if (nextLine && !nextLine.startsWith('//')) {
164
- const funcMatch = nextLine.match(/(?:pub\s+)?(?:fn|instruction|script)\s+(\w+)\s*\(/);
165
- if (funcMatch) {
166
- currentFunction = funcMatch[1];
167
- break;
168
- }
169
- }
170
- }
171
- continue;
172
- }
173
- // Check for pub function that matches test naming convention
174
- const funcMatch = line.match(/pub\s+(?:fn|instruction|script)\s+(test_\w+|_?\w+_test)\s*\(/);
153
+ // Match canonical DSL test function forms:
154
+ // pub test_name(...)
155
+ // pub fn test_name(...)
156
+ const funcMatch = line.match(/^pub\s+(?:fn\s+)?(test_[A-Za-z0-9_]*|[A-Za-z0-9_]*_test)\s*\([^)]*\)\s*(?:->\s*([A-Za-z0-9_<>\[\]]+))?/);
175
157
  if (funcMatch) {
176
158
  const functionName = funcMatch[1];
177
- // Check if we have parameters from @test-params comment
178
- if (currentParams || currentFunction) {
179
- const name = basename(file, '.v') + '::' + functionName;
180
- tests.push({
181
- name,
182
- path: file,
159
+ const returnType = funcMatch[2];
160
+ const hasReturnValue = !!returnType;
161
+ const [parameters, expectedResult, expectsResult] = this.splitParamsAndExpectation(pendingParams, hasReturnValue);
162
+ const name = `${basename(file, '.v')}::${functionName}`;
163
+ tests.push({
164
+ name,
165
+ path: file,
166
+ type: 'v-source',
167
+ source: {
183
168
  type: 'v-source',
184
- source: {
185
- type: 'v-source',
186
- file,
187
- functionName,
188
- parameters: currentParams || undefined,
189
- description: currentDescription || undefined
190
- },
191
- parameters: currentParams || undefined,
192
- description: currentDescription || undefined
193
- });
194
- currentParams = null;
195
- currentDescription = null;
196
- currentFunction = null;
197
- }
169
+ file,
170
+ functionName,
171
+ parameters: parameters.length > 0 ? parameters : undefined,
172
+ expectedResult,
173
+ expectsResult
174
+ },
175
+ parameters: parameters.length > 0 ? parameters : undefined,
176
+ expectedResult,
177
+ expectsResult
178
+ });
179
+ pendingParams = undefined;
198
180
  }
199
181
  }
200
182
  }
@@ -203,6 +185,29 @@ export class TestDiscovery {
203
185
  }
204
186
  return tests;
205
187
  }
188
+ static splitParamsAndExpectation(values, hasReturnValue) {
189
+ const parsed = Array.isArray(values) ? values : [];
190
+ if (!hasReturnValue || parsed.length === 0) {
191
+ return [parsed, undefined, false];
192
+ }
193
+ const params = parsed.slice(0, parsed.length - 1);
194
+ return [params, parsed[parsed.length - 1], true];
195
+ }
196
+ static parseTokenValue(token) {
197
+ if ((token.startsWith('"') && token.endsWith('"')) ||
198
+ (token.startsWith("'") && token.endsWith("'"))) {
199
+ return token.slice(1, -1);
200
+ }
201
+ if (token === 'true')
202
+ return true;
203
+ if (token === 'false')
204
+ return false;
205
+ const asNumber = Number(token);
206
+ if (!Number.isNaN(asNumber)) {
207
+ return asNumber;
208
+ }
209
+ return token;
210
+ }
206
211
  /**
207
212
  * Compile a .v test source file
208
213
  */