diff --git a/yarn-project/cli/src/index.ts b/yarn-project/cli/src/index.ts index fe38efd45081..efdb79f706b3 100644 --- a/yarn-project/cli/src/index.ts +++ b/yarn-project/cli/src/index.ts @@ -30,19 +30,14 @@ import { getAbiFunction, getContractAbi, getExampleContractArtifacts, + getSaltFromHexString, getTxSender, prepTx, + stripLeadingHex, } from './utils.js'; const accountCreationSalt = Fr.ZERO; -const stripLeadingHex = (hex: string) => { - if (hex.length > 2 && hex.startsWith('0x')) { - return hex.substring(2); - } - return hex; -}; - const { ETHEREUM_HOST, AZTEC_RPC_HOST, PRIVATE_KEY, API_KEY } = process.env; /** @@ -160,14 +155,18 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command { '-k, --public-key ', '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.', ) - .option('-s, --salt ', 'Optional deployment salt as a hex string for generating the deployment address.') + .option( + '-s, --salt ', + 'Optional deployment salt as a hex string for generating the deployment address.', + getSaltFromHexString, + ) .action(async (abiPath, options: any) => { 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 salt = options.salt ? Fr.fromBuffer(Buffer.from(stripLeadingHex(options.salt), 'hex')) : undefined; + const deployer = new ContractDeployer(contractAbi, client, publicKey); const constructor = getAbiFunction(contractAbi, 'constructor'); @@ -176,7 +175,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command { debugLogger(`Input arguments: ${options.args.map((x: any) => `"${x}"`).join(', ')}`); const args = encodeArgs(options.args, constructorAbi!.parameters); debugLogger(`Encoded arguments: ${args.join(', ')}`); - const tx = deployer.deploy(...args).send({ contractAddressSalt: salt }); + const tx = deployer.deploy(...args).send({ contractAddressSalt: options.salt }); debugLogger(`Deploy tx sent with hash ${await tx.getTxHash()}`); const deployed = await tx.wait(); log(`\nContract deployed at ${deployed.contractAddress!.toString()}\n`); diff --git a/yarn-project/cli/src/test/utils.test.ts b/yarn-project/cli/src/test/utils.test.ts index 3d34a48a2172..70e1a9c7d10e 100644 --- a/yarn-project/cli/src/test/utils.test.ts +++ b/yarn-project/cli/src/test/utils.test.ts @@ -1,10 +1,11 @@ import { AztecAddress, Fr } from '@aztec/aztec.js'; import { AztecRPC, CompleteAddress } from '@aztec/types'; +import { InvalidArgumentError } from 'commander'; import { MockProxy, mock } from 'jest-mock-extended'; import { encodeArgs } from '../encoding.js'; -import { getTxSender } from '../utils.js'; +import { getSaltFromHexString, getTxSender, stripLeadingHex } from '../utils.js'; import { mockContractAbi } from './mocks.js'; describe('CLI Utils', () => { @@ -128,4 +129,34 @@ describe('CLI Utils', () => { 'Invalid value passed for integerParam. Could not parse foo as an integer.', ); }); + + describe('stripLeadingHex', () => { + it.each([ + ['0x1', '1'], + ['1', '1'], + ['0x00', '00'], + ['00', '00'], + ])('removes optional leading hex', (hex, expected) => { + expect(stripLeadingHex(hex)).toEqual(expected); + }); + }); + + describe('getSaltFromHex', () => { + it.each([ + ['0', Fr.ZERO], + ['0x0', Fr.ZERO], + ['00', Fr.ZERO], + ['1', new Fr(1)], + ['0x1', new Fr(1)], + ['0x01', new Fr(1)], + ['0xa', new Fr(0xa)], + ['fff', new Fr(0xfff)], + ])('correctly generates salt from a hex string', (hex, expected) => { + expect(getSaltFromHexString(hex)).toEqual(expected); + }); + + it.each(['foo', '', ' ', ' 0x1', '01foo', 'foo1', '0xfoo'])('throws an error for invalid hex strings', str => { + expect(() => getSaltFromHexString(str)).toThrow(InvalidArgumentError); + }); + }); }); diff --git a/yarn-project/cli/src/utils.ts b/yarn-project/cli/src/utils.ts index d780af5412d9..aa8dc514ef65 100644 --- a/yarn-project/cli/src/utils.ts +++ b/yarn-project/cli/src/utils.ts @@ -1,8 +1,9 @@ -import { AztecAddress, AztecRPC } from '@aztec/aztec.js'; +import { AztecAddress, AztecRPC, Fr } from '@aztec/aztec.js'; import { createEthereumChain, deployL1Contracts } from '@aztec/ethereum'; import { ContractAbi } from '@aztec/foundation/abi'; import { DebugLogger, LogFn } from '@aztec/foundation/log'; +import { InvalidArgumentError } from 'commander'; import fs from 'fs'; import { mnemonicToAccount, privateKeyToAccount } from 'viem/accounts'; @@ -141,3 +142,36 @@ export async function prepTx( return { contractAddress, functionArgs, contractAbi }; } + +/** + * Removes the leading 0x from a hex string. If no leading 0x is found the string is returned unchanged. + * @param hex - A hex string + * @returns A new string with leading 0x removed + */ +export const stripLeadingHex = (hex: string) => { + if (hex.length > 2 && hex.startsWith('0x')) { + return hex.substring(2); + } + return hex; +}; + +/** + * Parses a hex encoded string to an Fr integer to be used as salt + * @param str - Hex encoded string + * @returns A integer to be used as salt + */ +export function getSaltFromHexString(str: string): Fr { + const hex = stripLeadingHex(str); + + // ensure it's a hex string + if (!hex.match(/^[0-9a-f]+$/i)) { + throw new InvalidArgumentError('Invalid hex string'); + } + + // pad it so that we may read it as a buffer. + // Buffer needs _exactly_ two hex characters per byte + const padded = hex.length % 2 === 1 ? '0' + hex : hex; + + // finally, turn it into an integer + return Fr.fromBuffer(Buffer.from(padded, 'hex')); +}