scrypt-testgen 1.0.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.
Files changed (52) hide show
  1. package/.prettierrc +7 -0
  2. package/README.md +17 -0
  3. package/bin/scrypt-testgen.js +2 -0
  4. package/dist/cli.d.ts +3 -0
  5. package/dist/cli.d.ts.map +1 -0
  6. package/dist/cli.js +48 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/generator/index.d.ts +6 -0
  9. package/dist/generator/index.d.ts.map +1 -0
  10. package/dist/generator/index.js +33 -0
  11. package/dist/generator/index.js.map +1 -0
  12. package/dist/generator/test-generator.d.ts +17 -0
  13. package/dist/generator/test-generator.d.ts.map +1 -0
  14. package/dist/generator/test-generator.js +177 -0
  15. package/dist/generator/test-generator.js.map +1 -0
  16. package/dist/generator/value-generator.d.ts +19 -0
  17. package/dist/generator/value-generator.d.ts.map +1 -0
  18. package/dist/generator/value-generator.js +130 -0
  19. package/dist/generator/value-generator.js.map +1 -0
  20. package/dist/model/contract-model.d.ts +42 -0
  21. package/dist/model/contract-model.d.ts.map +1 -0
  22. package/dist/model/contract-model.js +10 -0
  23. package/dist/model/contract-model.js.map +1 -0
  24. package/dist/parser/ast-utils.d.ts +7 -0
  25. package/dist/parser/ast-utils.d.ts.map +1 -0
  26. package/dist/parser/ast-utils.js +114 -0
  27. package/dist/parser/ast-utils.js.map +1 -0
  28. package/dist/parser/contract-parser.d.ts +18 -0
  29. package/dist/parser/contract-parser.d.ts.map +1 -0
  30. package/dist/parser/contract-parser.js +224 -0
  31. package/dist/parser/contract-parser.js.map +1 -0
  32. package/dist/parser/index.d.ts +3 -0
  33. package/dist/parser/index.d.ts.map +1 -0
  34. package/dist/parser/index.js +6 -0
  35. package/dist/parser/index.js.map +1 -0
  36. package/dist/utils/file-utils.d.ts +4 -0
  37. package/dist/utils/file-utils.d.ts.map +1 -0
  38. package/dist/utils/file-utils.js +26 -0
  39. package/dist/utils/file-utils.js.map +1 -0
  40. package/package.json +30 -0
  41. package/src/cli.ts +51 -0
  42. package/src/generator/index.ts +45 -0
  43. package/src/generator/test-generator.ts +216 -0
  44. package/src/generator/value-generator.ts +138 -0
  45. package/src/model/contract-model.ts +46 -0
  46. package/src/parser/ast-utils.ts +75 -0
  47. package/src/parser/contract-parser.ts +246 -0
  48. package/src/parser/index.ts +2 -0
  49. package/src/utils/file-utils.ts +22 -0
  50. package/templates/test-template.ts +0 -0
  51. package/test/integration/demo.ts +16 -0
  52. package/tsconfig.json +21 -0
@@ -0,0 +1,246 @@
1
+ import * as ts from 'typescript';
2
+ import * as path from 'path';
3
+ import {
4
+ ConstructorArg,
5
+ ContractProp,
6
+ ContractMethod,
7
+ MethodParameter,
8
+ MethodCategory,
9
+ ContractModel
10
+ } from '../model/contract-model';
11
+ import {
12
+ getDecoratorText,
13
+ getTypeText,
14
+ getDefaultValueForType,
15
+ isSmartContractClass,
16
+ findDecorator
17
+ } from './ast-utils';
18
+
19
+ export class ContractParser {
20
+ private program: ts.Program;
21
+ private checker: ts.TypeChecker;
22
+ private sourceFile: ts.SourceFile;
23
+
24
+ constructor(filePath: string) {
25
+ this.program = ts.createProgram([filePath], {
26
+ target: ts.ScriptTarget.ES2020,
27
+ module: ts.ModuleKind.CommonJS,
28
+ experimentalDecorators: true,
29
+ emitDecoratorMetadata: true
30
+ });
31
+
32
+ this.checker = this.program.getTypeChecker();
33
+ this.sourceFile = this.program.getSourceFile(filePath)!;
34
+
35
+ if (!this.sourceFile) {
36
+ throw new Error(`Cannot read source file: ${filePath}`);
37
+ }
38
+ }
39
+
40
+ parse(): ContractModel {
41
+ const contractClass = this.findContractClass();
42
+ if (!contractClass) {
43
+ throw new Error('No SmartContract class found in file');
44
+ }
45
+
46
+ const className = contractClass.name?.text || 'UnknownContract';
47
+
48
+ return {
49
+ name: className,
50
+ filePath: this.sourceFile.fileName,
51
+ constructorArgs: this.parseConstructor(contractClass),
52
+ props: this.parseProperties(contractClass),
53
+ methods: this.parseMethods(contractClass),
54
+ imports: this.parseImports(),
55
+ extendsClass: this.getExtendedClass(contractClass)
56
+ };
57
+ }
58
+
59
+ private findContractClass(): ts.ClassDeclaration | undefined {
60
+ let contractClass: ts.ClassDeclaration | undefined;
61
+
62
+ const visit = (node: ts.Node) => {
63
+ if (isSmartContractClass(node)) {
64
+ if (contractClass) {
65
+ throw new Error('Multiple SmartContract classes found. Only one per file is supported.');
66
+ }
67
+ contractClass = node as ts.ClassDeclaration;
68
+ }
69
+ ts.forEachChild(node, visit);
70
+ };
71
+
72
+ visit(this.sourceFile);
73
+ return contractClass;
74
+ }
75
+
76
+ private parseConstructor(classNode: ts.ClassDeclaration): ConstructorArg[] {
77
+ const constructor = classNode.members.find(
78
+ member => ts.isConstructorDeclaration(member)
79
+ ) as ts.ConstructorDeclaration;
80
+
81
+ if (!constructor || !constructor.parameters) {
82
+ return [];
83
+ }
84
+
85
+ return constructor.parameters.map(param => {
86
+ const paramName = param.name.getText();
87
+ const type = getTypeText(param.type, this.checker);
88
+
89
+ return {
90
+ name: paramName,
91
+ type: type,
92
+ defaultValue: param.initializer ? param.initializer.getText() : undefined
93
+ };
94
+ });
95
+ }
96
+
97
+ private parseProperties(classNode: ts.ClassDeclaration): ContractProp[] {
98
+ const properties: ContractProp[] = [];
99
+
100
+ for (const member of classNode.members) {
101
+ if (ts.isPropertyDeclaration(member)) {
102
+ const decorators = ts.getDecorators(member);
103
+ if (decorators && decorators.length > 0) {
104
+ const propDecorator = findDecorator(member, '@prop');
105
+ if (propDecorator) {
106
+ const propName = member.name.getText();
107
+ const type = getTypeText(member.type, this.checker);
108
+ const decoratorText = decorators[0].getText();
109
+ const isMutable = decoratorText.includes('true');
110
+
111
+ properties.push({
112
+ name: propName,
113
+ type: type,
114
+ isMutable: isMutable,
115
+ decoratorText: decoratorText
116
+ });
117
+ }
118
+ }
119
+ }
120
+ }
121
+
122
+ return properties;
123
+ }
124
+
125
+ private parseMethods(classNode: ts.ClassDeclaration): ContractMethod[] {
126
+ const methods: ContractMethod[] = [];
127
+
128
+ for (const member of classNode.members) {
129
+ if (ts.isMethodDeclaration(member)) {
130
+ const decorators = ts.getDecorators(member);
131
+ if (decorators && decorators.length > 0) {
132
+ const methodDecorator = findDecorator(member, '@method');
133
+ if (methodDecorator) {
134
+ const methodName = member.name.getText();
135
+
136
+ // Skip private methods
137
+ if (!member.modifiers?.some(m => m.kind === ts.SyntaxKind.PublicKeyword)) {
138
+ continue;
139
+ }
140
+
141
+ const parameters = this.parseMethodParameters(member);
142
+ const returnType = getTypeText(member.type, this.checker);
143
+
144
+ const methodText = member.getText();
145
+ const usesHashOutputs = methodText.includes('this.ctx.hashOutputs');
146
+ const hasAssert = methodText.includes('assert(');
147
+
148
+ // Classify method
149
+ const category = this.classifyMethod(member, methodText);
150
+
151
+ // Determine if method mutates state
152
+ const mutatesState = this.doesMethodMutateState(member, methodText);
153
+
154
+ methods.push({
155
+ name: methodName,
156
+ parameters: parameters,
157
+ returnType: returnType,
158
+ category: category,
159
+ isPublic: true,
160
+ mutatesState: mutatesState,
161
+ usesHashOutputs: usesHashOutputs,
162
+ hasAssert: hasAssert,
163
+ decoratorText: getDecoratorText(methodDecorator)
164
+ });
165
+ }
166
+ }
167
+ }
168
+ }
169
+
170
+ return methods;
171
+ }
172
+
173
+ private parseMethodParameters(method: ts.MethodDeclaration): MethodParameter[] {
174
+ if (!method.parameters) {
175
+ return [];
176
+ }
177
+
178
+ return method.parameters.map(param => {
179
+ const paramName = param.name.getText();
180
+ const type = getTypeText(param.type, this.checker);
181
+ const isOptional = !!param.questionToken || !!param.initializer;
182
+
183
+ return {
184
+ name: paramName,
185
+ type: type,
186
+ isOptional: isOptional
187
+ };
188
+ });
189
+ }
190
+
191
+ private classifyMethod(method: ts.MethodDeclaration, methodText: string): MethodCategory {
192
+ const returnType = method.type?.getText() || 'void';
193
+
194
+ // Methods that return boolean are often validation methods
195
+ if (returnType === 'boolean') {
196
+ return MethodCategory.PURE_VALIDATION;
197
+ }
198
+
199
+ // Methods that modify @prop(true) properties
200
+ if (this.doesMethodMutateState(method, methodText)) {
201
+ return MethodCategory.STATE_TRANSITION;
202
+ }
203
+
204
+ // Methods that check spending conditions
205
+ if (methodText.includes('this.ctx') || methodText.includes('hashOutputs')) {
206
+ return MethodCategory.SPENDING_CONSTRAINT;
207
+ }
208
+
209
+ return MethodCategory.PURE_VALIDATION;
210
+ }
211
+
212
+ private doesMethodMutateState(method: ts.MethodDeclaration, methodText: string): boolean {
213
+ // Check for assignment to this.props (especially mutable ones)
214
+ const assignmentRegex = /this\.\w+\s*=/;
215
+ return assignmentRegex.test(methodText);
216
+ }
217
+
218
+ private parseImports(): string[] {
219
+ const imports: string[] = [];
220
+
221
+ const visit = (node: ts.Node) => {
222
+ if (ts.isImportDeclaration(node)) {
223
+ imports.push(node.getText());
224
+ }
225
+ ts.forEachChild(node, visit);
226
+ };
227
+
228
+ visit(this.sourceFile);
229
+ return imports;
230
+ }
231
+
232
+ private getExtendedClass(classNode: ts.ClassDeclaration): string {
233
+ if (!classNode.heritageClauses) return '';
234
+
235
+ for (const heritage of classNode.heritageClauses) {
236
+ for (const type of heritage.types) {
237
+ const typeText = type.getText();
238
+ if (typeText.includes('SmartContract')) {
239
+ return typeText;
240
+ }
241
+ }
242
+ }
243
+
244
+ return '';
245
+ }
246
+ }
@@ -0,0 +1,2 @@
1
+ export { ContractParser } from './contract-parser';
2
+ export type { ContractModel } from '../model/contract-model';
@@ -0,0 +1,22 @@
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
2
+ import { dirname } from 'path';
3
+
4
+ export function ensureDirectoryExists(filePath: string): void {
5
+ const dir = dirname(filePath);
6
+ if (!existsSync(dir)) {
7
+ mkdirSync(dir, { recursive: true });
8
+ }
9
+ }
10
+
11
+ export function readContractFile(filePath: string): string {
12
+ try {
13
+ return readFileSync(filePath, 'utf-8');
14
+ } catch (error) {
15
+ throw new Error(`Cannot read contract file: ${filePath}. ${error}`);
16
+ }
17
+ }
18
+
19
+ export function writeTestFile(filePath: string, content: string): void {
20
+ ensureDirectoryExists(filePath);
21
+ writeFileSync(filePath, content, 'utf-8');
22
+ }
File without changes
@@ -0,0 +1,16 @@
1
+ import { SmartContract, prop, method, assert, PubKey, Sig } from 'scrypt-ts';
2
+
3
+ export class SimpleLock extends SmartContract {
4
+ @prop()
5
+ readonly owner: PubKey;
6
+
7
+ constructor(owner: PubKey) {
8
+ super();
9
+ this.owner = owner;
10
+ }
11
+
12
+ @method()
13
+ public unlock(sig: Sig) {
14
+ assert(this.checkSig(sig, this.owner), 'Signature check failed');
15
+ }
16
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "commonjs",
5
+ "lib": ["ES2020"],
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "resolveJsonModule": true,
13
+ "declaration": true,
14
+ "declarationMap": true,
15
+ "sourceMap": true,
16
+ "experimentalDecorators": true,
17
+ "emitDecoratorMetadata": true
18
+ },
19
+ "include": ["src/**/*"],
20
+ "exclude": ["node_modules", "dist", "test"]
21
+ }