@secondlayer/cli 0.1.0 → 0.2.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.
@@ -1,2622 +0,0 @@
1
- "use strict";
2
- var __create = Object.create;
3
- var __defProp = Object.defineProperty;
4
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
- var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __getProtoOf = Object.getPrototypeOf;
7
- var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __export = (target, all) => {
9
- for (var name in all)
10
- __defProp(target, name, { get: all[name], enumerable: true });
11
- };
12
- var __copyProps = (to, from, except, desc) => {
13
- if (from && typeof from === "object" || typeof from === "function") {
14
- for (let key of __getOwnPropNames(from))
15
- if (!__hasOwnProp.call(to, key) && key !== except)
16
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
- }
18
- return to;
19
- };
20
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
- // If the importer is in node compatibility mode or this is not an ESM
22
- // file that has been converted to a CommonJS file using a Babel-
23
- // compatible transform (i.e. "__esModule" has not been set), then set
24
- // "default" to the CommonJS "module.exports" for node compatibility.
25
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
- mod
27
- ));
28
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
-
30
- // src/plugins/index.ts
31
- var plugins_exports = {};
32
- __export(plugins_exports, {
33
- PluginManager: () => PluginManager,
34
- actions: () => actions,
35
- clarinet: () => clarinet,
36
- createPlugin: () => createPlugin,
37
- filterByOptions: () => filterByOptions,
38
- hasClarinetProject: () => hasClarinetProject,
39
- hiro: () => hiro,
40
- react: () => react
41
- });
42
- module.exports = __toCommonJS(plugins_exports);
43
-
44
- // src/core/plugin-manager.ts
45
- var import_prettier = require("prettier");
46
- var import_fs = require("fs");
47
- var import_path = __toESM(require("path"), 1);
48
- var import_transactions = require("@stacks/transactions");
49
- var PluginManager = class {
50
- constructor() {
51
- this.plugins = [];
52
- this.logger = this.createLogger();
53
- this.utils = this.createUtils();
54
- this.executionContext = {
55
- phase: "config",
56
- startTime: Date.now(),
57
- results: /* @__PURE__ */ new Map()
58
- };
59
- }
60
- /**
61
- * Register a plugin
62
- */
63
- register(plugin) {
64
- if (!plugin.name || !plugin.version) {
65
- throw new Error("Plugin must have a name and version");
66
- }
67
- const existing = this.plugins.find((p) => p.name === plugin.name);
68
- if (existing) {
69
- throw new Error(
70
- `Plugin "${plugin.name}" is already registered (version ${existing.version})`
71
- );
72
- }
73
- this.plugins.push(plugin);
74
- this.logger.debug(`Registered plugin: ${plugin.name}@${plugin.version}`);
75
- }
76
- /**
77
- * Get all registered plugins
78
- */
79
- getPlugins() {
80
- return [...this.plugins];
81
- }
82
- /**
83
- * Transform user config through all plugins
84
- */
85
- async transformConfig(config) {
86
- this.executionContext.phase = "config";
87
- let transformedConfig = { ...config };
88
- for (const plugin of this.plugins) {
89
- if (plugin.transformConfig) {
90
- this.executionContext.currentPlugin = plugin;
91
- try {
92
- const result = await plugin.transformConfig(transformedConfig);
93
- transformedConfig = result;
94
- this.recordHookResult(plugin.name, "transformConfig", {
95
- success: true
96
- });
97
- } catch (error) {
98
- const err = error;
99
- this.recordHookResult(plugin.name, "transformConfig", {
100
- success: false,
101
- error: err
102
- });
103
- throw new Error(
104
- `Plugin "${plugin.name}" failed during config transformation: ${err.message}`
105
- );
106
- }
107
- }
108
- }
109
- const resolvedConfig = {
110
- ...transformedConfig,
111
- plugins: this.plugins
112
- };
113
- return resolvedConfig;
114
- }
115
- /**
116
- * Transform contracts through all plugins
117
- */
118
- async transformContracts(contracts, _config) {
119
- const processedContracts = [];
120
- for (let contract of contracts) {
121
- if (contract._clarinetSource && contract.abi) {
122
- const address = typeof contract.address === "string" ? contract.address : "";
123
- const [contractAddress, contractName] = address.split(".");
124
- const processed = {
125
- name: contract.name || contractName,
126
- address: contractAddress,
127
- contractName,
128
- abi: contract.abi,
129
- source: "local",
130
- metadata: { source: "clarinet" }
131
- };
132
- processedContracts.push(processed);
133
- continue;
134
- }
135
- for (const plugin of this.plugins) {
136
- if (plugin.transformContract) {
137
- this.executionContext.currentPlugin = plugin;
138
- try {
139
- contract = await plugin.transformContract(contract);
140
- this.recordHookResult(plugin.name, "transformContract", {
141
- success: true
142
- });
143
- } catch (error) {
144
- const err = error;
145
- this.recordHookResult(plugin.name, "transformContract", {
146
- success: false,
147
- error: err
148
- });
149
- this.logger.warn(
150
- `Plugin "${plugin.name}" failed to transform contract: ${err.message}`
151
- );
152
- }
153
- }
154
- }
155
- if (contract.abi) {
156
- const processed = {
157
- name: contract.name || "unknown",
158
- address: typeof contract.address === "string" ? contract.address.split(".")[0] : "unknown",
159
- contractName: contract.name || "unknown",
160
- abi: contract.abi,
161
- source: "api",
162
- // Use "api" as default for plugin-processed contracts
163
- metadata: contract.metadata
164
- };
165
- processedContracts.push(processed);
166
- }
167
- }
168
- return processedContracts;
169
- }
170
- /**
171
- * Execute lifecycle hooks
172
- */
173
- async executeHook(hookName, context) {
174
- for (const plugin of this.plugins) {
175
- const hook = plugin[hookName];
176
- if (typeof hook === "function") {
177
- this.executionContext.currentPlugin = plugin;
178
- try {
179
- await hook.call(plugin, context);
180
- this.recordHookResult(plugin.name, hookName, {
181
- success: true
182
- });
183
- } catch (error) {
184
- const err = error;
185
- this.recordHookResult(plugin.name, hookName, {
186
- success: false,
187
- error: err
188
- });
189
- this.logger.error(
190
- `Plugin "${plugin.name}" failed during ${hookName}: ${err.message}`
191
- );
192
- }
193
- }
194
- }
195
- }
196
- /**
197
- * Execute generation phase with full context
198
- */
199
- async executeGeneration(contracts, config) {
200
- this.executionContext.phase = "generate";
201
- const outputs = /* @__PURE__ */ new Map();
202
- const context = {
203
- config,
204
- logger: this.logger,
205
- utils: this.utils,
206
- contracts,
207
- outputs,
208
- augment: (outputKey, contractName, content) => {
209
- this.augmentOutput(outputs, outputKey, contractName, content);
210
- },
211
- addOutput: (key, output) => {
212
- outputs.set(key, output);
213
- }
214
- };
215
- await this.executeHook("beforeGenerate", context);
216
- await this.executeHook("generate", context);
217
- await this.executeHook("afterGenerate", context);
218
- return outputs;
219
- }
220
- /**
221
- * Transform outputs through plugins
222
- */
223
- async transformOutputs(outputs) {
224
- this.executionContext.phase = "output";
225
- const transformedOutputs = /* @__PURE__ */ new Map();
226
- for (const [key, output] of outputs) {
227
- let transformedContent = output.content;
228
- for (const plugin of this.plugins) {
229
- if (plugin.transformOutput) {
230
- this.executionContext.currentPlugin = plugin;
231
- try {
232
- transformedContent = await plugin.transformOutput(
233
- transformedContent,
234
- output.type || "other"
235
- );
236
- this.recordHookResult(plugin.name, "transformOutput", {
237
- success: true
238
- });
239
- } catch (error) {
240
- const err = error;
241
- this.recordHookResult(plugin.name, "transformOutput", {
242
- success: false,
243
- error: err
244
- });
245
- this.logger.warn(
246
- `Plugin "${plugin.name}" failed to transform output: ${err.message}`
247
- );
248
- }
249
- }
250
- }
251
- transformedOutputs.set(key, {
252
- ...output,
253
- content: transformedContent
254
- });
255
- }
256
- return transformedOutputs;
257
- }
258
- /**
259
- * Write outputs to disk
260
- */
261
- async writeOutputs(outputs) {
262
- for (const [, output] of outputs) {
263
- try {
264
- const resolvedPath = import_path.default.resolve(process.cwd(), output.path);
265
- await this.utils.ensureDir(import_path.default.dirname(resolvedPath));
266
- await this.utils.writeFile(resolvedPath, output.content);
267
- } catch (error) {
268
- const err = error;
269
- this.logger.error(`Failed to write ${output.path}: ${err.message}`);
270
- throw err;
271
- }
272
- }
273
- }
274
- /**
275
- * Get execution results for debugging
276
- */
277
- getExecutionResults() {
278
- return new Map(this.executionContext.results);
279
- }
280
- /**
281
- * Augment existing output with additional content
282
- */
283
- augmentOutput(outputs, outputKey, contractName, content) {
284
- const existing = outputs.get(outputKey);
285
- if (!existing) {
286
- this.logger.warn(`Cannot augment non-existent output: ${outputKey}`);
287
- return;
288
- }
289
- const augmentedContent = `${existing.content}
290
-
291
- // Augmented by plugin for ${contractName}
292
- ${JSON.stringify(content, null, 2)}`;
293
- outputs.set(outputKey, {
294
- ...existing,
295
- content: augmentedContent
296
- });
297
- }
298
- /**
299
- * Record hook execution result
300
- */
301
- recordHookResult(pluginName, hookName, result) {
302
- const key = `${pluginName}:${hookName}`;
303
- const existing = this.executionContext.results.get(key) || [];
304
- existing.push({ ...result, plugin: pluginName });
305
- this.executionContext.results.set(key, existing);
306
- }
307
- /**
308
- * Create logger instance
309
- */
310
- createLogger() {
311
- return {
312
- info: (message) => console.log(`\u2139\uFE0F ${message}`),
313
- warn: (message) => console.warn(`\u26A0\uFE0F ${message}`),
314
- error: (message) => console.error(`\u274C ${message}`),
315
- debug: (message) => {
316
- if (process.env.DEBUG) {
317
- console.log(`\u{1F41B} ${message}`);
318
- }
319
- },
320
- success: (message) => console.log(`\u2705 ${message}`)
321
- };
322
- }
323
- /**
324
- * Create utils instance
325
- */
326
- createUtils() {
327
- return {
328
- toCamelCase: (str) => {
329
- return str.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
330
- },
331
- toKebabCase: (str) => {
332
- return str.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
333
- },
334
- validateAddress: (address) => {
335
- return (0, import_transactions.validateStacksAddress)(address.split(".")[0]);
336
- },
337
- parseContractId: (contractId) => {
338
- const [address, contractName] = contractId.split(".");
339
- return { address, contractName };
340
- },
341
- formatCode: async (code) => {
342
- return (0, import_prettier.format)(code, {
343
- parser: "typescript",
344
- singleQuote: true,
345
- semi: true,
346
- printWidth: 100,
347
- trailingComma: "es5"
348
- });
349
- },
350
- resolvePath: (relativePath) => {
351
- return import_path.default.resolve(process.cwd(), relativePath);
352
- },
353
- fileExists: async (filePath) => {
354
- try {
355
- await import_fs.promises.access(filePath);
356
- return true;
357
- } catch {
358
- return false;
359
- }
360
- },
361
- readFile: async (filePath) => {
362
- return import_fs.promises.readFile(filePath, "utf-8");
363
- },
364
- writeFile: async (filePath, content) => {
365
- await import_fs.promises.writeFile(filePath, content, "utf-8");
366
- },
367
- ensureDir: async (dirPath) => {
368
- await import_fs.promises.mkdir(dirPath, { recursive: true });
369
- }
370
- };
371
- }
372
- };
373
-
374
- // src/plugins/clarinet/index.ts
375
- var import_clarinet_sdk = require("@hirosystems/clarinet-sdk");
376
-
377
- // src/generators/contract.ts
378
- var import_prettier2 = require("prettier");
379
- async function generateContractInterface(contracts) {
380
- const imports = `import { Cl, validateStacksAddress } from '@stacks/transactions'`;
381
- const header = `/**
382
- * Generated by @stacks/codegen
383
- * DO NOT EDIT MANUALLY
384
- */`;
385
- const contractsCode = contracts.map((contract) => generateContract(contract)).join("\n\n");
386
- const code = `${imports}
387
-
388
- ${header}
389
-
390
- ${contractsCode}`;
391
- const formatted = await (0, import_prettier2.format)(code, {
392
- parser: "typescript",
393
- singleQuote: true,
394
- semi: true,
395
- printWidth: 100,
396
- trailingComma: "es5"
397
- });
398
- return formatted;
399
- }
400
- function generateContract(contract) {
401
- const { name, address, contractName, abi } = contract;
402
- const abiCode = generateAbiConstant(name, abi);
403
- const methods = abi.functions.filter((func) => func.access !== "private").map((func) => generateMethod(func, address, contractName)).join(",\n\n ");
404
- const contractCode = `export const ${name} = {
405
- address: '${address}',
406
- contractAddress: '${address}',
407
- contractName: '${contractName}',
408
-
409
- ${methods}
410
- } as const`;
411
- return `${abiCode}
412
-
413
- ${contractCode}`;
414
- }
415
- function generateAbiConstant(name, abi) {
416
- const abiJson = JSON.stringify(abi, null, 2).replace(/"([a-zA-Z_$][a-zA-Z0-9_$]*)":/g, "$1:").replace(/"/g, "'");
417
- return `export const ${name}Abi = ${abiJson} as const`;
418
- }
419
- function generateMethod(func, address, contractName) {
420
- const methodName = toCamelCase(func.name);
421
- if (func.args.length === 0) {
422
- return `${methodName}() {
423
- return {
424
- contractAddress: '${address}',
425
- contractName: '${contractName}',
426
- functionName: '${func.name}',
427
- functionArgs: []
428
- }
429
- }`;
430
- }
431
- if (func.args.length === 1) {
432
- const originalArgName = func.args[0].name;
433
- const argName = toCamelCase(originalArgName);
434
- const argType = getTypeForArg(func.args[0]);
435
- const clarityConversion = generateClarityConversion(argName, func.args[0]);
436
- return `${methodName}(...args: [{ ${argName}: ${argType} }] | [${argType}]) {
437
- const ${argName} = args.length === 1 && typeof args[0] === 'object' && args[0] !== null && '${argName}' in args[0]
438
- ? args[0].${argName}
439
- : args[0] as ${argType}
440
-
441
- return {
442
- contractAddress: '${address}',
443
- contractName: '${contractName}',
444
- functionName: '${func.name}',
445
- functionArgs: [${clarityConversion}]
446
- }
447
- }`;
448
- }
449
- const argsList = func.args.map((arg) => toCamelCase(arg.name)).join(", ");
450
- const argsTypes = func.args.map((arg) => {
451
- const camelName = toCamelCase(arg.name);
452
- return `${camelName}: ${getTypeForArg(arg)}`;
453
- }).join("; ");
454
- const argsArray = func.args.map((arg) => {
455
- const argName = toCamelCase(arg.name);
456
- return generateClarityConversion(argName, arg);
457
- }).join(", ");
458
- const objectAccess = func.args.map((arg) => {
459
- const camelName = toCamelCase(arg.name);
460
- return `args[0].${camelName}`;
461
- }).join(", ");
462
- const positionTypes = func.args.map((arg) => getTypeForArg(arg)).join(", ");
463
- return `${methodName}(...args: [{ ${argsTypes} }] | [${positionTypes}]) {
464
- const [${argsList}] = args.length === 1 && typeof args[0] === 'object' && args[0] !== null
465
- ? [${objectAccess}]
466
- : args as [${positionTypes}]
467
-
468
- return {
469
- contractAddress: '${address}',
470
- contractName: '${contractName}',
471
- functionName: '${func.name}',
472
- functionArgs: [${argsArray}]
473
- }
474
- }`;
475
- }
476
- function getTypeForArg(arg) {
477
- const type = arg.type;
478
- if (typeof type === "string") {
479
- switch (type) {
480
- case "uint128":
481
- case "int128":
482
- return "bigint";
483
- case "bool":
484
- return "boolean";
485
- case "principal":
486
- case "trait_reference":
487
- return "string";
488
- default:
489
- return "any";
490
- }
491
- }
492
- if (type["string-ascii"] || type["string-utf8"]) {
493
- return "string";
494
- }
495
- if (type.buff) {
496
- return "Uint8Array | string | { type: 'ascii' | 'utf8' | 'hex'; value: string }";
497
- }
498
- if (type.optional) {
499
- const innerType = getTypeForArg({ type: type.optional });
500
- return `${innerType} | null`;
501
- }
502
- if (type.list) {
503
- const innerType = getTypeForArg({ type: type.list.type });
504
- return `${innerType}[]`;
505
- }
506
- if (type.tuple) {
507
- const fields = type.tuple.map(
508
- (field) => `${toCamelCase(field.name)}: ${getTypeForArg({ type: field.type })}`
509
- ).join("; ");
510
- return `{ ${fields} }`;
511
- }
512
- if (type.response) {
513
- const okType = getTypeForArg({ type: type.response.ok });
514
- const errType = getTypeForArg({ type: type.response.error });
515
- return `{ ok: ${okType} } | { err: ${errType} }`;
516
- }
517
- return "any";
518
- }
519
- function toCamelCase(str) {
520
- return str.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()).replace(/-([A-Z])/g, (_, letter) => letter).replace(/-(\d)/g, (_, digit) => digit).replace(/-/g, "");
521
- }
522
- function generateClarityConversion(argName, argType) {
523
- const type = argType.type;
524
- if (typeof type === "string") {
525
- switch (type) {
526
- case "uint128":
527
- return `Cl.uint(${argName})`;
528
- case "int128":
529
- return `Cl.int(${argName})`;
530
- case "bool":
531
- return `Cl.bool(${argName})`;
532
- case "principal":
533
- case "trait_reference":
534
- return `(() => {
535
- const [address, contractName] = ${argName}.split(".") as [string, string];
536
- if (!validateStacksAddress(address)) {
537
- throw new Error("Invalid Stacks address format");
538
- }
539
- if (${argName}.includes(".")) {
540
- return Cl.contractPrincipal(address, contractName);
541
- } else {
542
- return Cl.standardPrincipal(${argName});
543
- }
544
- })()`;
545
- default:
546
- return `${argName}`;
547
- }
548
- }
549
- if (type["string-ascii"]) {
550
- return `Cl.stringAscii(${argName})`;
551
- }
552
- if (type["string-utf8"]) {
553
- return `Cl.stringUtf8(${argName})`;
554
- }
555
- if (type.buff) {
556
- return `(() => {
557
- const value = ${argName};
558
- // Direct Uint8Array
559
- if (value instanceof Uint8Array) {
560
- return Cl.buffer(value);
561
- }
562
- // Object notation with explicit type
563
- if (typeof value === 'object' && value !== null && value.type && value.value) {
564
- switch (value.type) {
565
- case 'ascii':
566
- return Cl.bufferFromAscii(value.value);
567
- case 'utf8':
568
- return Cl.bufferFromUtf8(value.value);
569
- case 'hex':
570
- return Cl.bufferFromHex(value.value);
571
- default:
572
- throw new Error(\`Unsupported buffer type: \${value.type}\`);
573
- }
574
- }
575
- // Auto-detect string type
576
- if (typeof value === 'string') {
577
- // Check for hex (0x prefix or pure hex pattern)
578
- if (value.startsWith('0x') || /^[0-9a-fA-F]+$/.test(value)) {
579
- return Cl.bufferFromHex(value);
580
- }
581
- // Check for non-ASCII characters (UTF-8)
582
- if (!/^[\\x00-\\x7F]*$/.test(value)) {
583
- return Cl.bufferFromUtf8(value);
584
- }
585
- // Default to ASCII for simple ASCII strings
586
- return Cl.bufferFromAscii(value);
587
- }
588
- throw new Error(\`Invalid buffer value: \${value}\`);
589
- })()`;
590
- }
591
- if (type.optional) {
592
- const innerConversion = generateClarityConversion(argName, {
593
- type: type.optional
594
- });
595
- return `${argName} !== null ? Cl.some(${innerConversion.replace(argName, `${argName}`)}) : Cl.none()`;
596
- }
597
- if (type.list) {
598
- const innerConversion = generateClarityConversion("item", {
599
- type: type.list.type
600
- });
601
- return `Cl.list(${argName}.map(item => ${innerConversion}))`;
602
- }
603
- if (type.tuple) {
604
- const fields = type.tuple.map((field) => {
605
- const camelFieldName = toCamelCase(field.name);
606
- const fieldConversion = generateClarityConversion(
607
- `${argName}.${camelFieldName}`,
608
- { type: field.type }
609
- );
610
- return `"${field.name}": ${fieldConversion}`;
611
- }).join(", ");
612
- return `Cl.tuple({ ${fields} })`;
613
- }
614
- if (type.response) {
615
- const okConversion = generateClarityConversion(`${argName}.ok`, {
616
- type: type.response.ok
617
- });
618
- const errConversion = generateClarityConversion(`${argName}.err`, {
619
- type: type.response.error
620
- });
621
- return `'ok' in ${argName} ? Cl.ok(${okConversion.replace(`${argName}.ok`, `${argName}.ok`)}) : Cl.error(${errConversion.replace(`${argName}.err`, `${argName}.err`)})`;
622
- }
623
- return `${argName}`;
624
- }
625
-
626
- // src/plugins/clarinet/index.ts
627
- function toCamelCase2(str) {
628
- return str.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()).replace(/-([A-Z])/g, (_, letter) => letter).replace(/-(\d)/g, (_, digit) => digit).replace(/-/g, "").replace(/^\d/, "_$&");
629
- }
630
- function sanitizeContractName(name) {
631
- return toCamelCase2(name);
632
- }
633
- async function isUserDefinedContract(contractId, manifestPath) {
634
- const [address, contractName] = contractId.split(".");
635
- try {
636
- const { promises: fs2 } = await import("fs");
637
- const tomlContent = await fs2.readFile(manifestPath, "utf-8");
638
- const contractSectionRegex = /^\[contracts\.([^\]]+)\]/gm;
639
- const userContracts = /* @__PURE__ */ new Set();
640
- let match;
641
- while ((match = contractSectionRegex.exec(tomlContent)) !== null) {
642
- userContracts.add(match[1]);
643
- }
644
- if (userContracts.has(contractName)) {
645
- return true;
646
- }
647
- } catch (error) {
648
- }
649
- const systemContractPatterns = [
650
- /^pox-\d+$/,
651
- // pox-2, pox-3, etc.
652
- /^bns$/,
653
- // Blockchain Name System
654
- /^costs-\d+$/,
655
- // costs-2, costs-3, etc.
656
- /^lockup$/
657
- // lockup contract
658
- ];
659
- if (systemContractPatterns.some((pattern) => pattern.test(contractName))) {
660
- return false;
661
- }
662
- const systemAddresses = [
663
- "SP000000000000000000002Q6VF78",
664
- // Boot contracts address
665
- "ST000000000000000000002AMW42H"
666
- // Boot contracts address (testnet)
667
- ];
668
- if (systemAddresses.includes(address)) {
669
- return false;
670
- }
671
- return true;
672
- }
673
- var clarinet = (options = {}) => {
674
- const manifestPath = options.path || "./Clarinet.toml";
675
- let simnet;
676
- return {
677
- name: "@stacks/codegen/plugin-clarinet",
678
- version: "1.0.0",
679
- async transformConfig(config) {
680
- try {
681
- simnet = await (0, import_clarinet_sdk.initSimnet)(manifestPath);
682
- const contractInterfaces = simnet.getContractsInterfaces();
683
- const contracts = [];
684
- for (const [contractId, abi] of contractInterfaces) {
685
- const [_, contractName] = contractId.split(".");
686
- if (!await isUserDefinedContract(contractId, manifestPath)) {
687
- if (options.debug) {
688
- console.log(`\u{1F6AB} Skipping system contract: ${contractId}`);
689
- }
690
- continue;
691
- }
692
- if (options.include && !options.include.includes(contractName)) {
693
- continue;
694
- }
695
- if (options.exclude && options.exclude.includes(contractName)) {
696
- continue;
697
- }
698
- const sanitizedName = sanitizeContractName(contractName);
699
- contracts.push({
700
- name: sanitizedName,
701
- address: contractId,
702
- abi,
703
- // Remove source field - this was causing the path resolution issue
704
- _clarinetSource: true
705
- // Internal flag for our plugin
706
- });
707
- }
708
- if (options.debug) {
709
- console.log(
710
- `\u{1F50D} Clarinet plugin found ${contracts.length} user-defined contracts`
711
- );
712
- }
713
- return {
714
- ...config,
715
- contracts: [...config.contracts || [], ...contracts]
716
- };
717
- } catch (error) {
718
- const err = error;
719
- if (options.debug) {
720
- console.warn(
721
- `\u26A0\uFE0F Clarinet plugin failed to load contracts: ${err.message}`
722
- );
723
- }
724
- return config;
725
- }
726
- },
727
- async generate(context) {
728
- const clarinetContracts = context.contracts.filter(
729
- (contract) => contract.metadata?.source === "clarinet"
730
- );
731
- if (clarinetContracts.length === 0) {
732
- return;
733
- }
734
- if (options.debug) {
735
- context.logger.debug(
736
- `Generating interfaces for ${clarinetContracts.length} Clarinet contracts`
737
- );
738
- }
739
- const contractsCode = await generateContractInterface(clarinetContracts);
740
- context.addOutput("contracts", {
741
- path: context.config.out,
742
- content: contractsCode,
743
- type: "contracts"
744
- });
745
- }
746
- };
747
- };
748
- async function hasClarinetProject(path2 = "./Clarinet.toml") {
749
- try {
750
- const { promises: fs2 } = await import("fs");
751
- await fs2.access(path2);
752
- return true;
753
- } catch {
754
- return false;
755
- }
756
- }
757
-
758
- // src/plugins/actions/generators.ts
759
- function toCamelCase3(str) {
760
- return str.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()).replace(/-([A-Z])/g, (_, letter) => letter).replace(/-(\d)/g, (_, digit) => digit).replace(/-/g, "").replace(/^\d/, "_$&");
761
- }
762
- function getTypeForArg2(arg) {
763
- const type = arg.type;
764
- if (typeof type === "string") {
765
- switch (type) {
766
- case "uint128":
767
- case "int128":
768
- return "bigint";
769
- case "bool":
770
- return "boolean";
771
- case "principal":
772
- case "trait_reference":
773
- return "string";
774
- default:
775
- return "any";
776
- }
777
- }
778
- if (type["string-ascii"] || type["string-utf8"]) {
779
- return "string";
780
- }
781
- if (type.buff) {
782
- return "Uint8Array | string | { type: 'ascii' | 'utf8' | 'hex'; value: string }";
783
- }
784
- if (type.optional) {
785
- const innerType = getTypeForArg2({ type: type.optional });
786
- return `${innerType} | null`;
787
- }
788
- if (type.list) {
789
- const innerType = getTypeForArg2({ type: type.list.type });
790
- return `${innerType}[]`;
791
- }
792
- if (type.tuple) {
793
- const fields = type.tuple.map(
794
- (field) => `${toCamelCase3(field.name)}: ${getTypeForArg2({ type: field.type })}`
795
- ).join("; ");
796
- return `{ ${fields} }`;
797
- }
798
- if (type.response) {
799
- const okType = getTypeForArg2({ type: type.response.ok });
800
- const errType = getTypeForArg2({ type: type.response.error });
801
- return `{ ok: ${okType} } | { err: ${errType} }`;
802
- }
803
- return "any";
804
- }
805
- function generateArgsSignature(args) {
806
- if (args.length === 0) return "";
807
- const argsTypes = args.map((arg) => {
808
- const camelName = toCamelCase3(arg.name);
809
- return `${camelName}: ${getTypeForArg2(arg)}`;
810
- }).join("; ");
811
- return `args: { ${argsTypes} }, `;
812
- }
813
- function generateClarityArgs(args, contractName) {
814
- if (args.length === 0) return "";
815
- return args.map((arg) => {
816
- const argName = `args.${toCamelCase3(arg.name)}`;
817
- return generateClarityConversion2(argName, arg);
818
- }).join(", ");
819
- }
820
- function generateClarityConversion2(argName, argType) {
821
- const type = argType.type;
822
- if (typeof type === "string") {
823
- switch (type) {
824
- case "uint128":
825
- return `Cl.uint(${argName})`;
826
- case "int128":
827
- return `Cl.int(${argName})`;
828
- case "bool":
829
- return `Cl.bool(${argName})`;
830
- case "principal":
831
- case "trait_reference":
832
- return `(() => {
833
- const [address, contractName] = ${argName}.split(".") as [string, string];
834
- if (!validateStacksAddress(address)) {
835
- throw new Error("Invalid Stacks address format");
836
- }
837
- if (${argName}.includes(".")) {
838
- return Cl.contractPrincipal(address, contractName);
839
- } else {
840
- return Cl.standardPrincipal(${argName});
841
- }
842
- })()`;
843
- default:
844
- return `${argName}`;
845
- }
846
- }
847
- if (type["string-ascii"]) {
848
- return `Cl.stringAscii(${argName})`;
849
- }
850
- if (type["string-utf8"]) {
851
- return `Cl.stringUtf8(${argName})`;
852
- }
853
- if (type.buff) {
854
- return `(() => {
855
- const value = ${argName};
856
- if (value instanceof Uint8Array) {
857
- return Cl.buffer(value);
858
- }
859
- if (typeof value === 'object' && value !== null && value.type && value.value) {
860
- switch (value.type) {
861
- case 'ascii':
862
- return Cl.bufferFromAscii(value.value);
863
- case 'utf8':
864
- return Cl.bufferFromUtf8(value.value);
865
- case 'hex':
866
- return Cl.bufferFromHex(value.value);
867
- default:
868
- throw new Error(\`Unsupported buffer type: \${value.type}\`);
869
- }
870
- }
871
- if (typeof value === 'string') {
872
- if (value.startsWith('0x') || /^[0-9a-fA-F]+$/.test(value)) {
873
- return Cl.bufferFromHex(value);
874
- }
875
- if (!/^[\\x00-\\x7F]*$/.test(value)) {
876
- return Cl.bufferFromUtf8(value);
877
- }
878
- return Cl.bufferFromAscii(value);
879
- }
880
- throw new Error(\`Invalid buffer value: \${value}\`);
881
- })()`;
882
- }
883
- if (type.optional) {
884
- const innerConversion = generateClarityConversion2(argName, {
885
- type: type.optional
886
- });
887
- return `${argName} !== null ? Cl.some(${innerConversion.replace(argName, `${argName}`)}) : Cl.none()`;
888
- }
889
- if (type.list) {
890
- const innerConversion = generateClarityConversion2("item", {
891
- type: type.list.type
892
- });
893
- return `Cl.list(${argName}.map(item => ${innerConversion}))`;
894
- }
895
- if (type.tuple) {
896
- const fields = type.tuple.map((field) => {
897
- const camelFieldName = toCamelCase3(field.name);
898
- const fieldConversion = generateClarityConversion2(
899
- `${argName}.${camelFieldName}`,
900
- { type: field.type }
901
- );
902
- return `"${field.name}": ${fieldConversion}`;
903
- }).join(", ");
904
- return `Cl.tuple({ ${fields} })`;
905
- }
906
- if (type.response) {
907
- const okConversion = generateClarityConversion2(`${argName}.ok`, {
908
- type: type.response.ok
909
- });
910
- const errConversion = generateClarityConversion2(`${argName}.err`, {
911
- type: type.response.error
912
- });
913
- return `'ok' in ${argName} ? Cl.ok(${okConversion.replace(`${argName}.ok`, `${argName}.ok`)}) : Cl.error(${errConversion.replace(`${argName}.err`, `${argName}.err`)})`;
914
- }
915
- return `${argName}`;
916
- }
917
- function generateReadHelpers(contract, options) {
918
- const { abi, name } = contract;
919
- const functions = abi.functions || [];
920
- const readOnlyFunctions = functions.filter(
921
- (f) => f.access === "read_only" || f.access === "read-only"
922
- );
923
- if (readOnlyFunctions.length === 0) {
924
- return "";
925
- }
926
- const filteredFunctions = readOnlyFunctions.filter(
927
- (func) => {
928
- if (options.includeFunctions && !options.includeFunctions.includes(func.name)) {
929
- return false;
930
- }
931
- if (options.excludeFunctions && options.excludeFunctions.includes(func.name)) {
932
- return false;
933
- }
934
- return true;
935
- }
936
- );
937
- if (filteredFunctions.length === 0) {
938
- return "";
939
- }
940
- const helpers = filteredFunctions.map((func) => {
941
- const methodName = toCamelCase3(func.name);
942
- const argsSignature = generateArgsSignature(func.args);
943
- const clarityArgs = generateClarityArgs(func.args, name);
944
- return `async ${methodName}(${argsSignature}options?: {
945
- network?: 'mainnet' | 'testnet' | 'devnet';
946
- senderAddress?: string;
947
- }) {
948
- return await fetchCallReadOnlyFunction({
949
- contractAddress: '${contract.address}',
950
- contractName: '${contract.contractName}',
951
- functionName: '${func.name}',
952
- functionArgs: [${clarityArgs}],
953
- network: options?.network || 'mainnet',
954
- senderAddress: options?.senderAddress || 'SP000000000000000000002Q6VF78'
955
- });
956
- }`;
957
- });
958
- return `read: {
959
- ${helpers.join(",\n\n ")}
960
- }`;
961
- }
962
- function generateWriteHelpers(contract, options) {
963
- const { abi, name } = contract;
964
- const functions = abi.functions || [];
965
- const publicFunctions = functions.filter(
966
- (f) => f.access === "public"
967
- );
968
- if (publicFunctions.length === 0) {
969
- return "";
970
- }
971
- const filteredFunctions = publicFunctions.filter((func) => {
972
- if (options.includeFunctions && !options.includeFunctions.includes(func.name)) {
973
- return false;
974
- }
975
- if (options.excludeFunctions && options.excludeFunctions.includes(func.name)) {
976
- return false;
977
- }
978
- return true;
979
- });
980
- if (filteredFunctions.length === 0) {
981
- return "";
982
- }
983
- const helpers = filteredFunctions.map((func) => {
984
- const methodName = toCamelCase3(func.name);
985
- const argsSignature = generateArgsSignature(func.args);
986
- const clarityArgs = generateClarityArgs(func.args, name);
987
- return `async ${methodName}(${argsSignature}options: {
988
- senderKey: string;
989
- network?: 'mainnet' | 'testnet' | 'devnet';
990
- fee?: string | number | undefined;
991
- nonce?: bigint;
992
- anchorMode?: 1 | 2 | 3; // AnchorMode: OnChainOnly = 1, OffChainOnly = 2, Any = 3
993
- postConditions?: any[]; // TODO: Add proper PostCondition types
994
- validateWithAbi?: boolean;
995
- }) {
996
- const { senderKey, network = 'mainnet', ...txOptions } = options;
997
-
998
- return await makeContractCall({
999
- contractAddress: '${contract.address}',
1000
- contractName: '${contract.contractName}',
1001
- functionName: '${func.name}',
1002
- functionArgs: [${clarityArgs}],
1003
- senderKey,
1004
- network,
1005
- validateWithAbi: true,
1006
- ...txOptions
1007
- });
1008
- }`;
1009
- });
1010
- return `write: {
1011
- ${helpers.join(",\n\n ")}
1012
- }`;
1013
- }
1014
- async function generateActionHelpers(contract, options) {
1015
- const readHelpers = generateReadHelpers(contract, options);
1016
- const writeHelpers = generateWriteHelpers(contract, options);
1017
- if (!readHelpers && !writeHelpers) {
1018
- return "";
1019
- }
1020
- const helpers = [readHelpers, writeHelpers].filter(Boolean);
1021
- return helpers.join(",\n\n");
1022
- }
1023
-
1024
- // src/plugins/actions/index.ts
1025
- var actions = (options = {}) => {
1026
- return {
1027
- name: "@stacks/codegen/plugin-actions",
1028
- version: "1.0.0",
1029
- async generate(context) {
1030
- const { contracts } = context;
1031
- const filteredContracts = contracts.filter((contract) => {
1032
- if (options.include && !options.include.includes(contract.name)) {
1033
- return false;
1034
- }
1035
- if (options.exclude && options.exclude.includes(contract.name)) {
1036
- return false;
1037
- }
1038
- return true;
1039
- });
1040
- if (filteredContracts.length === 0) {
1041
- if (options.debug) {
1042
- context.logger.debug("Actions plugin: No contracts to process");
1043
- }
1044
- return;
1045
- }
1046
- if (options.debug) {
1047
- context.logger.debug(
1048
- `Actions plugin: Generating read/write helpers for ${filteredContracts.length} contracts`
1049
- );
1050
- }
1051
- const contractHelpers = /* @__PURE__ */ new Map();
1052
- for (const contract of filteredContracts) {
1053
- const actionsCode = await generateActionHelpers(contract, options);
1054
- if (actionsCode) {
1055
- contractHelpers.set(contract.name, actionsCode);
1056
- }
1057
- }
1058
- if (contractHelpers.size > 0) {
1059
- const existingOutput = context.outputs.get("contracts");
1060
- if (existingOutput) {
1061
- let modifiedContent = addRequiredImports(existingOutput.content);
1062
- for (const [contractName, helpersCode] of contractHelpers) {
1063
- modifiedContent = injectHelpersIntoContract(
1064
- modifiedContent,
1065
- contractName,
1066
- helpersCode
1067
- );
1068
- }
1069
- context.outputs.set("contracts", {
1070
- ...existingOutput,
1071
- content: modifiedContent
1072
- });
1073
- }
1074
- }
1075
- }
1076
- };
1077
- };
1078
- function addRequiredImports(content) {
1079
- if (content.includes("fetchCallReadOnlyFunction") && content.includes("makeContractCall")) {
1080
- return content;
1081
- }
1082
- const transactionsImportRegex = /import\s+\{([^}]+)\}\s+from\s+['"]@stacks\/transactions['"];/;
1083
- const match = content.match(transactionsImportRegex);
1084
- if (match) {
1085
- const existingImports = match[1].trim();
1086
- const newImports = ["fetchCallReadOnlyFunction", "makeContractCall"].filter((imp) => !existingImports.includes(imp)).join(", ");
1087
- if (newImports) {
1088
- const updatedImport = `import { ${existingImports}, ${newImports} } from '@stacks/transactions';`;
1089
- return content.replace(transactionsImportRegex, updatedImport);
1090
- }
1091
- }
1092
- return content;
1093
- }
1094
- function injectHelpersIntoContract(content, contractName, helpersCode) {
1095
- const contractPattern = new RegExp(
1096
- `(export const ${contractName} = \\{[\\s\\S]*?)\\n\\} as const;`,
1097
- "g"
1098
- );
1099
- return content.replace(contractPattern, (_, contractBody) => {
1100
- const cleanBody = contractBody.replace(/,\s*$/, "");
1101
- const indentedHelpersCode = helpersCode.split("\n").map((line) => {
1102
- if (line.match(/^[a-zA-Z_$][a-zA-Z0-9_$]*\s*:/)) {
1103
- return ` ${line}`;
1104
- }
1105
- return line;
1106
- }).join("\n");
1107
- return `${cleanBody},
1108
-
1109
- ${indentedHelpersCode}
1110
- } as const;`;
1111
- });
1112
- }
1113
-
1114
- // src/plugins/react/provider/index.ts
1115
- var import_prettier3 = require("prettier");
1116
- async function generateProvider() {
1117
- const code = `/**
1118
- * Generated Stacks React Provider
1119
- * DO NOT EDIT MANUALLY
1120
- */
1121
-
1122
- import React, { createContext, useContext } from 'react'
1123
-
1124
- /**
1125
- * Stacks configuration interface
1126
- */
1127
- export interface StacksReactConfig {
1128
- /**
1129
- * Network to use for API calls
1130
- */
1131
- network: 'mainnet' | 'testnet' | 'devnet'
1132
-
1133
- /**
1134
- * API key for Stacks API (optional)
1135
- */
1136
- apiKey?: string
1137
-
1138
- /**
1139
- * Base URL for Stacks API (optional override)
1140
- */
1141
- apiUrl?: string
1142
-
1143
- /**
1144
- * Default sender address for read-only calls
1145
- */
1146
- senderAddress?: string
1147
- }
1148
-
1149
- /**
1150
- * Provider component props
1151
- */
1152
- export interface StacksProviderProps {
1153
- children: React.ReactNode
1154
- config: StacksReactConfig
1155
- }
1156
-
1157
- /**
1158
- * React context for Stacks configuration
1159
- */
1160
- const StacksContext = createContext<StacksReactConfig | undefined>(undefined)
1161
- StacksContext.displayName = 'StacksContext'
1162
-
1163
- /**
1164
- * Create a Stacks React configuration with defaults
1165
- */
1166
- export function createStacksConfig(config: StacksReactConfig): StacksReactConfig {
1167
- return {
1168
- network: config.network,
1169
- apiKey: config.apiKey,
1170
- apiUrl: config.apiUrl,
1171
- senderAddress: config.senderAddress || 'SP000000000000000000002Q6VF78'
1172
- }
1173
- }
1174
-
1175
- /**
1176
- * Provider component that makes Stacks configuration available to hooks
1177
- */
1178
- export function StacksProvider({ children, config }: StacksProviderProps) {
1179
- const resolvedConfig = createStacksConfig(config)
1180
-
1181
- return (
1182
- <StacksContext.Provider value={resolvedConfig}>
1183
- {children}
1184
- </StacksContext.Provider>
1185
- )
1186
- }
1187
-
1188
- /**
1189
- * Hook to access the Stacks configuration
1190
- */
1191
- export function useStacksConfig(): StacksReactConfig {
1192
- const context = useContext(StacksContext)
1193
-
1194
- if (context === undefined) {
1195
- throw new Error(
1196
- 'useStacksConfig must be used within a StacksProvider. ' +
1197
- 'Make sure to wrap your app with <StacksProvider config={{...}}>'
1198
- )
1199
- }
1200
-
1201
- return context
1202
- }`;
1203
- const formatted = await (0, import_prettier3.format)(code, {
1204
- parser: "typescript",
1205
- singleQuote: true,
1206
- semi: false,
1207
- printWidth: 100,
1208
- trailingComma: "es5"
1209
- });
1210
- return formatted;
1211
- }
1212
-
1213
- // src/plugins/react/generators/generic.ts
1214
- var import_prettier4 = require("prettier");
1215
- var GENERIC_HOOKS = [
1216
- "useAccount",
1217
- "useConnect",
1218
- "useDisconnect",
1219
- "useNetwork",
1220
- "useContract",
1221
- "useOpenSTXTransfer",
1222
- "useSignMessage",
1223
- "useDeployContract",
1224
- "useReadContract",
1225
- "useTransaction",
1226
- "useBlock",
1227
- "useAccountTransactions",
1228
- "useWaitForTransaction"
1229
- ];
1230
- async function generateGenericHooks(excludeList = []) {
1231
- const hooksToGenerate = GENERIC_HOOKS.filter(
1232
- (hookName) => !excludeList.includes(hookName)
1233
- );
1234
- const imports = `import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
1235
- import { useState, useCallback } from 'react'
1236
- import { useStacksConfig } from './provider'
1237
- import { connect, disconnect, isConnected, request, openContractCall as stacksOpenContractCall } from '@stacks/connect'
1238
- import { Cl, validateStacksAddress } from '@stacks/transactions'
1239
- import type { ExtractFunctionArgs, ExtractFunctionNames, ClarityContract } from '@secondlayer/clarity-types'`;
1240
- const header = `/**
1241
- * Generated generic Stacks React hooks
1242
- * DO NOT EDIT MANUALLY
1243
- */`;
1244
- const hooksCode = hooksToGenerate.map((hookName) => generateGenericHook(hookName)).filter(Boolean).join("\n\n");
1245
- const code = `${imports}
1246
-
1247
- ${header}
1248
-
1249
- ${hooksCode}`;
1250
- const formatted = await (0, import_prettier4.format)(code, {
1251
- parser: "typescript",
1252
- singleQuote: true,
1253
- semi: false,
1254
- printWidth: 100,
1255
- trailingComma: "es5"
1256
- });
1257
- return formatted;
1258
- }
1259
- function generateGenericHook(hookName) {
1260
- switch (hookName) {
1261
- case "useAccount":
1262
- return `export function useAccount() {
1263
- const config = useStacksConfig()
1264
-
1265
- return useQuery({
1266
- queryKey: ['stacks-account', config.network],
1267
- queryFn: async () => {
1268
- try {
1269
- // Check if already connected using @stacks/connect v8
1270
- const connected = isConnected()
1271
-
1272
- if (!connected) {
1273
- return {
1274
- address: undefined,
1275
- addresses: undefined,
1276
- isConnected: false,
1277
- isConnecting: false,
1278
- isDisconnected: true,
1279
- status: 'disconnected' as const
1280
- }
1281
- }
1282
-
1283
- // Get addresses using @stacks/connect v8 request method (SIP-030)
1284
- const result = await request('stx_getAddresses')
1285
-
1286
- if (!result || !result.addresses || result.addresses.length === 0) {
1287
- return {
1288
- address: undefined,
1289
- addresses: undefined,
1290
- isConnected: false,
1291
- isConnecting: false,
1292
- isDisconnected: true,
1293
- status: 'disconnected' as const
1294
- }
1295
- }
1296
-
1297
- // Extract STX addresses from the response
1298
- const stxAddresses = result.addresses
1299
- .filter((addr: any) => addr.address.startsWith('SP') || addr.address.startsWith('ST'))
1300
- .map((addr: any) => addr.address)
1301
-
1302
- return {
1303
- address: stxAddresses[0] || undefined,
1304
- addresses: stxAddresses,
1305
- isConnected: true,
1306
- isConnecting: false,
1307
- isDisconnected: false,
1308
- status: 'connected' as const
1309
- }
1310
- } catch (error) {
1311
- // Handle case where wallet is not available or user rejected
1312
- return {
1313
- address: undefined,
1314
- addresses: undefined,
1315
- isConnected: false,
1316
- isConnecting: false,
1317
- isDisconnected: true,
1318
- status: 'disconnected' as const
1319
- }
1320
- }
1321
- },
1322
- refetchOnWindowFocus: false,
1323
- retry: false,
1324
- staleTime: 1000 * 60 * 5, // 5 minutes
1325
- refetchInterval: 1000 * 30, // Refetch every 30 seconds to detect wallet changes
1326
- })
1327
- }`;
1328
- case "useConnect":
1329
- return `export function useConnect() {
1330
- const queryClient = useQueryClient()
1331
-
1332
- const mutation = useMutation({
1333
- mutationFn: async (options: { forceWalletSelect?: boolean } = {}) => {
1334
- // Use @stacks/connect v8 connect method
1335
- return await connect(options)
1336
- },
1337
- onSuccess: () => {
1338
- // Invalidate account queries to refetch connection state
1339
- queryClient.invalidateQueries({ queryKey: ['stacks-account'] })
1340
- },
1341
- onError: (error) => {
1342
- console.error('Connection failed:', error)
1343
- }
1344
- })
1345
-
1346
- return {
1347
- // Custom connect function that works without arguments
1348
- connect: (options?: { forceWalletSelect?: boolean }) => {
1349
- return mutation.mutate(options || {})
1350
- },
1351
- connectAsync: async (options?: { forceWalletSelect?: boolean }) => {
1352
- return mutation.mutateAsync(options || {})
1353
- },
1354
- // Expose all the mutation state
1355
- isPending: mutation.isPending,
1356
- isError: mutation.isError,
1357
- isSuccess: mutation.isSuccess,
1358
- error: mutation.error,
1359
- data: mutation.data,
1360
- reset: mutation.reset,
1361
- // Keep the original mutate/mutateAsync for advanced users
1362
- mutate: mutation.mutate,
1363
- mutateAsync: mutation.mutateAsync
1364
- }
1365
- }`;
1366
- case "useDisconnect":
1367
- return `export function useDisconnect() {
1368
- const queryClient = useQueryClient()
1369
-
1370
- const mutation = useMutation({
1371
- mutationFn: async () => {
1372
- // Use @stacks/connect v8 disconnect method
1373
- return await disconnect()
1374
- },
1375
- onSuccess: () => {
1376
- // Clear all cached data on disconnect
1377
- queryClient.clear()
1378
- },
1379
- onError: (error) => {
1380
- console.error('Disconnect failed:', error)
1381
- }
1382
- })
1383
-
1384
- return {
1385
- // Custom disconnect function
1386
- disconnect: () => {
1387
- return mutation.mutate()
1388
- },
1389
- disconnectAsync: async () => {
1390
- return mutation.mutateAsync()
1391
- },
1392
- // Expose all the mutation state
1393
- isPending: mutation.isPending,
1394
- isError: mutation.isError,
1395
- isSuccess: mutation.isSuccess,
1396
- error: mutation.error,
1397
- data: mutation.data,
1398
- reset: mutation.reset,
1399
- // Keep the original mutate/mutateAsync for advanced users
1400
- mutate: mutation.mutate,
1401
- mutateAsync: mutation.mutateAsync
1402
- }
1403
- }`;
1404
- case "useNetwork":
1405
- return `export function useNetwork() {
1406
- const config = useStacksConfig()
1407
-
1408
- return useQuery({
1409
- queryKey: ['stacks-network', config.network],
1410
- queryFn: async () => {
1411
- // Currently read-only from config
1412
- // Future: Use request('stx_getNetworks') when wallet support improves
1413
- const network = config.network
1414
-
1415
- return {
1416
- network,
1417
- isMainnet: network === 'mainnet',
1418
- isTestnet: network === 'testnet',
1419
- isDevnet: network === 'devnet',
1420
- // Future: Add switchNetwork when wallets support stx_networkChange
1421
- // switchNetwork: async (newNetwork: string) => {
1422
- // return await request('wallet_changeNetwork', { network: newNetwork })
1423
- // }
1424
- }
1425
- },
1426
- staleTime: Infinity, // Network config rarely changes
1427
- refetchOnWindowFocus: false,
1428
- retry: false
1429
- })
1430
- }`;
1431
- case "useContract":
1432
- return `export function useContract() {
1433
- const config = useStacksConfig()
1434
- const queryClient = useQueryClient()
1435
- const [isRequestPending, setIsRequestPending] = useState(false)
1436
-
1437
- // Helper function to convert JS values to Clarity values based on ABI
1438
- const convertArgsWithAbi = (args: any, abiArgs: any[]): any[] => {
1439
- if (!abiArgs || abiArgs.length === 0) return []
1440
-
1441
- return abiArgs.map((abiArg, index) => {
1442
- const argValue = Array.isArray(args)
1443
- ? args[index]
1444
- : args[abiArg.name] || args[abiArg.name.replace(/-/g, '').replace(/_/g, '')]
1445
- return convertJSValueToClarityValue(argValue, abiArg.type)
1446
- })
1447
- }
1448
-
1449
- // Helper function to convert buffer values with auto-detection
1450
- const convertBufferValue = (value: any): any => {
1451
- // Direct Uint8Array
1452
- if (value instanceof Uint8Array) {
1453
- return Cl.buffer(value)
1454
- }
1455
-
1456
- // Object notation with explicit type
1457
- if (typeof value === 'object' && value !== null && value.type && value.value) {
1458
- switch (value.type) {
1459
- case 'ascii':
1460
- return Cl.bufferFromAscii(value.value)
1461
- case 'utf8':
1462
- return Cl.bufferFromUtf8(value.value)
1463
- case 'hex':
1464
- return Cl.bufferFromHex(value.value)
1465
- default:
1466
- throw new Error(\`Unsupported buffer type: \${value.type}\`)
1467
- }
1468
- }
1469
-
1470
- // Auto-detect string type
1471
- if (typeof value === 'string') {
1472
- // 1. Check for hex (0x prefix or pure hex pattern)
1473
- if (value.startsWith('0x') || /^[0-9a-fA-F]+$/.test(value)) {
1474
- return Cl.bufferFromHex(value)
1475
- }
1476
-
1477
- // 2. Check for non-ASCII characters (UTF-8)
1478
- if (!/^[\\x00-\\x7F]*$/.test(value)) {
1479
- return Cl.bufferFromUtf8(value)
1480
- }
1481
-
1482
- // 3. Default to ASCII for simple ASCII strings
1483
- return Cl.bufferFromAscii(value)
1484
- }
1485
-
1486
- throw new Error(\`Invalid buffer value: \${value}\`)
1487
- }
1488
-
1489
- // Helper function to convert a single JS value to ClarityValue
1490
- const convertJSValueToClarityValue = (value: any, type: any): any => {
1491
- if (typeof type === 'string') {
1492
- switch (type) {
1493
- case 'uint128':
1494
- return Cl.uint(value)
1495
- case 'int128':
1496
- return Cl.int(value)
1497
- case 'bool':
1498
- return Cl.bool(value)
1499
- case 'principal':
1500
- if (!validateStacksAddress(value.split('.')[0])) {
1501
- throw new Error('Invalid Stacks address format')
1502
- }
1503
- if (value.includes('.')) {
1504
- const [address, contractName] = value.split('.')
1505
- return Cl.contractPrincipal(address, contractName)
1506
- } else {
1507
- return Cl.standardPrincipal(value)
1508
- }
1509
- default:
1510
- return value
1511
- }
1512
- }
1513
-
1514
- if (type['string-ascii']) {
1515
- return Cl.stringAscii(value)
1516
- }
1517
-
1518
- if (type['string-utf8']) {
1519
- return Cl.stringUtf8(value)
1520
- }
1521
-
1522
- if (type.buff) {
1523
- return convertBufferValue(value)
1524
- }
1525
-
1526
- if (type.optional) {
1527
- return value !== null ? Cl.some(convertJSValueToClarityValue(value, type.optional)) : Cl.none()
1528
- }
1529
-
1530
- if (type.list) {
1531
- return Cl.list(value.map((item: any) => convertJSValueToClarityValue(item, type.list.type)))
1532
- }
1533
-
1534
- if (type.tuple) {
1535
- const tupleData = type.tuple.reduce((acc: any, field: any) => {
1536
- acc[field.name] = convertJSValueToClarityValue(value[field.name], field.type)
1537
- return acc
1538
- }, {})
1539
- return Cl.tuple(tupleData)
1540
- }
1541
-
1542
- if (type.response) {
1543
- return 'ok' in value
1544
- ? Cl.ok(convertJSValueToClarityValue(value.ok, type.response.ok))
1545
- : Cl.error(convertJSValueToClarityValue(value.err, type.response.error))
1546
- }
1547
-
1548
- return value
1549
- }
1550
-
1551
- // Helper function to find a function in an ABI by name
1552
- const findFunctionInAbi = (abi: any, functionName: string): any => {
1553
- if (!abi || !abi.functions) return null
1554
- return abi.functions.find((func: any) => func.name === functionName)
1555
- }
1556
-
1557
- // Legacy function - unchanged, backward compatible
1558
- const legacyOpenContractCall = useCallback(async (params: {
1559
- contractAddress: string;
1560
- contractName: string;
1561
- functionName: string;
1562
- functionArgs: any[]; // Pre-converted Clarity values
1563
- network?: string;
1564
- postConditions?: any[];
1565
- attachment?: string;
1566
- onFinish?: (data: any) => void;
1567
- onCancel?: () => void;
1568
- }) => {
1569
- setIsRequestPending(true)
1570
-
1571
- try {
1572
- const { contractAddress, contractName, functionName, functionArgs, onFinish, onCancel, ...options } = params
1573
- const network = params.network || config.network || 'mainnet'
1574
- const contract = \`\${contractAddress}.\${contractName}\`
1575
-
1576
- // Try @stacks/connect v8 stx_callContract first (SIP-030)
1577
- try {
1578
- const result = await request('stx_callContract', {
1579
- contract,
1580
- functionName,
1581
- functionArgs,
1582
- network,
1583
- ...options
1584
- })
1585
-
1586
- // Invalidate relevant queries on success
1587
- queryClient.invalidateQueries({
1588
- queryKey: ['stacks-account']
1589
- })
1590
-
1591
- onFinish?.(result)
1592
- return result
1593
- } catch (connectError) {
1594
- // Fallback to openContractCall for broader wallet compatibility
1595
- console.warn('stx_callContract not supported, falling back to openContractCall:', connectError)
1596
-
1597
- return new Promise((resolve, reject) => {
1598
- stacksOpenContractCall({
1599
- contractAddress,
1600
- contractName,
1601
- functionName,
1602
- functionArgs,
1603
- network,
1604
- ...options,
1605
- onFinish: (data: any) => {
1606
- // Invalidate relevant queries on success
1607
- queryClient.invalidateQueries({
1608
- queryKey: ['stacks-account']
1609
- })
1610
-
1611
- onFinish?.(data)
1612
- resolve(data)
1613
- },
1614
- onCancel: () => {
1615
- onCancel?.()
1616
- reject(new Error('User cancelled transaction'))
1617
- }
1618
- })
1619
- })
1620
- }
1621
- } catch (error) {
1622
- console.error('Contract call failed:', error)
1623
- throw error instanceof Error ? error : new Error('Contract call failed')
1624
- } finally {
1625
- setIsRequestPending(false)
1626
- }
1627
- }, [config.network, queryClient])
1628
-
1629
- // Enhanced function - requires ABI, auto-converts JS values
1630
- const openContractCall = useCallback(async <
1631
- T extends ClarityContract,
1632
- FN extends ExtractFunctionNames<T>
1633
- >(params: {
1634
- contractAddress: string;
1635
- contractName: string;
1636
- functionName: FN;
1637
- abi: T;
1638
- functionArgs: ExtractFunctionArgs<T, FN>;
1639
- network?: string;
1640
- postConditions?: any[];
1641
- attachment?: string;
1642
- onFinish?: (data: any) => void;
1643
- onCancel?: () => void;
1644
- }) => {
1645
- setIsRequestPending(true)
1646
-
1647
- try {
1648
- const { contractAddress, contractName, functionName, functionArgs, abi, onFinish, onCancel, ...options } = params
1649
- const network = params.network || config.network || 'mainnet'
1650
- const contract = \`\${contractAddress}.\${contractName}\`
1651
-
1652
- // Find the function in the ABI and convert args
1653
- const abiFunction = findFunctionInAbi(abi, functionName)
1654
- if (!abiFunction) {
1655
- throw new Error(\`Function '\${functionName}' not found in ABI\`)
1656
- }
1657
-
1658
- const processedArgs = convertArgsWithAbi(functionArgs, abiFunction.args || [])
1659
-
1660
- // Try @stacks/connect v8 stx_callContract first (SIP-030)
1661
- try {
1662
- const result = await request('stx_callContract', {
1663
- contract,
1664
- functionName,
1665
- functionArgs: processedArgs,
1666
- network,
1667
- ...options
1668
- })
1669
-
1670
- // Invalidate relevant queries on success
1671
- queryClient.invalidateQueries({
1672
- queryKey: ['stacks-account']
1673
- })
1674
-
1675
- onFinish?.(result)
1676
- return result
1677
- } catch (connectError) {
1678
- // Fallback to openContractCall for broader wallet compatibility
1679
- console.warn('stx_callContract not supported, falling back to openContractCall:', connectError)
1680
-
1681
- return new Promise((resolve, reject) => {
1682
- stacksOpenContractCall({
1683
- contractAddress,
1684
- contractName,
1685
- functionName,
1686
- functionArgs: processedArgs,
1687
- network,
1688
- ...options,
1689
- onFinish: (data: any) => {
1690
- // Invalidate relevant queries on success
1691
- queryClient.invalidateQueries({
1692
- queryKey: ['stacks-account']
1693
- })
1694
-
1695
- onFinish?.(data)
1696
- resolve(data)
1697
- },
1698
- onCancel: () => {
1699
- onCancel?.()
1700
- reject(new Error('User cancelled transaction'))
1701
- }
1702
- })
1703
- })
1704
- }
1705
- } catch (error) {
1706
- console.error('Contract call failed:', error)
1707
- throw error instanceof Error ? error : new Error('Contract call failed')
1708
- } finally {
1709
- setIsRequestPending(false)
1710
- }
1711
- }, [config.network, queryClient])
1712
-
1713
- return {
1714
- legacyOpenContractCall,
1715
- openContractCall,
1716
- isRequestPending
1717
- }
1718
- }`;
1719
- case "useReadContract":
1720
- return `export function useReadContract<TArgs = any, TResult = any>(params: {
1721
- contractAddress: string;
1722
- contractName: string;
1723
- functionName: string;
1724
- args?: TArgs;
1725
- network?: 'mainnet' | 'testnet' | 'devnet';
1726
- enabled?: boolean;
1727
- }) {
1728
- const config = useStacksConfig()
1729
-
1730
- return useQuery<TResult>({
1731
- queryKey: ['read-contract', params.contractAddress, params.contractName, params.functionName, params.args, params.network || config.network],
1732
- queryFn: async () => {
1733
- const { fetchCallReadOnlyFunction } = await import('@stacks/transactions')
1734
-
1735
- // For now, we'll need to handle the args conversion here
1736
- // In the future, we could integrate with the contract interface for automatic conversion
1737
- let functionArgs: any[] = []
1738
-
1739
- if (params.args) {
1740
- // This is a simplified conversion - in practice, we'd need the ABI to do proper conversion
1741
- // For now, we'll assume the args are already in the correct format or simple types
1742
- if (Array.isArray(params.args)) {
1743
- functionArgs = params.args
1744
- } else if (typeof params.args === 'object') {
1745
- // Convert object args to array (this is a basic implementation)
1746
- functionArgs = Object.values(params.args)
1747
- } else {
1748
- functionArgs = [params.args]
1749
- }
1750
- }
1751
-
1752
- return await fetchCallReadOnlyFunction({
1753
- contractAddress: params.contractAddress,
1754
- contractName: params.contractName,
1755
- functionName: params.functionName,
1756
- functionArgs,
1757
- network: params.network || config.network || 'mainnet',
1758
- senderAddress: config.senderAddress || 'SP000000000000000000002Q6VF78'
1759
- }) as TResult
1760
- },
1761
- enabled: params.enabled ?? true
1762
- })
1763
- }`;
1764
- case "useTransaction":
1765
- return `export function useTransaction(txId?: string) {
1766
- const config = useStacksConfig()
1767
-
1768
- return useQuery({
1769
- queryKey: ['transaction', txId, config.network],
1770
- queryFn: () => fetchTransaction({
1771
- txId: txId!,
1772
- network: config.network,
1773
- apiUrl: config.apiUrl
1774
- }),
1775
- enabled: !!txId
1776
- })
1777
- }`;
1778
- case "useBlock":
1779
- return `export function useBlock(height?: number) {
1780
- const config = useStacksConfig()
1781
-
1782
- return useQuery({
1783
- queryKey: ['block', height, config.network],
1784
- queryFn: () => fetchBlock({
1785
- height: height!,
1786
- network: config.network,
1787
- apiUrl: config.apiUrl
1788
- }),
1789
- enabled: typeof height === 'number'
1790
- })
1791
- }`;
1792
- case "useAccountTransactions":
1793
- return `export function useAccountTransactions(address?: string) {
1794
- const config = useStacksConfig()
1795
-
1796
- return useQuery({
1797
- queryKey: ['account-transactions', address, config.network],
1798
- queryFn: () => fetchAccountTransactions({
1799
- address: address!,
1800
- network: config.network,
1801
- apiUrl: config.apiUrl
1802
- }),
1803
- enabled: !!address
1804
- })
1805
- }`;
1806
- case "useWaitForTransaction":
1807
- return `export function useWaitForTransaction(txId?: string) {
1808
- const config = useStacksConfig()
1809
-
1810
- return useQuery({
1811
- queryKey: ['wait-for-transaction', txId, config.network],
1812
- queryFn: () => fetchTransaction({
1813
- txId: txId!,
1814
- network: config.network,
1815
- apiUrl: config.apiUrl
1816
- }),
1817
- enabled: !!txId,
1818
- refetchInterval: (data) => {
1819
- // Stop polling when transaction is complete
1820
- if (data?.tx_status === 'success' ||
1821
- data?.tx_status === 'abort_by_response' ||
1822
- data?.tx_status === 'abort_by_post_condition') {
1823
- return false
1824
- }
1825
- return 2000 // Poll every 2 seconds
1826
- },
1827
- staleTime: 0 // Always refetch
1828
- })
1829
- }`;
1830
- case "useOpenSTXTransfer":
1831
- return `export function useOpenSTXTransfer() {
1832
- const config = useStacksConfig()
1833
- const queryClient = useQueryClient()
1834
-
1835
- const mutation = useMutation({
1836
- mutationFn: async (params: {
1837
- recipient: string;
1838
- amount: string | number;
1839
- memo?: string;
1840
- network?: string;
1841
- onFinish?: (data: any) => void;
1842
- onCancel?: () => void;
1843
- }) => {
1844
- const { recipient, amount, memo, onFinish, onCancel, ...options } = params
1845
- const network = params.network || config.network || 'mainnet'
1846
-
1847
- return new Promise((resolve, reject) => {
1848
- openSTXTransfer({
1849
- recipient,
1850
- amount: amount.toString(),
1851
- memo,
1852
- network,
1853
- ...options,
1854
- onFinish: (data: any) => {
1855
- onFinish?.(data)
1856
- resolve(data)
1857
- },
1858
- onCancel: () => {
1859
- onCancel?.()
1860
- reject(new Error('User cancelled transaction'))
1861
- }
1862
- })
1863
- })
1864
- },
1865
- onSuccess: () => {
1866
- // Invalidate relevant queries on success
1867
- queryClient.invalidateQueries({ queryKey: ['stacks-account'] })
1868
- },
1869
- onError: (error) => {
1870
- console.error('STX transfer failed:', error)
1871
- }
1872
- })
1873
-
1874
- const openSTXTransfer = useCallback(async (params: {
1875
- recipient: string;
1876
- amount: string | number;
1877
- memo?: string;
1878
- network?: string;
1879
- onFinish?: (data: any) => void;
1880
- onCancel?: () => void;
1881
- }) => {
1882
- return mutation.mutateAsync(params)
1883
- }, [mutation])
1884
-
1885
- return {
1886
- openSTXTransfer,
1887
- // Expose mutation state
1888
- isPending: mutation.isPending,
1889
- isError: mutation.isError,
1890
- isSuccess: mutation.isSuccess,
1891
- error: mutation.error,
1892
- data: mutation.data,
1893
- reset: mutation.reset
1894
- }
1895
- }`;
1896
- case "useSignMessage":
1897
- return `export function useSignMessage() {
1898
- const config = useStacksConfig()
1899
-
1900
- const mutation = useMutation({
1901
- mutationFn: async (params: {
1902
- message: string;
1903
- network?: string;
1904
- onFinish?: (data: any) => void;
1905
- onCancel?: () => void;
1906
- }) => {
1907
- const { message, onFinish, onCancel, ...options } = params
1908
- const network = params.network || config.network || 'mainnet'
1909
-
1910
- return new Promise((resolve, reject) => {
1911
- openSignatureRequestPopup({
1912
- message,
1913
- network,
1914
- ...options,
1915
- onFinish: (data: any) => {
1916
- onFinish?.(data)
1917
- resolve(data)
1918
- },
1919
- onCancel: () => {
1920
- onCancel?.()
1921
- reject(new Error('User cancelled message signing'))
1922
- }
1923
- })
1924
- })
1925
- },
1926
- onError: (error) => {
1927
- console.error('Message signing failed:', error)
1928
- }
1929
- })
1930
-
1931
- const signMessage = useCallback(async (params: {
1932
- message: string;
1933
- network?: string;
1934
- onFinish?: (data: any) => void;
1935
- onCancel?: () => void;
1936
- }) => {
1937
- return mutation.mutateAsync(params)
1938
- }, [mutation])
1939
-
1940
- return {
1941
- signMessage,
1942
- // Expose mutation state
1943
- isPending: mutation.isPending,
1944
- isError: mutation.isError,
1945
- isSuccess: mutation.isSuccess,
1946
- error: mutation.error,
1947
- data: mutation.data,
1948
- reset: mutation.reset
1949
- }
1950
- }`;
1951
- case "useDeployContract":
1952
- return `export function useDeployContract() {
1953
- const config = useStacksConfig()
1954
- const queryClient = useQueryClient()
1955
-
1956
- const mutation = useMutation({
1957
- mutationFn: async (params: {
1958
- contractName: string;
1959
- codeBody: string;
1960
- network?: string;
1961
- postConditions?: any[];
1962
- onFinish?: (data: any) => void;
1963
- onCancel?: () => void;
1964
- }) => {
1965
- const { contractName, codeBody, onFinish, onCancel, ...options } = params
1966
- const network = params.network || config.network || 'mainnet'
1967
-
1968
- return new Promise((resolve, reject) => {
1969
- openContractDeploy({
1970
- contractName,
1971
- codeBody,
1972
- network,
1973
- ...options,
1974
- onFinish: (data: any) => {
1975
- onFinish?.(data)
1976
- resolve(data)
1977
- },
1978
- onCancel: () => {
1979
- onCancel?.()
1980
- reject(new Error('User cancelled contract deployment'))
1981
- }
1982
- })
1983
- })
1984
- },
1985
- onSuccess: () => {
1986
- // Invalidate relevant queries on success
1987
- queryClient.invalidateQueries({ queryKey: ['stacks-account'] })
1988
- },
1989
- onError: (error) => {
1990
- console.error('Contract deployment failed:', error)
1991
- }
1992
- })
1993
-
1994
- const deployContract = useCallback(async (params: {
1995
- contractName: string;
1996
- codeBody: string;
1997
- network?: string;
1998
- postConditions?: any[];
1999
- onFinish?: (data: any) => void;
2000
- onCancel?: () => void;
2001
- }) => {
2002
- return mutation.mutateAsync(params)
2003
- }, [mutation])
2004
-
2005
- return {
2006
- deployContract,
2007
- // Expose mutation state
2008
- isPending: mutation.isPending,
2009
- isError: mutation.isError,
2010
- isSuccess: mutation.isSuccess,
2011
- error: mutation.error,
2012
- data: mutation.data,
2013
- reset: mutation.reset
2014
- }
2015
- }`;
2016
- default:
2017
- return "";
2018
- }
2019
- }
2020
-
2021
- // src/plugins/react/generators/contract.ts
2022
- var import_prettier5 = require("prettier");
2023
-
2024
- // src/plugins/react/generators/utils.ts
2025
- function toCamelCase4(str) {
2026
- return str.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
2027
- }
2028
- function capitalize(str) {
2029
- return str.charAt(0).toUpperCase() + str.slice(1);
2030
- }
2031
- function generateHookArgsSignature(args) {
2032
- if (args.length === 0) return "";
2033
- const argsList = args.map((arg) => `${toCamelCase4(arg.name)}: ${mapClarityTypeToTS(arg.type)}`).join(", ");
2034
- return `${argsList}`;
2035
- }
2036
- function generateArgsType(args) {
2037
- if (args.length === 0) return "void";
2038
- const argsList = args.map((arg) => `${toCamelCase4(arg.name)}: ${mapClarityTypeToTS(arg.type)}`).join("; ");
2039
- return `{ ${argsList} }`;
2040
- }
2041
- function generateQueryKeyArgs(args) {
2042
- if (args.length === 0) return "";
2043
- return args.map((arg) => toCamelCase4(arg.name)).join(", ");
2044
- }
2045
- function generateFunctionCallArgs(args) {
2046
- if (args.length === 0) return "";
2047
- return args.map((arg) => toCamelCase4(arg.name)).join(", ");
2048
- }
2049
- function generateEnabledCondition(args) {
2050
- return args.map((arg) => {
2051
- const camelName = toCamelCase4(arg.name);
2052
- const type = mapClarityTypeToTS(arg.type);
2053
- if (type === "string") return `!!${camelName}`;
2054
- if (type === "bigint") return `${camelName} !== undefined`;
2055
- return `${camelName} !== undefined`;
2056
- }).join(" && ");
2057
- }
2058
- function mapClarityTypeToTS(clarityType) {
2059
- if (typeof clarityType !== "string") {
2060
- if (clarityType?.uint || clarityType?.int) return "bigint";
2061
- if (clarityType?.principal) return "string";
2062
- if (clarityType?.bool) return "boolean";
2063
- if (clarityType?.string || clarityType?.ascii) return "string";
2064
- if (clarityType?.buff) return "Uint8Array";
2065
- if (clarityType?.optional) {
2066
- const innerType = mapClarityTypeToTS(clarityType.optional);
2067
- return `${innerType} | null`;
2068
- }
2069
- if (clarityType?.response) return "any";
2070
- if (clarityType?.tuple) return "any";
2071
- if (clarityType?.list) return "any[]";
2072
- return "any";
2073
- }
2074
- if (clarityType.includes("uint") || clarityType.includes("int"))
2075
- return "bigint";
2076
- if (clarityType.includes("principal")) return "string";
2077
- if (clarityType.includes("bool")) return "boolean";
2078
- if (clarityType.includes("string") || clarityType.includes("ascii"))
2079
- return "string";
2080
- if (clarityType.includes("buff")) return "Uint8Array";
2081
- if (clarityType.includes("optional")) {
2082
- const innerType = clarityType.replace(/optional\s*/, "").trim();
2083
- return `${mapClarityTypeToTS(innerType)} | null`;
2084
- }
2085
- if (clarityType.includes("response")) return "any";
2086
- if (clarityType.includes("tuple")) return "any";
2087
- if (clarityType.includes("list")) return "any[]";
2088
- return "any";
2089
- }
2090
- function generateObjectArgs(args) {
2091
- if (args.length === 0) return "";
2092
- return args.map((arg) => `${arg.name}: ${toCamelCase4(arg.name)}`).join(", ");
2093
- }
2094
-
2095
- // src/plugins/react/generators/contract.ts
2096
- async function generateContractHooks(contracts, excludeList = []) {
2097
- const imports = `import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
2098
- import { useCallback } from 'react'
2099
- import { useStacksConfig } from './provider'
2100
- import { request, openContractCall as stacksOpenContractCall } from '@stacks/connect'
2101
- import { ${contracts.map((c) => c.name).join(", ")} } from './contracts'`;
2102
- const header = `/**
2103
- * Generated contract-specific React hooks
2104
- * DO NOT EDIT MANUALLY
2105
- */`;
2106
- const hooksCode = contracts.map((contract) => generateContractHookMethods(contract, excludeList)).filter(Boolean).join("\n\n");
2107
- const code = `${imports}
2108
-
2109
- ${header}
2110
-
2111
- ${hooksCode}`;
2112
- const formatted = await (0, import_prettier5.format)(code, {
2113
- parser: "typescript",
2114
- singleQuote: true,
2115
- semi: false,
2116
- printWidth: 100,
2117
- trailingComma: "es5"
2118
- });
2119
- return formatted;
2120
- }
2121
- function generateContractHookMethods(contract, excludeList) {
2122
- const { abi, name } = contract;
2123
- const functions = abi.functions || [];
2124
- const readOnlyFunctions = functions.filter(
2125
- (f) => f.access === "read_only" || f.access === "read-only"
2126
- );
2127
- const publicFunctions = functions.filter(
2128
- (f) => f.access === "public"
2129
- );
2130
- const readHooks = readOnlyFunctions.map((func) => {
2131
- const hookName = `use${capitalize(name)}${capitalize(toCamelCase4(func.name))}`;
2132
- if (excludeList.includes(hookName)) {
2133
- return null;
2134
- }
2135
- return generateReadHook(func, name);
2136
- }).filter(Boolean);
2137
- const writeHooks = publicFunctions.map((func) => {
2138
- const hookName = `use${capitalize(name)}${capitalize(toCamelCase4(func.name))}`;
2139
- if (excludeList.includes(hookName)) {
2140
- return null;
2141
- }
2142
- return generateWriteHook(func, name);
2143
- }).filter(Boolean);
2144
- const allHooks = [...readHooks, ...writeHooks];
2145
- if (allHooks.length === 0) {
2146
- return "";
2147
- }
2148
- return allHooks.join("\n\n");
2149
- }
2150
- function generateReadHook(func, contractName) {
2151
- const hookName = `use${capitalize(contractName)}${capitalize(toCamelCase4(func.name))}`;
2152
- const argsSignature = generateHookArgsSignature(func.args);
2153
- const enabledParam = func.args.length > 0 ? ", options?: { enabled?: boolean }" : "options?: { enabled?: boolean }";
2154
- return `export function ${hookName}(${argsSignature}${enabledParam}) {
2155
- const config = useStacksConfig()
2156
-
2157
- return useQuery({
2158
- queryKey: ['${func.name}', ${contractName}.address, ${generateQueryKeyArgs(func.args)}],
2159
- queryFn: () => ${contractName}.read.${toCamelCase4(func.name)}(${generateFunctionCallArgs(func.args) ? `{ ${generateObjectArgs(func.args)} }, ` : ""}{
2160
- network: config.network,
2161
- senderAddress: config.senderAddress || 'SP000000000000000000002Q6VF78'
2162
- }),
2163
- ${func.args.length > 0 ? `enabled: ${generateEnabledCondition(func.args)} && (options?.enabled ?? true),` : ""}
2164
- ...options
2165
- })
2166
- }`;
2167
- }
2168
- function generateWriteHook(func, contractName) {
2169
- const hookName = `use${capitalize(contractName)}${capitalize(toCamelCase4(func.name))}`;
2170
- const argsType = generateArgsType(func.args);
2171
- return `export function ${hookName}() {
2172
- const config = useStacksConfig()
2173
- const queryClient = useQueryClient()
2174
-
2175
- const mutation = useMutation({
2176
- mutationFn: async (params: {
2177
- args: ${argsType};
2178
- options?: {
2179
- postConditions?: any[];
2180
- attachment?: string;
2181
- onFinish?: (data: any) => void;
2182
- onCancel?: () => void;
2183
- };
2184
- }) => {
2185
- const { args, options = {} } = params
2186
- const contractCallData = ${contractName}.${toCamelCase4(func.name)}(args)
2187
- const { contractAddress, contractName: name, functionName, functionArgs } = contractCallData
2188
- const network = config.network || 'mainnet'
2189
- const contract = \`\${contractAddress}.\${name}\`
2190
-
2191
- // Try @stacks/connect v8 stx_callContract first (SIP-030)
2192
- try {
2193
- const result = await request('stx_callContract', {
2194
- contract,
2195
- functionName,
2196
- functionArgs,
2197
- network,
2198
- ...options
2199
- })
2200
-
2201
- options.onFinish?.(result)
2202
- return result
2203
- } catch (connectError) {
2204
- // Fallback to openContractCall for broader wallet compatibility
2205
- console.warn('stx_callContract not supported, falling back to openContractCall:', connectError)
2206
-
2207
- return new Promise((resolve, reject) => {
2208
- stacksOpenContractCall({
2209
- contractAddress,
2210
- contractName: name,
2211
- functionName,
2212
- functionArgs,
2213
- network,
2214
- ...options,
2215
- onFinish: (data: any) => {
2216
- options.onFinish?.(data)
2217
- resolve(data)
2218
- },
2219
- onCancel: () => {
2220
- options.onCancel?.()
2221
- reject(new Error('User cancelled transaction'))
2222
- }
2223
- })
2224
- })
2225
- }
2226
- },
2227
- onSuccess: () => {
2228
- // Invalidate relevant queries on success
2229
- queryClient.invalidateQueries({ queryKey: ['stacks-account'] })
2230
- },
2231
- onError: (error) => {
2232
- console.error('Contract call failed:', error)
2233
- }
2234
- })
2235
-
2236
- const ${toCamelCase4(func.name)} = useCallback(async (
2237
- args: ${argsType},
2238
- options?: {
2239
- postConditions?: any[];
2240
- attachment?: string;
2241
- onFinish?: (data: any) => void;
2242
- onCancel?: () => void;
2243
- }
2244
- ) => {
2245
- return mutation.mutateAsync({ args, options })
2246
- }, [mutation])
2247
-
2248
- return {
2249
- ${toCamelCase4(func.name)},
2250
- // Expose mutation state
2251
- isPending: mutation.isPending,
2252
- isError: mutation.isError,
2253
- isSuccess: mutation.isSuccess,
2254
- error: mutation.error,
2255
- data: mutation.data,
2256
- reset: mutation.reset
2257
- }
2258
- }`;
2259
- }
2260
-
2261
- // src/plugins/react/index.ts
2262
- var react = (options = {}) => {
2263
- const excludeList = options.exclude || [];
2264
- return {
2265
- name: "@stacks/codegen/plugin-react",
2266
- version: "1.0.0",
2267
- async generate(context) {
2268
- if (options.debug) {
2269
- context.logger.debug(
2270
- `React plugin generating hooks (excluding: ${excludeList.join(", ") || "none"})`
2271
- );
2272
- }
2273
- const provider = await generateProvider();
2274
- context.addOutput("provider", {
2275
- path: "./src/generated/provider.tsx",
2276
- content: provider,
2277
- type: "config"
2278
- });
2279
- const genericHooks = await generateGenericHooks(excludeList);
2280
- context.addOutput("generic-hooks", {
2281
- path: "./src/generated/hooks.ts",
2282
- content: genericHooks,
2283
- type: "hooks"
2284
- });
2285
- if (context.contracts.length > 0) {
2286
- const contractHooks = await generateContractHooks(
2287
- context.contracts,
2288
- excludeList
2289
- );
2290
- if (contractHooks.trim()) {
2291
- context.addOutput("contract-hooks", {
2292
- path: "./src/generated/contract-hooks.ts",
2293
- content: contractHooks,
2294
- type: "hooks"
2295
- });
2296
- }
2297
- }
2298
- if (options.debug) {
2299
- context.logger.success(
2300
- `React plugin generated ${context.contracts.length} contract hook sets`
2301
- );
2302
- }
2303
- }
2304
- };
2305
- };
2306
-
2307
- // src/utils/api.ts
2308
- var import_got = __toESM(require("got"), 1);
2309
- var API_URLS = {
2310
- mainnet: "https://api.hiro.so",
2311
- testnet: "https://api.testnet.hiro.so",
2312
- devnet: "http://localhost:3999"
2313
- };
2314
- var StacksApiClient = class {
2315
- constructor(network = "mainnet", apiKey, apiUrl) {
2316
- this.baseUrl = apiUrl || API_URLS[network];
2317
- this.headers = apiKey ? { "x-api-key": apiKey } : {};
2318
- }
2319
- async getContractInfo(contractId) {
2320
- const [address, contractName] = contractId.split(".");
2321
- if (!address || !contractName) {
2322
- throw new Error(
2323
- `Invalid contract ID format: ${contractId}. Expected format: ADDRESS.CONTRACT_NAME`
2324
- );
2325
- }
2326
- const url = `${this.baseUrl}/v2/contracts/interface/${address}/${contractName}`;
2327
- try {
2328
- const response = await (0, import_got.default)(url, {
2329
- headers: this.headers,
2330
- responseType: "json"
2331
- });
2332
- return response.body;
2333
- } catch (error) {
2334
- if (error.response?.statusCode === 404) {
2335
- throw new Error(`Contract not found: ${contractId}`);
2336
- }
2337
- if (error.response?.statusCode === 429) {
2338
- throw new Error(
2339
- "Rate limited. Please provide an API key in your config."
2340
- );
2341
- }
2342
- throw new Error(`Failed to fetch contract: ${error.message}`);
2343
- }
2344
- }
2345
- async getContractSource(contractId) {
2346
- const [address, contractName] = contractId.split(".");
2347
- if (!address || !contractName) {
2348
- throw new Error(
2349
- `Invalid contract ID format: ${contractId}. Expected format: ADDRESS.CONTRACT_NAME`
2350
- );
2351
- }
2352
- const url = `${this.baseUrl}/v2/contracts/source/${address}/${contractName}`;
2353
- try {
2354
- const response = await (0, import_got.default)(url, {
2355
- headers: this.headers,
2356
- responseType: "json"
2357
- });
2358
- const data = response.body;
2359
- return data.source;
2360
- } catch (error) {
2361
- return "";
2362
- }
2363
- }
2364
- };
2365
-
2366
- // src/parsers/clarity.ts
2367
- function parseType(typeStr) {
2368
- typeStr = typeStr.replace(/[()]/g, "").trim();
2369
- switch (typeStr) {
2370
- case "uint":
2371
- case "uint128":
2372
- return "uint128";
2373
- case "int":
2374
- case "int128":
2375
- return "int128";
2376
- case "bool":
2377
- return "bool";
2378
- case "principal":
2379
- return "principal";
2380
- case "trait_reference":
2381
- return "principal";
2382
- default:
2383
- if (typeStr.startsWith("string-ascii")) {
2384
- return { "string-ascii": { length: 256 } };
2385
- }
2386
- if (typeStr.startsWith("string-utf8")) {
2387
- return { "string-utf8": { length: 256 } };
2388
- }
2389
- if (typeStr.startsWith("buff")) {
2390
- return { buff: { length: 32 } };
2391
- }
2392
- return "uint128";
2393
- }
2394
- }
2395
- function parseApiResponse(apiResponse) {
2396
- try {
2397
- const functions = [];
2398
- if (apiResponse.functions) {
2399
- for (const func of apiResponse.functions) {
2400
- const access = func.access === "read_only" ? "read-only" : func.access;
2401
- functions.push({
2402
- name: func.name,
2403
- access,
2404
- args: func.args.map((arg) => ({
2405
- name: arg.name,
2406
- type: convertApiType(arg.type)
2407
- })),
2408
- outputs: convertApiType(func.outputs.type)
2409
- });
2410
- }
2411
- }
2412
- return { functions };
2413
- } catch (error) {
2414
- throw new Error(`Failed to parse API response: ${error}`);
2415
- }
2416
- }
2417
- function convertApiType(apiType) {
2418
- if (typeof apiType === "string") {
2419
- if (apiType === "trait_reference") {
2420
- return "trait_reference";
2421
- }
2422
- return parseType(apiType) || "uint128";
2423
- }
2424
- if (apiType.response) {
2425
- return {
2426
- response: {
2427
- ok: convertApiType(apiType.response.ok),
2428
- error: convertApiType(apiType.response.error)
2429
- }
2430
- };
2431
- }
2432
- if (apiType.optional) {
2433
- return {
2434
- optional: convertApiType(apiType.optional)
2435
- };
2436
- }
2437
- if (apiType.list) {
2438
- return {
2439
- list: {
2440
- type: convertApiType(apiType.list.type),
2441
- length: apiType.list.length || 100
2442
- }
2443
- };
2444
- }
2445
- if (apiType.tuple) {
2446
- return {
2447
- tuple: apiType.tuple.map((field) => ({
2448
- name: field.name,
2449
- type: convertApiType(field.type)
2450
- }))
2451
- };
2452
- }
2453
- if (apiType.buffer) {
2454
- return {
2455
- buff: {
2456
- length: apiType.buffer.length || 32
2457
- }
2458
- };
2459
- }
2460
- if (apiType["string-ascii"]) {
2461
- return {
2462
- "string-ascii": {
2463
- length: apiType["string-ascii"].length || 256
2464
- }
2465
- };
2466
- }
2467
- if (apiType["string-utf8"]) {
2468
- return {
2469
- "string-utf8": {
2470
- length: apiType["string-utf8"].length || 256
2471
- }
2472
- };
2473
- }
2474
- if (apiType === "none") {
2475
- return "uint128";
2476
- }
2477
- return "uint128";
2478
- }
2479
-
2480
- // src/plugins/hiro/index.ts
2481
- var hiro = (options) => {
2482
- if (!options?.apiKey) {
2483
- throw new Error(
2484
- "Hiro plugin requires an API key. Get one for free at https://platform.hiro.so/"
2485
- );
2486
- }
2487
- if (!options.network) {
2488
- throw new Error("Hiro plugin requires a network ('mainnet' or 'testnet')");
2489
- }
2490
- if (!options.contracts || options.contracts.length === 0) {
2491
- throw new Error(
2492
- "Hiro plugin requires a contracts array with contract IDs to fetch"
2493
- );
2494
- }
2495
- return {
2496
- name: "@stacks/codegen/plugin-hiro",
2497
- version: "1.0.0",
2498
- async transformConfig(config) {
2499
- if (options.debug) {
2500
- console.log(
2501
- `\u{1F504} Hiro plugin: Fetching ABIs for ${options.contracts.length} contracts from ${options.network}`
2502
- );
2503
- }
2504
- const apiClient = new StacksApiClient(options.network, options.apiKey);
2505
- const fetchedContracts = [];
2506
- for (const contractId of options.contracts) {
2507
- const result = await fetchContractABI(
2508
- contractId,
2509
- apiClient,
2510
- options.debug
2511
- );
2512
- if (result.success && result.abi) {
2513
- const [address, contractName] = contractId.split(".");
2514
- const name = contractName ? contractName.replace(/-/g, "_").replace(/^\d/, "_$&") : address.replace(/^SP|^ST/, "").toLowerCase();
2515
- fetchedContracts.push({
2516
- name,
2517
- address: contractId,
2518
- abi: result.abi,
2519
- metadata: {
2520
- source: "hiro-api",
2521
- network: options.network,
2522
- fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
2523
- }
2524
- });
2525
- if (options.debug) {
2526
- console.log(
2527
- `\u2705 Hiro plugin: Successfully fetched ABI for ${contractId}`
2528
- );
2529
- }
2530
- } else {
2531
- if (options.debug) {
2532
- console.warn(
2533
- `\u26A0\uFE0F Hiro plugin: Failed to fetch ${contractId}: ${result.error}`
2534
- );
2535
- }
2536
- }
2537
- }
2538
- if (options.debug) {
2539
- console.log(
2540
- `\u{1F50D} Hiro plugin: Successfully fetched ${fetchedContracts.length}/${options.contracts.length} contracts`
2541
- );
2542
- }
2543
- return {
2544
- ...config,
2545
- contracts: [...config.contracts || [], ...fetchedContracts]
2546
- };
2547
- }
2548
- };
2549
- };
2550
- async function fetchContractABI(contractId, apiClient, debug) {
2551
- try {
2552
- if (debug) {
2553
- console.log(`\u{1F504} Fetching ABI for ${contractId}`);
2554
- }
2555
- const contractInfo = await apiClient.getContractInfo(contractId);
2556
- const abi = parseApiResponse(contractInfo);
2557
- return {
2558
- success: true,
2559
- contractId,
2560
- abi
2561
- };
2562
- } catch (error) {
2563
- if (error.message?.includes("Contract not found") || error.response?.statusCode === 404) {
2564
- return {
2565
- success: false,
2566
- contractId,
2567
- error: `Contract not found: ${contractId}`
2568
- };
2569
- }
2570
- if (error.message?.includes("Rate limited") || error.response?.statusCode === 429) {
2571
- return {
2572
- success: false,
2573
- contractId,
2574
- error: `Rate limited when fetching ${contractId}. Consider using an API key.`
2575
- };
2576
- }
2577
- return {
2578
- success: false,
2579
- contractId,
2580
- error: error.message || "Unknown error"
2581
- };
2582
- }
2583
- }
2584
-
2585
- // src/plugins/index.ts
2586
- function filterByOptions(items, options = {}) {
2587
- let filtered = items;
2588
- if (options.include && options.include.length > 0) {
2589
- filtered = filtered.filter(
2590
- (item) => options.include.some(
2591
- (pattern) => item.name.includes(pattern) || item.name.match(new RegExp(pattern))
2592
- )
2593
- );
2594
- }
2595
- if (options.exclude && options.exclude.length > 0) {
2596
- filtered = filtered.filter(
2597
- (item) => !options.exclude.some(
2598
- (pattern) => item.name.includes(pattern) || item.name.match(new RegExp(pattern))
2599
- )
2600
- );
2601
- }
2602
- return filtered;
2603
- }
2604
- function createPlugin(name, version, implementation) {
2605
- return {
2606
- name,
2607
- version,
2608
- ...implementation
2609
- };
2610
- }
2611
- // Annotate the CommonJS export names for ESM import in node:
2612
- 0 && (module.exports = {
2613
- PluginManager,
2614
- actions,
2615
- clarinet,
2616
- createPlugin,
2617
- filterByOptions,
2618
- hasClarinetProject,
2619
- hiro,
2620
- react
2621
- });
2622
- //# sourceMappingURL=index.cjs.map