diff --git a/yarn-project/cli/src/index.ts b/yarn-project/cli/src/index.ts index de7f1876c76..b1ac8bb4c7c 100644 --- a/yarn-project/cli/src/index.ts +++ b/yarn-project/cli/src/index.ts @@ -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'; @@ -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( + '', + `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); diff --git a/yarn-project/foundation/src/abi/decoder.test.ts b/yarn-project/foundation/src/abi/decoder.test.ts new file mode 100644 index 00000000000..165e4a4a61f --- /dev/null +++ b/yarn-project/foundation/src/abi/decoder.test.ts @@ -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; + + 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]))"`, + ); + }); +}); diff --git a/yarn-project/foundation/src/abi/decoder.ts b/yarn-project/foundation/src/abi/decoder.ts index 5ca889f9d63..0dfbe09708c 100644 --- a/yarn-project/foundation/src/abi/decoder.ts +++ b/yarn-project/foundation/src/abi/decoder.ts @@ -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'; /** @@ -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'; @@ -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(); +}