scrypt-testgen 1.0.0 โ 1.0.1
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 +12 -8
- package/package.json +17 -6
- package/.prettierrc +0 -7
- package/src/cli.ts +0 -51
- package/src/generator/index.ts +0 -45
- package/src/generator/test-generator.ts +0 -216
- package/src/generator/value-generator.ts +0 -138
- package/src/model/contract-model.ts +0 -46
- package/src/parser/ast-utils.ts +0 -75
- package/src/parser/contract-parser.ts +0 -246
- package/src/parser/index.ts +0 -2
- package/src/utils/file-utils.ts +0 -22
- package/templates/test-template.ts +0 -0
- package/test/integration/demo.ts +0 -16
- package/tsconfig.json +0 -21
package/README.md
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
# scrypt-testgen
|
|
2
2
|
|
|
3
|
-
Automatic test generator for sCrypt-ts smart contracts. Generates comprehensive Mocha test files by analyzing contract structure
|
|
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
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
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
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
```bash
|
|
21
|
+
scrypt-testgen path/to/contract.ts --minimal or -- coverage
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "scrypt-testgen",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Automatic test generator for sCrypt-ts smart contracts",
|
|
5
5
|
"main": "dist/cli.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"scrypt-testgen": "
|
|
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": [
|
|
16
|
-
|
|
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
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();
|
package/src/generator/index.ts
DELETED
|
@@ -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
|
-
}
|
package/src/parser/ast-utils.ts
DELETED
|
@@ -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
|
-
}
|
package/src/parser/index.ts
DELETED
package/src/utils/file-utils.ts
DELETED
|
@@ -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
|
package/test/integration/demo.ts
DELETED
|
@@ -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
|
-
}
|