Skip to content

Commit

Permalink
feat(cli): Inspect contract command (#2248)
Browse files Browse the repository at this point in the history
Adds an `inspect-contract` command that lists the functions available to
call. Omits internal functions and stev.

```
$ yarn aztec-cli inspect-contract PrivateTokenContractAbi
secret constructor(initial_supply: Field, owner: Field)
unconstrained getBalance(owner: Field)
secret mint(amount: Field, owner: Field)
secret transfer(amount: Field, recipient: Field)
```

Fixes #2180
  • Loading branch information
spalladino authored Sep 12, 2023
1 parent e9cac9c commit 381706e
Show file tree
Hide file tree
Showing 3 changed files with 131 additions and 10 deletions.
23 changes: 22 additions & 1 deletion yarn-project/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
getSchnorrAccount,
isContractDeployed,
} from '@aztec/aztec.js';
import { StructType } from '@aztec/foundation/abi';
import { StructType, decodeFunctionSignatureWithParameterNames } from '@aztec/foundation/abi';
import { JsonStringify } from '@aztec/foundation/json-rpc';
import { DebugLogger, LogFn } from '@aztec/foundation/log';
import { fileURLToPath } from '@aztec/foundation/url';
Expand Down Expand Up @@ -485,6 +485,27 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command {
Object.entries(info).map(([key, value]) => log(`${startCase(key)}: ${value}`));
});

program
.command('inspect-contract')
.description('Shows list of external callable functions for a contract')
.argument(
'<contractAbiFile>',
`A compiled Noir contract's ABI in JSON format or name of a contract ABI exported by @aztec/noir-contracts`,
)
.action(async (contractAbiFile: string) => {
const contractAbi = await getContractAbi(contractAbiFile, debugLogger);
const contractFns = contractAbi.functions.filter(
f => !f.isInternal && f.name !== 'compute_note_hash_and_nullifier',
);
if (contractFns.length === 0) {
log(`No external functions found for contract ${contractAbi.name}`);
}
for (const fn of contractFns) {
const signature = decodeFunctionSignatureWithParameterNames(fn.name, fn.parameters);
log(`${fn.functionType} ${signature}`);
}
});

compileContract(program, 'compile', log);
generateTypescriptInterface(program, 'generate-typescript', log);
generateNoirInterface(program, 'generate-noir-interface', log);
Expand Down
77 changes: 77 additions & 0 deletions yarn-project/foundation/src/abi/decoder.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { FunctionAbi } from './abi.js';
import { decodeFunctionSignature, decodeFunctionSignatureWithParameterNames } from './decoder.js';

describe('abi/decoder', () => {
// Copied from yarn-project/noir-contracts/src/contracts/test_contract/target/Test.json
const abi = {
name: 'testCodeGen',
parameters: [
{ name: 'aField', type: { kind: 'field' }, visibility: 'private' },
{ name: 'aBool', type: { kind: 'boolean' }, visibility: 'private' },
{ name: 'aNumber', type: { kind: 'integer', sign: 'unsigned', width: 32 }, visibility: 'private' },
{ name: 'anArray', type: { kind: 'array', length: 2, type: { kind: 'field' } }, visibility: 'private' },
{
name: 'aStruct',
type: {
kind: 'struct',
path: 'Test::DummyNote',
fields: [
{ name: 'amount', type: { kind: 'field' } },
{ name: 'secretHash', type: { kind: 'field' } },
],
},
visibility: 'private',
},
{
name: 'aDeepStruct',
type: {
kind: 'struct',
path: 'Test::DeepStruct',
fields: [
{ name: 'aField', type: { kind: 'field' } },
{ name: 'aBool', type: { kind: 'boolean' } },
{
name: 'aNote',
type: {
kind: 'struct',
path: 'Test::DummyNote',
fields: [
{ name: 'amount', type: { kind: 'field' } },
{ name: 'secretHash', type: { kind: 'field' } },
],
},
},
{
name: 'manyNotes',
type: {
kind: 'array',
length: 3,
type: {
kind: 'struct',
path: 'Test::DummyNote',
fields: [
{ name: 'amount', type: { kind: 'field' } },
{ name: 'secretHash', type: { kind: 'field' } },
],
},
},
},
],
},
visibility: 'private',
},
],
} as unknown as Pick<FunctionAbi, 'name' | 'parameters'>;

it('decodes function signature', () => {
expect(decodeFunctionSignature(abi.name, abi.parameters)).toMatchInlineSnapshot(
`"testCodeGen(Field,bool,u32,[Field;2],(Field,Field),(Field,bool,(Field,Field),[(Field,Field);3]))"`,
);
});

it('decodes function signature with parameter names', () => {
expect(decodeFunctionSignatureWithParameterNames(abi.name, abi.parameters)).toMatchInlineSnapshot(
`"testCodeGen(aField: Field, aBool: bool, aNumber: u32, anArray: [Field;2], aStruct: (amount: Field, secretHash: Field), aDeepStruct: (aField: Field, aBool: bool, aNote: (amount: Field, secretHash: Field), manyNotes: [(amount: Field, secretHash: Field);3]))"`,
);
});
});
41 changes: 32 additions & 9 deletions yarn-project/foundation/src/abi/decoder.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ABIParameter, ABIType, FunctionAbi } from '@aztec/foundation/abi';
import { ABIParameter, ABIType, ABIVariable, FunctionAbi } from '@aztec/foundation/abi';
import { Fr } from '@aztec/foundation/fields';

/**
Expand Down Expand Up @@ -91,14 +91,17 @@ export function decodeReturnValues(abi: FunctionAbi, returnValues: Fr[]) {
* Decodes the signature of a function from the name and parameters.
*/
export class FunctionSignatureDecoder {
constructor(private name: string, private parameters: ABIParameter[]) {}
private separator: string;
constructor(private name: string, private parameters: ABIParameter[], private includeNames = false) {
this.separator = includeNames ? ', ' : ',';
}

/**
* Decodes a single function parameter for the function signature.
* @param param - The parameter to decode.
* Decodes a single function parameter type for the function signature.
* @param param - The parameter type to decode.
* @returns A string representing the parameter type.
*/
private decodeParameter(param: ABIType): string {
private getParameterType(param: ABIType): string {
switch (param.kind) {
case 'field':
return 'Field';
Expand All @@ -110,29 +113,49 @@ export class FunctionSignatureDecoder {
case 'boolean':
return 'bool';
case 'array':
return `[${this.decodeParameter(param.type)};${param.length}]`;
return `[${this.getParameterType(param.type)};${param.length}]`;
case 'struct':
return `(${param.fields.map(field => `${this.decodeParameter(field.type)}`).join(',')})`;
return `(${param.fields.map(field => `${this.decodeParameter(field)}`).join(this.separator)})`;
default:
throw new Error(`Unsupported type: ${param.kind}`);
}
}

/**
* Decodes a single function parameter for the function signature.
* @param param - The parameter to decode.
* @returns A string representing the parameter type and optionally its name.
*/
private decodeParameter(param: ABIVariable): string {
const type = this.getParameterType(param.type);
return this.includeNames ? `${param.name}: ${type}` : type;
}

/**
* Decodes all the parameters and build the function signature
* @returns The function signature.
*/
public decode(): string {
return `${this.name}(${this.parameters.map(param => this.decodeParameter(param.type)).join(',')})`;
return `${this.name}(${this.parameters.map(param => this.decodeParameter(param)).join(this.separator)})`;
}
}

/**
* Decodes a function signature from the name and parameters.
* @param name - The name of the function-
* @param name - The name of the function.
* @param parameters - The parameters of the function.
* @returns - The function signature.
*/
export function decodeFunctionSignature(name: string, parameters: ABIParameter[]) {
return new FunctionSignatureDecoder(name, parameters).decode();
}

/**
* Decodes a function signature from the name and parameters including parameter names.
* @param name - The name of the function.
* @param parameters - The parameters of the function.
* @returns - The user-friendly function signature.
*/
export function decodeFunctionSignatureWithParameterNames(name: string, parameters: ABIParameter[]) {
return new FunctionSignatureDecoder(name, parameters, true).decode();
}

0 comments on commit 381706e

Please sign in to comment.