Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
0eb975f
feat/starknet-core-abi-exports
ljankovic-txfusion May 9, 2025
bc3f700
feat/starknet-utils
ljankovic-txfusion May 9, 2025
f0d7677
feat/starknet-sdk-integration
ljankovic-txfusion May 9, 2025
516c6c9
feat/starknet-generate-artifacts-test
ljankovic-txfusion May 9, 2025
66b7511
Merge remote-tracking branch 'origin/main' into feat/starknet-core-ab…
ljankovic-txfusion May 15, 2025
14d3792
Merge remote-tracking branch 'origin/feat/starknet-core-abi-exports' …
ljankovic-txfusion May 15, 2025
6a8299d
Merge remote-tracking branch 'origin/feat/starknet-utils' into feat/s…
ljankovic-txfusion May 15, 2025
c141359
Merge remote-tracking branch 'origin/feat/starknet-sdk-integration' i…
ljankovic-txfusion May 15, 2025
ede4e81
fix: correct repository name in fetch-contracts-release.sh script
ljankovic-txfusion May 15, 2025
5cc1b15
Merge remote-tracking branch 'origin/feat/starknet-core-abi-exports' …
ljankovic-txfusion May 15, 2025
b784a1e
Merge remote-tracking branch 'origin/feat/starknet-utils' into feat/s…
ljankovic-txfusion May 15, 2025
be301fc
Merge remote-tracking branch 'origin/feat/starknet-sdk-integration' i…
ljankovic-txfusion May 15, 2025
ecf82fa
Merge remote-tracking branch 'origin/main' into feat/starknet-utils
ljankovic-txfusion May 16, 2025
a0c584d
Merge remote-tracking branch 'origin/feat/starknet-utils' into feat/s…
ljankovic-txfusion May 16, 2025
c37b59b
Merge remote-tracking branch 'origin/feat/starknet-sdk-integration' i…
ljankovic-txfusion May 16, 2025
b906c0e
Merge remote-tracking branch 'origin/main' into feat/starknet-utils
ljankovic-txfusion May 16, 2025
0580192
Merge remote-tracking branch 'origin/feat/starknet-utils' into feat/s…
ljankovic-txfusion May 16, 2025
dd41726
Merge remote-tracking branch 'origin/feat/starknet-sdk-integration' i…
ljankovic-txfusion May 16, 2025
2d234e3
Merge remote-tracking branch 'origin/main' into feat/starknet-generat…
ljankovic-txfusion May 20, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/hip-papayas-kiss.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hyperlane-xyz/sdk': minor
---

feat: Starknet SDK logic integration
5 changes: 5 additions & 0 deletions .changeset/thirty-apricots-bathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hyperlane-xyz/starknet-core': minor
---

Cairo artifact generation tests
1 change: 1 addition & 0 deletions starknet/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.env
dist
release
tmp
5 changes: 5 additions & 0 deletions starknet/.mocharc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"extensions": ["ts"],
"spec": ["src/**/*.test.*"],
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Mocha configuration in .mocharc.json specifies "spec": ["src/**/*.test.*"], but the test files are located in the tests/ directory. This mismatch will prevent Mocha from finding any test files when running the test command.

Consider updating the spec pattern to "tests/**/*.test.*" to correctly target the test files in their actual location.

Suggested change
"spec": ["src/**/*.test.*"],
+ "spec": ["tests/**/*.test.*"],

Spotted by Diamond

Is this helpful? React 👍 or 👎 to let us know.

"node-option": ["experimental-specifier-resolution=node"]
}
2 changes: 1 addition & 1 deletion starknet/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ export default [
},
},
{
ignores: ['scripts/**/*'],
ignores: ['scripts/**/*', 'tests/**/*'],
},
];
4 changes: 3 additions & 1 deletion starknet/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"build": "tsc && yarn fetch-contracts && yarn generate-artifacts",
"clean": "rm -rf ./dist ./release",
"lint": "eslint -c ./eslint.config.mjs .",
"prettier": "prettier --write ./src ./package.json"
"prettier": "prettier --write ./src ./package.json",
"test": "mocha --require tsx 'tests/**/*.test.ts'"
},
"exports": {
".": {
Expand Down Expand Up @@ -41,6 +42,7 @@
"eslint-import-resolver-typescript": "^3.6.3",
"eslint-plugin-import": "^2.31.0",
"globby": "^14.1.0",
"mocha": "^10.2.0",
"prettier": "^3.5.3",
"tsx": "^4.19.1",
"typescript": "5.3.3"
Expand Down
286 changes: 286 additions & 0 deletions starknet/tests/StarknetArtifactGenerator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
import { assert, expect } from 'chai';
import { promises as fs } from 'fs';
import { afterEach, beforeEach, describe, it } from 'mocha';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';

import { StarknetArtifactGenerator } from '../scripts/StarknetArtifactGenerator.js';
import { CONTRACT_SUFFIXES } from '../src/const.js';
import { ContractClass, ContractType } from '../src/types.js';

import { createMockContractFiles, createMockSierraArtifact } from './utils.js';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const TMP_DIR = join(__dirname, './tmp');
const TEST_RELEASE_DIR = join(TMP_DIR, 'release');
const TEST_OUTPUT_DIR = join(TMP_DIR, 'dist/artifacts');

describe('StarknetArtifactGenerator', () => {
let generator: StarknetArtifactGenerator;

beforeEach(async () => {
await Promise.all([
fs.mkdir(TMP_DIR, { recursive: true }),
fs.mkdir(TEST_RELEASE_DIR, { recursive: true }),
fs.mkdir(TEST_OUTPUT_DIR, { recursive: true }),
]);

await createMockContractFiles(TEST_RELEASE_DIR);

generator = new StarknetArtifactGenerator(
TEST_RELEASE_DIR,
TEST_OUTPUT_DIR,
);
});

afterEach(async () => {
await fs.rm(TMP_DIR, { recursive: true, force: true }).catch(() => {});
});

it('should correctly identify contract types from filenames', () => {
expect(generator.getContractTypeFromPath('token_MyToken.json')).to.equal(
ContractType.TOKEN,
);
expect(
generator.getContractTypeFromPath('mocks_MockContract.json'),
).to.equal(ContractType.MOCK);
expect(
generator.getContractTypeFromPath('contracts_IMailbox.json'),
).to.equal(ContractType.CONTRACT);
// Defaults to CONTRACT for unknown prefixes
expect(generator.getContractTypeFromPath('random_name.json')).to.equal(
ContractType.CONTRACT,
);
});

it('should create the output directory if it does not exist', async () => {
await fs.rm(TEST_OUTPUT_DIR, { recursive: true, force: true });
await generator.createOutputDirectory();
const stats = await fs.stat(TEST_OUTPUT_DIR);
expect(stats.isDirectory()).to.be.true;
});

it('should read and parse a valid Sierra artifact file', async () => {
const filePath = join(
TEST_RELEASE_DIR,
`contracts_Test${CONTRACT_SUFFIXES.SIERRA_JSON}`,
);
const artifact = await generator.readArtifactFile(filePath);
expect(artifact).to.deep.include({
contract_class_version: '0.1.0',
sierra_program: [], // Based on createMockSierraArtifact
});
expect(artifact.abi).to.be.an('array');
});

it('should generate JS content with expected properties for Sierra contracts', () => {
const artifact = createMockSierraArtifact();
const jsContent = generator.generateJavaScriptContent(
'TestContract',
artifact,
ContractClass.SIERRA,
);

const jsonMatch = jsContent.match(/^export const \w+ = (\{.*\});$/s);
assert(jsonMatch, 'Should find exported JSON object in JS content');

const parsedArtifact = JSON.parse(jsonMatch[1]);
expect(parsedArtifact).to.be.an('object');
expect(parsedArtifact)
.to.have.property('sierra_program')
.that.is.an('array');
expect(parsedArtifact)
.to.have.property('entry_points_by_type')
.deep.equal(artifact.entry_points_by_type);
expect(parsedArtifact).to.have.property('abi').that.is.an('array');
// Check if a specific known item from the mock ABI exists
const hasTestFunction = parsedArtifact.abi.some(
(item: any) => item.name === 'test_function',
);
expect(hasTestFunction, 'ABI should contain "test_function"').to.be.true;
});

it('should generate correct TypeScript declaration for Sierra contracts', () => {
const dtsContent = generator.generateDeclarationContent(
'TestSierra',
true, // isSierra = true
);
expect(dtsContent).to.include(
'export declare const TestSierra: CompiledContract',
);
});

it('should generate correct TypeScript declaration for CASM contracts', () => {
const dtsContent = generator.generateDeclarationContent(
'TestCasm',
false, // isSierra = false
);
expect(dtsContent).to.include(
'export declare const TestCasm: CairoAssembly',
);
});

it('should process a Sierra artifact file correctly, generating JS and DTS files', async () => {
const fileName = `contracts_Test${CONTRACT_SUFFIXES.SIERRA_JSON}`;
const filePath = join(TEST_RELEASE_DIR, fileName);
const artifact = await generator.readArtifactFile(filePath); // Read expected artifact data

const processResult = await generator.processArtifact(filePath);

// Verify the returned info object
expect(processResult).to.deep.equal({
name: 'contracts_Test',
contractType: ContractType.CONTRACT,
contractClass: ContractClass.SIERRA,
});

const jsPath = join(
TEST_OUTPUT_DIR,
`contracts_Test.${ContractClass.SIERRA}.js`,
);
const dtsPath = join(
TEST_OUTPUT_DIR,
`contracts_Test.${ContractClass.SIERRA}.d.ts`,
);
await fs.access(jsPath); // Throws if file doesn't exist
await fs.access(dtsPath); // Throws if file doesn't exist

const jsContent = await fs.readFile(jsPath, 'utf-8');
const jsonMatch = jsContent.match(/^export const \w+ = (\{.*\});$/s);
assert(jsonMatch, 'Should find JSON object in generated JS file');
const parsedJsArtifact = JSON.parse(jsonMatch[1]);

expect(parsedJsArtifact).to.be.an('object');
expect(parsedJsArtifact)
.to.have.property('sierra_program')
.that.is.an('array');
expect(parsedJsArtifact).to.have.property(
'contract_class_version',
artifact.contract_class_version,
);
expect(parsedJsArtifact)
.to.have.property('entry_points_by_type')
.deep.equal(artifact.entry_points_by_type);
expect(parsedJsArtifact)
.to.have.property('abi')
.deep.equal(
typeof artifact.abi === 'string'
? JSON.parse(artifact.abi)
: artifact.abi,
);

const dtsContent = await fs.readFile(dtsPath, 'utf-8');
expect(dtsContent).to.include(
'export declare const contracts_Test: CompiledContract',
);
});

it('should process a standard set of artifacts, generate index files, and return correct summary', async () => {
// Assumes beforeEach creates: contracts_Test, token_HypERC20, mocks_MockContract (all Sierra)
const processedMap = await generator.generate();

expect(processedMap.size).to.equal(3);
expect(processedMap.get('contracts_Test')).to.deep.equal({
type: ContractType.CONTRACT,
sierra: true,
casm: false,
});
expect(processedMap.get('token_HypERC20')).to.deep.equal({
type: ContractType.TOKEN,
sierra: true,
casm: false,
});
expect(processedMap.get('mocks_MockContract')).to.deep.equal({
type: ContractType.MOCK,
sierra: true,
casm: false,
});

const indexJsPath = join(TEST_OUTPUT_DIR, 'index.js');
const indexDtsPath = join(TEST_OUTPUT_DIR, 'index.d.ts');
await fs.access(indexJsPath);
await fs.access(indexDtsPath);

// Check index file content (verify artifact paths are included)
const indexJsContent = await fs.readFile(indexJsPath, 'utf-8');

expect(indexJsContent).to.include(
`./contracts_Test.${ContractClass.SIERRA}.js`,
);
expect(indexJsContent).to.include(
`./token_HypERC20.${ContractClass.SIERRA}.js`,
);
expect(indexJsContent).to.include(
`./mocks_MockContract.${ContractClass.SIERRA}.js`,
);

// Check artifact file existence (spot check one)
const testSierraJsPath = join(
TEST_OUTPUT_DIR,
`contracts_Test.${ContractClass.SIERRA}.js`,
);
await fs.access(testSierraJsPath);
});

it('should handle malformed artifact files gracefully during generation', async () => {
const malformedFilePath = join(
TEST_RELEASE_DIR,
`malformed${CONTRACT_SUFFIXES.SIERRA_JSON}`,
);
await fs.writeFile(malformedFilePath, '{ this is not valid JSON }');

let errorThrown = false;
try {
await generator.generate(); // This should throw an error
} catch (error) {
errorThrown = true;
expect(error).to.be.instanceOf(Error);
}
// Expect generate() to throw when encountering invalid JSON
expect(errorThrown, 'Generator should throw an error for malformed JSON').to
.be.true;
});

it('should correctly process artifacts with unusual filenames during generation', async () => {
// Add a file with an unusual name that still fits the expected suffix pattern
const oddNamedFilePath = join(
TEST_RELEASE_DIR,
`unusual_prefix.contract_class.json`, // Uses SIERRA_JSON suffix
);
await fs.writeFile(
oddNamedFilePath,
JSON.stringify(createMockSierraArtifact()), // Use valid content
);

const processedMap = await generator.generate();

// Expect 4 total artifacts now (3 standard + 1 unusual)
expect(processedMap.size).to.equal(4);

expect(processedMap.has('unusual_prefix')).to.be.true;
const fileInfo = processedMap.get('unusual_prefix');
expect(fileInfo).to.deep.equal({
type: ContractType.CONTRACT, // Default type for unknown prefix
sierra: true,
casm: false,
});

const jsPath = join(
TEST_OUTPUT_DIR,
`unusual_prefix.${ContractClass.SIERRA}.js`,
);
const dtsPath = join(
TEST_OUTPUT_DIR,
`unusual_prefix.${ContractClass.SIERRA}.d.ts`,
);
await fs.access(jsPath);
await fs.access(dtsPath);

const indexJsPath = join(TEST_OUTPUT_DIR, 'index.js');
const indexJsContent = await fs.readFile(indexJsPath, 'utf-8');
expect(indexJsContent).to.include(
`./unusual_prefix.${ContractClass.SIERRA}.js`, // Check path inclusion
);
});
});
Loading
Loading