@secondlayer/cli 0.2.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- // @bun
1
+ import { createRequire } from "node:module";
2
2
  var __create = Object.create;
3
3
  var __getProtoOf = Object.getPrototypeOf;
4
4
  var __defProp = Object.defineProperty;
@@ -15,7 +15,7 @@ var __toESM = (mod, isNodeMode, target) => {
15
15
  });
16
16
  return to;
17
17
  };
18
- var __require = import.meta.require;
18
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
19
19
 
20
20
  // src/core/plugin-manager.ts
21
21
  import { promises as fs } from "fs";
@@ -252,15 +252,15 @@ ${JSON.stringify(content, null, 2)}`;
252
252
  }
253
253
  createLogger() {
254
254
  return {
255
- info: (message) => console.log(`\u2139\uFE0F ${message}`),
256
- warn: (message) => console.warn(`\u26A0\uFE0F ${message}`),
257
- error: (message) => console.error(`\u274C ${message}`),
255
+ info: (message) => console.log(`ℹ️ ${message}`),
256
+ warn: (message) => console.warn(`⚠️ ${message}`),
257
+ error: (message) => console.error(`❌ ${message}`),
258
258
  debug: (message) => {
259
259
  if (process.env.DEBUG) {
260
260
  console.log(`\uD83D\uDC1B ${message}`);
261
261
  }
262
262
  },
263
- success: (message) => console.log(`\u2705 ${message}`)
263
+ success: (message) => console.log(`✅ ${message}`)
264
264
  };
265
265
  }
266
266
  createUtils() {
@@ -311,15 +311,2594 @@ ${JSON.stringify(content, null, 2)}`;
311
311
  };
312
312
  }
313
313
  }
314
+ // src/plugins/clarinet/index.ts
315
+ import { initSimnet } from "@hirosystems/clarinet-sdk";
314
316
 
315
- // src/utils/config.ts
316
- function defineConfig(configOrDefiner) {
317
- return configOrDefiner;
317
+ // src/generators/contract.ts
318
+ import { format } from "prettier";
319
+ async function generateContractInterface(contracts) {
320
+ const imports = `import { Cl, validateStacksAddress } from '@stacks/transactions'`;
321
+ const header = `/**
322
+ * Generated by @secondlayer/cli
323
+ * DO NOT EDIT MANUALLY
324
+ */`;
325
+ const contractsCode = contracts.map((contract) => generateContract(contract)).join(`
326
+
327
+ `);
328
+ const code = `${imports}
329
+
330
+ ${header}
331
+
332
+ ${contractsCode}`;
333
+ const formatted = await format(code, {
334
+ parser: "typescript",
335
+ singleQuote: true,
336
+ semi: true,
337
+ printWidth: 100,
338
+ trailingComma: "es5"
339
+ });
340
+ return formatted;
341
+ }
342
+ function generateContract(contract) {
343
+ const { name, address, contractName, abi } = contract;
344
+ const abiCode = generateAbiConstant(name, abi);
345
+ const methods = abi.functions.filter((func) => func.access !== "private").map((func) => generateMethod(func, address, contractName)).join(`,
346
+
347
+ `);
348
+ const mapsObject = generateMapsObject(abi.maps || [], address, contractName);
349
+ const varsObject = generateVarsObject(abi.variables || [], address, contractName);
350
+ const constantsObject = generateConstantsObject(abi.variables || [], address, contractName);
351
+ const allMembers = [methods, mapsObject, varsObject, constantsObject].filter(Boolean);
352
+ const contractCode = `export const ${name} = {
353
+ address: '${address}',
354
+ contractAddress: '${address}',
355
+ contractName: '${contractName}',
356
+
357
+ ${allMembers.join(`,
358
+
359
+ `)}
360
+ } as const`;
361
+ return `${abiCode}
362
+
363
+ ${contractCode}`;
364
+ }
365
+ function generateAbiConstant(name, abi) {
366
+ const abiJson = JSON.stringify(abi, null, 2).replace(/"([a-zA-Z_$][a-zA-Z0-9_$]*)":/g, "$1:").replace(/"/g, "'");
367
+ return `export const ${name}Abi = ${abiJson} as const`;
368
+ }
369
+ function generateMethod(func, address, contractName) {
370
+ const methodName = toCamelCase(func.name);
371
+ if (func.args.length === 0) {
372
+ return `${methodName}() {
373
+ return {
374
+ contractAddress: '${address}',
375
+ contractName: '${contractName}',
376
+ functionName: '${func.name}',
377
+ functionArgs: []
378
+ }
379
+ }`;
380
+ }
381
+ if (func.args.length === 1) {
382
+ const originalArgName = func.args[0].name;
383
+ const argName = toCamelCase(originalArgName);
384
+ const argType = getTypeForArg(func.args[0]);
385
+ const clarityConversion = generateClarityConversion(argName, func.args[0]);
386
+ return `${methodName}(...args: [{ ${argName}: ${argType} }] | [${argType}]) {
387
+ const ${argName} = args.length === 1 && typeof args[0] === 'object' && args[0] !== null && '${argName}' in args[0]
388
+ ? args[0].${argName}
389
+ : args[0] as ${argType}
390
+
391
+ return {
392
+ contractAddress: '${address}',
393
+ contractName: '${contractName}',
394
+ functionName: '${func.name}',
395
+ functionArgs: [${clarityConversion}]
396
+ }
397
+ }`;
398
+ }
399
+ const argsList = func.args.map((arg) => toCamelCase(arg.name)).join(", ");
400
+ const argsTypes = func.args.map((arg) => {
401
+ const camelName = toCamelCase(arg.name);
402
+ return `${camelName}: ${getTypeForArg(arg)}`;
403
+ }).join("; ");
404
+ const argsArray = func.args.map((arg) => {
405
+ const argName = toCamelCase(arg.name);
406
+ return generateClarityConversion(argName, arg);
407
+ }).join(", ");
408
+ const objectAccess = func.args.map((arg) => {
409
+ const camelName = toCamelCase(arg.name);
410
+ return `args[0].${camelName}`;
411
+ }).join(", ");
412
+ const positionTypes = func.args.map((arg) => getTypeForArg(arg)).join(", ");
413
+ return `${methodName}(...args: [{ ${argsTypes} }] | [${positionTypes}]) {
414
+ const [${argsList}] = args.length === 1 && typeof args[0] === 'object' && args[0] !== null
415
+ ? [${objectAccess}]
416
+ : args as [${positionTypes}]
417
+
418
+ return {
419
+ contractAddress: '${address}',
420
+ contractName: '${contractName}',
421
+ functionName: '${func.name}',
422
+ functionArgs: [${argsArray}]
423
+ }
424
+ }`;
425
+ }
426
+ function getTypeForArg(arg) {
427
+ const type = arg.type;
428
+ if (typeof type === "string") {
429
+ switch (type) {
430
+ case "uint128":
431
+ case "int128":
432
+ return "bigint";
433
+ case "bool":
434
+ return "boolean";
435
+ case "principal":
436
+ case "trait_reference":
437
+ return "string";
438
+ default:
439
+ return "any";
440
+ }
441
+ }
442
+ if (type["string-ascii"] || type["string-utf8"]) {
443
+ return "string";
444
+ }
445
+ if (type.buff) {
446
+ return "Uint8Array | string | { type: 'ascii' | 'utf8' | 'hex'; value: string }";
447
+ }
448
+ if (type.optional) {
449
+ const innerType = getTypeForArg({ type: type.optional });
450
+ return `${innerType} | null`;
451
+ }
452
+ if (type.list) {
453
+ const innerType = getTypeForArg({ type: type.list.type });
454
+ return `${innerType}[]`;
455
+ }
456
+ if (type.tuple) {
457
+ const fields = type.tuple.map((field) => `${toCamelCase(field.name)}: ${getTypeForArg({ type: field.type })}`).join("; ");
458
+ return `{ ${fields} }`;
459
+ }
460
+ if (type.response) {
461
+ const okType = getTypeForArg({ type: type.response.ok });
462
+ const errType = getTypeForArg({ type: type.response.error });
463
+ return `{ ok: ${okType} } | { err: ${errType} }`;
464
+ }
465
+ return "any";
466
+ }
467
+ function toCamelCase(str) {
468
+ return str.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()).replace(/-([A-Z])/g, (_, letter) => letter).replace(/-(\d)/g, (_, digit) => digit).replace(/-/g, "");
469
+ }
470
+ function generateClarityConversion(argName, argType) {
471
+ const type = argType.type;
472
+ if (typeof type === "string") {
473
+ switch (type) {
474
+ case "uint128":
475
+ return `Cl.uint(${argName})`;
476
+ case "int128":
477
+ return `Cl.int(${argName})`;
478
+ case "bool":
479
+ return `Cl.bool(${argName})`;
480
+ case "principal":
481
+ case "trait_reference":
482
+ return `(() => {
483
+ const [address, contractName] = ${argName}.split(".") as [string, string];
484
+ if (!validateStacksAddress(address)) {
485
+ throw new Error("Invalid Stacks address format");
486
+ }
487
+ if (${argName}.includes(".")) {
488
+ return Cl.contractPrincipal(address, contractName);
489
+ } else {
490
+ return Cl.standardPrincipal(${argName});
491
+ }
492
+ })()`;
493
+ default:
494
+ return `${argName}`;
495
+ }
496
+ }
497
+ if (type["string-ascii"]) {
498
+ return `Cl.stringAscii(${argName})`;
499
+ }
500
+ if (type["string-utf8"]) {
501
+ return `Cl.stringUtf8(${argName})`;
502
+ }
503
+ if (type.buff) {
504
+ return `(() => {
505
+ const value = ${argName};
506
+ // Direct Uint8Array
507
+ if (value instanceof Uint8Array) {
508
+ return Cl.buffer(value);
509
+ }
510
+ // Object notation with explicit type
511
+ if (typeof value === 'object' && value !== null && value.type && value.value) {
512
+ switch (value.type) {
513
+ case 'ascii':
514
+ return Cl.bufferFromAscii(value.value);
515
+ case 'utf8':
516
+ return Cl.bufferFromUtf8(value.value);
517
+ case 'hex':
518
+ return Cl.bufferFromHex(value.value);
519
+ default:
520
+ throw new Error(\`Unsupported buffer type: \${value.type}\`);
521
+ }
522
+ }
523
+ // Auto-detect string type
524
+ if (typeof value === 'string') {
525
+ // Check for hex (0x prefix or pure hex pattern)
526
+ if (value.startsWith('0x') || /^[0-9a-fA-F]+$/.test(value)) {
527
+ return Cl.bufferFromHex(value);
528
+ }
529
+ // Check for non-ASCII characters (UTF-8)
530
+ if (!/^[\\x00-\\x7F]*$/.test(value)) {
531
+ return Cl.bufferFromUtf8(value);
532
+ }
533
+ // Default to ASCII for simple ASCII strings
534
+ return Cl.bufferFromAscii(value);
535
+ }
536
+ throw new Error(\`Invalid buffer value: \${value}\`);
537
+ })()`;
538
+ }
539
+ if (type.optional) {
540
+ const innerConversion = generateClarityConversion(argName, {
541
+ type: type.optional
542
+ });
543
+ return `${argName} !== null ? Cl.some(${innerConversion.replace(argName, `${argName}`)}) : Cl.none()`;
544
+ }
545
+ if (type.list) {
546
+ const innerConversion = generateClarityConversion("item", {
547
+ type: type.list.type
548
+ });
549
+ return `Cl.list(${argName}.map(item => ${innerConversion}))`;
550
+ }
551
+ if (type.tuple) {
552
+ const fields = type.tuple.map((field) => {
553
+ const camelFieldName = toCamelCase(field.name);
554
+ const fieldConversion = generateClarityConversion(`${argName}.${camelFieldName}`, { type: field.type });
555
+ return `"${field.name}": ${fieldConversion}`;
556
+ }).join(", ");
557
+ return `Cl.tuple({ ${fields} })`;
558
+ }
559
+ if (type.response) {
560
+ const okConversion = generateClarityConversion(`${argName}.ok`, {
561
+ type: type.response.ok
562
+ });
563
+ const errConversion = generateClarityConversion(`${argName}.err`, {
564
+ type: type.response.error
565
+ });
566
+ return `'ok' in ${argName} ? Cl.ok(${okConversion.replace(`${argName}.ok`, `${argName}.ok`)}) : Cl.error(${errConversion.replace(`${argName}.err`, `${argName}.err`)})`;
567
+ }
568
+ return `${argName}`;
569
+ }
570
+ function generateMapsObject(maps, address, contractName) {
571
+ if (!maps || maps.length === 0) {
572
+ return "";
573
+ }
574
+ const mapMethods = maps.map((map) => {
575
+ const methodName = toCamelCase(map.name);
576
+ const keyType = getTypeForArg({ type: map.key });
577
+ const valueType = getTypeForArg({ type: map.value });
578
+ const keyConversion = generateMapKeyConversion(map.key);
579
+ return `${methodName}: {
580
+ async get(key: ${keyType}, options?: { network?: 'mainnet' | 'testnet' | 'devnet' }): Promise<${valueType} | null> {
581
+ const { cvToJSON, serializeCV } = await import('@stacks/transactions');
582
+ const apiUrls: Record<string, string> = {
583
+ mainnet: 'https://api.hiro.so',
584
+ testnet: 'https://api.testnet.hiro.so',
585
+ devnet: 'http://localhost:3999'
586
+ };
587
+ const baseUrl = apiUrls[options?.network || 'mainnet'];
588
+ const mapKey = ${keyConversion};
589
+ const keyHex = serializeCV(mapKey).toString('hex');
590
+
591
+ const response = await fetch(
592
+ \`\${baseUrl}/v2/map_entry/${address}/${contractName}/${map.name}\`,
593
+ {
594
+ method: 'POST',
595
+ headers: { 'Content-Type': 'application/json' },
596
+ body: JSON.stringify(keyHex)
597
+ }
598
+ );
599
+
600
+ if (!response.ok) {
601
+ throw new Error(\`Failed to fetch map entry: \${response.statusText}\`);
602
+ }
603
+
604
+ const result = await response.json();
605
+ if (!result.data || result.data === '0x09') {
606
+ return null; // none value
607
+ }
608
+
609
+ const { deserializeCV } = await import('@stacks/transactions');
610
+ const cv = deserializeCV(result.data);
611
+ const parsed = cvToJSON(cv);
612
+ // Unwrap the (some ...) wrapper
613
+ return parsed.value?.value ?? parsed.value ?? null;
614
+ },
615
+ keyType: ${JSON.stringify(map.key)} as const,
616
+ valueType: ${JSON.stringify(map.value)} as const
617
+ }`;
618
+ });
619
+ return `maps: {
620
+ ${mapMethods.join(`,
621
+
622
+ `)}
623
+ }`;
624
+ }
625
+ function generateVarsObject(variables, address, contractName) {
626
+ if (!variables || variables.length === 0) {
627
+ return "";
628
+ }
629
+ const dataVars = variables.filter((v) => v.access === "variable");
630
+ if (dataVars.length === 0) {
631
+ return "";
632
+ }
633
+ const varMethods = dataVars.map((variable) => {
634
+ const methodName = toCamelCase(variable.name);
635
+ const valueType = getTypeForArg({ type: variable.type });
636
+ return `${methodName}: {
637
+ async get(options?: { network?: 'mainnet' | 'testnet' | 'devnet' }): Promise<${valueType}> {
638
+ const { cvToJSON, deserializeCV } = await import('@stacks/transactions');
639
+ const apiUrls: Record<string, string> = {
640
+ mainnet: 'https://api.hiro.so',
641
+ testnet: 'https://api.testnet.hiro.so',
642
+ devnet: 'http://localhost:3999'
643
+ };
644
+ const baseUrl = apiUrls[options?.network || 'mainnet'];
645
+
646
+ const response = await fetch(
647
+ \`\${baseUrl}/v2/data_var/${address}/${contractName}/${variable.name}?proof=0\`
648
+ );
649
+
650
+ if (!response.ok) {
651
+ throw new Error(\`Failed to fetch data var: \${response.statusText}\`);
652
+ }
653
+
654
+ const result = await response.json();
655
+ const cv = deserializeCV(result.data);
656
+ const parsed = cvToJSON(cv);
657
+ return parsed.value ?? parsed;
658
+ },
659
+ type: ${JSON.stringify(variable.type)} as const
660
+ }`;
661
+ });
662
+ return `vars: {
663
+ ${varMethods.join(`,
664
+
665
+ `)}
666
+ }`;
667
+ }
668
+ function generateConstantsObject(variables, address, contractName) {
669
+ if (!variables || variables.length === 0) {
670
+ return "";
671
+ }
672
+ const constants = variables.filter((v) => v.access === "constant");
673
+ if (constants.length === 0) {
674
+ return "";
675
+ }
676
+ const constMethods = constants.map((constant) => {
677
+ const methodName = toCamelCase(constant.name);
678
+ const valueType = getTypeForArg({ type: constant.type });
679
+ return `${methodName}: {
680
+ async get(options?: { network?: 'mainnet' | 'testnet' | 'devnet' }): Promise<${valueType}> {
681
+ const { cvToJSON, deserializeCV } = await import('@stacks/transactions');
682
+ const apiUrls: Record<string, string> = {
683
+ mainnet: 'https://api.hiro.so',
684
+ testnet: 'https://api.testnet.hiro.so',
685
+ devnet: 'http://localhost:3999'
686
+ };
687
+ const baseUrl = apiUrls[options?.network || 'mainnet'];
688
+
689
+ const response = await fetch(
690
+ \`\${baseUrl}/v2/constant_val/${address}/${contractName}/${constant.name}?proof=0\`
691
+ );
692
+
693
+ if (!response.ok) {
694
+ throw new Error(\`Failed to fetch constant: \${response.statusText}\`);
695
+ }
696
+
697
+ const result = await response.json();
698
+ const cv = deserializeCV(result.data);
699
+ const parsed = cvToJSON(cv);
700
+ return parsed.value ?? parsed;
701
+ },
702
+ type: ${JSON.stringify(constant.type)} as const
703
+ }`;
704
+ });
705
+ return `constants: {
706
+ ${constMethods.join(`,
707
+
708
+ `)}
709
+ }`;
710
+ }
711
+ function generateMapKeyConversion(keyType) {
712
+ if (keyType.tuple) {
713
+ const fields = keyType.tuple.map((field) => {
714
+ const camelFieldName = toCamelCase(field.name);
715
+ const fieldConversion = generateClarityConversion(`key.${camelFieldName}`, { type: field.type });
716
+ return `"${field.name}": ${fieldConversion}`;
717
+ }).join(", ");
718
+ return `Cl.tuple({ ${fields} })`;
719
+ }
720
+ return generateClarityConversion("key", { type: keyType });
721
+ }
722
+
723
+ // src/plugins/clarinet/index.ts
724
+ function toCamelCase2(str) {
725
+ return str.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()).replace(/-([A-Z])/g, (_, letter) => letter).replace(/-(\d)/g, (_, digit) => digit).replace(/-/g, "").replace(/^\d/, "_$&");
726
+ }
727
+ function sanitizeContractName(name) {
728
+ return toCamelCase2(name);
729
+ }
730
+ async function isUserDefinedContract(contractId, manifestPath) {
731
+ const [address, contractName] = contractId.split(".");
732
+ try {
733
+ const { promises: fs2 } = await import("fs");
734
+ const tomlContent = await fs2.readFile(manifestPath, "utf-8");
735
+ const contractSectionRegex = /^\[contracts\.([^\]]+)\]/gm;
736
+ const userContracts = new Set;
737
+ let match;
738
+ while ((match = contractSectionRegex.exec(tomlContent)) !== null) {
739
+ userContracts.add(match[1]);
740
+ }
741
+ if (userContracts.has(contractName)) {
742
+ return true;
743
+ }
744
+ } catch (error) {}
745
+ const systemContractPatterns = [
746
+ /^pox-\d+$/,
747
+ /^bns$/,
748
+ /^costs-\d+$/,
749
+ /^lockup$/
750
+ ];
751
+ if (systemContractPatterns.some((pattern) => pattern.test(contractName))) {
752
+ return false;
753
+ }
754
+ const systemAddresses = [
755
+ "SP000000000000000000002Q6VF78",
756
+ "ST000000000000000000002AMW42H"
757
+ ];
758
+ if (systemAddresses.includes(address)) {
759
+ return false;
760
+ }
761
+ return true;
762
+ }
763
+ var clarinet = (options = {}) => {
764
+ const manifestPath = options.path || "./Clarinet.toml";
765
+ let simnet;
766
+ return {
767
+ name: "@secondlayer/cli/plugin-clarinet",
768
+ version: "1.0.0",
769
+ async transformConfig(config) {
770
+ try {
771
+ simnet = await initSimnet(manifestPath);
772
+ const contractInterfaces = simnet.getContractsInterfaces();
773
+ const contracts = [];
774
+ for (const [contractId, abi] of contractInterfaces) {
775
+ const [_, contractName] = contractId.split(".");
776
+ if (!await isUserDefinedContract(contractId, manifestPath)) {
777
+ if (options.debug) {
778
+ console.log(`\uD83D\uDEAB Skipping system contract: ${contractId}`);
779
+ }
780
+ continue;
781
+ }
782
+ if (options.include && !options.include.includes(contractName)) {
783
+ continue;
784
+ }
785
+ if (options.exclude && options.exclude.includes(contractName)) {
786
+ continue;
787
+ }
788
+ const sanitizedName = sanitizeContractName(contractName);
789
+ contracts.push({
790
+ name: sanitizedName,
791
+ address: contractId,
792
+ abi,
793
+ _clarinetSource: true
794
+ });
795
+ }
796
+ if (options.debug) {
797
+ console.log(`\uD83D\uDD0D Clarinet plugin found ${contracts.length} user-defined contracts`);
798
+ }
799
+ return {
800
+ ...config,
801
+ contracts: [...config.contracts || [], ...contracts]
802
+ };
803
+ } catch (error) {
804
+ const err = error;
805
+ if (options.debug) {
806
+ console.warn(`⚠️ Clarinet plugin failed to load contracts: ${err.message}`);
807
+ }
808
+ return config;
809
+ }
810
+ },
811
+ async generate(context) {
812
+ const clarinetContracts = context.contracts.filter((contract) => contract.metadata?.source === "clarinet");
813
+ if (clarinetContracts.length === 0) {
814
+ return;
815
+ }
816
+ if (options.debug) {
817
+ context.logger.debug(`Generating interfaces for ${clarinetContracts.length} Clarinet contracts`);
818
+ }
819
+ const contractsCode = await generateContractInterface(clarinetContracts);
820
+ context.addOutput("contracts", {
821
+ path: context.config.out,
822
+ content: contractsCode,
823
+ type: "contracts"
824
+ });
825
+ }
826
+ };
827
+ };
828
+ async function hasClarinetProject(path2 = "./Clarinet.toml") {
829
+ try {
830
+ const { promises: fs2 } = await import("fs");
831
+ await fs2.access(path2);
832
+ return true;
833
+ } catch {
834
+ return false;
835
+ }
836
+ }
837
+ // src/plugins/actions/generators.ts
838
+ function toCamelCase3(str) {
839
+ return str.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()).replace(/-([A-Z])/g, (_, letter) => letter).replace(/-(\d)/g, (_, digit) => digit).replace(/-/g, "").replace(/^\d/, "_$&");
840
+ }
841
+ function getTypeForArg2(arg) {
842
+ const type = arg.type;
843
+ if (typeof type === "string") {
844
+ switch (type) {
845
+ case "uint128":
846
+ case "int128":
847
+ return "bigint";
848
+ case "bool":
849
+ return "boolean";
850
+ case "principal":
851
+ case "trait_reference":
852
+ return "string";
853
+ default:
854
+ return "any";
855
+ }
856
+ }
857
+ if (type["string-ascii"] || type["string-utf8"]) {
858
+ return "string";
859
+ }
860
+ if (type.buff) {
861
+ return "Uint8Array | string | { type: 'ascii' | 'utf8' | 'hex'; value: string }";
862
+ }
863
+ if (type.optional) {
864
+ const innerType = getTypeForArg2({ type: type.optional });
865
+ return `${innerType} | null`;
866
+ }
867
+ if (type.list) {
868
+ const innerType = getTypeForArg2({ type: type.list.type });
869
+ return `${innerType}[]`;
870
+ }
871
+ if (type.tuple) {
872
+ const fields = type.tuple.map((field) => `${toCamelCase3(field.name)}: ${getTypeForArg2({ type: field.type })}`).join("; ");
873
+ return `{ ${fields} }`;
874
+ }
875
+ if (type.response) {
876
+ const okType = getTypeForArg2({ type: type.response.ok });
877
+ const errType = getTypeForArg2({ type: type.response.error });
878
+ return `{ ok: ${okType} } | { err: ${errType} }`;
879
+ }
880
+ return "any";
881
+ }
882
+ function generateArgsSignature(args) {
883
+ if (args.length === 0)
884
+ return "";
885
+ const argsTypes = args.map((arg) => {
886
+ const camelName = toCamelCase3(arg.name);
887
+ return `${camelName}: ${getTypeForArg2(arg)}`;
888
+ }).join("; ");
889
+ return `args: { ${argsTypes} }, `;
890
+ }
891
+ function generateClarityArgs(args, _contractName) {
892
+ if (args.length === 0)
893
+ return "";
894
+ return args.map((arg) => {
895
+ const argName = `args.${toCamelCase3(arg.name)}`;
896
+ return generateClarityConversion2(argName, arg);
897
+ }).join(", ");
898
+ }
899
+ function generateClarityConversion2(argName, argType) {
900
+ const type = argType.type;
901
+ if (typeof type === "string") {
902
+ switch (type) {
903
+ case "uint128":
904
+ return `Cl.uint(${argName})`;
905
+ case "int128":
906
+ return `Cl.int(${argName})`;
907
+ case "bool":
908
+ return `Cl.bool(${argName})`;
909
+ case "principal":
910
+ case "trait_reference":
911
+ return `(() => {
912
+ const [address, contractName] = ${argName}.split(".") as [string, string];
913
+ if (!validateStacksAddress(address)) {
914
+ throw new Error("Invalid Stacks address format");
915
+ }
916
+ if (${argName}.includes(".")) {
917
+ return Cl.contractPrincipal(address, contractName);
918
+ } else {
919
+ return Cl.standardPrincipal(${argName});
920
+ }
921
+ })()`;
922
+ default:
923
+ return `${argName}`;
924
+ }
925
+ }
926
+ if (type["string-ascii"]) {
927
+ return `Cl.stringAscii(${argName})`;
928
+ }
929
+ if (type["string-utf8"]) {
930
+ return `Cl.stringUtf8(${argName})`;
931
+ }
932
+ if (type.buff) {
933
+ return `(() => {
934
+ const value = ${argName};
935
+ if (value instanceof Uint8Array) {
936
+ return Cl.buffer(value);
937
+ }
938
+ if (typeof value === 'object' && value !== null && value.type && value.value) {
939
+ switch (value.type) {
940
+ case 'ascii':
941
+ return Cl.bufferFromAscii(value.value);
942
+ case 'utf8':
943
+ return Cl.bufferFromUtf8(value.value);
944
+ case 'hex':
945
+ return Cl.bufferFromHex(value.value);
946
+ default:
947
+ throw new Error(\`Unsupported buffer type: \${value.type}\`);
948
+ }
949
+ }
950
+ if (typeof value === 'string') {
951
+ if (value.startsWith('0x') || /^[0-9a-fA-F]+$/.test(value)) {
952
+ return Cl.bufferFromHex(value);
953
+ }
954
+ if (!/^[\\x00-\\x7F]*$/.test(value)) {
955
+ return Cl.bufferFromUtf8(value);
956
+ }
957
+ return Cl.bufferFromAscii(value);
958
+ }
959
+ throw new Error(\`Invalid buffer value: \${value}\`);
960
+ })()`;
961
+ }
962
+ if (type.optional) {
963
+ const innerConversion = generateClarityConversion2(argName, {
964
+ type: type.optional
965
+ });
966
+ return `${argName} !== null ? Cl.some(${innerConversion.replace(argName, `${argName}`)}) : Cl.none()`;
967
+ }
968
+ if (type.list) {
969
+ const innerConversion = generateClarityConversion2("item", {
970
+ type: type.list.type
971
+ });
972
+ return `Cl.list(${argName}.map(item => ${innerConversion}))`;
973
+ }
974
+ if (type.tuple) {
975
+ const fields = type.tuple.map((field) => {
976
+ const camelFieldName = toCamelCase3(field.name);
977
+ const fieldConversion = generateClarityConversion2(`${argName}.${camelFieldName}`, { type: field.type });
978
+ return `"${field.name}": ${fieldConversion}`;
979
+ }).join(", ");
980
+ return `Cl.tuple({ ${fields} })`;
981
+ }
982
+ if (type.response) {
983
+ const okConversion = generateClarityConversion2(`${argName}.ok`, {
984
+ type: type.response.ok
985
+ });
986
+ const errConversion = generateClarityConversion2(`${argName}.err`, {
987
+ type: type.response.error
988
+ });
989
+ return `'ok' in ${argName} ? Cl.ok(${okConversion.replace(`${argName}.ok`, `${argName}.ok`)}) : Cl.error(${errConversion.replace(`${argName}.err`, `${argName}.err`)})`;
990
+ }
991
+ return `${argName}`;
992
+ }
993
+ function generateReadHelpers(contract, options) {
994
+ const { abi, name } = contract;
995
+ const functions = abi.functions || [];
996
+ const readOnlyFunctions = functions.filter((f) => f.access === "read_only" || f.access === "read-only");
997
+ if (readOnlyFunctions.length === 0) {
998
+ return "";
999
+ }
1000
+ const filteredFunctions = readOnlyFunctions.filter((func) => {
1001
+ if (options.includeFunctions && !options.includeFunctions.includes(func.name)) {
1002
+ return false;
1003
+ }
1004
+ if (options.excludeFunctions && options.excludeFunctions.includes(func.name)) {
1005
+ return false;
1006
+ }
1007
+ return true;
1008
+ });
1009
+ if (filteredFunctions.length === 0) {
1010
+ return "";
1011
+ }
1012
+ const helpers = filteredFunctions.map((func) => {
1013
+ const methodName = toCamelCase3(func.name);
1014
+ const argsSignature = generateArgsSignature(func.args);
1015
+ const clarityArgs = generateClarityArgs(func.args, name);
1016
+ return `async ${methodName}(${argsSignature}options?: {
1017
+ network?: 'mainnet' | 'testnet' | 'devnet';
1018
+ senderAddress?: string;
1019
+ }) {
1020
+ return await fetchCallReadOnlyFunction({
1021
+ contractAddress: '${contract.address}',
1022
+ contractName: '${contract.contractName}',
1023
+ functionName: '${func.name}',
1024
+ functionArgs: [${clarityArgs}],
1025
+ network: options?.network || 'mainnet',
1026
+ senderAddress: options?.senderAddress || 'SP000000000000000000002Q6VF78'
1027
+ });
1028
+ }`;
1029
+ });
1030
+ return `read: {
1031
+ ${helpers.join(`,
1032
+
1033
+ `)}
1034
+ }`;
1035
+ }
1036
+ function generateWriteHelpers(contract, options) {
1037
+ const { abi, name } = contract;
1038
+ const functions = abi.functions || [];
1039
+ const publicFunctions = functions.filter((f) => f.access === "public");
1040
+ if (publicFunctions.length === 0) {
1041
+ return "";
1042
+ }
1043
+ const filteredFunctions = publicFunctions.filter((func) => {
1044
+ if (options.includeFunctions && !options.includeFunctions.includes(func.name)) {
1045
+ return false;
1046
+ }
1047
+ if (options.excludeFunctions && options.excludeFunctions.includes(func.name)) {
1048
+ return false;
1049
+ }
1050
+ return true;
1051
+ });
1052
+ if (filteredFunctions.length === 0) {
1053
+ return "";
1054
+ }
1055
+ const helpers = filteredFunctions.map((func) => {
1056
+ const methodName = toCamelCase3(func.name);
1057
+ const argsSignature = generateArgsSignature(func.args);
1058
+ const clarityArgs = generateClarityArgs(func.args, name);
1059
+ return `async ${methodName}(${argsSignature}options: {
1060
+ senderKey: string;
1061
+ network?: 'mainnet' | 'testnet' | 'devnet';
1062
+ fee?: string | number | undefined;
1063
+ nonce?: bigint;
1064
+ anchorMode?: 1 | 2 | 3; // AnchorMode: OnChainOnly = 1, OffChainOnly = 2, Any = 3
1065
+ postConditions?: PostCondition[];
1066
+ validateWithAbi?: boolean;
1067
+ }) {
1068
+ const { senderKey, network = 'mainnet', ...txOptions } = options;
1069
+
1070
+ return await makeContractCall({
1071
+ contractAddress: '${contract.address}',
1072
+ contractName: '${contract.contractName}',
1073
+ functionName: '${func.name}',
1074
+ functionArgs: [${clarityArgs}],
1075
+ senderKey,
1076
+ network,
1077
+ validateWithAbi: true,
1078
+ ...txOptions
1079
+ });
1080
+ }`;
1081
+ });
1082
+ return `write: {
1083
+ ${helpers.join(`,
1084
+
1085
+ `)}
1086
+ }`;
1087
+ }
1088
+ async function generateActionHelpers(contract, options) {
1089
+ const readHelpers = generateReadHelpers(contract, options);
1090
+ const writeHelpers = generateWriteHelpers(contract, options);
1091
+ if (!readHelpers && !writeHelpers) {
1092
+ return "";
1093
+ }
1094
+ const helpers = [readHelpers, writeHelpers].filter(Boolean);
1095
+ return helpers.join(`,
1096
+
1097
+ `);
1098
+ }
1099
+
1100
+ // src/plugins/actions/index.ts
1101
+ var actions = (options = {}) => {
1102
+ return {
1103
+ name: "@secondlayer/cli/plugin-actions",
1104
+ version: "1.0.0",
1105
+ async generate(context) {
1106
+ const { contracts } = context;
1107
+ const filteredContracts = contracts.filter((contract) => {
1108
+ if (options.include && !options.include.includes(contract.name)) {
1109
+ return false;
1110
+ }
1111
+ if (options.exclude && options.exclude.includes(contract.name)) {
1112
+ return false;
1113
+ }
1114
+ return true;
1115
+ });
1116
+ if (filteredContracts.length === 0) {
1117
+ if (options.debug) {
1118
+ context.logger.debug("Actions plugin: No contracts to process");
1119
+ }
1120
+ return;
1121
+ }
1122
+ if (options.debug) {
1123
+ context.logger.debug(`Actions plugin: Generating read/write helpers for ${filteredContracts.length} contracts`);
1124
+ }
1125
+ const contractHelpers = new Map;
1126
+ for (const contract of filteredContracts) {
1127
+ const actionsCode = await generateActionHelpers(contract, options);
1128
+ if (actionsCode) {
1129
+ contractHelpers.set(contract.name, actionsCode);
1130
+ }
1131
+ }
1132
+ if (contractHelpers.size > 0) {
1133
+ const existingOutput = context.outputs.get("contracts");
1134
+ if (existingOutput) {
1135
+ let modifiedContent = addRequiredImports(existingOutput.content);
1136
+ for (const [contractName, helpersCode] of contractHelpers) {
1137
+ modifiedContent = injectHelpersIntoContract(modifiedContent, contractName, helpersCode);
1138
+ }
1139
+ context.outputs.set("contracts", {
1140
+ ...existingOutput,
1141
+ content: modifiedContent
1142
+ });
1143
+ }
1144
+ }
1145
+ }
1146
+ };
1147
+ };
1148
+ function addRequiredImports(content) {
1149
+ const transactionsImportRegex = /import\s+\{([^}]+)\}\s+from\s+['"]@stacks\/transactions['"];/;
1150
+ const match = content.match(transactionsImportRegex);
1151
+ if (match) {
1152
+ const existingImports = match[1].trim();
1153
+ const requiredImports = [
1154
+ "fetchCallReadOnlyFunction",
1155
+ "makeContractCall"
1156
+ ];
1157
+ const newImports = requiredImports.filter((imp) => !existingImports.includes(imp)).join(", ");
1158
+ if (newImports) {
1159
+ const updatedImport = `import { ${existingImports}, ${newImports} } from '@stacks/transactions';`;
1160
+ let updatedContent = content.replace(transactionsImportRegex, updatedImport);
1161
+ if (!updatedContent.includes("type PostCondition")) {
1162
+ updatedContent = updatedContent.replace(updatedImport, `${updatedImport}
1163
+ import type { PostCondition } from '@stacks/transactions';`);
1164
+ }
1165
+ return updatedContent;
1166
+ }
1167
+ if (!content.includes("type PostCondition")) {
1168
+ return content.replace(transactionsImportRegex, `${match[0]}
1169
+ import type { PostCondition } from '@stacks/transactions';`);
1170
+ }
1171
+ }
1172
+ return content;
1173
+ }
1174
+ function injectHelpersIntoContract(content, contractName, helpersCode) {
1175
+ const contractPattern = new RegExp(`(export const ${contractName} = \\{[\\s\\S]*?)\\n\\} as const;`, "g");
1176
+ return content.replace(contractPattern, (_, contractBody) => {
1177
+ const cleanBody = contractBody.replace(/,\s*$/, "");
1178
+ const indentedHelpersCode = helpersCode.split(`
1179
+ `).map((line) => {
1180
+ if (line.match(/^[a-zA-Z_$][a-zA-Z0-9_$]*\s*:/)) {
1181
+ return ` ${line}`;
1182
+ }
1183
+ return line;
1184
+ }).join(`
1185
+ `);
1186
+ return `${cleanBody},
1187
+
1188
+ ${indentedHelpersCode}
1189
+ } as const;`;
1190
+ });
1191
+ }
1192
+ // src/plugins/react/provider/index.ts
1193
+ import { format as format2 } from "prettier";
1194
+ async function generateProvider() {
1195
+ const code = `/**
1196
+ * Generated Stacks React Provider
1197
+ * DO NOT EDIT MANUALLY
1198
+ */
1199
+
1200
+ import React, { createContext, useContext } from 'react'
1201
+
1202
+ /**
1203
+ * Stacks configuration interface
1204
+ */
1205
+ export interface StacksReactConfig {
1206
+ /**
1207
+ * Network to use for API calls
1208
+ */
1209
+ network: 'mainnet' | 'testnet' | 'devnet'
1210
+
1211
+ /**
1212
+ * API key for Stacks API (optional)
1213
+ */
1214
+ apiKey?: string
1215
+
1216
+ /**
1217
+ * Base URL for Stacks API (optional override)
1218
+ */
1219
+ apiUrl?: string
1220
+
1221
+ /**
1222
+ * Default sender address for read-only calls
1223
+ */
1224
+ senderAddress?: string
1225
+ }
1226
+
1227
+ /**
1228
+ * Provider component props
1229
+ */
1230
+ export interface StacksProviderProps {
1231
+ children: React.ReactNode
1232
+ config: StacksReactConfig
1233
+ }
1234
+
1235
+ /**
1236
+ * React context for Stacks configuration
1237
+ */
1238
+ const StacksContext = createContext<StacksReactConfig | undefined>(undefined)
1239
+ StacksContext.displayName = 'StacksContext'
1240
+
1241
+ /**
1242
+ * Create a Stacks React configuration with defaults
1243
+ */
1244
+ export function createStacksConfig(config: StacksReactConfig): StacksReactConfig {
1245
+ return {
1246
+ network: config.network,
1247
+ apiKey: config.apiKey,
1248
+ apiUrl: config.apiUrl,
1249
+ senderAddress: config.senderAddress || 'SP000000000000000000002Q6VF78'
1250
+ }
1251
+ }
1252
+
1253
+ /**
1254
+ * Provider component that makes Stacks configuration available to hooks
1255
+ */
1256
+ export function StacksProvider({ children, config }: StacksProviderProps) {
1257
+ const resolvedConfig = createStacksConfig(config)
1258
+
1259
+ return (
1260
+ <StacksContext.Provider value={resolvedConfig}>
1261
+ {children}
1262
+ </StacksContext.Provider>
1263
+ )
1264
+ }
1265
+
1266
+ /**
1267
+ * Hook to access the Stacks configuration
1268
+ */
1269
+ export function useStacksConfig(): StacksReactConfig {
1270
+ const context = useContext(StacksContext)
1271
+
1272
+ if (context === undefined) {
1273
+ throw new Error(
1274
+ 'useStacksConfig must be used within a StacksProvider. ' +
1275
+ 'Make sure to wrap your app with <StacksProvider config={{...}}>'
1276
+ )
1277
+ }
1278
+
1279
+ return context
1280
+ }`;
1281
+ const formatted = await format2(code, {
1282
+ parser: "typescript",
1283
+ singleQuote: true,
1284
+ semi: false,
1285
+ printWidth: 100,
1286
+ trailingComma: "es5"
1287
+ });
1288
+ return formatted;
1289
+ }
1290
+
1291
+ // src/plugins/react/generators/generic.ts
1292
+ import { format as format3 } from "prettier";
1293
+ var GENERIC_HOOKS = [
1294
+ "useAccount",
1295
+ "useConnect",
1296
+ "useDisconnect",
1297
+ "useNetwork",
1298
+ "useContract",
1299
+ "useOpenSTXTransfer",
1300
+ "useSignMessage",
1301
+ "useDeployContract",
1302
+ "useReadContract",
1303
+ "useTransaction",
1304
+ "useBlock",
1305
+ "useAccountTransactions",
1306
+ "useWaitForTransaction"
1307
+ ];
1308
+ async function generateGenericHooks(excludeList = []) {
1309
+ const hooksToGenerate = GENERIC_HOOKS.filter((hookName) => !excludeList.includes(hookName));
1310
+ const imports = `import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
1311
+ import { useState, useCallback } from 'react'
1312
+ import { useStacksConfig } from './provider'
1313
+ import { connect, disconnect, isConnected, request, openContractCall as stacksOpenContractCall } from '@stacks/connect'
1314
+ import { Cl, validateStacksAddress } from '@stacks/transactions'
1315
+ import type { PostCondition } from '@stacks/transactions'
1316
+ import type { ExtractFunctionArgs, ExtractFunctionNames, ClarityContract } from '@secondlayer/clarity-types'`;
1317
+ const header = `/**
1318
+ * Generated generic Stacks React hooks
1319
+ * DO NOT EDIT MANUALLY
1320
+ */`;
1321
+ const hooksCode = hooksToGenerate.map((hookName) => generateGenericHook(hookName)).filter(Boolean).join(`
1322
+
1323
+ `);
1324
+ const code = `${imports}
1325
+
1326
+ ${header}
1327
+
1328
+ ${hooksCode}`;
1329
+ const formatted = await format3(code, {
1330
+ parser: "typescript",
1331
+ singleQuote: true,
1332
+ semi: false,
1333
+ printWidth: 100,
1334
+ trailingComma: "es5"
1335
+ });
1336
+ return formatted;
1337
+ }
1338
+ function generateGenericHook(hookName) {
1339
+ switch (hookName) {
1340
+ case "useAccount":
1341
+ return `export function useAccount() {
1342
+ const config = useStacksConfig()
1343
+
1344
+ return useQuery({
1345
+ queryKey: ['stacks-account', config.network],
1346
+ queryFn: async () => {
1347
+ try {
1348
+ // Check if already connected using @stacks/connect v8
1349
+ const connected = isConnected()
1350
+
1351
+ if (!connected) {
1352
+ return {
1353
+ address: undefined,
1354
+ addresses: undefined,
1355
+ isConnected: false,
1356
+ isConnecting: false,
1357
+ isDisconnected: true,
1358
+ status: 'disconnected' as const
1359
+ }
1360
+ }
1361
+
1362
+ // Get addresses using @stacks/connect v8 request method (SIP-030)
1363
+ const result = await request('stx_getAddresses')
1364
+
1365
+ if (!result || !result.addresses || result.addresses.length === 0) {
1366
+ return {
1367
+ address: undefined,
1368
+ addresses: undefined,
1369
+ isConnected: false,
1370
+ isConnecting: false,
1371
+ isDisconnected: true,
1372
+ status: 'disconnected' as const
1373
+ }
1374
+ }
1375
+
1376
+ // Extract STX addresses from the response
1377
+ const stxAddresses = result.addresses
1378
+ .filter((addr: any) => addr.address.startsWith('SP') || addr.address.startsWith('ST'))
1379
+ .map((addr: any) => addr.address)
1380
+
1381
+ return {
1382
+ address: stxAddresses[0] || undefined,
1383
+ addresses: stxAddresses,
1384
+ isConnected: true,
1385
+ isConnecting: false,
1386
+ isDisconnected: false,
1387
+ status: 'connected' as const
1388
+ }
1389
+ } catch (error) {
1390
+ // Handle case where wallet is not available or user rejected
1391
+ return {
1392
+ address: undefined,
1393
+ addresses: undefined,
1394
+ isConnected: false,
1395
+ isConnecting: false,
1396
+ isDisconnected: true,
1397
+ status: 'disconnected' as const
1398
+ }
1399
+ }
1400
+ },
1401
+ refetchOnWindowFocus: false,
1402
+ retry: false,
1403
+ staleTime: 1000 * 60 * 5, // 5 minutes
1404
+ refetchInterval: 1000 * 30, // Refetch every 30 seconds to detect wallet changes
1405
+ })
1406
+ }`;
1407
+ case "useConnect":
1408
+ return `export function useConnect() {
1409
+ const queryClient = useQueryClient()
1410
+
1411
+ const mutation = useMutation({
1412
+ mutationFn: async (options: { forceWalletSelect?: boolean } = {}) => {
1413
+ // Use @stacks/connect v8 connect method
1414
+ return await connect(options)
1415
+ },
1416
+ onSuccess: () => {
1417
+ // Invalidate account queries to refetch connection state
1418
+ queryClient.invalidateQueries({ queryKey: ['stacks-account'] })
1419
+ },
1420
+ onError: (error) => {
1421
+ console.error('Connection failed:', error)
1422
+ }
1423
+ })
1424
+
1425
+ return {
1426
+ // Custom connect function that works without arguments
1427
+ connect: (options?: { forceWalletSelect?: boolean }) => {
1428
+ return mutation.mutate(options || {})
1429
+ },
1430
+ connectAsync: async (options?: { forceWalletSelect?: boolean }) => {
1431
+ return mutation.mutateAsync(options || {})
1432
+ },
1433
+ // Expose all the mutation state
1434
+ isPending: mutation.isPending,
1435
+ isError: mutation.isError,
1436
+ isSuccess: mutation.isSuccess,
1437
+ error: mutation.error,
1438
+ data: mutation.data,
1439
+ reset: mutation.reset,
1440
+ // Keep the original mutate/mutateAsync for advanced users
1441
+ mutate: mutation.mutate,
1442
+ mutateAsync: mutation.mutateAsync
1443
+ }
1444
+ }`;
1445
+ case "useDisconnect":
1446
+ return `export function useDisconnect() {
1447
+ const queryClient = useQueryClient()
1448
+
1449
+ const mutation = useMutation({
1450
+ mutationFn: async () => {
1451
+ // Use @stacks/connect v8 disconnect method
1452
+ return await disconnect()
1453
+ },
1454
+ onSuccess: () => {
1455
+ // Clear all cached data on disconnect
1456
+ queryClient.clear()
1457
+ },
1458
+ onError: (error) => {
1459
+ console.error('Disconnect failed:', error)
1460
+ }
1461
+ })
1462
+
1463
+ return {
1464
+ // Custom disconnect function
1465
+ disconnect: () => {
1466
+ return mutation.mutate()
1467
+ },
1468
+ disconnectAsync: async () => {
1469
+ return mutation.mutateAsync()
1470
+ },
1471
+ // Expose all the mutation state
1472
+ isPending: mutation.isPending,
1473
+ isError: mutation.isError,
1474
+ isSuccess: mutation.isSuccess,
1475
+ error: mutation.error,
1476
+ data: mutation.data,
1477
+ reset: mutation.reset,
1478
+ // Keep the original mutate/mutateAsync for advanced users
1479
+ mutate: mutation.mutate,
1480
+ mutateAsync: mutation.mutateAsync
1481
+ }
1482
+ }`;
1483
+ case "useNetwork":
1484
+ return `export function useNetwork() {
1485
+ const config = useStacksConfig()
1486
+
1487
+ return useQuery({
1488
+ queryKey: ['stacks-network', config.network],
1489
+ queryFn: async () => {
1490
+ // Currently read-only from config
1491
+ // Future: Use request('stx_getNetworks') when wallet support improves
1492
+ const network = config.network
1493
+
1494
+ return {
1495
+ network,
1496
+ isMainnet: network === 'mainnet',
1497
+ isTestnet: network === 'testnet',
1498
+ isDevnet: network === 'devnet',
1499
+ // Future: Add switchNetwork when wallets support stx_networkChange
1500
+ // switchNetwork: async (newNetwork: string) => {
1501
+ // return await request('wallet_changeNetwork', { network: newNetwork })
1502
+ // }
1503
+ }
1504
+ },
1505
+ staleTime: Infinity, // Network config rarely changes
1506
+ refetchOnWindowFocus: false,
1507
+ retry: false
1508
+ })
1509
+ }`;
1510
+ case "useContract":
1511
+ return `export function useContract() {
1512
+ const config = useStacksConfig()
1513
+ const queryClient = useQueryClient()
1514
+ const [isRequestPending, setIsRequestPending] = useState(false)
1515
+
1516
+ // Helper function to convert JS values to Clarity values based on ABI
1517
+ const convertArgsWithAbi = (args: any, abiArgs: any[]): any[] => {
1518
+ if (!abiArgs || abiArgs.length === 0) return []
1519
+
1520
+ return abiArgs.map((abiArg, index) => {
1521
+ const argValue = Array.isArray(args)
1522
+ ? args[index]
1523
+ : args[abiArg.name] || args[abiArg.name.replace(/-/g, '').replace(/_/g, '')]
1524
+ return convertJSValueToClarityValue(argValue, abiArg.type)
1525
+ })
1526
+ }
1527
+
1528
+ // Helper function to convert buffer values with auto-detection
1529
+ const convertBufferValue = (value: any): any => {
1530
+ // Direct Uint8Array
1531
+ if (value instanceof Uint8Array) {
1532
+ return Cl.buffer(value)
1533
+ }
1534
+
1535
+ // Object notation with explicit type
1536
+ if (typeof value === 'object' && value !== null && value.type && value.value) {
1537
+ switch (value.type) {
1538
+ case 'ascii':
1539
+ return Cl.bufferFromAscii(value.value)
1540
+ case 'utf8':
1541
+ return Cl.bufferFromUtf8(value.value)
1542
+ case 'hex':
1543
+ return Cl.bufferFromHex(value.value)
1544
+ default:
1545
+ throw new Error(\`Unsupported buffer type: \${value.type}\`)
1546
+ }
1547
+ }
1548
+
1549
+ // Auto-detect string type
1550
+ if (typeof value === 'string') {
1551
+ // 1. Check for hex (0x prefix or pure hex pattern)
1552
+ if (value.startsWith('0x') || /^[0-9a-fA-F]+$/.test(value)) {
1553
+ return Cl.bufferFromHex(value)
1554
+ }
1555
+
1556
+ // 2. Check for non-ASCII characters (UTF-8)
1557
+ if (!/^[\\x00-\\x7F]*$/.test(value)) {
1558
+ return Cl.bufferFromUtf8(value)
1559
+ }
1560
+
1561
+ // 3. Default to ASCII for simple ASCII strings
1562
+ return Cl.bufferFromAscii(value)
1563
+ }
1564
+
1565
+ throw new Error(\`Invalid buffer value: \${value}\`)
1566
+ }
1567
+
1568
+ // Helper function to convert a single JS value to ClarityValue
1569
+ const convertJSValueToClarityValue = (value: any, type: any): any => {
1570
+ if (typeof type === 'string') {
1571
+ switch (type) {
1572
+ case 'uint128':
1573
+ return Cl.uint(value)
1574
+ case 'int128':
1575
+ return Cl.int(value)
1576
+ case 'bool':
1577
+ return Cl.bool(value)
1578
+ case 'principal':
1579
+ if (!validateStacksAddress(value.split('.')[0])) {
1580
+ throw new Error('Invalid Stacks address format')
1581
+ }
1582
+ if (value.includes('.')) {
1583
+ const [address, contractName] = value.split('.')
1584
+ return Cl.contractPrincipal(address, contractName)
1585
+ } else {
1586
+ return Cl.standardPrincipal(value)
1587
+ }
1588
+ default:
1589
+ return value
1590
+ }
1591
+ }
1592
+
1593
+ if (type['string-ascii']) {
1594
+ return Cl.stringAscii(value)
1595
+ }
1596
+
1597
+ if (type['string-utf8']) {
1598
+ return Cl.stringUtf8(value)
1599
+ }
1600
+
1601
+ if (type.buff) {
1602
+ return convertBufferValue(value)
1603
+ }
1604
+
1605
+ if (type.optional) {
1606
+ return value !== null ? Cl.some(convertJSValueToClarityValue(value, type.optional)) : Cl.none()
1607
+ }
1608
+
1609
+ if (type.list) {
1610
+ return Cl.list(value.map((item: any) => convertJSValueToClarityValue(item, type.list.type)))
1611
+ }
1612
+
1613
+ if (type.tuple) {
1614
+ const tupleData = type.tuple.reduce((acc: any, field: any) => {
1615
+ acc[field.name] = convertJSValueToClarityValue(value[field.name], field.type)
1616
+ return acc
1617
+ }, {})
1618
+ return Cl.tuple(tupleData)
1619
+ }
1620
+
1621
+ if (type.response) {
1622
+ return 'ok' in value
1623
+ ? Cl.ok(convertJSValueToClarityValue(value.ok, type.response.ok))
1624
+ : Cl.error(convertJSValueToClarityValue(value.err, type.response.error))
1625
+ }
1626
+
1627
+ return value
1628
+ }
1629
+
1630
+ // Helper function to find a function in an ABI by name
1631
+ const findFunctionInAbi = (abi: any, functionName: string): any => {
1632
+ if (!abi || !abi.functions) return null
1633
+ return abi.functions.find((func: any) => func.name === functionName)
1634
+ }
1635
+
1636
+ // Legacy function - unchanged, backward compatible
1637
+ const legacyOpenContractCall = useCallback(async (params: {
1638
+ contractAddress: string;
1639
+ contractName: string;
1640
+ functionName: string;
1641
+ functionArgs: any[]; // Pre-converted Clarity values
1642
+ network?: string;
1643
+ postConditions?: PostCondition[];
1644
+ attachment?: string;
1645
+ onFinish?: (data: any) => void;
1646
+ onCancel?: () => void;
1647
+ }) => {
1648
+ setIsRequestPending(true)
1649
+
1650
+ try {
1651
+ const { contractAddress, contractName, functionName, functionArgs, onFinish, onCancel, ...options } = params
1652
+ const network = params.network || config.network || 'mainnet'
1653
+ const contract = \`\${contractAddress}.\${contractName}\`
1654
+
1655
+ // Try @stacks/connect v8 stx_callContract first (SIP-030)
1656
+ try {
1657
+ const result = await request('stx_callContract', {
1658
+ contract,
1659
+ functionName,
1660
+ functionArgs,
1661
+ network,
1662
+ ...options
1663
+ })
1664
+
1665
+ // Invalidate relevant queries on success
1666
+ queryClient.invalidateQueries({
1667
+ queryKey: ['stacks-account']
1668
+ })
1669
+
1670
+ onFinish?.(result)
1671
+ return result
1672
+ } catch (connectError) {
1673
+ // Fallback to openContractCall for broader wallet compatibility
1674
+ console.warn('stx_callContract not supported, falling back to openContractCall:', connectError)
1675
+
1676
+ return new Promise((resolve, reject) => {
1677
+ stacksOpenContractCall({
1678
+ contractAddress,
1679
+ contractName,
1680
+ functionName,
1681
+ functionArgs,
1682
+ network,
1683
+ ...options,
1684
+ onFinish: (data: any) => {
1685
+ // Invalidate relevant queries on success
1686
+ queryClient.invalidateQueries({
1687
+ queryKey: ['stacks-account']
1688
+ })
1689
+
1690
+ onFinish?.(data)
1691
+ resolve(data)
1692
+ },
1693
+ onCancel: () => {
1694
+ onCancel?.()
1695
+ reject(new Error('User cancelled transaction'))
1696
+ }
1697
+ })
1698
+ })
1699
+ }
1700
+ } catch (error) {
1701
+ console.error('Contract call failed:', error)
1702
+ throw error instanceof Error ? error : new Error('Contract call failed')
1703
+ } finally {
1704
+ setIsRequestPending(false)
1705
+ }
1706
+ }, [config.network, queryClient])
1707
+
1708
+ // Enhanced function - requires ABI, auto-converts JS values
1709
+ const openContractCall = useCallback(async <
1710
+ T extends ClarityContract,
1711
+ FN extends ExtractFunctionNames<T>
1712
+ >(params: {
1713
+ contractAddress: string;
1714
+ contractName: string;
1715
+ functionName: FN;
1716
+ abi: T;
1717
+ functionArgs: ExtractFunctionArgs<T, FN>;
1718
+ network?: string;
1719
+ postConditions?: PostCondition[];
1720
+ attachment?: string;
1721
+ onFinish?: (data: any) => void;
1722
+ onCancel?: () => void;
1723
+ }) => {
1724
+ setIsRequestPending(true)
1725
+
1726
+ try {
1727
+ const { contractAddress, contractName, functionName, functionArgs, abi, onFinish, onCancel, ...options } = params
1728
+ const network = params.network || config.network || 'mainnet'
1729
+ const contract = \`\${contractAddress}.\${contractName}\`
1730
+
1731
+ // Find the function in the ABI and convert args
1732
+ const abiFunction = findFunctionInAbi(abi, functionName)
1733
+ if (!abiFunction) {
1734
+ throw new Error(\`Function '\${functionName}' not found in ABI\`)
1735
+ }
1736
+
1737
+ const processedArgs = convertArgsWithAbi(functionArgs, abiFunction.args || [])
1738
+
1739
+ // Try @stacks/connect v8 stx_callContract first (SIP-030)
1740
+ try {
1741
+ const result = await request('stx_callContract', {
1742
+ contract,
1743
+ functionName,
1744
+ functionArgs: processedArgs,
1745
+ network,
1746
+ ...options
1747
+ })
1748
+
1749
+ // Invalidate relevant queries on success
1750
+ queryClient.invalidateQueries({
1751
+ queryKey: ['stacks-account']
1752
+ })
1753
+
1754
+ onFinish?.(result)
1755
+ return result
1756
+ } catch (connectError) {
1757
+ // Fallback to openContractCall for broader wallet compatibility
1758
+ console.warn('stx_callContract not supported, falling back to openContractCall:', connectError)
1759
+
1760
+ return new Promise((resolve, reject) => {
1761
+ stacksOpenContractCall({
1762
+ contractAddress,
1763
+ contractName,
1764
+ functionName,
1765
+ functionArgs: processedArgs,
1766
+ network,
1767
+ ...options,
1768
+ onFinish: (data: any) => {
1769
+ // Invalidate relevant queries on success
1770
+ queryClient.invalidateQueries({
1771
+ queryKey: ['stacks-account']
1772
+ })
1773
+
1774
+ onFinish?.(data)
1775
+ resolve(data)
1776
+ },
1777
+ onCancel: () => {
1778
+ onCancel?.()
1779
+ reject(new Error('User cancelled transaction'))
1780
+ }
1781
+ })
1782
+ })
1783
+ }
1784
+ } catch (error) {
1785
+ console.error('Contract call failed:', error)
1786
+ throw error instanceof Error ? error : new Error('Contract call failed')
1787
+ } finally {
1788
+ setIsRequestPending(false)
1789
+ }
1790
+ }, [config.network, queryClient])
1791
+
1792
+ return {
1793
+ legacyOpenContractCall,
1794
+ openContractCall,
1795
+ isRequestPending
1796
+ }
1797
+ }`;
1798
+ case "useReadContract":
1799
+ return `export function useReadContract<TArgs = any, TResult = any>(params: {
1800
+ contractAddress: string;
1801
+ contractName: string;
1802
+ functionName: string;
1803
+ args?: TArgs;
1804
+ network?: 'mainnet' | 'testnet' | 'devnet';
1805
+ enabled?: boolean;
1806
+ }) {
1807
+ const config = useStacksConfig()
1808
+
1809
+ return useQuery<TResult>({
1810
+ queryKey: ['read-contract', params.contractAddress, params.contractName, params.functionName, params.args, params.network || config.network],
1811
+ queryFn: async () => {
1812
+ const { fetchCallReadOnlyFunction } = await import('@stacks/transactions')
1813
+
1814
+ // For now, we'll need to handle the args conversion here
1815
+ // In the future, we could integrate with the contract interface for automatic conversion
1816
+ let functionArgs: any[] = []
1817
+
1818
+ if (params.args) {
1819
+ // This is a simplified conversion - in practice, we'd need the ABI to do proper conversion
1820
+ // For now, we'll assume the args are already in the correct format or simple types
1821
+ if (Array.isArray(params.args)) {
1822
+ functionArgs = params.args
1823
+ } else if (typeof params.args === 'object') {
1824
+ // Convert object args to array (this is a basic implementation)
1825
+ functionArgs = Object.values(params.args)
1826
+ } else {
1827
+ functionArgs = [params.args]
1828
+ }
1829
+ }
1830
+
1831
+ return await fetchCallReadOnlyFunction({
1832
+ contractAddress: params.contractAddress,
1833
+ contractName: params.contractName,
1834
+ functionName: params.functionName,
1835
+ functionArgs,
1836
+ network: params.network || config.network || 'mainnet',
1837
+ senderAddress: config.senderAddress || 'SP000000000000000000002Q6VF78'
1838
+ }) as TResult
1839
+ },
1840
+ enabled: params.enabled ?? true
1841
+ })
1842
+ }`;
1843
+ case "useTransaction":
1844
+ return `export function useTransaction(txId?: string) {
1845
+ const config = useStacksConfig()
1846
+
1847
+ return useQuery({
1848
+ queryKey: ['transaction', txId, config.network],
1849
+ queryFn: () => fetchTransaction({
1850
+ txId: txId!,
1851
+ network: config.network,
1852
+ apiUrl: config.apiUrl
1853
+ }),
1854
+ enabled: !!txId
1855
+ })
1856
+ }`;
1857
+ case "useBlock":
1858
+ return `export function useBlock(height?: number) {
1859
+ const config = useStacksConfig()
1860
+
1861
+ return useQuery({
1862
+ queryKey: ['block', height, config.network],
1863
+ queryFn: () => fetchBlock({
1864
+ height: height!,
1865
+ network: config.network,
1866
+ apiUrl: config.apiUrl
1867
+ }),
1868
+ enabled: typeof height === 'number'
1869
+ })
1870
+ }`;
1871
+ case "useAccountTransactions":
1872
+ return `export function useAccountTransactions(address?: string) {
1873
+ const config = useStacksConfig()
1874
+
1875
+ return useQuery({
1876
+ queryKey: ['account-transactions', address, config.network],
1877
+ queryFn: () => fetchAccountTransactions({
1878
+ address: address!,
1879
+ network: config.network,
1880
+ apiUrl: config.apiUrl
1881
+ }),
1882
+ enabled: !!address
1883
+ })
1884
+ }`;
1885
+ case "useWaitForTransaction":
1886
+ return `export function useWaitForTransaction(txId?: string) {
1887
+ const config = useStacksConfig()
1888
+
1889
+ return useQuery({
1890
+ queryKey: ['wait-for-transaction', txId, config.network],
1891
+ queryFn: () => fetchTransaction({
1892
+ txId: txId!,
1893
+ network: config.network,
1894
+ apiUrl: config.apiUrl
1895
+ }),
1896
+ enabled: !!txId,
1897
+ refetchInterval: (data) => {
1898
+ // Stop polling when transaction is complete
1899
+ if (data?.tx_status === 'success' ||
1900
+ data?.tx_status === 'abort_by_response' ||
1901
+ data?.tx_status === 'abort_by_post_condition') {
1902
+ return false
1903
+ }
1904
+ return 2000 // Poll every 2 seconds
1905
+ },
1906
+ staleTime: 0 // Always refetch
1907
+ })
1908
+ }`;
1909
+ case "useOpenSTXTransfer":
1910
+ return `export function useOpenSTXTransfer() {
1911
+ const config = useStacksConfig()
1912
+ const queryClient = useQueryClient()
1913
+
1914
+ const mutation = useMutation({
1915
+ mutationFn: async (params: {
1916
+ recipient: string;
1917
+ amount: string | number;
1918
+ memo?: string;
1919
+ network?: string;
1920
+ onFinish?: (data: any) => void;
1921
+ onCancel?: () => void;
1922
+ }) => {
1923
+ const { recipient, amount, memo, onFinish, onCancel, ...options } = params
1924
+ const network = params.network || config.network || 'mainnet'
1925
+
1926
+ return new Promise((resolve, reject) => {
1927
+ openSTXTransfer({
1928
+ recipient,
1929
+ amount: amount.toString(),
1930
+ memo,
1931
+ network,
1932
+ ...options,
1933
+ onFinish: (data: any) => {
1934
+ onFinish?.(data)
1935
+ resolve(data)
1936
+ },
1937
+ onCancel: () => {
1938
+ onCancel?.()
1939
+ reject(new Error('User cancelled transaction'))
1940
+ }
1941
+ })
1942
+ })
1943
+ },
1944
+ onSuccess: () => {
1945
+ // Invalidate relevant queries on success
1946
+ queryClient.invalidateQueries({ queryKey: ['stacks-account'] })
1947
+ },
1948
+ onError: (error) => {
1949
+ console.error('STX transfer failed:', error)
1950
+ }
1951
+ })
1952
+
1953
+ const openSTXTransfer = useCallback(async (params: {
1954
+ recipient: string;
1955
+ amount: string | number;
1956
+ memo?: string;
1957
+ network?: string;
1958
+ onFinish?: (data: any) => void;
1959
+ onCancel?: () => void;
1960
+ }) => {
1961
+ return mutation.mutateAsync(params)
1962
+ }, [mutation])
1963
+
1964
+ return {
1965
+ openSTXTransfer,
1966
+ // Expose mutation state
1967
+ isPending: mutation.isPending,
1968
+ isError: mutation.isError,
1969
+ isSuccess: mutation.isSuccess,
1970
+ error: mutation.error,
1971
+ data: mutation.data,
1972
+ reset: mutation.reset
1973
+ }
1974
+ }`;
1975
+ case "useSignMessage":
1976
+ return `export function useSignMessage() {
1977
+ const config = useStacksConfig()
1978
+
1979
+ const mutation = useMutation({
1980
+ mutationFn: async (params: {
1981
+ message: string;
1982
+ network?: string;
1983
+ onFinish?: (data: any) => void;
1984
+ onCancel?: () => void;
1985
+ }) => {
1986
+ const { message, onFinish, onCancel, ...options } = params
1987
+ const network = params.network || config.network || 'mainnet'
1988
+
1989
+ return new Promise((resolve, reject) => {
1990
+ openSignatureRequestPopup({
1991
+ message,
1992
+ network,
1993
+ ...options,
1994
+ onFinish: (data: any) => {
1995
+ onFinish?.(data)
1996
+ resolve(data)
1997
+ },
1998
+ onCancel: () => {
1999
+ onCancel?.()
2000
+ reject(new Error('User cancelled message signing'))
2001
+ }
2002
+ })
2003
+ })
2004
+ },
2005
+ onError: (error) => {
2006
+ console.error('Message signing failed:', error)
2007
+ }
2008
+ })
2009
+
2010
+ const signMessage = useCallback(async (params: {
2011
+ message: string;
2012
+ network?: string;
2013
+ onFinish?: (data: any) => void;
2014
+ onCancel?: () => void;
2015
+ }) => {
2016
+ return mutation.mutateAsync(params)
2017
+ }, [mutation])
2018
+
2019
+ return {
2020
+ signMessage,
2021
+ // Expose mutation state
2022
+ isPending: mutation.isPending,
2023
+ isError: mutation.isError,
2024
+ isSuccess: mutation.isSuccess,
2025
+ error: mutation.error,
2026
+ data: mutation.data,
2027
+ reset: mutation.reset
2028
+ }
2029
+ }`;
2030
+ case "useDeployContract":
2031
+ return `export function useDeployContract() {
2032
+ const config = useStacksConfig()
2033
+ const queryClient = useQueryClient()
2034
+
2035
+ const mutation = useMutation({
2036
+ mutationFn: async (params: {
2037
+ contractName: string;
2038
+ codeBody: string;
2039
+ network?: string;
2040
+ postConditions?: PostCondition[];
2041
+ onFinish?: (data: any) => void;
2042
+ onCancel?: () => void;
2043
+ }) => {
2044
+ const { contractName, codeBody, onFinish, onCancel, ...options } = params
2045
+ const network = params.network || config.network || 'mainnet'
2046
+
2047
+ return new Promise((resolve, reject) => {
2048
+ openContractDeploy({
2049
+ contractName,
2050
+ codeBody,
2051
+ network,
2052
+ ...options,
2053
+ onFinish: (data: any) => {
2054
+ onFinish?.(data)
2055
+ resolve(data)
2056
+ },
2057
+ onCancel: () => {
2058
+ onCancel?.()
2059
+ reject(new Error('User cancelled contract deployment'))
2060
+ }
2061
+ })
2062
+ })
2063
+ },
2064
+ onSuccess: () => {
2065
+ // Invalidate relevant queries on success
2066
+ queryClient.invalidateQueries({ queryKey: ['stacks-account'] })
2067
+ },
2068
+ onError: (error) => {
2069
+ console.error('Contract deployment failed:', error)
2070
+ }
2071
+ })
2072
+
2073
+ const deployContract = useCallback(async (params: {
2074
+ contractName: string;
2075
+ codeBody: string;
2076
+ network?: string;
2077
+ postConditions?: PostCondition[];
2078
+ onFinish?: (data: any) => void;
2079
+ onCancel?: () => void;
2080
+ }) => {
2081
+ return mutation.mutateAsync(params)
2082
+ }, [mutation])
2083
+
2084
+ return {
2085
+ deployContract,
2086
+ // Expose mutation state
2087
+ isPending: mutation.isPending,
2088
+ isError: mutation.isError,
2089
+ isSuccess: mutation.isSuccess,
2090
+ error: mutation.error,
2091
+ data: mutation.data,
2092
+ reset: mutation.reset
2093
+ }
2094
+ }`;
2095
+ default:
2096
+ return "";
2097
+ }
2098
+ }
2099
+
2100
+ // src/plugins/react/generators/contract.ts
2101
+ import { format as format4 } from "prettier";
2102
+
2103
+ // src/plugins/react/generators/utils.ts
2104
+ function toCamelCase4(str) {
2105
+ return str.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
2106
+ }
2107
+ function capitalize(str) {
2108
+ return str.charAt(0).toUpperCase() + str.slice(1);
2109
+ }
2110
+ function generateHookArgsSignature(args) {
2111
+ if (args.length === 0)
2112
+ return "";
2113
+ const argsList = args.map((arg) => `${toCamelCase4(arg.name)}: ${mapClarityTypeToTS(arg.type)}`).join(", ");
2114
+ return `${argsList}`;
2115
+ }
2116
+ function generateArgsType(args) {
2117
+ if (args.length === 0)
2118
+ return "void";
2119
+ const argsList = args.map((arg) => `${toCamelCase4(arg.name)}: ${mapClarityTypeToTS(arg.type)}`).join("; ");
2120
+ return `{ ${argsList} }`;
2121
+ }
2122
+ function generateQueryKeyArgs(args) {
2123
+ if (args.length === 0)
2124
+ return "";
2125
+ return args.map((arg) => toCamelCase4(arg.name)).join(", ");
2126
+ }
2127
+ function generateFunctionCallArgs(args) {
2128
+ if (args.length === 0)
2129
+ return "";
2130
+ return args.map((arg) => toCamelCase4(arg.name)).join(", ");
2131
+ }
2132
+ function generateEnabledCondition(args) {
2133
+ return args.map((arg) => {
2134
+ const camelName = toCamelCase4(arg.name);
2135
+ const type = mapClarityTypeToTS(arg.type);
2136
+ if (type === "string")
2137
+ return `!!${camelName}`;
2138
+ if (type === "bigint")
2139
+ return `${camelName} !== undefined`;
2140
+ return `${camelName} !== undefined`;
2141
+ }).join(" && ");
2142
+ }
2143
+ function mapClarityTypeToTS(clarityType) {
2144
+ if (typeof clarityType !== "string") {
2145
+ if (clarityType?.uint || clarityType?.int)
2146
+ return "bigint";
2147
+ if (clarityType?.principal)
2148
+ return "string";
2149
+ if (clarityType?.bool)
2150
+ return "boolean";
2151
+ if (clarityType?.string || clarityType?.ascii)
2152
+ return "string";
2153
+ if (clarityType?.["string-ascii"] || clarityType?.["string-utf8"])
2154
+ return "string";
2155
+ if (clarityType?.buff)
2156
+ return "Uint8Array";
2157
+ if (clarityType?.optional) {
2158
+ const innerType = mapClarityTypeToTS(clarityType.optional);
2159
+ return `${innerType} | null`;
2160
+ }
2161
+ if (clarityType?.response) {
2162
+ const okType = mapClarityTypeToTS(clarityType.response.ok);
2163
+ const errType = mapClarityTypeToTS(clarityType.response.error);
2164
+ return `{ ok: ${okType} } | { err: ${errType} }`;
2165
+ }
2166
+ if (clarityType?.tuple) {
2167
+ const fields = clarityType.tuple.map((field) => `${toCamelCase4(field.name)}: ${mapClarityTypeToTS(field.type)}`).join("; ");
2168
+ return `{ ${fields} }`;
2169
+ }
2170
+ if (clarityType?.list) {
2171
+ const innerType = mapClarityTypeToTS(clarityType.list.type);
2172
+ if (innerType.includes(" | ")) {
2173
+ return `(${innerType})[]`;
2174
+ }
2175
+ return `${innerType}[]`;
2176
+ }
2177
+ return "any";
2178
+ }
2179
+ if (clarityType.includes("uint") || clarityType.includes("int"))
2180
+ return "bigint";
2181
+ if (clarityType.includes("principal"))
2182
+ return "string";
2183
+ if (clarityType.includes("bool"))
2184
+ return "boolean";
2185
+ if (clarityType.includes("string") || clarityType.includes("ascii"))
2186
+ return "string";
2187
+ if (clarityType.includes("buff"))
2188
+ return "Uint8Array";
2189
+ if (clarityType.includes("optional")) {
2190
+ const innerType = clarityType.replace(/optional\s*/, "").trim();
2191
+ return `${mapClarityTypeToTS(innerType)} | null`;
2192
+ }
2193
+ return "any";
2194
+ }
2195
+ function generateObjectArgs(args) {
2196
+ if (args.length === 0)
2197
+ return "";
2198
+ return args.map((arg) => `${arg.name}: ${toCamelCase4(arg.name)}`).join(", ");
2199
+ }
2200
+
2201
+ // src/plugins/react/generators/contract.ts
2202
+ async function generateContractHooks(contracts, excludeList = []) {
2203
+ const imports = `import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
2204
+ import { useCallback } from 'react'
2205
+ import { useStacksConfig } from './provider'
2206
+ import { request, openContractCall as stacksOpenContractCall } from '@stacks/connect'
2207
+ import type { PostCondition } from '@stacks/transactions'
2208
+ import { ${contracts.map((c) => c.name).join(", ")} } from './contracts'`;
2209
+ const header = `/**
2210
+ * Generated contract-specific React hooks
2211
+ * DO NOT EDIT MANUALLY
2212
+ */`;
2213
+ const hooksCode = contracts.map((contract) => generateContractHookMethods(contract, excludeList)).filter(Boolean).join(`
2214
+
2215
+ `);
2216
+ const code = `${imports}
2217
+
2218
+ ${header}
2219
+
2220
+ ${hooksCode}`;
2221
+ const formatted = await format4(code, {
2222
+ parser: "typescript",
2223
+ singleQuote: true,
2224
+ semi: false,
2225
+ printWidth: 100,
2226
+ trailingComma: "es5"
2227
+ });
2228
+ return formatted;
2229
+ }
2230
+ function generateContractHookMethods(contract, excludeList) {
2231
+ const { abi, name, address, contractName } = contract;
2232
+ const functions = abi.functions || [];
2233
+ const maps = abi.maps || [];
2234
+ const variables = abi.variables || [];
2235
+ const readOnlyFunctions = functions.filter((f) => f.access === "read_only" || f.access === "read-only");
2236
+ const publicFunctions = functions.filter((f) => f.access === "public");
2237
+ const readHooks = readOnlyFunctions.map((func) => {
2238
+ const hookName = `use${capitalize(name)}${capitalize(toCamelCase4(func.name))}`;
2239
+ if (excludeList.includes(hookName)) {
2240
+ return null;
2241
+ }
2242
+ return generateReadHook(func, name);
2243
+ }).filter(Boolean);
2244
+ const writeHooks = publicFunctions.map((func) => {
2245
+ const hookName = `use${capitalize(name)}${capitalize(toCamelCase4(func.name))}`;
2246
+ if (excludeList.includes(hookName)) {
2247
+ return null;
2248
+ }
2249
+ return generateWriteHook(func, name);
2250
+ }).filter(Boolean);
2251
+ const mapHooks = maps.map((map) => {
2252
+ const hookName = `use${capitalize(name)}${capitalize(toCamelCase4(map.name))}`;
2253
+ if (excludeList.includes(hookName)) {
2254
+ return null;
2255
+ }
2256
+ return generateMapHook(map, name, address, contractName);
2257
+ }).filter(Boolean);
2258
+ const dataVars = variables.filter((v) => v.access === "variable");
2259
+ const varHooks = dataVars.map((variable) => {
2260
+ const hookName = `use${capitalize(name)}${capitalize(toCamelCase4(variable.name))}`;
2261
+ if (excludeList.includes(hookName)) {
2262
+ return null;
2263
+ }
2264
+ return generateVarHook(variable, name, address, contractName);
2265
+ }).filter(Boolean);
2266
+ const constants = variables.filter((v) => v.access === "constant");
2267
+ const constantHooks = constants.map((constant) => {
2268
+ const hookName = `use${capitalize(name)}${capitalize(toCamelCase4(constant.name))}`;
2269
+ if (excludeList.includes(hookName)) {
2270
+ return null;
2271
+ }
2272
+ return generateConstantHook(constant, name, address, contractName);
2273
+ }).filter(Boolean);
2274
+ const allHooks = [...readHooks, ...writeHooks, ...mapHooks, ...varHooks, ...constantHooks];
2275
+ if (allHooks.length === 0) {
2276
+ return "";
2277
+ }
2278
+ return allHooks.join(`
2279
+
2280
+ `);
2281
+ }
2282
+ function generateReadHook(func, contractName) {
2283
+ const hookName = `use${capitalize(contractName)}${capitalize(toCamelCase4(func.name))}`;
2284
+ const argsSignature = generateHookArgsSignature(func.args);
2285
+ const enabledParam = func.args.length > 0 ? ", options?: { enabled?: boolean }" : "options?: { enabled?: boolean }";
2286
+ const returnType = mapClarityTypeToTS(func.outputs);
2287
+ return `export function ${hookName}(${argsSignature}${enabledParam}) {
2288
+ const config = useStacksConfig()
2289
+
2290
+ return useQuery<${returnType}>({
2291
+ queryKey: ['${func.name}', ${contractName}.address, ${generateQueryKeyArgs(func.args)}],
2292
+ queryFn: () => ${contractName}.read.${toCamelCase4(func.name)}(${generateFunctionCallArgs(func.args) ? `{ ${generateObjectArgs(func.args)} }, ` : ""}{
2293
+ network: config.network,
2294
+ senderAddress: config.senderAddress || 'SP000000000000000000002Q6VF78'
2295
+ }),
2296
+ ${func.args.length > 0 ? `enabled: ${generateEnabledCondition(func.args)} && (options?.enabled ?? true),` : ""}
2297
+ ...options
2298
+ })
2299
+ }`;
2300
+ }
2301
+ function generateWriteHook(func, contractName) {
2302
+ const hookName = `use${capitalize(contractName)}${capitalize(toCamelCase4(func.name))}`;
2303
+ const argsType = generateArgsType(func.args);
2304
+ return `export function ${hookName}() {
2305
+ const config = useStacksConfig()
2306
+ const queryClient = useQueryClient()
2307
+
2308
+ const mutation = useMutation({
2309
+ mutationFn: async (params: {
2310
+ args: ${argsType};
2311
+ options?: {
2312
+ postConditions?: PostCondition[];
2313
+ attachment?: string;
2314
+ onFinish?: (data: any) => void;
2315
+ onCancel?: () => void;
2316
+ };
2317
+ }) => {
2318
+ const { args, options = {} } = params
2319
+ const contractCallData = ${contractName}.${toCamelCase4(func.name)}(args)
2320
+ const { contractAddress, contractName: name, functionName, functionArgs } = contractCallData
2321
+ const network = config.network || 'mainnet'
2322
+ const contract = \`\${contractAddress}.\${name}\`
2323
+
2324
+ // Try @stacks/connect v8 stx_callContract first (SIP-030)
2325
+ try {
2326
+ const result = await request('stx_callContract', {
2327
+ contract,
2328
+ functionName,
2329
+ functionArgs,
2330
+ network,
2331
+ ...options
2332
+ })
2333
+
2334
+ options.onFinish?.(result)
2335
+ return result
2336
+ } catch (connectError) {
2337
+ // Fallback to openContractCall for broader wallet compatibility
2338
+ console.warn('stx_callContract not supported, falling back to openContractCall:', connectError)
2339
+
2340
+ return new Promise((resolve, reject) => {
2341
+ stacksOpenContractCall({
2342
+ contractAddress,
2343
+ contractName: name,
2344
+ functionName,
2345
+ functionArgs,
2346
+ network,
2347
+ ...options,
2348
+ onFinish: (data: any) => {
2349
+ options.onFinish?.(data)
2350
+ resolve(data)
2351
+ },
2352
+ onCancel: () => {
2353
+ options.onCancel?.()
2354
+ reject(new Error('User cancelled transaction'))
2355
+ }
2356
+ })
2357
+ })
2358
+ }
2359
+ },
2360
+ onSuccess: () => {
2361
+ // Invalidate relevant queries on success
2362
+ queryClient.invalidateQueries({ queryKey: ['stacks-account'] })
2363
+ },
2364
+ onError: (error) => {
2365
+ console.error('Contract call failed:', error)
2366
+ }
2367
+ })
2368
+
2369
+ const ${toCamelCase4(func.name)} = useCallback(async (
2370
+ args: ${argsType},
2371
+ options?: {
2372
+ postConditions?: PostCondition[];
2373
+ attachment?: string;
2374
+ onFinish?: (data: any) => void;
2375
+ onCancel?: () => void;
2376
+ }
2377
+ ) => {
2378
+ return mutation.mutateAsync({ args, options })
2379
+ }, [mutation])
2380
+
2381
+ return {
2382
+ ${toCamelCase4(func.name)},
2383
+ // Expose mutation state
2384
+ isPending: mutation.isPending,
2385
+ isError: mutation.isError,
2386
+ isSuccess: mutation.isSuccess,
2387
+ error: mutation.error,
2388
+ data: mutation.data,
2389
+ reset: mutation.reset
2390
+ }
2391
+ }`;
2392
+ }
2393
+ function generateMapHook(map, contractVarName, _address, _contractName) {
2394
+ const hookName = `use${capitalize(contractVarName)}${capitalize(toCamelCase4(map.name))}`;
2395
+ const keyType = mapClarityTypeToTS(map.key);
2396
+ const valueType = mapClarityTypeToTS(map.value);
2397
+ return `export function ${hookName}(key: ${keyType}, options?: { enabled?: boolean }) {
2398
+ const config = useStacksConfig()
2399
+
2400
+ return useQuery<${valueType} | null>({
2401
+ queryKey: ['${contractVarName}', '${map.name}', 'map', key, config.network],
2402
+ queryFn: async () => {
2403
+ return ${contractVarName}.maps.${toCamelCase4(map.name)}.get(key, { network: config.network })
2404
+ },
2405
+ enabled: options?.enabled ?? true
2406
+ })
2407
+ }`;
2408
+ }
2409
+ function generateVarHook(variable, contractVarName, _address, _contractName) {
2410
+ const hookName = `use${capitalize(contractVarName)}${capitalize(toCamelCase4(variable.name))}`;
2411
+ const valueType = mapClarityTypeToTS(variable.type);
2412
+ return `export function ${hookName}(options?: { enabled?: boolean }) {
2413
+ const config = useStacksConfig()
2414
+
2415
+ return useQuery<${valueType}>({
2416
+ queryKey: ['${contractVarName}', '${variable.name}', 'var', config.network],
2417
+ queryFn: async () => {
2418
+ return ${contractVarName}.vars.${toCamelCase4(variable.name)}.get({ network: config.network })
2419
+ },
2420
+ enabled: options?.enabled ?? true
2421
+ })
2422
+ }`;
2423
+ }
2424
+ function generateConstantHook(constant, contractVarName, _address, _contractName) {
2425
+ const hookName = `use${capitalize(contractVarName)}${capitalize(toCamelCase4(constant.name))}`;
2426
+ const valueType = mapClarityTypeToTS(constant.type);
2427
+ return `export function ${hookName}(options?: { enabled?: boolean }) {
2428
+ const config = useStacksConfig()
2429
+
2430
+ return useQuery<${valueType}>({
2431
+ queryKey: ['${contractVarName}', '${constant.name}', 'constant', config.network],
2432
+ queryFn: async () => {
2433
+ return ${contractVarName}.constants.${toCamelCase4(constant.name)}.get({ network: config.network })
2434
+ },
2435
+ enabled: options?.enabled ?? true,
2436
+ staleTime: Infinity // Constants never change
2437
+ })
2438
+ }`;
2439
+ }
2440
+
2441
+ // src/plugins/react/index.ts
2442
+ var react = (options = {}) => {
2443
+ const excludeList = options.exclude || [];
2444
+ return {
2445
+ name: "@secondlayer/cli/plugin-react",
2446
+ version: "1.0.0",
2447
+ async generate(context) {
2448
+ if (options.debug) {
2449
+ context.logger.debug(`React plugin generating hooks (excluding: ${excludeList.join(", ") || "none"})`);
2450
+ }
2451
+ const provider = await generateProvider();
2452
+ context.addOutput("provider", {
2453
+ path: "./src/generated/provider.tsx",
2454
+ content: provider,
2455
+ type: "config"
2456
+ });
2457
+ const genericHooks = await generateGenericHooks(excludeList);
2458
+ context.addOutput("generic-hooks", {
2459
+ path: "./src/generated/hooks.ts",
2460
+ content: genericHooks,
2461
+ type: "hooks"
2462
+ });
2463
+ if (context.contracts.length > 0) {
2464
+ const contractHooks = await generateContractHooks(context.contracts, excludeList);
2465
+ if (contractHooks.trim()) {
2466
+ context.addOutput("contract-hooks", {
2467
+ path: "./src/generated/contract-hooks.ts",
2468
+ content: contractHooks,
2469
+ type: "hooks"
2470
+ });
2471
+ }
2472
+ }
2473
+ if (options.debug) {
2474
+ context.logger.success(`React plugin generated ${context.contracts.length} contract hook sets`);
2475
+ }
2476
+ }
2477
+ };
2478
+ };
2479
+ // src/plugins/testing/generators.ts
2480
+ function toCamelCase5(str) {
2481
+ return str.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()).replace(/-([A-Z])/g, (_, letter) => letter).replace(/-(\d)/g, (_, digit) => digit).replace(/-/g, "").replace(/^\d/, "_$&");
2482
+ }
2483
+ function toPascalCase(str) {
2484
+ const camel = toCamelCase5(str);
2485
+ return camel.charAt(0).toUpperCase() + camel.slice(1);
2486
+ }
2487
+ function getTypeForArg3(arg) {
2488
+ const type = arg.type;
2489
+ if (typeof type === "string") {
2490
+ switch (type) {
2491
+ case "uint128":
2492
+ case "int128":
2493
+ return "bigint";
2494
+ case "bool":
2495
+ return "boolean";
2496
+ case "principal":
2497
+ case "trait_reference":
2498
+ return "string";
2499
+ default:
2500
+ return "any";
2501
+ }
2502
+ }
2503
+ if (type["string-ascii"] || type["string-utf8"]) {
2504
+ return "string";
2505
+ }
2506
+ if (type.buff) {
2507
+ return "Uint8Array | string | { type: 'ascii' | 'utf8' | 'hex'; value: string }";
2508
+ }
2509
+ if (type.optional) {
2510
+ const innerType = getTypeForArg3({ type: type.optional });
2511
+ return `${innerType} | null`;
2512
+ }
2513
+ if (type.list) {
2514
+ const innerType = getTypeForArg3({ type: type.list.type });
2515
+ return `${innerType}[]`;
2516
+ }
2517
+ if (type.tuple) {
2518
+ const fields = type.tuple.map((field) => `${toCamelCase5(field.name)}: ${getTypeForArg3({ type: field.type })}`).join("; ");
2519
+ return `{ ${fields} }`;
2520
+ }
2521
+ if (type.response) {
2522
+ const okType = getTypeForArg3({ type: type.response.ok });
2523
+ const errType = getTypeForArg3({ type: type.response.error });
2524
+ return `{ ok: ${okType} } | { err: ${errType} }`;
2525
+ }
2526
+ return "any";
2527
+ }
2528
+ function generateArgsSignature2(args) {
2529
+ if (args.length === 0)
2530
+ return "";
2531
+ const argsTypes = args.map((arg) => {
2532
+ const camelName = toCamelCase5(arg.name);
2533
+ return `${camelName}: ${getTypeForArg3(arg)}`;
2534
+ }).join("; ");
2535
+ return `args: { ${argsTypes} }, `;
2536
+ }
2537
+ function generateClarityConversion3(argName, argType) {
2538
+ const type = argType.type;
2539
+ if (typeof type === "string") {
2540
+ switch (type) {
2541
+ case "uint128":
2542
+ return `Cl.uint(${argName})`;
2543
+ case "int128":
2544
+ return `Cl.int(${argName})`;
2545
+ case "bool":
2546
+ return `Cl.bool(${argName})`;
2547
+ case "principal":
2548
+ case "trait_reference":
2549
+ return `(() => {
2550
+ const principal = ${argName};
2551
+ if (principal.includes(".")) {
2552
+ const [address, contractName] = principal.split(".") as [string, string];
2553
+ return Cl.contractPrincipal(address, contractName);
2554
+ } else {
2555
+ return Cl.standardPrincipal(principal);
2556
+ }
2557
+ })()`;
2558
+ default:
2559
+ return `${argName}`;
2560
+ }
2561
+ }
2562
+ if (type["string-ascii"]) {
2563
+ return `Cl.stringAscii(${argName})`;
2564
+ }
2565
+ if (type["string-utf8"]) {
2566
+ return `Cl.stringUtf8(${argName})`;
2567
+ }
2568
+ if (type.buff) {
2569
+ return `(() => {
2570
+ const value = ${argName};
2571
+ if (value instanceof Uint8Array) {
2572
+ return Cl.buffer(value);
2573
+ }
2574
+ if (typeof value === 'object' && value !== null && 'type' in value && 'value' in value) {
2575
+ switch (value.type) {
2576
+ case 'ascii':
2577
+ return Cl.bufferFromAscii(value.value);
2578
+ case 'utf8':
2579
+ return Cl.bufferFromUtf8(value.value);
2580
+ case 'hex':
2581
+ return Cl.bufferFromHex(value.value);
2582
+ default:
2583
+ throw new Error(\`Unsupported buffer type: \${value.type}\`);
2584
+ }
2585
+ }
2586
+ if (typeof value === 'string') {
2587
+ if (value.startsWith('0x') || /^[0-9a-fA-F]+$/.test(value)) {
2588
+ return Cl.bufferFromHex(value);
2589
+ }
2590
+ if (!/^[\\x00-\\x7F]*$/.test(value)) {
2591
+ return Cl.bufferFromUtf8(value);
2592
+ }
2593
+ return Cl.bufferFromAscii(value);
2594
+ }
2595
+ throw new Error(\`Invalid buffer value: \${value}\`);
2596
+ })()`;
2597
+ }
2598
+ if (type.optional) {
2599
+ const innerConversion = generateClarityConversion3("inner", {
2600
+ type: type.optional
2601
+ });
2602
+ return `${argName} !== null ? Cl.some((() => { const inner = ${argName}; return ${innerConversion}; })()) : Cl.none()`;
2603
+ }
2604
+ if (type.list) {
2605
+ const innerConversion = generateClarityConversion3("item", {
2606
+ type: type.list.type
2607
+ });
2608
+ return `Cl.list(${argName}.map(item => ${innerConversion}))`;
2609
+ }
2610
+ if (type.tuple) {
2611
+ const fields = type.tuple.map((field) => {
2612
+ const camelFieldName = toCamelCase5(field.name);
2613
+ const fieldConversion = generateClarityConversion3(`${argName}.${camelFieldName}`, { type: field.type });
2614
+ return `"${field.name}": ${fieldConversion}`;
2615
+ }).join(", ");
2616
+ return `Cl.tuple({ ${fields} })`;
2617
+ }
2618
+ if (type.response) {
2619
+ const okConversion = generateClarityConversion3(`${argName}.ok`, {
2620
+ type: type.response.ok
2621
+ });
2622
+ const errConversion = generateClarityConversion3(`${argName}.err`, {
2623
+ type: type.response.error
2624
+ });
2625
+ return `'ok' in ${argName} ? Cl.ok(${okConversion}) : Cl.error(${errConversion})`;
2626
+ }
2627
+ return `${argName}`;
2628
+ }
2629
+ function generateClarityArgs2(args) {
2630
+ if (args.length === 0)
2631
+ return "";
2632
+ return args.map((arg) => {
2633
+ const argName = `args.${toCamelCase5(arg.name)}`;
2634
+ return generateClarityConversion3(argName, arg);
2635
+ }).join(", ");
2636
+ }
2637
+ function generatePublicFunction(func, contractId) {
2638
+ const methodName = toCamelCase5(func.name);
2639
+ const argsSignature = generateArgsSignature2(func.args);
2640
+ const clarityArgs = generateClarityArgs2(func.args);
2641
+ return `${methodName}: (${argsSignature}caller: string) => {
2642
+ const callerAddr = accounts.get(caller) ?? caller;
2643
+ return simnet.callPublicFn(
2644
+ '${contractId}',
2645
+ '${func.name}',
2646
+ [${clarityArgs}],
2647
+ callerAddr
2648
+ );
2649
+ }`;
2650
+ }
2651
+ function generateReadOnlyFunction(func, contractId) {
2652
+ const methodName = toCamelCase5(func.name);
2653
+ const argsSignature = generateArgsSignature2(func.args);
2654
+ const clarityArgs = generateClarityArgs2(func.args);
2655
+ const hasArgs = func.args.length > 0;
2656
+ const argsParam = hasArgs ? argsSignature : "";
2657
+ return `${methodName}: (${argsParam}) => {
2658
+ return simnet.callReadOnlyFn(
2659
+ '${contractId}',
2660
+ '${func.name}',
2661
+ [${clarityArgs}],
2662
+ accounts.get('deployer')!
2663
+ );
2664
+ }`;
2665
+ }
2666
+ function generatePrivateFunction(func, contractId) {
2667
+ const methodName = toCamelCase5(func.name);
2668
+ const argsSignature = generateArgsSignature2(func.args);
2669
+ const clarityArgs = generateClarityArgs2(func.args);
2670
+ return `${methodName}: (${argsSignature}caller: string) => {
2671
+ const callerAddr = accounts.get(caller) ?? caller;
2672
+ return simnet.callPrivateFn(
2673
+ '${contractId}',
2674
+ '${func.name}',
2675
+ [${clarityArgs}],
2676
+ callerAddr
2677
+ );
2678
+ }`;
2679
+ }
2680
+ function generateDataVarHelper(variable, contractId) {
2681
+ const methodName = toCamelCase5(variable.name);
2682
+ return `${methodName}: () => {
2683
+ return simnet.getDataVar('${contractId}', '${variable.name}');
2684
+ }`;
2685
+ }
2686
+ function getMapKeyType(keyType) {
2687
+ if (keyType.tuple) {
2688
+ const fields = keyType.tuple.map((field) => `${toCamelCase5(field.name)}: ${getTypeForArg3({ type: field.type })}`).join("; ");
2689
+ return `{ ${fields} }`;
2690
+ }
2691
+ return getTypeForArg3({ type: keyType });
2692
+ }
2693
+ function generateMapKeyConversion2(keyType) {
2694
+ if (keyType.tuple) {
2695
+ const fields = keyType.tuple.map((field) => {
2696
+ const camelFieldName = toCamelCase5(field.name);
2697
+ const fieldConversion = generateClarityConversion3(`key.${camelFieldName}`, { type: field.type });
2698
+ return `"${field.name}": ${fieldConversion}`;
2699
+ }).join(", ");
2700
+ return `Cl.tuple({ ${fields} })`;
2701
+ }
2702
+ return generateClarityConversion3("key", { type: keyType });
2703
+ }
2704
+ function generateMapEntryHelper(map, contractId) {
2705
+ const methodName = toCamelCase5(map.name);
2706
+ const keyType = getMapKeyType(map.key);
2707
+ const keyConversion = generateMapKeyConversion2(map.key);
2708
+ return `${methodName}: (key: ${keyType}) => {
2709
+ return simnet.getMapEntry(
2710
+ '${contractId}',
2711
+ '${map.name}',
2712
+ ${keyConversion}
2713
+ );
2714
+ }`;
2715
+ }
2716
+ function generateVarsObject2(variables, contractId) {
2717
+ const dataVars = variables.filter((v) => v.access === "variable");
2718
+ if (dataVars.length === 0) {
2719
+ return "";
2720
+ }
2721
+ const varHelpers = dataVars.map((v) => generateDataVarHelper(v, contractId));
2722
+ return `vars: {
2723
+ ${varHelpers.join(`,
2724
+
2725
+ `)}
2726
+ }`;
2727
+ }
2728
+ function generateMapsObject2(maps, contractId) {
2729
+ if (maps.length === 0) {
2730
+ return "";
2731
+ }
2732
+ const mapHelpers = maps.map((m) => generateMapEntryHelper(m, contractId));
2733
+ return `maps: {
2734
+ ${mapHelpers.join(`,
2735
+
2736
+ `)}
2737
+ }`;
2738
+ }
2739
+ function generateContractHelper(contract, options) {
2740
+ const { abi, name, address } = contract;
2741
+ const functions = abi.functions || [];
2742
+ const variables = abi.variables || [];
2743
+ const maps = abi.maps || [];
2744
+ const pascalName = toPascalCase(name);
2745
+ const publicFns = functions.filter((f) => f.access === "public");
2746
+ const readOnlyFns = functions.filter((f) => f.access === "read_only" || f.access === "read-only");
2747
+ const privateFns = options.includePrivate ? functions.filter((f) => f.access === "private") : [];
2748
+ const publicHelpers = publicFns.map((f) => generatePublicFunction(f, address));
2749
+ const readOnlyHelpers = readOnlyFns.map((f) => generateReadOnlyFunction(f, address));
2750
+ const privateHelpers = privateFns.map((f) => generatePrivateFunction(f, address));
2751
+ const varsObject = generateVarsObject2(variables, address);
2752
+ const mapsObject = generateMapsObject2(maps, address);
2753
+ const allHelpers = [...publicHelpers, ...readOnlyHelpers, ...privateHelpers];
2754
+ if (varsObject) {
2755
+ allHelpers.push(varsObject);
2756
+ }
2757
+ if (mapsObject) {
2758
+ allHelpers.push(mapsObject);
2759
+ }
2760
+ if (allHelpers.length === 0) {
2761
+ return "";
2762
+ }
2763
+ return `export function get${pascalName}(simnet: Simnet) {
2764
+ const accounts = simnet.getAccounts();
2765
+
2766
+ return {
2767
+ ${allHelpers.join(`,
2768
+
2769
+ `)}
2770
+ };
2771
+ }`;
2772
+ }
2773
+ function generateGetContracts(contracts) {
2774
+ const contractEntries = contracts.map((contract) => {
2775
+ const camelName = toCamelCase5(contract.name);
2776
+ const pascalName = toPascalCase(contract.name);
2777
+ return `${camelName}: get${pascalName}(simnet)`;
2778
+ }).join(`,
2779
+ `);
2780
+ return `export function getContracts(simnet: Simnet) {
2781
+ const accounts = simnet.getAccounts();
2782
+
2783
+ return {
2784
+ accounts,
2785
+ ${contractEntries}
2786
+ };
2787
+ }`;
2788
+ }
2789
+ function generateTypeExports(contracts) {
2790
+ const typeExports = contracts.map((contract) => {
2791
+ const pascalName = toPascalCase(contract.name);
2792
+ return `export type ${pascalName}Helpers = ReturnType<typeof get${pascalName}>;`;
2793
+ }).join(`
2794
+ `);
2795
+ return `${typeExports}
2796
+ export type Contracts = ReturnType<typeof getContracts>;`;
2797
+ }
2798
+ async function generateTestingHelpers(contracts, options) {
2799
+ const contractHelpers = contracts.map((contract) => generateContractHelper(contract, options)).filter(Boolean);
2800
+ if (contractHelpers.length === 0) {
2801
+ return `// No contracts with functions to generate helpers for
2802
+ export {};`;
2803
+ }
2804
+ const getContractsCode = generateGetContracts(contracts);
2805
+ const typeExports = generateTypeExports(contracts);
2806
+ return `/**
2807
+ * Generated by @secondlayer/cli testing plugin
2808
+ * Type-safe helpers for Clarinet SDK unit tests
2809
+ */
2810
+
2811
+ import { type Simnet, Cl } from '@hirosystems/clarinet-sdk';
2812
+
2813
+ // ============================================
2814
+ // Per-contract factory functions
2815
+ // ============================================
2816
+
2817
+ ${contractHelpers.join(`
2818
+
2819
+ `)}
2820
+
2821
+ // ============================================
2822
+ // Convenience: all contracts at once
2823
+ // ============================================
2824
+
2825
+ ${getContractsCode}
2826
+
2827
+ // ============================================
2828
+ // Type exports
2829
+ // ============================================
2830
+
2831
+ ${typeExports}
2832
+ `;
2833
+ }
2834
+
2835
+ // src/plugins/testing/index.ts
2836
+ var testing = (options = {}) => {
2837
+ return {
2838
+ name: "@secondlayer/cli/plugin-testing",
2839
+ version: "1.0.0",
2840
+ async generate(context) {
2841
+ const { contracts } = context;
2842
+ const filteredContracts = contracts.filter((contract) => {
2843
+ if (options.include && !options.include.includes(contract.name)) {
2844
+ return false;
2845
+ }
2846
+ if (options.exclude && options.exclude.includes(contract.name)) {
2847
+ return false;
2848
+ }
2849
+ return true;
2850
+ });
2851
+ if (filteredContracts.length === 0) {
2852
+ if (options.debug) {
2853
+ context.logger.debug("Testing plugin: No contracts to process");
2854
+ }
2855
+ return;
2856
+ }
2857
+ if (options.debug) {
2858
+ context.logger.debug(`Testing plugin: Generating helpers for ${filteredContracts.length} contracts`);
2859
+ }
2860
+ const testingCode = await generateTestingHelpers(filteredContracts, options);
2861
+ const outputPath = options.out || "./src/generated/testing.ts";
2862
+ context.addOutput("testing", {
2863
+ path: outputPath,
2864
+ content: testingCode,
2865
+ type: "utils"
2866
+ });
2867
+ if (options.debug) {
2868
+ context.logger.debug(`Testing plugin: Generated helpers for ${filteredContracts.length} contracts`);
2869
+ }
2870
+ }
2871
+ };
2872
+ };
2873
+
2874
+ // src/plugins/index.ts
2875
+ function filterByOptions(items, options = {}) {
2876
+ let filtered = items;
2877
+ if (options.include && options.include.length > 0) {
2878
+ filtered = filtered.filter((item) => options.include.some((pattern) => item.name.includes(pattern) || item.name.match(new RegExp(pattern))));
2879
+ }
2880
+ if (options.exclude && options.exclude.length > 0) {
2881
+ filtered = filtered.filter((item) => !options.exclude.some((pattern) => item.name.includes(pattern) || item.name.match(new RegExp(pattern))));
2882
+ }
2883
+ return filtered;
2884
+ }
2885
+ function createPlugin(name, version, implementation) {
2886
+ return {
2887
+ name,
2888
+ version,
2889
+ ...implementation
2890
+ };
318
2891
  }
319
2892
  export {
320
- defineConfig,
2893
+ testing,
2894
+ react,
2895
+ hasClarinetProject,
2896
+ filterByOptions,
2897
+ createPlugin,
2898
+ clarinet,
2899
+ actions,
321
2900
  PluginManager
322
2901
  };
323
2902
 
324
- //# debugId=B1FF31C30533345F64756E2164756E21
2903
+ //# debugId=3D064257DAF105D164756E2164756E21
325
2904
  //# sourceMappingURL=index.js.map