Skip to content

Commit 516c6c9

Browse files
feat/starknet-generate-artifacts-test
1 parent f0d7677 commit 516c6c9

9 files changed

Lines changed: 446 additions & 2 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@hyperlane-xyz/starknet-core': minor
3+
---
4+
5+
Cairo artifact generation tests

starknet/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
.env
22
dist
33
release
4+
tmp

starknet/.mocharc.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"extensions": ["ts"],
3+
"spec": ["src/**/*.test.*"],
4+
"node-option": ["experimental-specifier-resolution=node"]
5+
}

starknet/eslint.config.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,6 @@ export default [
1010
},
1111
},
1212
{
13-
ignores: ['scripts/**/*'],
13+
ignores: ['scripts/**/*', 'tests/**/*'],
1414
},
1515
];

starknet/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
"build": "tsc && yarn fetch-contracts && yarn generate-artifacts",
1212
"clean": "rm -rf ./dist ./release",
1313
"lint": "eslint -c ./eslint.config.mjs .",
14-
"prettier": "prettier --write ./src ./package.json"
14+
"prettier": "prettier --write ./src ./package.json",
15+
"test": "mocha --require tsx 'tests/**/*.test.ts'"
1516
},
1617
"exports": {
1718
".": {
@@ -40,6 +41,7 @@
4041
"eslint-import-resolver-typescript": "^3.6.3",
4142
"eslint-plugin-import": "^2.31.0",
4243
"globby": "^14.1.0",
44+
"mocha": "^10.2.0",
4345
"prettier": "^3.5.3",
4446
"tsx": "^4.19.1",
4547
"typescript": "5.3.3"
Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
import { assert, expect } from 'chai';
2+
import { promises as fs } from 'fs';
3+
import { afterEach, beforeEach, describe, it } from 'mocha';
4+
import { dirname, join } from 'path';
5+
import { fileURLToPath } from 'url';
6+
7+
import { StarknetArtifactGenerator } from '../scripts/StarknetArtifactGenerator.js';
8+
import { CONTRACT_SUFFIXES } from '../src/const.js';
9+
import { ContractClass, ContractType } from '../src/types.js';
10+
11+
import { createMockContractFiles, createMockSierraArtifact } from './utils.js';
12+
13+
const __filename = fileURLToPath(import.meta.url);
14+
const __dirname = dirname(__filename);
15+
const TMP_DIR = join(__dirname, './tmp');
16+
const TEST_RELEASE_DIR = join(TMP_DIR, 'release');
17+
const TEST_OUTPUT_DIR = join(TMP_DIR, 'dist/artifacts');
18+
19+
describe('StarknetArtifactGenerator', () => {
20+
let generator: StarknetArtifactGenerator;
21+
22+
beforeEach(async () => {
23+
await Promise.all([
24+
fs.mkdir(TMP_DIR, { recursive: true }),
25+
fs.mkdir(TEST_RELEASE_DIR, { recursive: true }),
26+
fs.mkdir(TEST_OUTPUT_DIR, { recursive: true }),
27+
]);
28+
29+
await createMockContractFiles(TEST_RELEASE_DIR);
30+
31+
generator = new StarknetArtifactGenerator(
32+
TEST_RELEASE_DIR,
33+
TEST_OUTPUT_DIR,
34+
);
35+
});
36+
37+
afterEach(async () => {
38+
await fs.rm(TMP_DIR, { recursive: true, force: true }).catch(() => {});
39+
});
40+
41+
it('should correctly identify contract types from filenames', () => {
42+
expect(generator.getContractTypeFromPath('token_MyToken.json')).to.equal(
43+
ContractType.TOKEN,
44+
);
45+
expect(
46+
generator.getContractTypeFromPath('mocks_MockContract.json'),
47+
).to.equal(ContractType.MOCK);
48+
expect(
49+
generator.getContractTypeFromPath('contracts_IMailbox.json'),
50+
).to.equal(ContractType.CONTRACT);
51+
// Defaults to CONTRACT for unknown prefixes
52+
expect(generator.getContractTypeFromPath('random_name.json')).to.equal(
53+
ContractType.CONTRACT,
54+
);
55+
});
56+
57+
it('should create the output directory if it does not exist', async () => {
58+
await fs.rm(TEST_OUTPUT_DIR, { recursive: true, force: true });
59+
await generator.createOutputDirectory();
60+
const stats = await fs.stat(TEST_OUTPUT_DIR);
61+
expect(stats.isDirectory()).to.be.true;
62+
});
63+
64+
it('should read and parse a valid Sierra artifact file', async () => {
65+
const filePath = join(
66+
TEST_RELEASE_DIR,
67+
`contracts_Test${CONTRACT_SUFFIXES.SIERRA_JSON}`,
68+
);
69+
const artifact = await generator.readArtifactFile(filePath);
70+
expect(artifact).to.deep.include({
71+
contract_class_version: '0.1.0',
72+
sierra_program: [], // Based on createMockSierraArtifact
73+
});
74+
expect(artifact.abi).to.be.an('array');
75+
});
76+
77+
it('should generate JS content with expected properties for Sierra contracts', () => {
78+
const artifact = createMockSierraArtifact();
79+
const jsContent = generator.generateJavaScriptContent(
80+
'TestContract',
81+
artifact,
82+
ContractClass.SIERRA,
83+
);
84+
85+
const jsonMatch = jsContent.match(/^export const \w+ = (\{.*\});$/s);
86+
assert(jsonMatch, 'Should find exported JSON object in JS content');
87+
88+
const parsedArtifact = JSON.parse(jsonMatch[1]);
89+
expect(parsedArtifact).to.be.an('object');
90+
expect(parsedArtifact)
91+
.to.have.property('sierra_program')
92+
.that.is.an('array');
93+
expect(parsedArtifact)
94+
.to.have.property('entry_points_by_type')
95+
.deep.equal(artifact.entry_points_by_type);
96+
expect(parsedArtifact).to.have.property('abi').that.is.an('array');
97+
// Check if a specific known item from the mock ABI exists
98+
const hasTestFunction = parsedArtifact.abi.some(
99+
(item: any) => item.name === 'test_function',
100+
);
101+
expect(hasTestFunction, 'ABI should contain "test_function"').to.be.true;
102+
});
103+
104+
it('should generate correct TypeScript declaration for Sierra contracts', () => {
105+
const dtsContent = generator.generateDeclarationContent(
106+
'TestSierra',
107+
true, // isSierra = true
108+
);
109+
expect(dtsContent).to.include(
110+
'export declare const TestSierra: CompiledContract',
111+
);
112+
});
113+
114+
it('should generate correct TypeScript declaration for CASM contracts', () => {
115+
const dtsContent = generator.generateDeclarationContent(
116+
'TestCasm',
117+
false, // isSierra = false
118+
);
119+
expect(dtsContent).to.include(
120+
'export declare const TestCasm: CairoAssembly',
121+
);
122+
});
123+
124+
it('should process a Sierra artifact file correctly, generating JS and DTS files', async () => {
125+
const fileName = `contracts_Test${CONTRACT_SUFFIXES.SIERRA_JSON}`;
126+
const filePath = join(TEST_RELEASE_DIR, fileName);
127+
const artifact = await generator.readArtifactFile(filePath); // Read expected artifact data
128+
129+
const processResult = await generator.processArtifact(filePath);
130+
131+
// Verify the returned info object
132+
expect(processResult).to.deep.equal({
133+
name: 'contracts_Test',
134+
contractType: ContractType.CONTRACT,
135+
contractClass: ContractClass.SIERRA,
136+
});
137+
138+
const jsPath = join(
139+
TEST_OUTPUT_DIR,
140+
`contracts_Test.${ContractClass.SIERRA}.js`,
141+
);
142+
const dtsPath = join(
143+
TEST_OUTPUT_DIR,
144+
`contracts_Test.${ContractClass.SIERRA}.d.ts`,
145+
);
146+
await fs.access(jsPath); // Throws if file doesn't exist
147+
await fs.access(dtsPath); // Throws if file doesn't exist
148+
149+
const jsContent = await fs.readFile(jsPath, 'utf-8');
150+
const jsonMatch = jsContent.match(/^export const \w+ = (\{.*\});$/s);
151+
assert(jsonMatch, 'Should find JSON object in generated JS file');
152+
const parsedJsArtifact = JSON.parse(jsonMatch[1]);
153+
154+
expect(parsedJsArtifact).to.be.an('object');
155+
expect(parsedJsArtifact)
156+
.to.have.property('sierra_program')
157+
.that.is.an('array');
158+
expect(parsedJsArtifact).to.have.property(
159+
'contract_class_version',
160+
artifact.contract_class_version,
161+
);
162+
expect(parsedJsArtifact)
163+
.to.have.property('entry_points_by_type')
164+
.deep.equal(artifact.entry_points_by_type);
165+
expect(parsedJsArtifact)
166+
.to.have.property('abi')
167+
.deep.equal(
168+
typeof artifact.abi === 'string'
169+
? JSON.parse(artifact.abi)
170+
: artifact.abi,
171+
);
172+
173+
const dtsContent = await fs.readFile(dtsPath, 'utf-8');
174+
expect(dtsContent).to.include(
175+
'export declare const contracts_Test: CompiledContract',
176+
);
177+
});
178+
179+
it('should process a standard set of artifacts, generate index files, and return correct summary', async () => {
180+
// Assumes beforeEach creates: contracts_Test, token_HypERC20, mocks_MockContract (all Sierra)
181+
const processedMap = await generator.generate();
182+
183+
expect(processedMap.size).to.equal(3);
184+
expect(processedMap.get('contracts_Test')).to.deep.equal({
185+
type: ContractType.CONTRACT,
186+
sierra: true,
187+
casm: false,
188+
});
189+
expect(processedMap.get('token_HypERC20')).to.deep.equal({
190+
type: ContractType.TOKEN,
191+
sierra: true,
192+
casm: false,
193+
});
194+
expect(processedMap.get('mocks_MockContract')).to.deep.equal({
195+
type: ContractType.MOCK,
196+
sierra: true,
197+
casm: false,
198+
});
199+
200+
const indexJsPath = join(TEST_OUTPUT_DIR, 'index.js');
201+
const indexDtsPath = join(TEST_OUTPUT_DIR, 'index.d.ts');
202+
await fs.access(indexJsPath);
203+
await fs.access(indexDtsPath);
204+
205+
// Check index file content (verify artifact paths are included)
206+
const indexJsContent = await fs.readFile(indexJsPath, 'utf-8');
207+
208+
expect(indexJsContent).to.include(
209+
`./contracts_Test.${ContractClass.SIERRA}.js`,
210+
);
211+
expect(indexJsContent).to.include(
212+
`./token_HypERC20.${ContractClass.SIERRA}.js`,
213+
);
214+
expect(indexJsContent).to.include(
215+
`./mocks_MockContract.${ContractClass.SIERRA}.js`,
216+
);
217+
218+
// Check artifact file existence (spot check one)
219+
const testSierraJsPath = join(
220+
TEST_OUTPUT_DIR,
221+
`contracts_Test.${ContractClass.SIERRA}.js`,
222+
);
223+
await fs.access(testSierraJsPath);
224+
});
225+
226+
it('should handle malformed artifact files gracefully during generation', async () => {
227+
const malformedFilePath = join(
228+
TEST_RELEASE_DIR,
229+
`malformed${CONTRACT_SUFFIXES.SIERRA_JSON}`,
230+
);
231+
await fs.writeFile(malformedFilePath, '{ this is not valid JSON }');
232+
233+
let errorThrown = false;
234+
try {
235+
await generator.generate(); // This should throw an error
236+
} catch (error) {
237+
errorThrown = true;
238+
expect(error).to.be.instanceOf(Error);
239+
}
240+
// Expect generate() to throw when encountering invalid JSON
241+
expect(errorThrown, 'Generator should throw an error for malformed JSON').to
242+
.be.true;
243+
});
244+
245+
it('should correctly process artifacts with unusual filenames during generation', async () => {
246+
// Add a file with an unusual name that still fits the expected suffix pattern
247+
const oddNamedFilePath = join(
248+
TEST_RELEASE_DIR,
249+
`unusual_prefix.contract_class.json`, // Uses SIERRA_JSON suffix
250+
);
251+
await fs.writeFile(
252+
oddNamedFilePath,
253+
JSON.stringify(createMockSierraArtifact()), // Use valid content
254+
);
255+
256+
const processedMap = await generator.generate();
257+
258+
// Expect 4 total artifacts now (3 standard + 1 unusual)
259+
expect(processedMap.size).to.equal(4);
260+
261+
expect(processedMap.has('unusual_prefix')).to.be.true;
262+
const fileInfo = processedMap.get('unusual_prefix');
263+
expect(fileInfo).to.deep.equal({
264+
type: ContractType.CONTRACT, // Default type for unknown prefix
265+
sierra: true,
266+
casm: false,
267+
});
268+
269+
const jsPath = join(
270+
TEST_OUTPUT_DIR,
271+
`unusual_prefix.${ContractClass.SIERRA}.js`,
272+
);
273+
const dtsPath = join(
274+
TEST_OUTPUT_DIR,
275+
`unusual_prefix.${ContractClass.SIERRA}.d.ts`,
276+
);
277+
await fs.access(jsPath);
278+
await fs.access(dtsPath);
279+
280+
const indexJsPath = join(TEST_OUTPUT_DIR, 'index.js');
281+
const indexJsContent = await fs.readFile(indexJsPath, 'utf-8');
282+
expect(indexJsContent).to.include(
283+
`./unusual_prefix.${ContractClass.SIERRA}.js`, // Check path inclusion
284+
);
285+
});
286+
});

starknet/tests/Templates.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { expect } from 'chai';
2+
import { describe, it } from 'mocha';
3+
4+
import { Templates } from '../scripts/Templates.js';
5+
6+
describe('Templates', () => {
7+
it('should generate correct JS content for CompiledContract - Artifact', () => {
8+
const name = 'TestContract';
9+
const artifact = {
10+
abi: [{ name: 'test_func' }],
11+
sierra_program: ['0x1', '0x2'],
12+
contract_class_version: '0.1.0',
13+
};
14+
const expectedOutput = `export const ${name} = ${JSON.stringify(
15+
artifact,
16+
)};`;
17+
18+
const result = Templates.jsArtifact(name, artifact);
19+
expect(result).to.equal(expectedOutput);
20+
});
21+
22+
it('should generate correct DTS content for CompiledContract type', () => {
23+
const name = 'SierraContract';
24+
const type = 'CompiledContract';
25+
const result = Templates.dtsArtifact(name, type);
26+
27+
expect(result).to.include(
28+
`import type { CompiledContract, CairoAssembly } from 'starknet';`,
29+
);
30+
expect(result).to.include(`export declare const ${name}: ${type};`);
31+
});
32+
33+
it('should generate correct JS index file content', () => {
34+
const imports = `import { A as A_sierra } from './A.sierra.js';\nimport { B as B_sierra } from './B.sierra.js';`;
35+
const contractExports = `A: { contract_class: A_sierra },`;
36+
const tokenExports = `B: { contract_class: B_sierra },`;
37+
const mockExports = ``; // Empty for this test
38+
39+
const result = Templates.jsIndex(
40+
imports,
41+
[contractExports],
42+
[tokenExports],
43+
[mockExports],
44+
);
45+
46+
// Check for the overall structure and interpolation
47+
expect(result).to.include(imports);
48+
expect(result).to.include('export const starknetContracts = {');
49+
expect(result).to.include('contracts: {');
50+
expect(result).to.include(contractExports);
51+
expect(result).to.include('token: {');
52+
expect(result).to.include(tokenExports);
53+
expect(result).to.include('mocks: {');
54+
expect(result).to.include(mockExports); // Should correctly interpolate empty string
55+
expect(result).to.include('};'); // Closing brace
56+
});
57+
58+
it('should generate correct DTS index file content', () => {
59+
const result = Templates.dtsIndex();
60+
61+
expect(result).to.include(
62+
`import type { CairoAssembly, CompiledContract } from 'starknet';`,
63+
);
64+
expect(result).to.include(
65+
'export declare const starknetContracts: StarknetContracts;',
66+
);
67+
});
68+
});

0 commit comments

Comments
 (0)