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

test: CLI tests #1786

Merged
merged 8 commits into from
Aug 29, 2023
Merged
Show file tree
Hide file tree
Changes from 5 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
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
3 changes: 3 additions & 0 deletions yarn-project/aztec-cli/src/encoding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ 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(`Wrong number of args provided. Expected: ${params.length}, received: ${args.length}`);
}
return args
.map((arg: any, index) => {
const paramType = params[index].type;
Expand Down
18 changes: 9 additions & 9 deletions yarn-project/aztec-cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ import { dirname, resolve } from 'path';
import { fileURLToPath } from 'url';
import { mnemonicToAccount } from 'viem/accounts';

import { createClient } from './client.js';
import { encodeArgs, parseStructString } from './encoding.js';
import {
createClient,
deployAztecContracts,
getAbiFunction,
getContractAbi,
Expand Down Expand Up @@ -129,7 +129,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command {
.action(async options => {
const client = createClient(options.rpcUrl);
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 = createClient(options.rpcUrl);
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 = createClient(options.rpcUrl);
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 = createClient(options.rpcUrl);
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 = createClient(options.rpcUrl);
const num = await client.getBlockNumber();
Expand Down
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
130 changes: 120 additions & 10 deletions yarn-project/end-to-end/src/e2e_cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ import { startHttpRpcServer } from '@aztec/aztec-sandbox/http';
import { createDebugLogger } from '@aztec/aztec.js';
import { getProgram } from '@aztec/cli';
import { DebugLogger } from '@aztec/foundation/log';
import { AztecRPC } from '@aztec/types';
import { AztecRPC, CompleteAddress } from '@aztec/types';

import stringArgv from 'string-argv';
import { format } from 'util';

import { setup } from './fixtures/utils.js';

const HTTP_PORT = 9009;
const INITIAL_BALANCE = 33000;
const TRANSFER_BALANCE = 3000;

// Spins up a new http server wrapping the set up rpc server, and tests cli commands against it
describe('cli', () => {
Expand All @@ -20,6 +22,9 @@ describe('cli', () => {
let debug: DebugLogger;
let aztecNode: AztecNodeService | undefined;
let aztecRpcServer: AztecRPC;
let existingAccounts: CompleteAddress[];
let contractAddress: AztecAddress;
let log: (...args: any[]) => void;

// All logs emitted by the cli will be collected here, and reset between tests
const logs: string[] = [];
Expand All @@ -32,13 +37,17 @@ describe('cli', () => {
({ aztecNode, aztecRpcServer } = context);
http = startHttpRpcServer(aztecRpcServer, deployL1ContractsValues, HTTP_PORT);
debug(`HTTP RPC server started in port ${HTTP_PORT}`);
const log = (...args: any[]) => {
log = (...args: any[]) => {
logs.push(format(...args));
debug(...args);
};
cli = getProgram(log, debug);
});

// in order to run the same command twice, we need to create a new CLI instance
const resetCli = () => {
cli = getProgram(log, debug);
};

afterAll(async () => {
http.close();
await aztecNode?.stop();
Expand All @@ -47,11 +56,17 @@ describe('cli', () => {

beforeEach(() => {
logs.splice(0);
resetCli();
});

// Run a command on the CLI
const run = (cmd: string) =>
cli.parseAsync(stringArgv(cmd, 'node', 'dest/bin/index.js').concat(['--rpc-url', `http://localhost:${HTTP_PORT}`]));
const run = (cmd: string, addRpcUrl = true) => {
const args = stringArgv(cmd, 'node', 'dest/bin/index.js');
if (addRpcUrl) {
args.push('--rpc-url', `http://localhost:${HTTP_PORT}`);
}
return cli.parseAsync(args);
};

// Returns first match across all logs collected so far
const findInLogs = (regex: RegExp) => {
Expand All @@ -61,14 +76,109 @@ describe('cli', () => {
}
};

it('creates an account', async () => {
const accountsBefore = await aztecRpcServer.getAccounts();
const findMultipleInLogs = (regex: RegExp) => {
const matches = [];
for (const log of logs) {
const match = regex.exec(log);
if (match) matches.push(match);
}
return matches;
};

const clearLogs = () => {
logs.splice(0);
};

it('creates & retrieves an account', async () => {
existingAccounts = await aztecRpcServer.getAccounts();
debug('Create an account');
await run(`create-account`);
const newAddress = findInLogs(/Address:\s+(?<address>0x[a-fA-F0-9]+)/)?.groups?.address;
expect(newAddress).toBeDefined();
const foundAddress = findInLogs(/Address:\s+(?<address>0x[a-fA-F0-9]+)/)?.groups?.address;
expect(foundAddress).toBeDefined();
const newAddress = AztecAddress.fromString(foundAddress!);

const accountsAfter = await aztecRpcServer.getAccounts();
const expectedAccounts = [...accountsBefore.map(a => a.address), AztecAddress.fromString(newAddress!)];
const expectedAccounts = [...existingAccounts.map(a => a.address), newAddress];
expect(accountsAfter.map(a => a.address)).toEqual(expectedAccounts);
const newCompleteAddress = accountsAfter[accountsAfter.length - 1];

// Test get-accounts
debug('Check that account was added to the list of accs in RPC');
await run('get-accounts');
const fetchedAddresses = findMultipleInLogs(/Address:\s+(?<address>0x[a-fA-F0-9]+)/);
const foundFetchedAddress = fetchedAddresses.find(match => match.groups?.address === newAddress.toString());
expect(foundFetchedAddress).toBeDefined();

// Test get-account
debug('Check we can retrieve the specific account');
clearLogs();
await run(`get-account ${newAddress.toString()}`);
const fetchedAddress = findInLogs(/Public Key:\s+(?<address>0x[a-fA-F0-9]+)/)?.groups?.address;
expect(fetchedAddress).toEqual(newCompleteAddress.publicKey.toString());
});

it('deploys a contract & sends transactions', async () => {
// generate a private key
debug('Create an account using a private key');
await run('generate-private-key', false);
const privKey = findInLogs(/Private\sKey:\s+(?<privKey>[a-fA-F0-9]+)/)?.groups?.privKey;
expect(privKey).toHaveLength(64);
await run(`create-account --private-key ${privKey}`);
const foundAddress = findInLogs(/Address:\s+(?<address>0x[a-fA-F0-9]+)/)?.groups?.address;
expect(foundAddress).toBeDefined();
const ownerAddress = AztecAddress.fromString(foundAddress!);

debug('Deploy Private Token Contract using created account.');
await run(`deploy PrivateTokenContractAbi --args ${INITIAL_BALANCE} ${ownerAddress} --salt 0`);
const loggedAddress = findInLogs(/Contract\sdeployed\sat\s+(?<address>0x[a-fA-F0-9]+)/)?.groups?.address;
expect(loggedAddress).toBeDefined();
contractAddress = AztecAddress.fromString(loggedAddress!);

const deployedContract = await aztecRpcServer.getContractData(contractAddress);
expect(deployedContract?.contractAddress).toEqual(contractAddress);

debug('Check contract can be found in returned address');
await run(`check-deploy -ca ${loggedAddress}`);
const checkResult = findInLogs(/Contract\sfound\sat\s+(?<address>0x[a-fA-F0-9]+)/)?.groups?.address;
expect(checkResult).toEqual(deployedContract?.contractAddress.toString());

// clear logs
clearLogs();
await run(`get-contract-data ${loggedAddress}`);
const contractDataAddress = findInLogs(/Address:\s+(?<address>0x[a-fA-F0-9]+)/)?.groups?.address;
expect(contractDataAddress).toEqual(deployedContract?.contractAddress.toString());

debug("Check owner's balance");
await run(
`call getBalance --args ${ownerAddress} --contract-abi PrivateTokenContractAbi --contract-address ${contractAddress.toString()}`,
);
const balance = findInLogs(/View\sresult:\s+(?<data>\S+)/)?.groups?.data;
expect(balance!).toEqual(`${BigInt(INITIAL_BALANCE).toString()}n`);

debug('Transfer some tokens');
const existingAccounts = await aztecRpcServer.getAccounts();
// ensure we pick a different acc
const receiver = existingAccounts.find(acc => acc.address.toString() !== ownerAddress.toString());

await run(
`send transfer --args ${TRANSFER_BALANCE} ${ownerAddress.toString()} ${receiver?.address.toString()} --contract-address ${contractAddress.toString()} --contract-abi PrivateTokenContractAbi --private-key ${privKey}`,
);
const txHash = findInLogs(/Transaction\shash:\s+(?<txHash>\S+)/)?.groups?.txHash;

debug('Check the transfer receipt');
await run(`get-tx-receipt ${txHash}`);
const txResult = findInLogs(/Transaction receipt:\s*(?<txHash>[\s\S]*?\})/)?.groups?.txHash;
const parsedResult = JSON.parse(txResult!);
expect(parsedResult.txHash).toEqual(txHash);
expect(parsedResult.status).toEqual('mined');
debug("Check Receiver's balance");
// Reset CLI as we're calling getBalance again
resetCli();
clearLogs();
await run(
`call getBalance --args ${receiver?.address.toString()} --contract-abi PrivateTokenContractAbi --contract-address ${contractAddress.toString()}`,
);
const receiverBalance = findInLogs(/View\sresult:\s+(?<data>\S+)/)?.groups?.data;
expect(receiverBalance).toEqual(`${BigInt(TRANSFER_BALANCE).toString()}n`);
});
});