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.
- package/.prettierrc +7 -0
- package/README.md +17 -0
- package/bin/scrypt-testgen.js +2 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +48 -0
- package/dist/cli.js.map +1 -0
- package/dist/generator/index.d.ts +6 -0
- package/dist/generator/index.d.ts.map +1 -0
- package/dist/generator/index.js +33 -0
- package/dist/generator/index.js.map +1 -0
- package/dist/generator/test-generator.d.ts +17 -0
- package/dist/generator/test-generator.d.ts.map +1 -0
- package/dist/generator/test-generator.js +177 -0
- package/dist/generator/test-generator.js.map +1 -0
- package/dist/generator/value-generator.d.ts +19 -0
- package/dist/generator/value-generator.d.ts.map +1 -0
- package/dist/generator/value-generator.js +130 -0
- package/dist/generator/value-generator.js.map +1 -0
- package/dist/model/contract-model.d.ts +42 -0
- package/dist/model/contract-model.d.ts.map +1 -0
- package/dist/model/contract-model.js +10 -0
- package/dist/model/contract-model.js.map +1 -0
- package/dist/parser/ast-utils.d.ts +7 -0
- package/dist/parser/ast-utils.d.ts.map +1 -0
- package/dist/parser/ast-utils.js +114 -0
- package/dist/parser/ast-utils.js.map +1 -0
- package/dist/parser/contract-parser.d.ts +18 -0
- package/dist/parser/contract-parser.d.ts.map +1 -0
- package/dist/parser/contract-parser.js +224 -0
- package/dist/parser/contract-parser.js.map +1 -0
- package/dist/parser/index.d.ts +3 -0
- package/dist/parser/index.d.ts.map +1 -0
- package/dist/parser/index.js +6 -0
- package/dist/parser/index.js.map +1 -0
- package/dist/utils/file-utils.d.ts +4 -0
- package/dist/utils/file-utils.d.ts.map +1 -0
- package/dist/utils/file-utils.js +26 -0
- package/dist/utils/file-utils.js.map +1 -0
- package/package.json +30 -0
- package/src/cli.ts +51 -0
- package/src/generator/index.ts +45 -0
- package/src/generator/test-generator.ts +216 -0
- package/src/generator/value-generator.ts +138 -0
- package/src/model/contract-model.ts +46 -0
- package/src/parser/ast-utils.ts +75 -0
- package/src/parser/contract-parser.ts +246 -0
- package/src/parser/index.ts +2 -0
- package/src/utils/file-utils.ts +22 -0
- package/templates/test-template.ts +0 -0
- package/test/integration/demo.ts +16 -0
- 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,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
|
+
}
|