From 2d576343f60f5822fc3e6d6b3ef16063e7a343a4 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Tue, 12 Sep 2023 10:21:29 -0300 Subject: [PATCH 1/2] feat(cli): Add commands for generating ts and nr interfaces --- yarn-project/cli/src/index.ts | 4 +- yarn-project/noir-compiler/src/cli.ts | 8 ++- yarn-project/noir-compiler/src/cli/index.ts | 2 + .../noir-compiler/src/cli/noir-interface.ts | 62 ++++++++++++++++++ .../noir-compiler/src/cli/typescript.ts | 63 +++++++++++++++++++ yarn-project/noir-compiler/src/utils.ts | 21 +++++++ 6 files changed, 157 insertions(+), 3 deletions(-) create mode 100644 yarn-project/noir-compiler/src/cli/noir-interface.ts create mode 100644 yarn-project/noir-compiler/src/cli/typescript.ts create mode 100644 yarn-project/noir-compiler/src/utils.ts 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; +} From 742f0850e0565c9c7a0a6a935804f3c362b4b57b Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Tue, 12 Sep 2023 10:24:58 -0300 Subject: [PATCH 2/2] Update docs --- docs/docs/dev_docs/contracts/compiling.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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