diff --git a/docs/docs/dev_docs/contracts/compiling.md b/docs/docs/dev_docs/contracts/compiling.md index 52a0c672fd3..109aa96732b 100644 --- a/docs/docs/dev_docs/contracts/compiling.md +++ b/docs/docs/dev_docs/contracts/compiling.md @@ -43,6 +43,12 @@ To generate them, include a `--typescript` option in the compile command with a aztec-cli compile --typescript ./path/to/typescript/src ./path/to/my_aztec_contract_project ``` +You can also generate these interfaces from prebuilt artifacts using the `generate-typescript` command: + +``` +aztec-cli generate-typescript ./path/to/my_aztec_contract_project +``` + Example code generated from the [PrivateToken](https://github.com/AztecProtocol/aztec-packages/blob/master/yarn-project/noir-contracts/src/contracts/private_token_contract/src/main.nr) contract: ```ts showLineNumbers @@ -82,6 +88,12 @@ To generate them, include a `--interface` option in the compile command with a p aztec-cli compile --interface ./path/to/another_aztec_contract_project/src ./path/to/my_aztec_contract_project ``` +You can also generate these interfaces from prebuilt artifacts using the `generate-noir-interface` command: + +``` +aztec-cli generate-noir-interface ./path/to/my_aztec_contract_project +``` + Example code generated from the [PrivateToken](https://github.com/AztecProtocol/aztec-packages/blob/master/yarn-project/noir-contracts/src/contracts/private_token_contract/src/main.nr) contract: ```rust diff --git a/yarn-project/cli/src/index.ts b/yarn-project/cli/src/index.ts index d06d4062aba..de7f1876c76 100644 --- a/yarn-project/cli/src/index.ts +++ b/yarn-project/cli/src/index.ts @@ -13,7 +13,7 @@ import { StructType } from '@aztec/foundation/abi'; import { JsonStringify } from '@aztec/foundation/json-rpc'; import { DebugLogger, LogFn } from '@aztec/foundation/log'; import { fileURLToPath } from '@aztec/foundation/url'; -import { compileContract } from '@aztec/noir-compiler/cli'; +import { compileContract, generateNoirInterface, generateTypescriptInterface } from '@aztec/noir-compiler/cli'; import { CompleteAddress, ContractData, L2BlockL2Logs, TxHash } from '@aztec/types'; import { Command } from 'commander'; @@ -486,6 +486,8 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command { }); compileContract(program, 'compile', log); + generateTypescriptInterface(program, 'generate-typescript', log); + generateNoirInterface(program, 'generate-noir-interface', log); return program; } diff --git a/yarn-project/noir-compiler/src/cli.ts b/yarn-project/noir-compiler/src/cli.ts index a585727235a..013791602b1 100644 --- a/yarn-project/noir-compiler/src/cli.ts +++ b/yarn-project/noir-compiler/src/cli.ts @@ -4,13 +4,17 @@ import { createConsoleLogger } from '@aztec/foundation/log'; import { Command } from 'commander'; import { compileContract } from './cli/contract.js'; +import { generateNoirInterface } from './cli/noir-interface.js'; +import { generateTypescriptInterface } from './cli/typescript.js'; const program = new Command(); const log = createConsoleLogger('aztec:compiler-cli'); const main = async () => { - compileContract(program.name('aztec-compile'), 'contract', log); - + program.name('aztec-compile'); + compileContract(program, 'contract', log); + generateTypescriptInterface(program, 'typescript', log); + generateNoirInterface(program, 'interface', log); await program.parseAsync(process.argv); }; diff --git a/yarn-project/noir-compiler/src/cli/index.ts b/yarn-project/noir-compiler/src/cli/index.ts index 32877f262b6..8d312f9ec5c 100644 --- a/yarn-project/noir-compiler/src/cli/index.ts +++ b/yarn-project/noir-compiler/src/cli/index.ts @@ -1 +1,3 @@ export { compileContract } from './contract.js'; +export { generateNoirInterface } from './noir-interface.js'; +export { generateTypescriptInterface } from './typescript.js'; diff --git a/yarn-project/noir-compiler/src/cli/noir-interface.ts b/yarn-project/noir-compiler/src/cli/noir-interface.ts new file mode 100644 index 00000000000..63afee9633e --- /dev/null +++ b/yarn-project/noir-compiler/src/cli/noir-interface.ts @@ -0,0 +1,62 @@ +import { LogFn } from '@aztec/foundation/log'; + +import { Command } from 'commander'; +import { readFileSync, readdirSync, statSync, writeFileSync } from 'fs'; +import { mkdirpSync } from 'fs-extra'; +import path, { resolve } from 'path'; + +import { generateNoirContractInterface } from '../index.js'; +import { isContractAbi } from '../utils.js'; + +/** + * Registers a 'interface' command on the given commander program that generates a Noir interface out of an ABI. + * @param program - Commander program. + * @param log - Optional logging function. + * @returns The program with the command registered. + */ +export function generateNoirInterface(program: Command, name = 'interface', log: LogFn = () => {}): Command { + return program + .command(name) + .argument('', 'Path to the noir project') + .option('--artifacts ', 'Folder containing the compiled artifacts, relative to the project path', 'target') + .option( + '-o, --outdir ', + 'Output folder for the generated noir interfaces, relative to the project path', + 'interfaces', + ) + .description('Generates Noir interfaces from the artifacts in the given project') + + .action( + ( + projectPath: string, + /* eslint-disable jsdoc/require-jsdoc */ + options: { + outdir: string; + artifacts: string; + }, + /* eslint-enable jsdoc/require-jsdoc */ + ) => { + const { outdir, artifacts } = options; + if (typeof projectPath !== 'string') throw new Error(`Missing project path argument`); + const currentDir = process.cwd(); + + const artifactsDir = resolve(projectPath, artifacts); + for (const artifactsDirItem of readdirSync(artifactsDir)) { + const artifactPath = resolve(artifactsDir, artifactsDirItem); + if (statSync(artifactPath).isFile() && artifactPath.endsWith('.json')) { + const contract = JSON.parse(readFileSync(artifactPath).toString()); + if (!isContractAbi(contract)) continue; + const interfacePath = resolve(projectPath, outdir, `${contract.name}_interface.nr`); + log(`Writing ${contract.name} Noir external interface to ${path.relative(currentDir, interfacePath)}`); + try { + const noirInterface = generateNoirContractInterface(contract); + mkdirpSync(path.dirname(interfacePath)); + writeFileSync(interfacePath, noirInterface); + } catch (err) { + log(`Error generating interface for ${artifactPath}: ${err}`); + } + } + } + }, + ); +} diff --git a/yarn-project/noir-compiler/src/cli/typescript.ts b/yarn-project/noir-compiler/src/cli/typescript.ts new file mode 100644 index 00000000000..8a4142ddc09 --- /dev/null +++ b/yarn-project/noir-compiler/src/cli/typescript.ts @@ -0,0 +1,63 @@ +import { LogFn } from '@aztec/foundation/log'; + +import { Command } from 'commander'; +import { readFileSync, readdirSync, statSync, writeFileSync } from 'fs'; +import { mkdirpSync } from 'fs-extra'; +import path, { resolve } from 'path'; + +import { generateTypescriptContractInterface } from '../index.js'; +import { isContractAbi } from '../utils.js'; + +/** + * Registers a 'typescript' command on the given commander program that generates typescript interface out of an ABI. + * @param program - Commander program. + * @param log - Optional logging function. + * @returns The program with the command registered. + */ +export function generateTypescriptInterface(program: Command, name = 'typescript', log: LogFn = () => {}): Command { + return program + .command(name) + .argument('', 'Path to the noir project') + .option('--artifacts ', 'Folder containing the compiled artifacts, relative to the project path', 'target') + .option( + '-o, --outdir ', + 'Output folder for the generated typescript wrappers, relative to the project path', + 'types', + ) + .description('Generates typescript interfaces from the artifacts in the given project') + + .action( + ( + projectPath: string, + /* eslint-disable jsdoc/require-jsdoc */ + options: { + outdir: string; + artifacts: string; + }, + /* eslint-enable jsdoc/require-jsdoc */ + ) => { + const { outdir, artifacts } = options; + if (typeof projectPath !== 'string') throw new Error(`Missing project path argument`); + const currentDir = process.cwd(); + + const artifactsDir = resolve(projectPath, artifacts); + for (const artifactsDirItem of readdirSync(artifactsDir)) { + const artifactPath = resolve(artifactsDir, artifactsDirItem); + if (statSync(artifactPath).isFile() && artifactPath.endsWith('.json')) { + const contract = JSON.parse(readFileSync(artifactPath).toString()); + if (!isContractAbi(contract)) continue; + const tsPath = resolve(projectPath, outdir, `${contract.name}.ts`); + log(`Writing ${contract.name} typescript interface to ${path.relative(currentDir, tsPath)}`); + const relativeArtifactPath = path.relative(path.dirname(tsPath), artifactPath); + try { + const tsWrapper = generateTypescriptContractInterface(contract, relativeArtifactPath); + mkdirpSync(path.dirname(tsPath)); + writeFileSync(tsPath, tsWrapper); + } catch (err) { + log(`Error generating interface for ${artifactPath}: ${err}`); + } + } + } + }, + ); +} diff --git a/yarn-project/noir-compiler/src/utils.ts b/yarn-project/noir-compiler/src/utils.ts new file mode 100644 index 00000000000..c1cbb83a4c7 --- /dev/null +++ b/yarn-project/noir-compiler/src/utils.ts @@ -0,0 +1,21 @@ +import { ContractAbi } from '@aztec/foundation/abi'; + +/** + * Checks if the given input looks like a valid ContractAbi. The check is not exhaustive, + * and it's just meant to differentiate between nargo raw build artifacts and the ones + * produced by this compiler. + * @param input - Input object. + * @returns True if it looks like a ContractAbi. + */ +export function isContractAbi(input: any): input is ContractAbi { + if (typeof input !== 'object') return false; + const maybeContractAbi = input as ContractAbi; + if (typeof maybeContractAbi.name !== 'string') return false; + if (!Array.isArray(maybeContractAbi.functions)) return false; + for (const fn of maybeContractAbi.functions) { + if (typeof fn.name !== 'string') return false; + if (typeof fn.functionType !== 'string') return false; + if (typeof fn.isInternal !== 'boolean') return false; + } + return true; +}