scrypt-testgen 1.0.0 โ†’ 1.0.2

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/README.md CHANGED
@@ -1,17 +1,22 @@
1
1
  # scrypt-testgen
2
2
 
3
- Automatic test generator for sCrypt-ts smart contracts. Generates comprehensive Mocha test files by analyzing contract structure using TypeScript Compiler API.
3
+ Automatic test generator for sCrypt-ts smart contracts. Generates comprehensive Mocha test files by analyzing contract structure.
4
4
 
5
5
  ## Features
6
6
 
7
- - ๐Ÿ“ Parses sCrypt-ts contracts using TypeScript AST
8
- - ๐Ÿงช Generates ready-to-run Mocha test files
9
- - โœ… Includes happy-path tests for all public methods
10
- - ๐Ÿšซ Generates revert tests for assertion failures
11
- - ๐ŸŽฏ Creates edge-case tests for comprehensive coverage
12
- - ๐ŸŽจ Produces clean, readable, and idiomatic test code
7
+ - Parses sCrypt-ts contracts
8
+ - Generates ready-to-run Mocha test files
9
+ - Includes happy-path tests for all public methods
10
+ - Creates edge-case tests for comprehensive coverage
11
+ - Produces clean and readable test code
13
12
 
14
13
  ## Installation
15
14
 
16
15
  ```bash
17
- npm install -g scrypt-testgen
16
+ npm install -g scrypt-testgen@latest
17
+
18
+
19
+ npx scrypt-testgen path/to/contract.ts
20
+
21
+ # optional
22
+ --minimal or -- coverage
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "scrypt-testgen",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Automatic test generator for sCrypt-ts smart contracts",
5
5
  "main": "dist/cli.js",
6
6
  "bin": {
7
- "scrypt-testgen": "./bin/scrypt-testgen.js"
7
+ "scrypt-testgen": "bin/scrypt-testgen.js"
8
8
  },
9
9
  "scripts": {
10
10
  "build": "tsc",
@@ -12,8 +12,14 @@
12
12
  "dev": "ts-node src/cli.ts",
13
13
  "prepublishOnly": "npm run build"
14
14
  },
15
- "keywords": ["scrypt", "bitcoin", "smart-contract", "test", "mocha"],
16
- "author": "",
15
+ "keywords": [
16
+ "scrypt",
17
+ "bitcoin",
18
+ "smart-contract",
19
+ "test",
20
+ "mocha"
21
+ ],
22
+ "author": "Yusuf Idi Maina",
17
23
  "license": "MIT",
18
24
  "dependencies": {
19
25
  "@types/node": "^20.0.0",
@@ -26,5 +32,10 @@
26
32
  "@types/mocha": "^10.0.0",
27
33
  "ts-node": "^10.9.0",
28
34
  "prettier": "^3.0.0"
29
- }
30
- }
35
+ },
36
+ "files": [
37
+ "dist",
38
+ "bin",
39
+ "README.md"
40
+ ]
41
+ }
package/.prettierrc DELETED
@@ -1,7 +0,0 @@
1
- {
2
- "semi": true,
3
- "trailingComma": "es5",
4
- "singleQuote": true,
5
- "printWidth": 100,
6
- "tabWidth": 2
7
- }
package/src/cli.ts DELETED
@@ -1,51 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { Command } from 'commander';
4
- import { generateTests } from './generator';
5
- import { resolve, dirname, basename, join } from 'path';
6
- import { mkdirSync, existsSync } from 'fs';
7
- import chalk from 'chalk';
8
-
9
- const program = new Command();
10
-
11
- program
12
- .name('scrypt-testgen')
13
- .description('Generate Mocha tests for sCrypt-ts smart contracts')
14
- .version('1.0.0');
15
-
16
- program
17
- .argument('<contract-path>', 'Path to the sCrypt contract TypeScript file')
18
- .option('-m, --minimal', 'Generate only happy-path tests', false)
19
- .option('-c, --coverage', 'Include revert and edge-case tests', false)
20
- .option('-o, --overwrite', 'Replace existing test file', false)
21
- .option('-o, --output <path>', 'Custom output path for test file')
22
- .action(async (contractPath, options) => {
23
- try {
24
- const resolvedPath = resolve(process.cwd(), contractPath);
25
-
26
- if (!existsSync(resolvedPath)) {
27
- console.error(chalk.red(`Error: Contract file not found: ${resolvedPath}`));
28
- process.exit(1);
29
- }
30
-
31
- const outputPath = options.output
32
- ? resolve(process.cwd(), options.output)
33
- : join(dirname(resolvedPath), '..', 'test', basename(resolvedPath).replace('.ts', '.test.ts'));
34
-
35
- // Ensure test directory exists
36
- mkdirSync(dirname(outputPath), { recursive: true });
37
-
38
- await generateTests(resolvedPath, outputPath, {
39
- minimal: options.minimal,
40
- coverage: options.coverage,
41
- overwrite: options.overwrite
42
- });
43
-
44
- console.log(chalk.green(`โœ“ Tests generated: ${outputPath}`));
45
- } catch (error) {
46
- console.error(chalk.red(`Error: ${error}`));
47
- process.exit(1);
48
- }
49
- });
50
-
51
- program.parse();
@@ -1,45 +0,0 @@
1
- import { ContractParser } from '../parser';
2
- import { TestGenerator } from './test-generator';
3
- import { existsSync, writeFileSync, readFileSync } from 'fs';
4
- import * as path from 'path';
5
-
6
- export async function generateTests(
7
- contractPath: string,
8
- outputPath: string,
9
- options: {
10
- minimal: boolean;
11
- coverage: boolean;
12
- overwrite: boolean;
13
- }
14
- ): Promise<void> {
15
-
16
- // Check if output file exists
17
- if (existsSync(outputPath) && !options.overwrite) {
18
- throw new Error(`Test file already exists: ${outputPath}. Use --overwrite to replace.`);
19
- }
20
-
21
- // Parse the contract
22
- const parser = new ContractParser(contractPath);
23
- const model = parser.parse();
24
-
25
- // Generate tests
26
- const generator = new TestGenerator();
27
- const testCode = generator.generateTestFile(model, {
28
- minimal: options.minimal,
29
- coverage: options.coverage
30
- });
31
-
32
- // Format the output (simplified - in production, use prettier API)
33
- const formattedCode = formatCode(testCode);
34
-
35
- // Write to file
36
- writeFileSync(outputPath, formattedCode, 'utf-8');
37
- }
38
-
39
- function formatCode(code: string): string {
40
- // Simple formatting - in production, integrate with Prettier
41
- return code
42
- .replace(/\n\s*\n\s*\n/g, '\n\n')
43
- .replace(/\s+$/gm, '')
44
- .trim() + '\n';
45
- }
@@ -1,216 +0,0 @@
1
- import { ContractModel, MethodCategory } from '../model/contract-model';
2
- import { ValueGenerator } from './value-generator';
3
-
4
- export class TestGenerator {
5
- private valueGenerator: ValueGenerator;
6
-
7
- constructor() {
8
- this.valueGenerator = new ValueGenerator();
9
- }
10
-
11
- generateTestFile(model: ContractModel, options: {
12
- minimal: boolean;
13
- coverage: boolean;
14
- }): string {
15
- this.valueGenerator.reset();
16
-
17
- const imports = this.generateImports(model);
18
- const testSuite = this.generateTestSuite(model, options);
19
-
20
- return `${imports}\n\n${testSuite}`;
21
- }
22
-
23
- private generateImports(model: ContractModel): string {
24
- const contractName = model.name;
25
- const contractPath = this.getRelativeTestPath(model.filePath);
26
-
27
- return `import { expect } from 'chai';
28
- import { ${contractName} } from '${contractPath.replace('.ts', '')}';
29
- import {
30
- bsv,
31
- TestWallet,
32
- DefaultProvider,
33
- sha256,
34
- toByteString,
35
- PubKey,
36
- Sig,
37
- findSig,
38
- MethodCallOptions,
39
- ContractTransaction,
40
- getDefaultSigner
41
- } from 'scrypt-ts';
42
-
43
- describe('${contractName}', () => {
44
- let signer: TestWallet;
45
- let instance: ${contractName};`;
46
- }
47
-
48
- private getRelativeTestPath(contractPath: string): string {
49
- // Convert absolute path to relative from test directory
50
- const pathParts = contractPath.split('/');
51
- const srcIndex = pathParts.lastIndexOf('src');
52
-
53
- if (srcIndex !== -1) {
54
- const relativeParts = pathParts.slice(srcIndex);
55
- return `../${relativeParts.join('/')}`;
56
- }
57
-
58
- return contractPath;
59
- }
60
-
61
- private generateTestSuite(model: ContractModel, options: {
62
- minimal: boolean;
63
- coverage: boolean;
64
- }): string {
65
- let output = `
66
- before(async () => {
67
- await ${model.name}.loadArtifact();
68
-
69
- signer = await getDefaultSigner();
70
- });
71
-
72
- beforeEach(async () => {
73
- instance = new ${model.name}(${this.valueGenerator.generateConstructorArgs(model.constructorArgs)});
74
- await instance.connect(signer);
75
- });
76
- `;
77
-
78
- // Generate tests for each method
79
- for (const method of model.methods) {
80
- output += this.generateMethodTests(method, model, options);
81
- }
82
-
83
- output += `});`;
84
- return output;
85
- }
86
-
87
- private generateMethodTests(method: ContractModel['methods'][0], model: ContractModel, options: {
88
- minimal: boolean;
89
- coverage: boolean;
90
- }): string {
91
- let output = `\n\n describe('#${method.name}', () => {`;
92
-
93
- // Happy path test
94
- output += this.generateHappyPathTest(method, model);
95
-
96
- // Revert test (unless minimal mode)
97
- if (!options.minimal) {
98
- output += this.generateRevertTest(method, model);
99
- }
100
-
101
- // Edge case tests (if coverage mode)
102
- if (options.coverage) {
103
- output += this.generateEdgeCaseTests(method, model);
104
- }
105
-
106
- output += `\n });`;
107
- return output;
108
- }
109
-
110
- private generateHappyPathTest(method: ContractModel['methods'][0], model: ContractModel): string {
111
- const args = this.valueGenerator.generateMethodArgs(method.parameters, method.name);
112
- const methodCall = `instance.methods.${method.name}(${args})`;
113
-
114
- let test = `
115
- it('should succeed with valid parameters', async () => {
116
- const deployTx = await instance.deploy(1)
117
- const callTx = await ${methodCall};
118
-
119
- console.log('Deploy TX:', deployTx.id);
120
- console.log('Call TX:', callTx.tx.id);
121
- });
122
- `;
123
-
124
- // Add assertions for state transitions
125
- if (method.mutatesState) {
126
- test = `
127
- it('should succeed with valid parameters', async () => {
128
- const deployTx = await instance.deploy(1)
129
- const oldInstance = instance;
130
- const callTx = await ${methodCall};
131
-
132
- console.log('Deploy TX:', deployTx.id);
133
- console.log('Call TX:', callTx.tx.id);
134
-
135
- // Verify state transition if applicable
136
- const result = await callTx.tx.id;
137
- if (result.nexts && result.nexts.length > 0) {
138
- const nextInstance = result.nexts[0].instance;
139
- // Add state assertions here based on method logic
140
- }
141
- });
142
- `;
143
- }
144
-
145
- return test;
146
- }
147
-
148
- private generateRevertTest(method: ContractModel['methods'][0], model: ContractModel): string {
149
- const args = this.valueGenerator.generateInvalidMethodArgs(method.parameters, method.name);
150
- const methodCall = `instance.methods.${method.name}(${args})`;
151
-
152
- let test = `
153
- it('should revert with invalid parameters', async () => {
154
- try {
155
- const deployTx = await instance.deploy(1)
156
- const callTx = await ${methodCall};
157
- console.error('Expected method to revert, but it succeeded:', callTx.tx.id);
158
-
159
- } catch (error) {
160
- // Expected to throw an error
161
- expect(error).to.be.an('error');
162
- }
163
- });
164
- `;
165
-
166
- // Special handling for assert statements
167
- if (method.hasAssert) {
168
- test = `
169
- it('should revert when assertion fails', async () => {
170
- // Use parameters that will cause assert() to fail
171
- const deployTx = await instance.deploy(1)
172
- const callTx = await ${methodCall};
173
-
174
- console.log('Deploy TX:', deployTx.id);
175
- console.log('Call TX:', callTx.tx.id);
176
-
177
-
178
- });
179
- `;
180
- }
181
-
182
- return test;
183
- }
184
-
185
- private generateEdgeCaseTests(method: ContractModel['methods'][0], model: ContractModel): string {
186
- let tests = '';
187
-
188
- // Generate edge case for each parameter
189
- method.parameters.forEach((param, index) => {
190
- const edgeCaseValue = this.valueGenerator.getEdgeCaseValue(param.type);
191
- const modifiedParams = method.parameters.map((p, i) =>
192
- i === index ? { ...p, type: param.type } : p
193
- );
194
- const args = this.valueGenerator.generateMethodArgs(modifiedParams, method.name).replace(this.valueGenerator.generateMethodArgs(method.parameters, method.name), '').trim() || edgeCaseValue;
195
-
196
- const methodCall = `instance.methods.${method.name}(${args})`;
197
-
198
- tests += `
199
- it('should handle edge case for ${param.name} (${edgeCaseValue})', async () => {
200
- try {
201
- const deployTx = await instance.deploy(1)
202
- const callTx = await ${methodCall};
203
- // Edge case might succeed or fail depending on contract logic
204
- console.log('Deploy TX:', deployTx.id);
205
- console.log('Call TX:', callTx.tx.id);
206
- } catch (error) {
207
- // Handle expected errors gracefully
208
- expect(error).to.be.an('error');
209
- }
210
- });
211
- `;
212
- });
213
-
214
- return tests;
215
- }
216
- }
@@ -1,138 +0,0 @@
1
- import { ConstructorArg, MethodParameter } from '../model/contract-model';
2
- import { ContractModel } from '../model/contract-model';
3
-
4
- export class ValueGenerator {
5
- private usedVariableNames = new Set<string>();
6
- private testKeyPairs = new Map<string, { pubKey: string; privKey: string }>();
7
-
8
- constructor() {
9
- // Initialize with some test key pairs
10
- this.testKeyPairs.set('default', {
11
- pubKey: 'myPublicKey',
12
- privKey: 'myPrivateKey'
13
- });
14
- this.testKeyPairs.set('alice', {
15
- pubKey: 'alicePublicKey',
16
- privKey: 'alicePrivateKey'
17
- });
18
- this.testKeyPairs.set('bob', {
19
- pubKey: 'bobPublicKey',
20
- privKey: 'bobPrivateKey'
21
- });
22
- }
23
-
24
- generateConstructorArgs(args: ConstructorArg[]): string {
25
- if (args.length === 0) return '';
26
-
27
- return args.map(arg => {
28
- if (arg.defaultValue) {
29
- return arg.defaultValue;
30
- }
31
- return this.generateValueForType(arg.type, arg.name);
32
- }).join(', ');
33
- }
34
-
35
- generateMethodArgs(parameters: MethodParameter[], methodName: string): string {
36
- return parameters.map(param => {
37
- return this.generateValueForType(param.type, `${methodName}_${param.name}`);
38
- }).join(', ');
39
- }
40
-
41
- generateInvalidMethodArgs(parameters: MethodParameter[], methodName: string): string {
42
- return parameters.map(param => {
43
- return this.generateInvalidValueForType(param.type, `${methodName}_${param.name}`);
44
- }).join(', ');
45
- }
46
-
47
- private generateValueForType(type: string, variableName: string, model?: ContractModel): string {
48
- // Ensure unique variable names
49
- let uniqueName = variableName;
50
- let counter = 1;
51
- while (this.usedVariableNames.has(uniqueName)) {
52
- uniqueName = `${variableName}${counter}`;
53
- counter++;
54
- }
55
- this.usedVariableNames.add(uniqueName);
56
-
57
- switch (type) {
58
- case 'bigint':
59
- return '1n';
60
- case 'boolean':
61
- return 'true';
62
- case 'ByteString':
63
- return `toByteString('00', false)`;
64
- case 'PubKey':
65
- return this.getTestKey('default').pubKey;
66
- case 'Sig':
67
- return ` (sigResps) => findSig(sigResps, myPublicKey)),
68
- {
69
- pubKeyOrAddrToSign: myPublicKey,
70
- } as MethodCallOptions<${model?.name}>`;
71
- case 'number':
72
- return '42';
73
- case 'string':
74
- return `'test'`;
75
- default:
76
- if (type.includes('bigint[]')) {
77
- return '[1n, 2n, 3n]';
78
- }
79
- if (type.includes('[]')) {
80
- return `[]`;
81
- }
82
- return 'undefined';
83
- }
84
- }
85
-
86
- private generateInvalidValueForType(type: string, variableName: string): string {
87
- switch (type) {
88
- case 'bigint':
89
- return '0n'; // Often invalid for amounts
90
- case 'boolean':
91
- return 'false';
92
- case 'ByteString':
93
- return `toByteString('', false)`; // Empty byte string
94
- case 'PubKey':
95
- return `PubKey(toByteString('00', false))`; // Invalid pubkey
96
- case 'Sig':
97
- return `Sig(toByteString('00', false))`; // Invalid signature
98
- case 'number':
99
- return '-1';
100
- case 'string':
101
- return `''`;
102
- default:
103
- return this.generateValueForType(type, variableName);
104
- }
105
- }
106
-
107
- getTestKey(name: string = 'default'): { pubKey: string; privKey: string } {
108
- const keyPair = this.testKeyPairs.get(name);
109
- if (!keyPair) {
110
- throw new Error(`Test key pair '${name}' not found`);
111
- }
112
- return keyPair;
113
- }
114
-
115
- getEdgeCaseValue(type: string, model?: ContractModel): string {
116
- switch (type) {
117
- case 'bigint':
118
- return '0n';
119
- case 'boolean':
120
- return 'false';
121
- case 'ByteString':
122
- return `toByteString('ff', false)`;
123
- case 'PubKey':
124
- return `PubKey(toByteString('00'.repeat(33), false))`;
125
- case 'Sig':
126
- return ` (sigResps) => findSig(sigResps, myPublickey)),
127
- {
128
- pubKeyOrAddrToSign: myPublicKey,
129
- } as MethodCallOptions<${model?.name}>`;
130
- default:
131
- return this.generateValueForType(type, 'edgeCase');
132
- }
133
- }
134
-
135
- reset(): void {
136
- this.usedVariableNames.clear();
137
- }
138
- }
@@ -1,46 +0,0 @@
1
- export interface ConstructorArg {
2
- name: string;
3
- type: string;
4
- defaultValue?: string;
5
- }
6
-
7
- export interface ContractProp {
8
- name: string;
9
- type: string;
10
- isMutable: boolean;
11
- decoratorText: string;
12
- }
13
-
14
- export interface MethodParameter {
15
- name: string;
16
- type: string;
17
- isOptional: boolean;
18
- }
19
-
20
- export enum MethodCategory {
21
- PURE_VALIDATION = 'pure-validation',
22
- STATE_TRANSITION = 'state-transition',
23
- SPENDING_CONSTRAINT = 'spending-constraint'
24
- }
25
-
26
- export interface ContractMethod {
27
- name: string;
28
- parameters: MethodParameter[];
29
- returnType: string;
30
- category: MethodCategory;
31
- isPublic: boolean;
32
- mutatesState: boolean;
33
- usesHashOutputs: boolean;
34
- hasAssert: boolean;
35
- decoratorText: string;
36
- }
37
-
38
- export interface ContractModel {
39
- name: string;
40
- filePath: string;
41
- constructorArgs: ConstructorArg[];
42
- props: ContractProp[];
43
- methods: ContractMethod[];
44
- imports: string[];
45
- extendsClass: string;
46
- }
@@ -1,75 +0,0 @@
1
- import * as ts from 'typescript';
2
-
3
- export function getDecoratorText(decorator: ts.Decorator): string {
4
- const decoratorText = decorator.getText();
5
- if (decoratorText.includes('@prop')) {
6
- return '@prop';
7
- } else if (decoratorText.includes('@method')) {
8
- return '@method';
9
- }
10
- return decoratorText;
11
- }
12
-
13
- export function getTypeText(type: ts.TypeNode | undefined, checker: ts.TypeChecker): string {
14
- if (!type) return 'any';
15
-
16
- const typeText = type.getText();
17
-
18
- // Handle sCrypt specific types
19
- if (typeText.includes('bigint')) return 'bigint';
20
- if (typeText.includes('ByteString')) return 'ByteString';
21
- if (typeText.includes('PubKey')) return 'PubKey';
22
- if (typeText.includes('Sig')) return 'Sig';
23
- if (typeText.includes('boolean')) return 'boolean';
24
- if (typeText.includes('number')) return 'number';
25
-
26
- return typeText;
27
- }
28
-
29
- export function getDefaultValueForType(type: string): string {
30
- switch (type) {
31
- case 'bigint':
32
- return '1n';
33
- case 'boolean':
34
- return 'true';
35
- case 'ByteString':
36
- return `toByteString('00', false)`;
37
- case 'PubKey':
38
- return `myPublicKey`;
39
- case 'Sig':
40
- return `mySignature`;
41
- case 'number':
42
- return '0';
43
- case 'string':
44
- return "''";
45
- default:
46
- if (type.includes('[]')) return '[]';
47
- return 'undefined';
48
- }
49
- }
50
-
51
- export function isSmartContractClass(node: ts.Node): boolean {
52
- if (!ts.isClassDeclaration(node)) return false;
53
-
54
- // Check if extends SmartContract
55
- if (node.heritageClauses) {
56
- for (const heritage of node.heritageClauses) {
57
- for (const type of heritage.types) {
58
- if (type.getText().includes('SmartContract')) {
59
- return true;
60
- }
61
- }
62
- }
63
- }
64
-
65
- return false;
66
- }
67
-
68
- export function findDecorator(node: ts.Node, decoratorName: string): ts.Decorator | undefined {
69
- if (!ts.canHaveDecorators(node)) return undefined;
70
-
71
- const decorators = ts.getDecorators(node);
72
- return decorators?.find((decorator: ts.Decorator) =>
73
- decorator.getText().includes(decoratorName)
74
- );
75
- }
@@ -1,246 +0,0 @@
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
- }
@@ -1,2 +0,0 @@
1
- export { ContractParser } from './contract-parser';
2
- export type { ContractModel } from '../model/contract-model';
@@ -1,22 +0,0 @@
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
@@ -1,16 +0,0 @@
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 DELETED
@@ -1,21 +0,0 @@
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
- }