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