@aztec/cli 0.7.9 → 0.8.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/unbox.ts CHANGED
@@ -17,18 +17,8 @@ import * as path from 'path';
17
17
  const GITHUB_OWNER = 'AztecProtocol';
18
18
  const GITHUB_REPO = 'aztec-packages';
19
19
  const GITHUB_TAG_PREFIX = 'aztec-packages';
20
- const NOIR_CONTRACTS_PATH = 'yarn-project/noir-contracts/src/contracts';
21
20
  const BOXES_PATH = 'yarn-project/boxes';
22
21
 
23
- /**
24
- * Converts a contract name in "upper camel case" to a folder name in snake case or kebab case.
25
- * @param contractName - The contract name.
26
- * @returns The folder name.
27
- * */
28
- function contractNameToFolder(contractName: string, separator = '-'): string {
29
- return contractName.replace(/[\w]([A-Z])/g, m => `${m[0]}${separator}${m[1]}`).toLowerCase();
30
- }
31
-
32
22
  /**
33
23
  * If the box contains the noir contract source code, we don't need to download it from github.
34
24
  * Otherwise, we download the contract source code from the `noir-contracts` and `noir-libs` subpackages.
@@ -50,7 +40,7 @@ async function isDirectoryNonEmpty(directoryPath: string): Promise<boolean> {
50
40
  * @param localOutputPath - local path to copy to
51
41
  */
52
42
  async function copyFolderFromGithub(data: JSZip, repositoryFolderPath: string, localOutputPath: string, log: LogFn) {
53
- log(`Downloading from github: ${repositoryFolderPath}`);
43
+ log(`Downloading folder from github: ${repositoryFolderPath}`);
54
44
  const repositoryDirectories = Object.values(data.files).filter(file => {
55
45
  return file.dir && file.name.startsWith(repositoryFolderPath);
56
46
  });
@@ -61,11 +51,11 @@ async function copyFolderFromGithub(data: JSZip, repositoryFolderPath: string, l
61
51
  await fs.mkdir(targetPath, { recursive: true });
62
52
  }
63
53
 
64
- const starterFiles = Object.values(data.files).filter(file => {
54
+ const folderFiles = Object.values(data.files).filter(file => {
65
55
  return !file.dir && file.name.startsWith(repositoryFolderPath);
66
56
  });
67
57
 
68
- for (const file of starterFiles) {
58
+ for (const file of folderFiles) {
69
59
  const relativePath = file.name.replace(repositoryFolderPath, '');
70
60
  const targetPath = `${localOutputPath}/${relativePath}`;
71
61
  const content = await file.async('nodebuffer');
@@ -73,6 +63,27 @@ async function copyFolderFromGithub(data: JSZip, repositoryFolderPath: string, l
73
63
  }
74
64
  }
75
65
 
66
+ /**
67
+ * @param data - in memory unzipped clone of a github repo
68
+ * @param repositoryFile - path of the file to copy from github repo
69
+ * @param localOutputPath - local path to copy the file to
70
+ */
71
+ async function copyFileFromGithub(data: JSZip, repositoryFile: string, localOutputPath: string, log: LogFn) {
72
+ log(`Downloading file from github: ${repositoryFile}`);
73
+
74
+ const file = data.files[repositoryFile];
75
+
76
+ if (!file || file.dir) {
77
+ throw new Error(`File not found or it's a directory: ${repositoryFile}`);
78
+ }
79
+
80
+ const filename = path.basename(repositoryFile);
81
+ const targetPath = `${localOutputPath}/${filename}`;
82
+
83
+ const content = await file.async('nodebuffer');
84
+ await fs.writeFile(targetPath, content);
85
+ }
86
+
76
87
  /**
77
88
  * Not flexible at at all, but quick fix to download a noir smart contract from our
78
89
  * monorepo on github. this will copy over the `yarn-projects/boxes/{contract_name}` folder
@@ -92,8 +103,7 @@ async function downloadContractAndBoxFromGithub(
92
103
  // small string conversion, in the ABI the contract name looks like PrivateToken
93
104
  // but in the repostory it looks like private_token
94
105
 
95
- const kebabCaseContractName = contractNameToFolder(contractName, '-');
96
- log(`Downloading @aztex/boxes/${kebabCaseContractName} to ${outputPath}...`);
106
+ log(`Downloading @aztex/boxes/${contractName} to ${outputPath}...`);
97
107
  // Step 1: Fetch the monorepo ZIP from GitHub, matching the CLI version
98
108
  const url = `https://github.com/${GITHUB_OWNER}/${GITHUB_REPO}/archive/refs/tags/${tag}.zip`;
99
109
  const response = await fetch(url);
@@ -103,40 +113,26 @@ async function downloadContractAndBoxFromGithub(
103
113
  const data = await zip.loadAsync(buffer);
104
114
 
105
115
  // Step 2: copy the '@aztec/boxes/{contract-name}' subpackage to the output directory
106
- // this is currently only implemented for PrivateToken under 'boxes/private-token/'
116
+ // this is currently only implemented for `blank` and `private-token` under 'boxes/{box-name}/'
107
117
  const repoDirectoryPrefix = `${GITHUB_REPO}-${tag}`;
108
118
 
109
- const boxPath = `${repoDirectoryPrefix}/${BOXES_PATH}/${kebabCaseContractName}`;
119
+ const boxPath = `${repoDirectoryPrefix}/${BOXES_PATH}/${contractName}`;
110
120
  await copyFolderFromGithub(data, boxPath, outputPath, log);
111
121
 
122
+ // the expected noir version is contained in
123
+ // aztec-packages/yarn-project/noir-compiler/src/noir-version.json
124
+ // copy it in and use to update the package.json script to install that version of noir
125
+ const noirVersionPath = `${repoDirectoryPrefix}/yarn-project/noir-compiler/src/noir-version.json`;
126
+ await copyFileFromGithub(data, noirVersionPath, outputPath, log);
127
+
112
128
  const contractTargetDirectory = path.join(outputPath, 'src', 'contracts');
113
129
  const boxContainsNoirSource = await isDirectoryNonEmpty(contractTargetDirectory);
114
130
  if (boxContainsNoirSource) {
115
131
  return;
116
- }
117
-
118
- // this remaining logic only kicks in if the box doesn't already have a src/contracts folder
119
- // in which case we optimistically grab the noir source files from the
120
- // noir-contracts and noir-libs subpackages and pray that the versions are compatible
121
- log('Copying noir contracts...');
122
-
123
- // source noir files for the contract are in this folder
124
- const snakeCaseContractName = contractNameToFolder(contractName, '_');
125
- const contractDirectoryPath = `${repoDirectoryPrefix}/${NOIR_CONTRACTS_PATH}/${snakeCaseContractName}_contract`;
126
- // copy the noir contracts to the output directory under subdir /src/contracts/
127
- const contractFiles = Object.values(data.files).filter(file => {
128
- return !file.dir && file.name.startsWith(contractDirectoryPath);
129
- });
130
-
131
- // Nargo.toml file needs to be in the root of the contracts directory,
132
- // and noir files in the src/ subdirectory
133
- await fs.mkdir(path.join(contractTargetDirectory, 'src'), { recursive: true });
134
- for (const file of contractFiles) {
135
- const filename = file.name.replace(`${contractDirectoryPath}/`, '');
136
- const targetPath = path.join(contractTargetDirectory, filename);
137
- const content = await file.async('nodebuffer');
138
- await fs.writeFile(targetPath, content);
139
- log(` ✓ ${filename}`);
132
+ } else {
133
+ // we used to support downloading from the noir contracts monorepo but now force box to contain source code
134
+ // This should never happen, because of the check we do initially on the box name.
135
+ throw Error(`Box ${contractName} does not contain noir source code.`);
140
136
  }
141
137
  }
142
138
  /**
@@ -232,16 +228,23 @@ async function updatePackageJsonVersions(packageVersion: string, outputPath: str
232
228
  }
233
229
  }
234
230
  }
231
+ // read the `noir-version.json`, grab the expected noir version, and patch the noir install script
232
+ const noirVersionPath = path.join(outputPath, 'noir-version.json');
233
+ const noirVersionContent = await fs.readFile(noirVersionPath, 'utf-8');
234
+ const noirVersionJSON = JSON.parse(noirVersionContent);
235
+ const noirTag = noirVersionJSON.tag;
236
+ packageData.scripts['install:noir'] = packageData.scripts['install:noir'].replace('NOIR_VERSION', `${noirTag}`);
237
+ log(`Updated Noir version to: ${noirTag}`);
235
238
 
236
239
  // modify the version of the sandbox to pull - it's set to "latest" version in the monorepo,
237
240
  // but we need to replace with the same tagVersion as the cli and the other aztec npm packages
238
241
  // similarly, make sure we spinup the sandbox with the same version.
239
242
  packageData.scripts['install:sandbox'] = packageData.scripts['install:sandbox'].replace(
240
243
  'latest',
241
- `v${packageVersion}`,
244
+ `${packageVersion}`,
242
245
  );
243
246
 
244
- packageData.scripts['start:sandbox'] = packageData.scripts['start:sandbox'].replace('latest', `v${packageVersion}`);
247
+ packageData.scripts['start:sandbox'] = packageData.scripts['start:sandbox'].replace('latest', `${packageVersion}`);
245
248
 
246
249
  // Convert back to a string and write back to the package.json file
247
250
  const updatedContent = JSON.stringify(packageData, null, 2);
@@ -294,13 +297,13 @@ export async function unboxContract(
294
297
  packageVersion: string,
295
298
  log: LogFn,
296
299
  ) {
297
- const contractNames = ['PrivateToken'];
300
+ const contractNames = ['private-token', 'blank', 'blank-react'];
298
301
 
299
302
  if (!contractNames.includes(contractName)) {
300
303
  log(
301
304
  `The noir contract named "${contractName}" was not found in "@aztec/boxes" package. Valid options are:
302
305
  ${contractNames.join('\n\t')}
303
- We recommend "PrivateToken" as a default.`,
306
+ We recommend "private-token" as a default.`,
304
307
  );
305
308
  return;
306
309
  }
@@ -308,17 +311,17 @@ export async function unboxContract(
308
311
 
309
312
  const tag = `${GITHUB_TAG_PREFIX}-v${packageVersion}`;
310
313
  // downloads the selected contract's relevant folder in @aztec/boxes/{contract_name}
311
- // and the noir source code from `noir-contracts` into `${outputDirectoryName}/src/contracts`
312
- // if not present in the box
313
314
  await downloadContractAndBoxFromGithub(tag, contractName, outputPath, log);
314
315
  // make adjustments for packaging to work as a standalone, as opposed to part of yarn workspace
315
- // as in the monorepo source files
316
+ // as in the monorepo source files. replace things like "workspace^" with the actual version number
316
317
  await updatePackagingConfigurations(packageVersion, tag, outputPath, log);
317
318
 
318
319
  log('');
319
- log(`${contractName} has been successfully initialised!`);
320
+ log(`${contractName} has been successfully initialized!`);
320
321
  log('To get started, simply run the following commands:');
321
322
  log(` cd ${outputDirectoryName}`);
322
323
  log(' yarn');
324
+ log(' yarn start:sandbox');
325
+ log('And in another terminal in the same directory,');
323
326
  log(' yarn start:dev');
324
327
  }
package/src/utils.ts CHANGED
@@ -1,14 +1,26 @@
1
- import { AztecAddress, AztecRPC } from '@aztec/aztec.js';
2
- import { createEthereumChain, deployL1Contracts } from '@aztec/ethereum';
1
+ import { AztecAddress, Fr, GrumpkinScalar, PXE, Point, TxHash } from '@aztec/aztec.js';
2
+ import { L1ContractArtifactsForDeployment, createEthereumChain, deployL1Contracts } from '@aztec/ethereum';
3
3
  import { ContractAbi } from '@aztec/foundation/abi';
4
4
  import { DebugLogger, LogFn } from '@aztec/foundation/log';
5
+ import {
6
+ ContractDeploymentEmitterAbi,
7
+ ContractDeploymentEmitterBytecode,
8
+ InboxAbi,
9
+ InboxBytecode,
10
+ OutboxAbi,
11
+ OutboxBytecode,
12
+ RegistryAbi,
13
+ RegistryBytecode,
14
+ RollupAbi,
15
+ RollupBytecode,
16
+ } from '@aztec/l1-artifacts';
5
17
 
18
+ import { InvalidArgumentError } from 'commander';
6
19
  import fs from 'fs';
7
20
  import { mnemonicToAccount, privateKeyToAccount } from 'viem/accounts';
8
21
 
9
22
  import { encodeArgs } from './encoding.js';
10
23
 
11
- export { createClient } from './client.js';
12
24
  /**
13
25
  * Helper type to dynamically import contracts.
14
26
  */
@@ -46,7 +58,29 @@ export async function deployAztecContracts(
46
58
  ) {
47
59
  const account = !privateKey ? mnemonicToAccount(mnemonic!) : privateKeyToAccount(`0x${privateKey}`);
48
60
  const chain = createEthereumChain(rpcUrl, apiKey);
49
- return await deployL1Contracts(chain.rpcUrl, account, chain.chainInfo, debugLogger);
61
+ const l1Artifacts: L1ContractArtifactsForDeployment = {
62
+ contractDeploymentEmitter: {
63
+ contractAbi: ContractDeploymentEmitterAbi,
64
+ contractBytecode: ContractDeploymentEmitterBytecode,
65
+ },
66
+ registry: {
67
+ contractAbi: RegistryAbi,
68
+ contractBytecode: RegistryBytecode,
69
+ },
70
+ inbox: {
71
+ contractAbi: InboxAbi,
72
+ contractBytecode: InboxBytecode,
73
+ },
74
+ outbox: {
75
+ contractAbi: OutboxAbi,
76
+ contractBytecode: OutboxBytecode,
77
+ },
78
+ rollup: {
79
+ contractAbi: RollupAbi,
80
+ contractBytecode: RollupBytecode,
81
+ },
82
+ };
83
+ return await deployL1Contracts(chain.rpcUrl, account, chain.chainInfo, debugLogger, l1Artifacts);
50
84
  }
51
85
 
52
86
  /**
@@ -90,23 +124,23 @@ export async function getContractAbi(fileDir: string, log: LogFn) {
90
124
 
91
125
  /**
92
126
  * Utility to select a TX sender either from user input
93
- * or from the first account that is found in an Aztec RPC instance.
94
- * @param client - The Aztec RPC instance that will be checked for an account.
127
+ * or from the first account that is found in a PXE instance.
128
+ * @param pxe - The PXE instance that will be checked for an account.
95
129
  * @param _from - The user input.
96
130
  * @returns An Aztec address. Will throw if one can't be found in either options.
97
131
  */
98
- export async function getTxSender(client: AztecRPC, _from?: string) {
132
+ export async function getTxSender(pxe: PXE, _from?: string) {
99
133
  let from: AztecAddress;
100
134
  if (_from) {
101
135
  try {
102
136
  from = AztecAddress.fromString(_from);
103
137
  } catch {
104
- throw new Error(`Invalid option 'from' passed: ${_from}`);
138
+ throw new InvalidArgumentError(`Invalid option 'from' passed: ${_from}`);
105
139
  }
106
140
  } else {
107
- const accounts = await client.getRegisteredAccounts();
141
+ const accounts = await pxe.getRegisteredAccounts();
108
142
  if (!accounts.length) {
109
- throw new Error('No accounts found in Aztec RPC instance.');
143
+ throw new Error('No accounts found in PXE instance.');
110
144
  }
111
145
  from = accounts[0].address;
112
146
  }
@@ -116,28 +150,161 @@ export async function getTxSender(client: AztecRPC, _from?: string) {
116
150
  /**
117
151
  * Performs necessary checks, conversions & operations to call a contract fn from the CLI.
118
152
  * @param contractFile - Directory of the compiled contract ABI.
119
- * @param _contractAddress - Aztec Address of the contract.
153
+ * @param contractAddress - Aztec Address of the contract.
120
154
  * @param functionName - Name of the function to be called.
121
155
  * @param _functionArgs - Arguments to call the function with.
122
156
  * @param log - Logger instance that will output to the CLI
123
157
  * @returns Formatted contract address, function arguments and caller's aztec address.
124
158
  */
125
- export async function prepTx(
126
- contractFile: string,
127
- _contractAddress: string,
128
- functionName: string,
129
- _functionArgs: string[],
130
- log: LogFn,
131
- ) {
132
- let contractAddress;
133
- try {
134
- contractAddress = AztecAddress.fromString(_contractAddress);
135
- } catch {
136
- throw new Error(`Unable to parse contract address ${_contractAddress}.`);
137
- }
159
+ export async function prepTx(contractFile: string, functionName: string, _functionArgs: string[], log: LogFn) {
138
160
  const contractAbi = await getContractAbi(contractFile, log);
139
161
  const functionAbi = getAbiFunction(contractAbi, functionName);
140
162
  const functionArgs = encodeArgs(_functionArgs, functionAbi.parameters);
141
163
 
142
- return { contractAddress, functionArgs, contractAbi };
164
+ return { functionArgs, contractAbi };
165
+ }
166
+
167
+ /**
168
+ * Removes the leading 0x from a hex string. If no leading 0x is found the string is returned unchanged.
169
+ * @param hex - A hex string
170
+ * @returns A new string with leading 0x removed
171
+ */
172
+ export const stripLeadingHex = (hex: string) => {
173
+ if (hex.length > 2 && hex.startsWith('0x')) {
174
+ return hex.substring(2);
175
+ }
176
+ return hex;
177
+ };
178
+
179
+ /**
180
+ * Parses a hex encoded string to an Fr integer to be used as salt
181
+ * @param str - Hex encoded string
182
+ * @returns A integer to be used as salt
183
+ */
184
+ export function parseSaltFromHexString(str: string): Fr {
185
+ const hex = stripLeadingHex(str);
186
+
187
+ // ensure it's a hex string
188
+ if (!hex.match(/^[0-9a-f]+$/i)) {
189
+ throw new InvalidArgumentError('Invalid hex string');
190
+ }
191
+
192
+ // pad it so that we may read it as a buffer.
193
+ // Buffer needs _exactly_ two hex characters per byte
194
+ const padded = hex.length % 2 === 1 ? '0' + hex : hex;
195
+
196
+ // finally, turn it into an integer
197
+ return Fr.fromBuffer(Buffer.from(padded, 'hex'));
198
+ }
199
+
200
+ /**
201
+ * Parses an AztecAddress from a string. Throws InvalidArgumentError if the string is not a valid.
202
+ * @param address - A serialised Aztec address
203
+ * @returns An Aztec address
204
+ */
205
+ export function parseAztecAddress(address: string): AztecAddress {
206
+ try {
207
+ return AztecAddress.fromString(address);
208
+ } catch {
209
+ throw new InvalidArgumentError(`Invalid address: ${address}`);
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Parses a TxHash from a string. Throws InvalidArgumentError if the string is not a valid.
215
+ * @param txHash - A transaction hash
216
+ * @returns A TxHash instance
217
+ */
218
+ export function parseTxHash(txHash: string): TxHash {
219
+ try {
220
+ return TxHash.fromString(txHash);
221
+ } catch {
222
+ throw new InvalidArgumentError(`Invalid transaction hash: ${txHash}`);
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Parses a public key from a string. Throws InvalidArgumentError if the string is not a valid.
228
+ * @param publicKey - A public key
229
+ * @returns A Point instance
230
+ */
231
+ export function parsePublicKey(publicKey: string): Point {
232
+ try {
233
+ return Point.fromString(publicKey);
234
+ } catch (err) {
235
+ throw new InvalidArgumentError(`Invalid public key: ${publicKey}`);
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Parses a partial address from a string. Throws InvalidArgumentError if the string is not a valid.
241
+ * @param address - A partial address
242
+ * @returns A Fr instance
243
+ */
244
+ export function parsePartialAddress(address: string): Fr {
245
+ try {
246
+ return Fr.fromString(address);
247
+ } catch (err) {
248
+ throw new InvalidArgumentError(`Invalid partial address: ${address}`);
249
+ }
250
+ }
251
+
252
+ /**
253
+ * Parses a private key from a string. Throws InvalidArgumentError if the string is not a valid.
254
+ * @param privateKey - A string
255
+ * @returns A private key
256
+ */
257
+ export function parsePrivateKey(privateKey: string): GrumpkinScalar {
258
+ try {
259
+ const value = GrumpkinScalar.fromString(privateKey);
260
+ // most likely a badly formatted key was passed
261
+ if (value.isZero()) {
262
+ throw new Error('Private key must not be zero');
263
+ }
264
+
265
+ return value;
266
+ } catch (err) {
267
+ throw new InvalidArgumentError(`Invalid private key: ${privateKey}`);
268
+ }
269
+ }
270
+
271
+ /**
272
+ * Parses a field from a string. Throws InvalidArgumentError if the string is not a valid field value.
273
+ * @param field - A string representing the field.
274
+ * @returns A field.
275
+ */
276
+ export function parseField(field: string): Fr {
277
+ try {
278
+ const isHex = field.startsWith('0x') || field.match(new RegExp(`^[0-9a-f]{${Fr.SIZE_IN_BYTES * 2}}$`, 'i'));
279
+ if (isHex) {
280
+ return Fr.fromString(field);
281
+ }
282
+
283
+ if (['true', 'false'].includes(field)) {
284
+ return new Fr(field === 'true');
285
+ }
286
+
287
+ const isNumber = +field || field === '0';
288
+ if (isNumber) {
289
+ return new Fr(BigInt(field));
290
+ }
291
+
292
+ const isBigInt = field.endsWith('n');
293
+ if (isBigInt) {
294
+ return new Fr(BigInt(field.replace(/n$/, '')));
295
+ }
296
+
297
+ return new Fr(BigInt(field));
298
+ } catch (err) {
299
+ throw new InvalidArgumentError(`Invalid field: ${field}`);
300
+ }
301
+ }
302
+
303
+ /**
304
+ * Parses an array of strings to Frs.
305
+ * @param fields - An array of strings representing the fields.
306
+ * @returns An array of Frs.
307
+ */
308
+ export function parseFields(fields: string[]): Fr[] {
309
+ return fields.map(parseField);
143
310
  }
package/.eslintrc.cjs DELETED
@@ -1 +0,0 @@
1
- module.exports = require('@aztec/foundation/eslint');