@aztec/cli 0.6.7 → 0.7.2

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/Dockerfile ADDED
@@ -0,0 +1,15 @@
1
+ FROM 278380418400.dkr.ecr.eu-west-2.amazonaws.com/yarn-project-base AS builder
2
+
3
+ COPY . .
4
+
5
+ WORKDIR /usr/src/yarn-project/cli
6
+ RUN yarn build && yarn formatting && yarn test
7
+
8
+ # Prune dev dependencies. See comment in base image.
9
+ RUN yarn cache clean
10
+ RUN yarn workspaces focus --production > /dev/null
11
+
12
+ FROM node:18-alpine
13
+ COPY --from=builder /usr/src/ /usr/src/
14
+ WORKDIR /usr/src/yarn-project/cli
15
+ ENTRYPOINT ["yarn", "start"]
package/README.md CHANGED
@@ -135,7 +135,7 @@ aztec-cli create-account
135
135
 
136
136
  ### deploy
137
137
 
138
- Deploys a compiled Noir contract to Aztec.
138
+ Deploys a compiled Aztec.nr contract to Aztec.
139
139
 
140
140
  Syntax:
141
141
 
@@ -145,12 +145,12 @@ aztec-cli deploy <contractAbi> [options]
145
145
 
146
146
  Options:
147
147
 
148
- - `-c, --contract-abi <fileLocation>`: Path to the compiled Noir contract's ABI file 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. You can get a full ist of the available contracts with `aztec-cli example-contracts`
148
+ - `-c, --contract-abi <fileLocation>`: Path to the compiled Aztec.nr contract's ABI file 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. You can get a full ist of the available contracts with `aztec-cli example-contracts`
149
149
  - `-a, --args <constructorArgs...>` (optional): Contract constructor arguments Default: [].
150
150
  - `-u, --rpc-url <string>`: URL of the Aztec RPC. Default: `http://localhost:8080`.
151
151
  - `-k, --public-key <string>`: Public key of the deployer. If not provided, it will check the RPC for existing ones.
152
152
 
153
- This command deploys a compiled Noir contract to Aztec. It requires the path to the contract's ABI file in JSON format. Optionally, you can specify the public key of the deployer and provide constructor arguments for the contract. The command displays the address of the deployed contract.
153
+ This command deploys a compiled Aztec.nr contract to Aztec. It requires the path to the contract's ABI file in JSON format. Optionally, you can specify the public key of the deployer and provide constructor arguments for the contract. The command displays the address of the deployed contract.
154
154
 
155
155
  Example usage:
156
156
 
package/package.json CHANGED
@@ -1,17 +1,19 @@
1
1
  {
2
2
  "name": "@aztec/cli",
3
- "version": "0.6.7",
3
+ "version": "0.7.2",
4
4
  "main": "./dest/index.js",
5
5
  "type": "module",
6
6
  "dependencies": {
7
- "@aztec/aztec.js": "0.6.7",
8
- "@aztec/ethereum": "0.6.7",
9
- "@aztec/foundation": "0.6.7",
10
- "@aztec/noir-compiler": "0.6.7",
11
- "@aztec/noir-contracts": "0.6.7",
12
- "@aztec/types": "0.6.7",
7
+ "@aztec/aztec.js": "0.7.2",
8
+ "@aztec/ethereum": "0.7.2",
9
+ "@aztec/foundation": "0.7.2",
10
+ "@aztec/noir-compiler": "0.7.2",
11
+ "@aztec/noir-contracts": "0.7.2",
12
+ "@aztec/types": "0.7.2",
13
13
  "commander": "^9.0.0",
14
+ "jszip": "^3.10.1",
14
15
  "lodash.startcase": "^4.4.0",
16
+ "node-fetch": "^3.3.2",
15
17
  "semver": "^7.5.4",
16
18
  "tslib": "^2.4.0",
17
19
  "viem": "^1.2.5"
package/src/index.ts CHANGED
@@ -6,16 +6,14 @@ import {
6
6
  GrumpkinScalar,
7
7
  Point,
8
8
  generatePublicKey,
9
- getAccountWallets,
10
9
  getSchnorrAccount,
11
10
  isContractDeployed,
12
11
  } from '@aztec/aztec.js';
13
- import { StructType } from '@aztec/foundation/abi';
12
+ import { StructType, decodeFunctionSignatureWithParameterNames } from '@aztec/foundation/abi';
14
13
  import { JsonStringify } from '@aztec/foundation/json-rpc';
15
14
  import { DebugLogger, LogFn } from '@aztec/foundation/log';
16
15
  import { fileURLToPath } from '@aztec/foundation/url';
17
- import { compileContract } from '@aztec/noir-compiler/cli';
18
- import { SchnorrAccountContractAbi } from '@aztec/noir-contracts/artifacts';
16
+ import { compileContract, generateNoirInterface, generateTypescriptInterface } from '@aztec/noir-compiler/cli';
19
17
  import { CompleteAddress, ContractData, L2BlockL2Logs, TxHash } from '@aztec/types';
20
18
 
21
19
  import { Command } from 'commander';
@@ -26,6 +24,7 @@ import { mnemonicToAccount } from 'viem/accounts';
26
24
 
27
25
  import { createCompatibleClient } from './client.js';
28
26
  import { encodeArgs, parseStructString } from './encoding.js';
27
+ import { unboxContract } from './unbox.js';
29
28
  import {
30
29
  deployAztecContracts,
31
30
  getAbiFunction,
@@ -150,10 +149,10 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command {
150
149
 
151
150
  program
152
151
  .command('deploy')
153
- .description('Deploys a compiled Noir contract to Aztec.')
152
+ .description('Deploys a compiled Aztec.nr contract to Aztec.')
154
153
  .argument(
155
154
  '<abi>',
156
- "A compiled Noir contract's ABI in JSON format or name of a contract ABI exported by @aztec/noir-contracts",
155
+ "A compiled Aztec.nr contract's ABI in JSON format or name of a contract ABI exported by @aztec/noir-contracts",
157
156
  )
158
157
  .option('-a, --args <constructorArgs...>', 'Contract constructor arguments', [])
159
158
  .option('-u, --rpc-url <string>', 'URL of the Aztec RPC', AZTEC_RPC_HOST || 'http://localhost:8080')
@@ -362,7 +361,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command {
362
361
  .option('-a, --args [functionArgs...]', 'Function arguments', [])
363
362
  .requiredOption(
364
363
  '-c, --contract-abi <fileLocation>',
365
- "A compiled Noir contract's ABI in JSON format or name of a contract ABI exported by @aztec/noir-contracts",
364
+ "A compiled Aztec.nr contract's ABI in JSON format or name of a contract ABI exported by @aztec/noir-contracts",
366
365
  )
367
366
  .requiredOption('-ca, --contract-address <address>', 'Aztec address of the contract.')
368
367
  .option('-k, --private-key <string>', "The sender's private key.", PRIVATE_KEY)
@@ -387,13 +386,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command {
387
386
  const privateKey = GrumpkinScalar.fromString(stripLeadingHex(options.privateKey));
388
387
 
389
388
  const client = await createCompatibleClient(options.rpcUrl, debugLogger);
390
- const wallet = await getAccountWallets(
391
- client,
392
- SchnorrAccountContractAbi,
393
- [privateKey],
394
- [privateKey],
395
- [accountCreationSalt],
396
- );
389
+ const wallet = await getSchnorrAccount(client, privateKey, privateKey, accountCreationSalt).getWallet();
397
390
  const contract = await Contract.at(contractAddress, contractAbi, wallet);
398
391
  const tx = contract.methods[functionName](...functionArgs).send();
399
392
  await tx.wait();
@@ -414,7 +407,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command {
414
407
  .option('-a, --args [functionArgs...]', 'Function arguments', [])
415
408
  .requiredOption(
416
409
  '-c, --contract-abi <fileLocation>',
417
- "A compiled Noir contract's ABI in JSON format or name of a contract ABI exported by @aztec/noir-contracts",
410
+ "A compiled Aztec.nr contract's ABI in JSON format or name of a contract ABI exported by @aztec/noir-contracts",
418
411
  )
419
412
  .requiredOption('-ca, --contract-address <address>', 'Aztec address of the contract.')
420
413
  .option('-f, --from <string>', 'Public key of the TX viewer. If empty, will try to find account in RPC.')
@@ -446,7 +439,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command {
446
439
  .argument('<encodedString>', 'The encoded hex string')
447
440
  .requiredOption(
448
441
  '-c, --contract-abi <fileLocation>',
449
- "A compiled Noir contract's ABI in JSON format or name of a contract ABI exported by @aztec/noir-contracts",
442
+ "A compiled Aztec.nr contract's ABI in JSON format or name of a contract ABI exported by @aztec/noir-contracts",
450
443
  )
451
444
  .requiredOption('-p, --parameter <parameterName>', 'The name of the struct parameter to decode into')
452
445
  .action(async (encodedString, options) => {
@@ -482,6 +475,18 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command {
482
475
  names.forEach(name => log(name));
483
476
  });
484
477
 
478
+ program
479
+ .command('unbox')
480
+ .description(
481
+ 'Unboxes an example contract from @aztec/boxes. Also Copies `noir-libs` dependencies and setup simple frontend for the contract using its ABI.',
482
+ )
483
+ .argument('<contractName>', 'Name of the contract to unbox, e.g. "PrivateToken"')
484
+ .argument('[localDirectory]', 'Local directory to unbox to (relative or absolute), defaults to `<contractName>`')
485
+ .action(async (contractName, localDirectory) => {
486
+ const unboxTo: string = localDirectory ? localDirectory : contractName;
487
+ await unboxContract(contractName, unboxTo, version, log);
488
+ });
489
+
485
490
  program
486
491
  .command('get-node-info')
487
492
  .description('Gets the information of an aztec node at a URL.')
@@ -493,7 +498,30 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command {
493
498
  Object.entries(info).map(([key, value]) => log(`${startCase(key)}: ${value}`));
494
499
  });
495
500
 
501
+ program
502
+ .command('inspect-contract')
503
+ .description('Shows list of external callable functions for a contract')
504
+ .argument(
505
+ '<contractAbiFile>',
506
+ `A compiled Noir contract's ABI in JSON format or name of a contract ABI exported by @aztec/noir-contracts`,
507
+ )
508
+ .action(async (contractAbiFile: string) => {
509
+ const contractAbi = await getContractAbi(contractAbiFile, debugLogger);
510
+ const contractFns = contractAbi.functions.filter(
511
+ f => !f.isInternal && f.name !== 'compute_note_hash_and_nullifier',
512
+ );
513
+ if (contractFns.length === 0) {
514
+ log(`No external functions found for contract ${contractAbi.name}`);
515
+ }
516
+ for (const fn of contractFns) {
517
+ const signature = decodeFunctionSignatureWithParameterNames(fn.name, fn.parameters);
518
+ log(`${fn.functionType} ${signature}`);
519
+ }
520
+ });
521
+
496
522
  compileContract(program, 'compile', log);
523
+ generateTypescriptInterface(program, 'generate-typescript', log);
524
+ generateNoirInterface(program, 'generate-noir-interface', log);
497
525
 
498
526
  return program;
499
527
  }
package/src/unbox.ts ADDED
@@ -0,0 +1,303 @@
1
+ // inspired by https://github.com/trufflesuite/truffle/blob/develop/packages/box/lib/utils/unbox.ts
2
+ // however, their boxes are stored as standalone git repositories, while ours are subpackages in a monorepo
3
+ // so we do some hacky conversions post copy to make them work as standalone packages.
4
+ // We download the master branch of the monorepo, and then
5
+ // (1) copy "boxes/{CONTRACT_NAME}" subpackage to the specified output directory
6
+ // (2) if the box doesnt include noir source code, we copy it from the "noir-contracts" subpackage to into a new subdirectory "X/src/contracts",
7
+ // These are used by a simple frontend to interact with the contract and deploy to a local sandbox instance of aztec3.
8
+ // The unbox logic can be tested locally by running `$ts-node --esm src/bin/index.ts unbox PrivateToken`
9
+ // from `yarn-project/cli/`
10
+ import { LogFn } from '@aztec/foundation/log';
11
+
12
+ import { promises as fs } from 'fs';
13
+ import JSZip from 'jszip';
14
+ import fetch from 'node-fetch';
15
+ import * as path from 'path';
16
+
17
+ const GITHUB_OWNER = 'AztecProtocol';
18
+ const GITHUB_REPO = 'aztec-packages';
19
+ const NOIR_CONTRACTS_PATH = 'yarn-project/noir-contracts/src/contracts';
20
+ const BOXES_PATH = 'yarn-project/boxes';
21
+
22
+ /**
23
+ * Converts a contract name in "upper camel case" to a folder name in snake case.
24
+ * @param contractName - The contract name.
25
+ * @returns The folder name.
26
+ * */
27
+ function contractNameToFolder(contractName: string): string {
28
+ return contractName.replace(/[\w]([A-Z])/g, m => m[0] + '_' + m[1]).toLowerCase();
29
+ }
30
+
31
+ /**
32
+ * If the box contains the noir contract source code, we don't need to download it from github.
33
+ * Otherwise, we download the contract source code from the `noir-contracts` and `noir-libs` subpackages.
34
+ */
35
+ async function isDirectoryNonEmpty(directoryPath: string): Promise<boolean> {
36
+ const files = await fs.readdir(directoryPath);
37
+ return files.length > 0;
38
+ }
39
+
40
+ /**
41
+ *
42
+ * @param data - in memory unzipped clone of a github repo
43
+ * @param repositoryFolderPath - folder to copy from github repo
44
+ * @param localOutputPath - local path to copy to
45
+ */
46
+ async function copyFolderFromGithub(data: JSZip, repositoryFolderPath: string, localOutputPath: string, log: LogFn) {
47
+ log('downloading from github:', repositoryFolderPath);
48
+ const repositoryDirectories = Object.values(data.files).filter(file => {
49
+ return file.dir && file.name.startsWith(repositoryFolderPath);
50
+ });
51
+
52
+ for (const directory of repositoryDirectories) {
53
+ const relativePath = directory.name.replace(repositoryFolderPath, '');
54
+ const targetPath = `${localOutputPath}/${relativePath}`;
55
+ await fs.mkdir(targetPath, { recursive: true });
56
+ }
57
+
58
+ const starterFiles = Object.values(data.files).filter(file => {
59
+ return !file.dir && file.name.startsWith(repositoryFolderPath);
60
+ });
61
+
62
+ for (const file of starterFiles) {
63
+ const relativePath = file.name.replace(repositoryFolderPath, '');
64
+ const targetPath = `${localOutputPath}/${relativePath}`;
65
+ const content = await file.async('nodebuffer');
66
+ await fs.writeFile(targetPath, content);
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Not flexible at at all, but quick fix to download a noir smart contract from our
72
+ * monorepo on github. this will copy over the `yarn-projects/boxes/{contract_name}` folder
73
+ * as well as the specified `directoryPath` if the box doesn't include source code
74
+ * `directoryPath` should point to a single noir contract in `yarn-projects/noir-contracts/src/contracts/...`
75
+ * @param tagVersion - the version of the CLI that is running. we pull from the corresponding tag in the monorepo
76
+ * @param directoryPath - path to a noir contract's source code (folder) in the github repo
77
+ * @param outputPath - local path that we will copy the noir contracts and web3 starter kit to
78
+ * @returns
79
+ */
80
+ async function downloadContractAndBoxFromGithub(
81
+ tagVersion: string,
82
+ contractName: string,
83
+ outputPath: string,
84
+ log: LogFn,
85
+ ): Promise<void> {
86
+ // small string conversion, in the ABI the contract name looks like PrivateToken
87
+ // but in the repostory it looks like private_token
88
+ const snakeCaseContractName = contractNameToFolder(contractName);
89
+
90
+ log(`Downloaded '@aztex/boxes/${snakeCaseContractName}' to ${outputPath}`);
91
+ // Step 1: Fetch the monorepo ZIP from GitHub, matching the CLI version
92
+ const url = `https://github.com/${GITHUB_OWNER}/${GITHUB_REPO}/archive/refs/tags/aztec-packages-v${tagVersion}.zip`;
93
+ const response = await fetch(url);
94
+ const buffer = await response.arrayBuffer();
95
+
96
+ const zip = new JSZip();
97
+ const data = await zip.loadAsync(buffer);
98
+
99
+ // Step 2: copy the '@aztec/boxes/{contract-name}' subpackage to the output directory
100
+ // this is currently only implemented for PrivateToken under 'boxes/private-token/'
101
+ const repoDirectoryPrefix = `${GITHUB_REPO}-v${tagVersion}/`;
102
+
103
+ const boxPath = `${repoDirectoryPrefix}${BOXES_PATH}/${snakeCaseContractName}`;
104
+ await copyFolderFromGithub(data, boxPath, outputPath, log);
105
+
106
+ const boxContainsNoirSource = await isDirectoryNonEmpty(`${outputPath}/src/contracts`);
107
+ if (boxContainsNoirSource) {
108
+ return;
109
+ }
110
+ // this remaining logic only kicks in if the box doesn't already have a src/contracts folder
111
+ // in which case we optimistically grab the noir source files from the
112
+ // noir-contracts and noir-libs subpackages and pray that the versions are compatible
113
+
114
+ // source noir files for the contract are in this folder
115
+ const contractFolder = `${NOIR_CONTRACTS_PATH}/${snakeCaseContractName}_contract`;
116
+ // copy the noir contracts to the output directory under subdir /src/contracts/
117
+ const contractDirectoryPath = `${repoDirectoryPrefix}${contractFolder}/`;
118
+
119
+ const contractFiles = Object.values(data.files).filter(file => {
120
+ return !file.dir && file.name.startsWith(contractDirectoryPath);
121
+ });
122
+
123
+ const contractTargetDirectory = path.join(outputPath, 'src', 'contracts');
124
+ await fs.mkdir(contractTargetDirectory, { recursive: true });
125
+ // Nargo.toml file needs to be in the root of the contracts directory,
126
+ // and noir files in the src/ subdirectory
127
+ await fs.mkdir(path.join(contractTargetDirectory, 'src'), { recursive: true });
128
+ for (const file of contractFiles) {
129
+ const targetPath = path.join(contractTargetDirectory, file.name.replace(contractDirectoryPath, ''));
130
+ log(`Copying ${file.name} to ${targetPath}`);
131
+ const content = await file.async('nodebuffer');
132
+ await fs.writeFile(targetPath, content);
133
+ log(`Copied ${file.name} to ${targetPath}`);
134
+ }
135
+ }
136
+ /**
137
+ * Does some conversion from the package/build configurations in the monorepo to the
138
+ * something usable by the copied standalone unboxed folder. Adjusts relative paths
139
+ * and package versions.
140
+ * @param packageVersion - CLI npm version, which determines what npm version to grab
141
+ * @param outputPath - relative path where we are copying everything
142
+ * @param log - logger
143
+ */
144
+ async function updatePackagingConfigurations(packageVersion: string, outputPath: string, log: LogFn): Promise<void> {
145
+ await updatePackageJsonVersions(packageVersion, outputPath, log);
146
+ await updateTsConfig(outputPath, log);
147
+ await updateNargoToml(packageVersion, outputPath, log);
148
+ }
149
+
150
+ /**
151
+ * adjust the contract Nargo.toml file to use the same repository version as the npm packages
152
+ * @param packageVersion - CLI npm version, which determines what npm version to grab
153
+ * @param outputPath - relative path where we are copying everything
154
+ * @param log - logger
155
+ */
156
+ async function updateNargoToml(packageVersion: string, outputPath: string, log: LogFn): Promise<void> {
157
+ const nargoTomlPath = path.join(outputPath, 'src', 'contracts', 'Nargo.toml');
158
+ const fileContent = await fs.readFile(nargoTomlPath, 'utf-8');
159
+ const lines = fileContent.split('\n');
160
+ const updatedLines = lines.map(line => line.replace(/tag="master"/g, `tag="v${packageVersion}"`));
161
+ const updatedContent = updatedLines.join('\n');
162
+ await fs.writeFile(nargoTomlPath, updatedContent);
163
+ log(`Updated Nargo.toml to point to local copy of noir-libs`);
164
+ }
165
+
166
+ /**
167
+ * The `tsconfig.json` also needs to be updated to remove the "references" section, which
168
+ * points to the monorepo's subpackages. Those are unavailable in the cloned subpackage,
169
+ * so we remove the entries to install the the workspace packages from npm.
170
+ * @param outputPath - directory we are unboxing to
171
+ */
172
+ async function updateTsConfig(outputPath: string, log: LogFn) {
173
+ try {
174
+ const tsconfigJsonPath = path.join(outputPath, 'tsconfig.json');
175
+ const data = await fs.readFile(tsconfigJsonPath, 'utf8');
176
+ const config = JSON.parse(data);
177
+
178
+ delete config.references;
179
+
180
+ const updatedData = JSON.stringify(config, null, 2);
181
+ await fs.writeFile(tsconfigJsonPath, updatedData, 'utf8');
182
+
183
+ log('tsconfig.json has been updated');
184
+ } catch (error) {
185
+ log('Error updating tsconfig.json:', error);
186
+ throw error;
187
+ }
188
+ }
189
+
190
+ /**
191
+ * We pin to "workspace:^" in the package.json for subpackages, but we need to replace it with
192
+ * an the actual version number in the cloned starter kit
193
+ * We modify the copied package.json and pin to the version of the package that was downloaded
194
+ * @param packageVersion - CLI npm version, which determines what npm version to grab
195
+ * @param outputPath - directory we are unboxing to
196
+ * @param log - logger
197
+ */
198
+ async function updatePackageJsonVersions(packageVersion: string, outputPath: string, log: LogFn): Promise<void> {
199
+ const packageJsonPath = path.join(outputPath, 'package.json');
200
+ const fileContent = await fs.readFile(packageJsonPath, 'utf-8');
201
+ const packageData = JSON.parse(fileContent);
202
+
203
+ // Check and replace "workspace^" pins in dependencies, which are monorepo yarn workspace references
204
+ if (packageData.dependencies) {
205
+ for (const [key, value] of Object.entries(packageData.dependencies)) {
206
+ if (value === 'workspace:^') {
207
+ packageData.dependencies[key] = `^${packageVersion}`;
208
+ }
209
+ }
210
+ }
211
+
212
+ // Check and replace in devDependencies
213
+ if (packageData.devDependencies) {
214
+ for (const [key, value] of Object.entries(packageData.devDependencies)) {
215
+ if (value === 'workspace:^') {
216
+ // TODO: check if this right post landing. the package.json version looks like 0.1.0
217
+ // but the npm versions look like v0.1.0-alpha63 so we are not fully pinned
218
+ packageData.devDependencies[key] = `^${packageVersion}`;
219
+ }
220
+ }
221
+ }
222
+
223
+ // modify the version of the sandbox to pull - it's set to "latest" version in the monorepo,
224
+ // but we need to replace with the same tagVersion as the cli and the other aztec npm packages
225
+ // similarly, make sure we spinup the sandbox with the same version.
226
+ packageData.scripts['install:sandbox'] = packageData.scripts['install:sandbox'].replace(
227
+ 'latest',
228
+ `v${packageVersion}`,
229
+ );
230
+
231
+ packageData.scripts['start:sandbox'] = packageData.scripts['start:sandbox'].replace('latest', `v${packageVersion}`);
232
+
233
+ // Convert back to a string and write back to the package.json file
234
+ const updatedContent = JSON.stringify(packageData, null, 2);
235
+ await fs.writeFile(packageJsonPath, updatedContent);
236
+
237
+ log(`Updated package.json versions to ${packageVersion}`);
238
+ }
239
+
240
+ /**
241
+ *
242
+ * @param outputDirectoryName - user specified directory we are "unboxing" files into
243
+ * @param log - logger
244
+ * @returns
245
+ */
246
+ async function createDirectory(outputDirectoryName: string, log: LogFn): Promise<string> {
247
+ const absolutePath = path.resolve(outputDirectoryName);
248
+
249
+ try {
250
+ // Checking if the path exists and if it is a directory
251
+ const stats = await fs.stat(absolutePath);
252
+ if (!stats.isDirectory()) {
253
+ throw new Error(`The specified path ${outputDirectoryName} is not a directory/folder.`);
254
+ }
255
+ } catch (error: any) {
256
+ if (error.code === 'ENOENT') {
257
+ await fs.mkdir(absolutePath, { recursive: true });
258
+ log(`The directory did not exist and has been created: ${absolutePath}`);
259
+ } else {
260
+ throw error;
261
+ }
262
+ }
263
+
264
+ return absolutePath;
265
+ }
266
+
267
+ /**
268
+ * Unboxes a contract from `@aztec/boxes` by performing the following operations:
269
+ * 1. Copies the frontend template from `@aztec/boxes/{contract_name}` to the outputDirectory
270
+ * 2. Checks if the contract source was copied over from `@aztec/boxes/{contract_name}/src/contracts`
271
+ * 3. If not, copies the contract from the appropriate `@aztec/noir-contracts/src/contracts/...` folder.
272
+ *
273
+ * The box provides a simple React app which parses the contract ABI
274
+ * and generates a UI to deploy + interact with the contract on a local aztec testnet.
275
+ * @param contractName - name of contract from `@aztec/noir-contracts`, in a format like "PrivateToken" (rather than "private_token", as it appears in the noir-contracts repo)
276
+ * @param log - Logger instance that will output to the CLI
277
+ */
278
+ export async function unboxContract(
279
+ contractName: string,
280
+ outputDirectoryName: string,
281
+ packageVersion: string,
282
+ log: LogFn,
283
+ ) {
284
+ const contractNames = ['PrivateToken'];
285
+
286
+ if (!contractNames.includes(contractName)) {
287
+ log(
288
+ `The noir contract named "${contractName}" was not found in "@aztec/boxes" package. Valid options are:
289
+ ${contractNames.join('\n\t')}
290
+ We recommend "PrivateToken" as a default.`,
291
+ );
292
+ return;
293
+ }
294
+ const outputPath = await createDirectory(outputDirectoryName, log);
295
+
296
+ // downloads the selected contract's relevant folder in @aztec/boxes/{contract_name}
297
+ // and the noir source code from `noir-contracts` into `${outputDirectoryName}/src/contracts`
298
+ // if not present in the box
299
+ await downloadContractAndBoxFromGithub(packageVersion, contractName, outputPath, log);
300
+ // make adjustments for packaging to work as a standalone, as opposed to part of yarn workspace
301
+ // as in the monorepo source files
302
+ await updatePackagingConfigurations(packageVersion, outputPath, log);
303
+ }