-
Notifications
You must be signed in to change notification settings - Fork 831
WIP feat: contractLoader #1556
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
tabaktoni
wants to merge
2
commits into
develop
Choose a base branch
from
experiment/contract-loader
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+301
−9
Open
WIP feat: contractLoader #1556
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| import path from 'path'; | ||
| import { contractLoader } from '../../src/utils/contract'; | ||
|
|
||
| describe('contractLoader', () => { | ||
| const mocksBase = path.resolve(__dirname, '../../__mocks__'); | ||
| const mockDir = path.join(mocksBase, 'cairo/cairo210'); | ||
| const sierraFile = path.join(mockDir, 'cairo210.sierra.json'); | ||
| const casmFile = path.join(mockDir, 'cairo210.casm'); | ||
|
|
||
| describe('Loading from directory', () => { | ||
| test('should load both sierra and casm from directory', () => { | ||
| const contract = contractLoader(mockDir); | ||
|
|
||
| expect(contract).toBeDefined(); | ||
| expect(contract.casm).toBeDefined(); | ||
| expect(contract.sierra).toBeDefined(); | ||
| expect(contract.compiler).toBe('2.1.0'); | ||
| }); | ||
| }); | ||
|
|
||
| describe('Loading from sierra file', () => { | ||
| test('should load sierra and find casm in same directory', () => { | ||
| const contract = contractLoader(sierraFile); | ||
|
|
||
| expect(contract).toBeDefined(); | ||
| expect(contract.casm).toBeDefined(); | ||
| expect(contract.sierra).toBeDefined(); | ||
| expect(contract.compiler).toBe('2.1.0'); | ||
| }); | ||
| }); | ||
|
|
||
| describe('Loading from casm file', () => { | ||
| test('should load casm and find sierra in same directory', () => { | ||
| const contract = contractLoader(casmFile); | ||
|
|
||
| expect(contract).toBeDefined(); | ||
| expect(contract.casm).toBeDefined(); | ||
| expect(contract.sierra).toBeDefined(); | ||
| expect(contract.compiler).toBe('2.1.0'); | ||
| }); | ||
| }); | ||
|
|
||
| describe('Error handling', () => { | ||
| test('should throw error if no sierra file found', () => { | ||
| // Try to load a casm-only file (blake2s) which should fail | ||
| const casmOnlyFile = path.join(mocksBase, 'cairo/blake2s_verification_contract.casm'); | ||
|
|
||
| expect(() => contractLoader(casmOnlyFile)).toThrow('No .sierra.json file found'); | ||
| }); | ||
| }); | ||
|
|
||
| describe('Sierra-only contracts', () => { | ||
| test('should return sierra without casm if no casm file exists', () => { | ||
| // Use complexInput which has only sierra file | ||
| const sierraOnlyDir = path.join(mocksBase, 'cairo/complexInput'); | ||
| const contract = contractLoader(sierraOnlyDir); | ||
|
|
||
| expect(contract).toBeDefined(); | ||
| expect(contract.sierra).toBeDefined(); | ||
| expect(contract.casm).toBeUndefined(); | ||
| expect(contract.compiler).toBeUndefined(); | ||
| }); | ||
| }); | ||
|
|
||
| describe('Integration with Contract class and declare', () => { | ||
| test('loaded contract should have correct structure for Contract and declare', () => { | ||
| // Load contract using contractLoader | ||
| const loadedContract = contractLoader(mockDir); | ||
|
|
||
| // Verify the contract has correct structure for declare/deploy | ||
| expect(loadedContract.sierra).toBeDefined(); | ||
| expect(loadedContract.casm).toBeDefined(); | ||
|
|
||
| // Verify sierra has required properties for Contract class | ||
| expect(loadedContract.sierra.abi).toBeDefined(); | ||
| expect(Array.isArray(loadedContract.sierra.abi)).toBe(true); | ||
| expect('sierra_program' in loadedContract.sierra).toBe(true); | ||
|
|
||
| // Verify casm has required properties for declare | ||
| expect('bytecode' in loadedContract.casm!).toBe(true); | ||
| expect('compiler_version' in loadedContract.casm!).toBe(true); | ||
|
|
||
| // Verify compiler version is extracted | ||
| expect(loadedContract.compiler).toBe('2.1.0'); | ||
| // Note: compiler_version exists on CompiledSierraCasm but not on LegacyContractClass | ||
| if (loadedContract.casm && 'compiler_version' in loadedContract.casm) { | ||
| expect(loadedContract.compiler).toBe(loadedContract.casm.compiler_version); | ||
| } | ||
|
|
||
| // Verify the loaded contract structure matches what declareAndDeploy expects | ||
| // This validates that contractLoader returns properly parsed contracts with correct types | ||
| const declarePayload = { | ||
| contract: loadedContract.sierra, | ||
| casm: loadedContract.casm, | ||
| }; | ||
|
|
||
| expect(declarePayload.contract).toBeDefined(); | ||
| expect(declarePayload.contract.abi).toBeDefined(); | ||
| expect(declarePayload.casm).toBeDefined(); | ||
|
|
||
| // Verify ABI is properly parsed (not just a string) | ||
| expect(typeof declarePayload.contract.abi).toBe('object'); | ||
| expect(Array.isArray(declarePayload.contract.abi)).toBe(true); | ||
| }); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| import { CairoAssembly, CompiledSierra } from '../types/lib/contract/index'; | ||
|
|
||
| export interface LoadedContract { | ||
| sierra: CompiledSierra; | ||
| casm?: CairoAssembly; | ||
| compiler?: string; | ||
| } | ||
|
|
||
| /** | ||
| * Browser stub for contractLoader. | ||
| * This function is not available in browser environments. | ||
| * Use it only in Node.js environments (server-side or build scripts). | ||
| */ | ||
| export function contractLoader(_contractPath: string): LoadedContract { | ||
| throw new Error( | ||
| 'contractLoader is only available in Node.js environment. ' + | ||
| 'This function requires filesystem access and cannot be used in browsers. ' + | ||
| 'Load your contracts using other methods (e.g., import, fetch) in browser environments.' | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| import { CompiledSierra, CairoAssembly } from '../types/lib/contract/index'; | ||
| import { parse } from './json'; | ||
|
|
||
| export interface LoadedContract { | ||
| sierra: CompiledSierra; | ||
| casm?: CairoAssembly; | ||
| compiler?: string; | ||
| } | ||
|
|
||
| /** | ||
| * Loads a Cairo contract from the filesystem. | ||
| * | ||
| * Accepts either a directory path or a direct path to a .sierra.json or .casm file. | ||
| * - If a directory is provided: searches for .sierra.json and .casm files | ||
| * - If a file path is provided: loads that file and attempts to find the complementary file | ||
| * | ||
| * @param {string} contractPath - Path to a directory or a .sierra.json/.casm file | ||
| * @return {LoadedContract} - Object containing sierra (required), casm (optional), and compiler version (optional) | ||
| * @throws {Error} - If no .sierra.json file is found, multiple .sierra.json files are found, or file reading fails | ||
| * @example | ||
| * ```typescript | ||
| * // Load from directory | ||
| * const contract = contractLoader('./contracts/my_contract'); | ||
| * // contract = { sierra: {...}, casm: {...}, compiler: '2.6.0' } | ||
| * | ||
| * // Load from .sierra.json file | ||
| * const contract = contractLoader('./contracts/my_contract.sierra.json'); | ||
| * | ||
| * // Load from .casm file (will find matching .sierra.json) | ||
| * const contract = contractLoader('./contracts/my_contract.casm'); | ||
| * ``` | ||
| */ | ||
| export function contractLoader(contractPath: string): LoadedContract { | ||
| // eslint-disable-next-line @typescript-eslint/no-var-requires, global-require | ||
| const fs = require('fs'); | ||
| // eslint-disable-next-line @typescript-eslint/no-var-requires, global-require | ||
| const path = require('path'); | ||
|
|
||
| // Resolve relative paths to absolute paths | ||
| // This ensures consistent path handling throughout the function | ||
| const resolvedPath = path.resolve(contractPath); | ||
|
|
||
| let dirPath: string; | ||
| let specifiedSierraFile: string | undefined; | ||
| let specifiedCasmFile: string | undefined; | ||
|
|
||
| // Check if the path is a file or directory | ||
| const stats = fs.statSync(resolvedPath); | ||
|
|
||
| if (stats.isFile()) { | ||
| // If it's a file, extract the directory and remember which file was specified | ||
| dirPath = path.dirname(resolvedPath); | ||
| const fileName = path.basename(resolvedPath); | ||
|
|
||
| if ( | ||
| fileName.endsWith('.sierra.json') || | ||
| (fileName.endsWith('.json') && !fileName.endsWith('.casm')) | ||
| ) { | ||
| specifiedSierraFile = fileName; | ||
| } else if (fileName.endsWith('.casm')) { | ||
| specifiedCasmFile = fileName; | ||
| } else { | ||
| throw new Error( | ||
| `Invalid file type. Expected .json, .sierra.json, or .casm file, got: ${fileName}` | ||
| ); | ||
| } | ||
| } else if (stats.isDirectory()) { | ||
| dirPath = resolvedPath; | ||
| } else { | ||
| throw new Error(`Path is neither a file nor a directory: ${contractPath}`); | ||
| } | ||
|
|
||
| // Read all files in the directory | ||
| const files = fs.readdirSync(dirPath); | ||
|
|
||
| let sierraFile: string | undefined; | ||
| let casmFile: string | undefined; | ||
|
|
||
| if (specifiedSierraFile) { | ||
| // User specified a .json file - look for matching .casm | ||
| sierraFile = specifiedSierraFile; | ||
| const baseName = sierraFile.replace(/\.sierra\.json$/, '').replace(/\.json$/, ''); | ||
| casmFile = files.find((f: string) => f === `${baseName}.casm`); | ||
| } else if (specifiedCasmFile) { | ||
| // User specified a .casm file - look for matching .json | ||
| casmFile = specifiedCasmFile; | ||
| const baseName = casmFile.replace(/\.casm$/, ''); | ||
| // Try .sierra.json first, then .json | ||
| sierraFile = | ||
| files.find((f: string) => f === `${baseName}.sierra.json`) || | ||
| files.find((f: string) => f === `${baseName}.json`); | ||
| } else { | ||
| // User specified a directory - find .sierra.json files and their matching .casm | ||
| const sierraFiles = files.filter( | ||
| (f: string) => f.endsWith('.sierra.json') || (f.endsWith('.json') && !f.endsWith('.casm')) | ||
| ); | ||
|
|
||
| if (sierraFiles.length === 0) { | ||
| throw new Error(`No .sierra.json file found in ${dirPath}. Sierra file is required.`); | ||
| } | ||
|
|
||
| if (sierraFiles.length > 1) { | ||
| throw new Error( | ||
| `Multiple .sierra.json files found in ${dirPath}: ${sierraFiles.join(', ')}. Please specify which file to use.` | ||
| ); | ||
| } | ||
|
|
||
| [sierraFile] = sierraFiles; | ||
| // At this point sierraFile is guaranteed to be defined because we checked sierraFiles.length | ||
| const baseName = sierraFile!.replace(/\.sierra\.json$/, '').replace(/\.json$/, ''); | ||
| casmFile = files.find((f: string) => f === `${baseName}.casm`); | ||
| } | ||
|
|
||
| // Sierra is required | ||
| if (!sierraFile) { | ||
| throw new Error(`No .sierra.json file found in ${dirPath}. Sierra file is required.`); | ||
| } | ||
|
|
||
| // Load sierra file | ||
| const sierraPath = path.join(dirPath, sierraFile); | ||
| const sierraContent = parse(fs.readFileSync(sierraPath, 'utf8')); | ||
|
|
||
| const result: LoadedContract = { | ||
| sierra: sierraContent, | ||
| }; | ||
|
|
||
| // Load casm if available | ||
| if (casmFile) { | ||
| const casmPath = path.join(dirPath, casmFile); | ||
| const casmContent = parse(fs.readFileSync(casmPath, 'utf8')); | ||
| result.casm = casmContent; | ||
| result.compiler = casmContent.compiler_version; | ||
| } | ||
|
|
||
| return result; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,30 @@ | ||
| import path from 'path'; | ||
| import { defineConfig } from 'tsup'; | ||
|
|
||
| export default defineConfig({ | ||
| entry: ['src/index.ts'], | ||
| sourcemap: true, | ||
| clean: true, | ||
| format: ['cjs'], | ||
| globalName: 'starknet', | ||
| export default defineConfig((options) => { | ||
| const isBrowser = options.platform === 'browser'; | ||
|
|
||
| return { | ||
| entry: ['src/index.ts'], | ||
| sourcemap: true, | ||
| clean: true, | ||
| format: ['cjs'], | ||
| globalName: 'starknet', | ||
| esbuildPlugins: isBrowser | ||
| ? [ | ||
| { | ||
| name: 'replace-node-modules', | ||
| setup(build) { | ||
| // Replace contractLoaderNode with contractLoaderBrowser in browser builds | ||
| build.onResolve({ filter: /contractLoaderNode$/ }, (args) => { | ||
| const dir = path.dirname(path.resolve(args.resolveDir, args.path)); | ||
| return { | ||
| path: path.join(dir, 'contractLoaderBrowser.ts'), | ||
| }; | ||
| }); | ||
| }, | ||
| }, | ||
| ] | ||
| : [], | ||
| }; | ||
| }); | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that the
browserplatform setting is only used for theiifebuild. I believe that most usages of starknet.js in the browser happen by bundling theesm(orcjs) build within an application. With the node specific imports being dynamic, I surmise that most users wouldn't encounter the graceful platform compatibility error but a reference error forrequire.I suggest trying a different approach. Maybe something like
if (typeof require === 'undefined') throw graceful error;at the start of thecontractLoaderfunction.