Skip to content

Commit

Permalink
test: CLI tests (#1786)
Browse files Browse the repository at this point in the history
Fixes #1450
  • Loading branch information
spypsy authored Aug 29, 2023
1 parent 857821f commit 2987065
Show file tree
Hide file tree
Showing 7 changed files with 361 additions and 35 deletions.
4 changes: 2 additions & 2 deletions yarn-project/aztec-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ Options:
- `-c, --contract-abi <fileLocation>`: The compiled contract's ABI in JSON format. You can also use one of Aztec's example contracts found in (@aztec/noir-contracts)[https://www.npmjs.com/package/@aztec/noir-contracts], e.g. PrivateTokenContractAbi.
- `-ca, --contract-address <address>`: Address of the contract.
- `-k, --private-key <string>`: The sender's private key.
- `-u, --rpcUrl <string>`: URL of the Aztec RPC. Default: `http://localhost:8080`.
- `-u, --rpc-url <string>`: URL of the Aztec RPC. Default: `http://localhost:8080`.
This command calls a function on an Aztec contract. It requires the contract's ABI, address, function name, and optionally, function arguments. The command executes the function call and displays the transaction details.
Expand Down Expand Up @@ -352,7 +352,7 @@ Options:
- `-c, --contract-abi <fileLocation>`: The compiled contract's ABI in JSON format. You can also use one of Aztec's example contracts found in (@aztec/noir-contracts)[https://www.npmjs.com/package/@aztec/noir-contracts], e.g. PrivateTokenContractAbi.
- `-ca, --contract-address <address>`: Address of the contract.
- `-f, --from <string>`: Public key of the transaction viewer. If empty, it will try to find an account in the RPC.
- `-u, --rpcUrl <string>`: URL of the Aztec RPC. Default: `http://localhost:8080`.
- `-u, --rpc-url <string>`: URL of the Aztec RPC. Default: `http://localhost:8080`.
This command simulates the execution of a view function on a deployed contract without modifying the state. It requires the contract's ABI, address, function name, and optionally, function arguments. The command displays the result of the view function.
Expand Down
49 changes: 34 additions & 15 deletions yarn-project/aztec-cli/src/encoding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,24 +29,37 @@ export function parseStructString(str: string, abiType: StructType) {
* @param abiType - The type as described by the contract's ABI.
* @returns The encoded argument.
*/
function encodeArg(arg: string, abiType: ABIType): any {
function encodeArg(arg: string, abiType: ABIType, name: string): any {
const { kind } = abiType;
if (kind === 'field' || kind === 'integer') {
return BigInt(arg);
let res: bigint;
try {
res = BigInt(arg);
} catch (err) {
throw new Error(
`Invalid value passed for ${name}. Could not parse ${arg} as a${kind === 'integer' ? 'n' : ''} ${kind}.`,
);
}
return res;
} else if (kind === 'boolean') {
if (arg === 'true') return true;
if (arg === 'false') return false;
else throw Error(`Invalid boolean value passed for ${name}: ${arg}.`);
} else if (kind === 'array') {
let arr;
const res = [];
try {
arr = JSON.parse(arg);
if (!Array.isArray(arr)) throw Error();
for (let i = 0; i < abiType.length; i += 1) {
return encodeArg(arg[i], abiType.type);
}
} catch {
throw new Error(`Unable to parse arg ${arg} as array`);
throw new Error(`Unable to parse arg ${arg} as array for ${name} parameter`);
}
if (!Array.isArray(arr)) throw Error(`Invalid argument ${arg} passed for array parameter ${name}.`);
if (arr.length !== abiType.length)
throw Error(`Invalid array length passed for ${name}. Expected ${abiType.length}, received ${arr.length}.`);
for (let i = 0; i < abiType.length; i += 1) {
res.push(encodeArg(arr[i], abiType.type, name));
}
return res;
} else if (kind === 'struct') {
// check if input is encoded long string
if (arg.startsWith('0x')) {
Expand All @@ -55,15 +68,18 @@ function encodeArg(arg: string, abiType: ABIType): any {
let obj;
try {
obj = JSON.parse(arg);
if (Array.isArray(obj)) throw Error();
const res = [];
for (const field of abiType.fields) {
res.push(encodeArg(obj[field.name], field.type));
}
return res;
} catch {
throw new Error(`Unable to parse arg ${arg} as struct`);
}
if (Array.isArray(obj)) throw Error(`Array passed for arg ${name}. Expected a struct.`);
const res = [];
for (const field of abiType.fields) {
// Remove field name from list as it's present
const arg = obj[field.name];
if (!arg) throw Error(`Expected field ${field.name} not found in struct ${name}.`);
res.push(encodeArg(obj[field.name], field.type, field.name));
}
return res;
}
}

Expand All @@ -73,10 +89,13 @@ function encodeArg(arg: string, abiType: ABIType): any {
* @returns The encoded array.
*/
export function encodeArgs(args: any[], params: ABIParameter[]) {
if (args.length !== params.length) {
throw new Error(`Invalid number of args provided. Expected: ${params.length}, received: ${args.length}`);
}
return args
.map((arg: any, index) => {
const paramType = params[index].type;
return encodeArg(arg, paramType);
const { type, name } = params[index];
return encodeArg(arg, type, name);
})
.flat();
}
16 changes: 8 additions & 8 deletions yarn-project/aztec-cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command {
.action(async options => {
const client = await createCompatibleClient(options.rpcUrl, debugLogger);
const privateKey = options.privateKey
? new PrivateKey(Buffer.from(stripLeadingHex(options.privateKey), 'hex'))
? PrivateKey.fromString(stripLeadingHex(options.privateKey))
: PrivateKey.random();

const account = getSchnorrAccount(client, privateKey, privateKey, accountCreationSalt);
Expand Down Expand Up @@ -198,8 +198,8 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command {

program
.command('get-tx-receipt')
.argument('<txHash>', 'A transaction hash to get the receipt for.')
.description('Gets the receipt for the specified transaction hash.')
.argument('<txHash>', 'A transaction hash to get the receipt for.')
.option('-u, --rpc-url <string>', 'URL of the Aztec RPC', AZTEC_RPC_HOST || 'http://localhost:8080')
.action(async (_txHash, options) => {
const client = await createCompatibleClient(options.rpcUrl, debugLogger);
Expand Down Expand Up @@ -361,7 +361,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command {
)
.requiredOption('-ca, --contract-address <address>', 'Aztec address of the contract.')
.option('-k, --private-key <string>', "The sender's private key.", PRIVATE_KEY)
.option('-u, --rpcUrl <string>', 'URL of the Aztec RPC', AZTEC_RPC_HOST || 'http://localhost:8080')
.option('-u, --rpc-url <string>', 'URL of the Aztec RPC', AZTEC_RPC_HOST || 'http://localhost:8080')

.action(async (functionName, options) => {
const { contractAddress, functionArgs, contractAbi } = await prepTx(
Expand All @@ -379,7 +379,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command {
);
}

const privateKey = new PrivateKey(Buffer.from(stripLeadingHex(options.privateKey), 'hex'));
const privateKey = PrivateKey.fromString(stripLeadingHex(options.privateKey));

const client = await createCompatibleClient(options.rpcUrl, debugLogger);
const wallet = await getAccountWallets(
Expand All @@ -391,7 +391,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command {
);
const contract = await Contract.at(contractAddress, contractAbi, wallet);
const tx = contract.methods[functionName](...functionArgs).send();
await tx.isMined();
await tx.wait();
log('\nTransaction has been mined');
const receipt = await tx.getReceipt();
log(`Transaction hash: ${(await tx.getTxHash()).toString()}`);
Expand All @@ -413,7 +413,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command {
)
.requiredOption('-ca, --contract-address <address>', 'Aztec address of the contract.')
.option('-f, --from <string>', 'Public key of the TX viewer. If empty, will try to find account in RPC.')
.option('-u, --rpcUrl <string>', 'URL of the Aztec RPC', AZTEC_RPC_HOST || 'http://localhost:8080')
.option('-u, --rpc-url <string>', 'URL of the Aztec RPC', AZTEC_RPC_HOST || 'http://localhost:8080')
.action(async (functionName, options) => {
const { contractAddress, functionArgs, contractAbi } = await prepTx(
options.contractAbi,
Expand All @@ -431,7 +431,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command {
const client = await createCompatibleClient(options.rpcUrl, debugLogger);
const from = await getTxSender(client, options.from);
const result = await client.viewTx(functionName, functionArgs, contractAddress, from);
log('\nView result: ', JsonStringify(result, true), '\n');
log('\nView result: ', result, '\n');
});

// Helper for users to decode hex strings into structs if needed
Expand Down Expand Up @@ -461,7 +461,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command {
program
.command('block-number')
.description('Gets the current Aztec L2 block number.')
.option('-u, --rpcUrl <string>', 'URL of the Aztec RPC', AZTEC_RPC_HOST || 'http://localhost:8080')
.option('-u, --rpc-url <string>', 'URL of the Aztec RPC', AZTEC_RPC_HOST || 'http://localhost:8080')
.action(async (options: any) => {
const client = await createCompatibleClient(options.rpcUrl, debugLogger);
const num = await client.getBlockNumber();
Expand Down
63 changes: 63 additions & 0 deletions yarn-project/aztec-cli/src/test/mocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { ABIParameterVisibility, ContractAbi, FunctionType } from '@aztec/foundation/abi';

export const mockContractAbi: ContractAbi = {
name: 'MockContract',
functions: [
{
name: 'constructor',
functionType: FunctionType.SECRET,
isInternal: false,
parameters: [
{
name: 'constructorParam1',
type: {
kind: 'field',
},
visibility: ABIParameterVisibility.SECRET,
},
],
returnTypes: [],
bytecode: 'constructorBytecode',
},
{
name: 'mockFunction',
functionType: FunctionType.SECRET,
isInternal: false,
parameters: [
{
name: 'fieldParam',
type: { kind: 'field' },
visibility: ABIParameterVisibility.SECRET,
},
{
name: 'boolParam',
type: { kind: 'boolean' },
visibility: ABIParameterVisibility.SECRET,
},
{
name: 'integerParam',
type: { kind: 'integer', sign: 'signed', width: 32 },
visibility: ABIParameterVisibility.SECRET,
},
{
name: 'arrayParam',
type: { kind: 'array', length: 3, type: { kind: 'field' } },
visibility: ABIParameterVisibility.SECRET,
},
{
name: 'structParam',
type: {
kind: 'struct',
fields: [
{ name: 'subField1', type: { kind: 'field' } },
{ name: 'subField2', type: { kind: 'boolean' } },
],
},
visibility: ABIParameterVisibility.SECRET,
},
],
returnTypes: [{ kind: 'boolean' }],
bytecode: 'mockBytecode',
},
],
};
133 changes: 133 additions & 0 deletions yarn-project/aztec-cli/src/test/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { AztecAddress, Fr } from '@aztec/aztec.js';
import { AztecRPC, CompleteAddress } from '@aztec/types';

import { MockProxy, mock } from 'jest-mock-extended';

import { encodeArgs } from '../encoding.js';
import { getTxSender } from '../utils.js';
import { mockContractAbi } from './mocks.js';

describe('CLI Utils', () => {
let client: MockProxy<AztecRPC>;

// test values
const addr1 = AztecAddress.random();
const addr2 = AztecAddress.random();
const addr3 = AztecAddress.random();
const fieldArray = [addr1.toString(), addr2.toString(), addr3.toString()];
const num = 33;
const field = Fr.random();
const struct = {
subField1: field.toString(),
subField2: 'true',
};
beforeEach(() => {
client = mock<AztecRPC>();
});
it('Gets a txSender correctly or throw error', async () => {
// returns a parsed Aztec Address
const aztecAddress = AztecAddress.random();
const result = await getTxSender(client, aztecAddress.toString());
expect(client.getAccounts).toHaveBeenCalledTimes(0);
expect(result).toEqual(aztecAddress);

// returns an address found in the aztec client
const completeAddress = await CompleteAddress.random();
client.getAccounts.mockResolvedValueOnce([completeAddress]);
const resultWithoutString = await getTxSender(client);
expect(client.getAccounts).toHaveBeenCalled();
expect(resultWithoutString).toEqual(completeAddress.address);

// throws when invalid parameter passed
const errorAddr = 'foo';
await expect(
(async () => {
await getTxSender(client, errorAddr);
})(),
).rejects.toThrow(`Invalid option 'from' passed: ${errorAddr}`);

// Throws error when no string is passed & no accounts found in RPC
client.getAccounts.mockResolvedValueOnce([]);
await expect(
(async () => {
await getTxSender(client);
})(),
).rejects.toThrow('No accounts found in Aztec RPC instance.');
});

it('Encodes args correctly', () => {
const args = [addr1.toString(), 'false', num.toString(), `${JSON.stringify(fieldArray)}`, JSON.stringify(struct)];
const result = encodeArgs(args, mockContractAbi.functions[1].parameters);
const exp = [
addr1.toBigInt(),
false,
33n,
addr1.toBigInt(),
addr2.toBigInt(),
addr3.toBigInt(),
field.toBigInt(),
true,
];
expect(result).toEqual(exp);
});

it('Errors on invalid inputs', () => {
// invalid number of args
const args1 = [field.toString(), 'false'];
expect(() => encodeArgs(args1, mockContractAbi.functions[1].parameters)).toThrow(
'Invalid number of args provided. Expected: 5, received: 2',
);

// invalid array length
const invalidArray = fieldArray.concat([Fr.random().toString()]);
const args2 = [
addr1.toString(),
'false',
num.toString(),
`${JSON.stringify(invalidArray)}`,
JSON.stringify(struct),
];
expect(() => encodeArgs(args2, mockContractAbi.functions[1].parameters)).toThrow(
'Invalid array length passed for arrayParam. Expected 3, received 4.',
);

// invalid struct
const invalidStruct = {
subField1: Fr.random().toString(),
};
const args3 = [
addr1.toString(),
'false',
num.toString(),
`${JSON.stringify(fieldArray)}`,
JSON.stringify(invalidStruct),
];
expect(() => encodeArgs(args3, mockContractAbi.functions[1].parameters)).toThrow(
'Expected field subField2 not found in struct structParam.',
);

// invalid bool
const args4 = [
addr1.toString(),
'foo',
num.toString(),
`${JSON.stringify(fieldArray)}`,
JSON.stringify(invalidStruct),
];
expect(() => encodeArgs(args4, mockContractAbi.functions[1].parameters)).toThrow(
'Invalid boolean value passed for boolParam: foo.',
);

// invalid field
const args5 = ['foo', 'false', num.toString(), `${JSON.stringify(fieldArray)}`, JSON.stringify(invalidStruct)];
expect(() => encodeArgs(args5, mockContractAbi.functions[1].parameters)).toThrow(
'Invalid value passed for fieldParam. Could not parse foo as a field.',
);

// invalid int
const args6 = [addr1.toString(), 'false', 'foo', `${JSON.stringify(fieldArray)}`, JSON.stringify(invalidStruct)];
expect(() => encodeArgs(args6, mockContractAbi.functions[1].parameters)).toThrow(
'Invalid value passed for integerParam. Could not parse foo as an integer.',
);
});
});
1 change: 1 addition & 0 deletions yarn-project/aztec-cli/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { mnemonicToAccount, privateKeyToAccount } from 'viem/accounts';

import { encodeArgs } from './encoding.js';

export { createClient } from './client.js';
/**
* Helper type to dynamically import contracts.
*/
Expand Down
Loading

0 comments on commit 2987065

Please sign in to comment.