Skip to content

Commit

Permalink
fix: cli type check arguments and options (#2571)
Browse files Browse the repository at this point in the history
This PR adds type checking to CLI parameters.
 
Fix #2508 

# Checklist:
Remove the checklist to signal you've completed it. Enable auto-merge if
the PR is ready to merge.
- [ ] If the pull request requires a cryptography review (e.g.
cryptographic algorithm implementations) I have added the 'crypto' tag.
- [x] I have reviewed my diff in github, line by line and removed
unexpected formatting changes, testing logs, or commented-out code.
- [x] Every change is related to the PR description.
- [x] I have
[linked](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue)
this pull request to relevant issues (if any exist).
  • Loading branch information
alexghr authored Sep 28, 2023
1 parent b33eea5 commit ecffc36
Show file tree
Hide file tree
Showing 3 changed files with 164 additions and 113 deletions.
174 changes: 83 additions & 91 deletions yarn-project/cli/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import {
AztecAddress,
Contract,
ContractDeployer,
Fr,
GrumpkinScalar,
Point,
generatePublicKey,
getSchnorrAccount,
isContractDeployed,
Expand All @@ -14,10 +12,10 @@ import { JsonStringify } from '@aztec/foundation/json-rpc';
import { DebugLogger, LogFn } from '@aztec/foundation/log';
import { fileURLToPath } from '@aztec/foundation/url';
import { compileContract, generateNoirInterface, generateTypescriptInterface } from '@aztec/noir-compiler/cli';
import { CompleteAddress, ContractData, L2BlockL2Logs, TxHash } from '@aztec/types';
import { CompleteAddress, ContractData, L2BlockL2Logs } from '@aztec/types';

import { createSecp256k1PeerId } from '@libp2p/peer-id-factory';
import { Command } from 'commander';
import { Command, Option } from 'commander';
import { readFileSync } from 'fs';
import { dirname, resolve } from 'path';
import { mnemonicToAccount } from 'viem/accounts';
Expand All @@ -30,20 +28,19 @@ import {
getAbiFunction,
getContractAbi,
getExampleContractArtifacts,
getSaltFromHexString,
getTxSender,
parseAztecAddress,
parsePartialAddress,
parsePrivateKey,
parsePublicKey,
parseSaltFromHexString,
parseTxHash,
prepTx,
stripLeadingHex,
} from './utils.js';

const accountCreationSalt = Fr.ZERO;

const {
ETHEREUM_HOST = 'http://localhost:8545',
PXE_HOST = 'http://localhost:8080',
PRIVATE_KEY,
API_KEY,
} = process.env;
const { ETHEREUM_HOST = 'http://localhost:8545', PRIVATE_KEY, API_KEY } = process.env;

/**
* Returns commander program that defines the CLI.
Expand All @@ -59,6 +56,17 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command {

program.name('aztec-cli').description('CLI for interacting with Aztec.').version(version);

const pxeOption = new Option('-u, --rpc-url <string>', 'URL of the PXE')
.env('PXE_HOST')
.default('http://localhost:8080')
.makeOptionMandatory(true);

const createPrivateKeyOption = (description: string, mandatory: boolean) =>
new Option('-k, --private-key <string>', description)
.env('PRIVATE_KEY')
.argParser(parsePrivateKey)
.makeOptionMandatory(mandatory);

program
.command('deploy-l1-contracts')
.description('Deploys all necessary Ethereum contracts for Aztec.')
Expand Down Expand Up @@ -134,17 +142,13 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command {
'Creates an aztec account that can be used for sending transactions. Registers the account on the PXE and deploys an account contract. Uses a Schnorr single-key account which uses the same key for encryption and authentication (not secure for production usage).',
)
.summary('Creates an aztec account that can be used for sending transactions.')
.option(
'-k, --private-key <string>',
'Private key for note encryption and transaction signing. Uses random by default.',
PRIVATE_KEY,
.addOption(
createPrivateKeyOption('Private key for note encryption and transaction signing. Uses random by default.', false),
)
.requiredOption('-u, --rpc-url <string>', 'URL of the PXE', PXE_HOST)
.addOption(pxeOption)
.action(async options => {
const client = await createCompatibleClient(options.rpcUrl, debugLogger);
const privateKey = options.privateKey
? GrumpkinScalar.fromString(stripLeadingHex(options.privateKey))
: GrumpkinScalar.random();
const privateKey = options.privateKey ?? GrumpkinScalar.random();

const account = getSchnorrAccount(client, privateKey, privateKey, accountCreationSalt);
const wallet = await account.waitDeploy();
Expand All @@ -165,38 +169,38 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command {
"A compiled Aztec.nr contract's ABI in JSON format or name of a contract ABI exported by @aztec/noir-contracts",
)
.option('-a, --args <constructorArgs...>', 'Contract constructor arguments', [])
.requiredOption('-u, --rpc-url <string>', 'URL of the PXE', PXE_HOST)
.addOption(pxeOption)
.option(
'-k, --public-key <string>',
'Optional encryption public key for this address. Set this value only if this contract is expected to receive private notes, which will be encrypted using this public key.',
parsePublicKey,
)
.option(
'-s, --salt <hex string>',
'Optional deployment salt as a hex string for generating the deployment address.',
getSaltFromHexString,
parseSaltFromHexString,
)
// `options.wait` is default true. Passing `--no-wait` will set it to false.
// https://github.com/tj/commander.js#other-option-types-negatable-boolean-and-booleanvalue
.option('--no-wait', 'Skip waiting for the contract to be deployed. Print the hash of deployment transaction')
.action(async (abiPath, options: any) => {
.action(async (abiPath, { rpcUrl, publicKey, args: rawArgs, salt, wait }) => {
const contractAbi = await getContractAbi(abiPath, log);
const constructorAbi = contractAbi.functions.find(({ name }) => name === 'constructor');

const client = await createCompatibleClient(options.rpcUrl, debugLogger);
const publicKey = options.publicKey ? Point.fromString(options.publicKey) : undefined;

const client = await createCompatibleClient(rpcUrl, debugLogger);
const deployer = new ContractDeployer(contractAbi, client, publicKey);

const constructor = getAbiFunction(contractAbi, 'constructor');
if (!constructor) throw new Error(`Constructor not found in contract ABI`);

debugLogger(`Input arguments: ${options.args.map((x: any) => `"${x}"`).join(', ')}`);
const args = encodeArgs(options.args, constructorAbi!.parameters);
debugLogger(`Input arguments: ${rawArgs.map((x: any) => `"${x}"`).join(', ')}`);
const args = encodeArgs(rawArgs, constructorAbi!.parameters);
debugLogger(`Encoded arguments: ${args.join(', ')}`);
const tx = deployer.deploy(...args).send({ contractAddressSalt: options.salt });

const tx = deployer.deploy(...args).send({ contractAddressSalt: salt });
const txHash = await tx.getTxHash();
debugLogger(`Deploy tx sent with hash ${txHash}`);
if (options.wait) {
if (wait) {
const deployed = await tx.wait();
log(`\nContract deployed at ${deployed.contractAddress!.toString()}\n`);
} else {
Expand All @@ -207,11 +211,15 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command {
program
.command('check-deploy')
.description('Checks if a contract is deployed to the specified Aztec address.')
.requiredOption('-ca, --contract-address <address>', 'An Aztec address to check if contract has been deployed to.')
.requiredOption('-u, --rpc-url <url>', 'URL of the PXE', PXE_HOST)
.requiredOption(
'-ca, --contract-address <address>',
'An Aztec address to check if contract has been deployed to.',
parseAztecAddress,
)
.addOption(pxeOption)
.action(async options => {
const client = await createCompatibleClient(options.rpcUrl, debugLogger);
const address = AztecAddress.fromString(options.contractAddress);
const address = options.contractAddress;
const isDeployed = await isContractDeployed(client, address);
if (isDeployed) log(`\nContract found at ${address.toString()}\n`);
else log(`\nNo contract found at ${address.toString()}\n`);
Expand All @@ -220,14 +228,13 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command {
program
.command('get-tx-receipt')
.description('Gets the receipt for the specified transaction hash.')
.argument('<txHash>', 'A transaction hash to get the receipt for.')
.requiredOption('-u, --rpc-url <string>', 'URL of the PXE', PXE_HOST)
.action(async (_txHash, options) => {
.argument('<txHash>', 'A transaction hash to get the receipt for.', parseTxHash)
.addOption(pxeOption)
.action(async (txHash, options) => {
const client = await createCompatibleClient(options.rpcUrl, debugLogger);
const txHash = TxHash.fromString(_txHash);
const receipt = await client.getTxReceipt(txHash);
if (!receipt) {
log(`No receipt found for transaction hash ${_txHash}`);
log(`No receipt found for transaction hash ${txHash.toString()}`);
} else {
log(`\nTransaction receipt: \n${JsonStringify(receipt, true)}\n`);
}
Expand All @@ -236,15 +243,14 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command {
program
.command('get-contract-data')
.description('Gets information about the Aztec contract deployed at the specified address.')
.argument('<contractAddress>', 'Aztec address of the contract.')
.requiredOption('-u, --rpc-url <string>', 'URL of the PXE', PXE_HOST)
.argument('<contractAddress>', 'Aztec address of the contract.', parseAztecAddress)
.addOption(pxeOption)
.option('-b, --include-bytecode <boolean>', "Include the contract's public function bytecode, if any.", false)
.action(async (contractAddress, options) => {
const client = await createCompatibleClient(options.rpcUrl, debugLogger);
const address = AztecAddress.fromString(contractAddress);
const contractDataWithOrWithoutBytecode = options.includeBytecode
? await client.getExtendedContractData(address)
: await client.getContractData(address);
? await client.getExtendedContractData(contractAddress)
: await client.getContractData(contractAddress);

if (!contractDataWithOrWithoutBytecode) {
log(`No contract data found at ${contractAddress}`);
Expand All @@ -270,7 +276,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command {
.description('Gets all the unencrypted logs from L2 blocks in the range specified.')
.option('-f, --from <blockNum>', 'Initial block number for getting logs (defaults to 1).')
.option('-l, --limit <blockCount>', 'How many blocks to fetch (defaults to 100).')
.requiredOption('-u, --rpc-url <string>', 'URL of the PXE', PXE_HOST)
.addOption(pxeOption)
.action(async options => {
const { from, limit } = options;
const fromBlock = from ? parseInt(from) : 1;
Expand All @@ -289,24 +295,24 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command {
program
.command('register-recipient')
.description('Register a recipient in the PXE.')
.requiredOption('-a, --address <aztecAddress>', "The account's Aztec address.")
.requiredOption('-p, --public-key <publicKey>', 'The account public key.')
.requiredOption('-pa, --partial-address <partialAddress', 'The partially computed address of the account contract.')
.requiredOption('-u, --rpc-url <string>', 'URL of the PXE', PXE_HOST)
.action(async options => {
const client = await createCompatibleClient(options.rpcUrl, debugLogger);
const address = AztecAddress.fromString(options.address);
const publicKey = Point.fromString(options.publicKey);
const partialAddress = Fr.fromString(options.partialAddress);

.requiredOption('-a, --address <aztecAddress>', "The account's Aztec address.", parseAztecAddress)
.requiredOption('-p, --public-key <publicKey>', 'The account public key.', parsePublicKey)
.requiredOption(
'-pa, --partial-address <partialAddress>',
'The partially computed address of the account contract.',
parsePartialAddress,
)
.addOption(pxeOption)
.action(async ({ address, publicKey, partialAddress, rpcUrl }) => {
const client = await createCompatibleClient(rpcUrl, debugLogger);
await client.registerRecipient(await CompleteAddress.create(address, publicKey, partialAddress));
log(`\nRegistered details for account with address: ${options.address}\n`);
log(`\nRegistered details for account with address: ${address}\n`);
});

program
.command('get-accounts')
.description('Gets all the Aztec accounts stored in the PXE.')
.requiredOption('-u, --rpc-url <string>', 'URL of the PXE', PXE_HOST)
.addOption(pxeOption)
.action(async (options: any) => {
const client = await createCompatibleClient(options.rpcUrl, debugLogger);
const accounts = await client.getRegisteredAccounts();
Expand All @@ -323,15 +329,14 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command {
program
.command('get-account')
.description('Gets an account given its Aztec address.')
.argument('<address>', 'The Aztec address to get account for')
.requiredOption('-u, --rpc-url <string>', 'URL of the PXE', PXE_HOST)
.action(async (_address, options) => {
.argument('<address>', 'The Aztec address to get account for', parseAztecAddress)
.addOption(pxeOption)
.action(async (address, options) => {
const client = await createCompatibleClient(options.rpcUrl, debugLogger);
const address = AztecAddress.fromString(_address);
const account = await client.getRegisteredAccount(address);

if (!account) {
log(`Unknown account ${_address}`);
log(`Unknown account ${address.toString()}`);
} else {
log(account.toReadableString());
}
Expand All @@ -340,7 +345,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command {
program
.command('get-recipients')
.description('Gets all the recipients stored in the PXE.')
.requiredOption('-u, --rpc-url <string>', 'URL of the PXE', PXE_HOST)
.addOption(pxeOption)
.action(async (options: any) => {
const client = await createCompatibleClient(options.rpcUrl, debugLogger);
const recipients = await client.getRecipients();
Expand All @@ -357,15 +362,14 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command {
program
.command('get-recipient')
.description('Gets a recipient given its Aztec address.')
.argument('<address>', 'The Aztec address to get recipient for')
.requiredOption('-u, --rpc-url <string>', 'URL of the PXE', PXE_HOST)
.action(async (_address, options) => {
.argument('<address>', 'The Aztec address to get recipient for', parseAztecAddress)
.addOption(pxeOption)
.action(async (address, options) => {
const client = await createCompatibleClient(options.rpcUrl, debugLogger);
const address = AztecAddress.fromString(_address);
const recipient = await client.getRecipient(address);

if (!recipient) {
log(`Unknown recipient ${_address}`);
log(`Unknown recipient ${address.toString()}`);
} else {
log(recipient.toReadableString());
}
Expand All @@ -380,20 +384,13 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command {
'-c, --contract-abi <fileLocation>',
"A compiled Aztec.nr contract's ABI in JSON format or name of a contract ABI exported by @aztec/noir-contracts",
)
.requiredOption('-ca, --contract-address <address>', 'Aztec address of the contract.')
.requiredOption('-k, --private-key <string>', "The sender's private key.", PRIVATE_KEY)
.requiredOption('-u, --rpc-url <string>', 'URL of the PXE', PXE_HOST)
.requiredOption('-ca, --contract-address <address>', 'Aztec address of the contract.', parseAztecAddress)
.addOption(createPrivateKeyOption("The sender's private key.", true))
.addOption(pxeOption)
.option('--no-wait', 'Print transaction hash without waiting for it to be mined')
.action(async (functionName, options) => {
const { contractAddress, functionArgs, contractAbi } = await prepTx(
options.contractAbi,
options.contractAddress,
functionName,
options.args,
log,
);

const privateKey = GrumpkinScalar.fromString(stripLeadingHex(options.privateKey));
const { functionArgs, contractAbi } = await prepTx(options.contractAbi, functionName, options.args, log);
const { contractAddress, privateKey } = options;

const client = await createCompatibleClient(options.rpcUrl, debugLogger);
const wallet = await getSchnorrAccount(client, privateKey, privateKey, accountCreationSalt).getWallet();
Expand Down Expand Up @@ -425,17 +422,12 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command {
'-c, --contract-abi <fileLocation>',
"A compiled Aztec.nr contract's ABI in JSON format or name of a contract ABI exported by @aztec/noir-contracts",
)
.requiredOption('-ca, --contract-address <address>', 'Aztec address of the contract.')
.requiredOption('-ca, --contract-address <address>', 'Aztec address of the contract.', parseAztecAddress)
.option('-f, --from <string>', 'Aztec address of the caller. If empty, will use the first account from RPC.')
.requiredOption('-u, --rpc-url <string>', 'URL of the PXE', PXE_HOST)
.addOption(pxeOption)
.action(async (functionName, options) => {
const { contractAddress, functionArgs, contractAbi } = await prepTx(
options.contractAbi,
options.contractAddress,
functionName,
options.args,
log,
);
const { functionArgs, contractAbi } = await prepTx(options.contractAbi, functionName, options.args, log);

const fnAbi = getAbiFunction(contractAbi, functionName);
if (fnAbi.parameters.length !== options.args.length) {
throw Error(
Expand All @@ -444,7 +436,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);
const result = await client.viewTx(functionName, functionArgs, options.contractAddress, from);
log('\nView result: ', result, '\n');
});

Expand Down Expand Up @@ -475,7 +467,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command {
program
.command('block-number')
.description('Gets the current Aztec L2 block number.')
.requiredOption('-u, --rpc-url <string>', 'URL of the PXE', PXE_HOST)
.addOption(pxeOption)
.action(async (options: any) => {
const client = await createCompatibleClient(options.rpcUrl, debugLogger);
const num = await client.getBlockNumber();
Expand Down Expand Up @@ -506,7 +498,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command {
program
.command('get-node-info')
.description('Gets the information of an aztec node at a URL.')
.requiredOption('-u, --rpc-url <string>', 'URL of the PXE', PXE_HOST)
.addOption(pxeOption)
.action(async options => {
const client = await createCompatibleClient(options.rpcUrl, debugLogger);
const info = await client.getNodeInfo();
Expand Down
Loading

0 comments on commit ecffc36

Please sign in to comment.