Skip to content
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

feat(cli): Add commands for generating ts and nr interfaces #2241

Merged
merged 2 commits into from
Sep 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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;
}