Skip to content

Commit

Permalink
feat(cli): Add commands for generating ts and nr interfaces (#2241)
Browse files Browse the repository at this point in the history
Adds `generate-typescript` and `generate-noir-interface` commands to the
CLI to generate typescript types and noir external interfaces from
already built artifacts. Both commands take a path to the noir project,
an optional `artifacts` relative path to the artifacts folder (defaults
to `target`), and an optional outdir relative to the noir project as
well.

Fixes #2183
  • Loading branch information
spalladino authored Sep 12, 2023
1 parent 428ac5e commit c11b70d
Show file tree
Hide file tree
Showing 7 changed files with 169 additions and 3 deletions.
12 changes: 12 additions & 0 deletions docs/docs/dev_docs/contracts/compiling.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion yarn-project/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
}
8 changes: 6 additions & 2 deletions yarn-project/noir-compiler/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};

Expand Down
2 changes: 2 additions & 0 deletions yarn-project/noir-compiler/src/cli/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export { compileContract } from './contract.js';
export { generateNoirInterface } from './noir-interface.js';
export { generateTypescriptInterface } from './typescript.js';
62 changes: 62 additions & 0 deletions yarn-project/noir-compiler/src/cli/noir-interface.ts
Original file line number Diff line number Diff line change
@@ -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('<project-path>', 'Path to the noir project')
.option('--artifacts <path>', 'Folder containing the compiled artifacts, relative to the project path', 'target')
.option(
'-o, --outdir <path>',
'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}`);
}
}
}
},
);
}
63 changes: 63 additions & 0 deletions yarn-project/noir-compiler/src/cli/typescript.ts
Original file line number Diff line number Diff line change
@@ -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('<project-path>', 'Path to the noir project')
.option('--artifacts <path>', 'Folder containing the compiled artifacts, relative to the project path', 'target')
.option(
'-o, --outdir <path>',
'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}`);
}
}
}
},
);
}
21 changes: 21 additions & 0 deletions yarn-project/noir-compiler/src/utils.ts
Original file line number Diff line number Diff line change
@@ -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;
}

0 comments on commit c11b70d

Please sign in to comment.