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

feat: aztec-cli "unbox" command #1825

Closed
wants to merge 27 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
e306668
recopy changes
dan-aztec Aug 25, 2023
2dd2b14
also add generated TS class temporarily for dev
dan-aztec Aug 25, 2023
42e286b
update README
dan-aztec Aug 25, 2023
08e1e54
cleanup the contract form slightly
dan-aztec Aug 25, 2023
4b7ddce
sketch of deploy function
dan-aztec Aug 25, 2023
9fffad0
create rpcClient in top level component
dan-aztec Aug 25, 2023
467aa4e
remove publickey from deploy command
dan-aztec Aug 25, 2023
a60c2c5
make undefined publickey explicit, to match default aztec-cli deploy …
dan-aztec Aug 25, 2023
a4f3170
lint
dan-aztec Aug 25, 2023
666c674
stub view logic
dan-aztec Aug 26, 2023
a10bd6f
unconnected wallet selector
dan-aztec Aug 26, 2023
ec46e02
linting
dan-aztec Aug 26, 2023
9e8d693
bad CSS but less reloading of setWallet
dan-aztec Aug 26, 2023
942d4da
alvaro helped with types and wiring
dan-aztec Aug 28, 2023
26530f4
Merge branch 'master' into dan/aztec-cli-unbox-gitfix
dan-aztec Aug 31, 2023
3dd4076
Merge branch 'master' into dan/aztec-cli-unbox-gitfix
dan-aztec Sep 1, 2023
6e1f88b
fix wallet selector and default set default param for owner to ALICE
dan-aztec Sep 1, 2023
8bbcb01
still broken copying e2e
dan-aztec Sep 1, 2023
dc95b4e
revert yarn.lock
dan-aztec Sep 1, 2023
6876d26
undo old copy
dan-aztec Sep 1, 2023
66a0403
fix: polyfill by bundling fileURLToPath
ludamad Sep 1, 2023
d17b6a8
fix: workspaceify starter-kit
ludamad Sep 1, 2023
e8b0753
update sandbox link
dan-aztec Sep 5, 2023
eac5f06
Revert "fix: polyfill by bundling fileURLToPath"
dan-aztec Sep 5, 2023
6f4dce0
revert polyfill and log
dan-aztec Sep 5, 2023
7dbccd3
Merge branch 'master' into dan/aztec-cli-unbox-gitfix
dan-aztec Sep 5, 2023
a41e1c9
remove package-lock.json
dan-aztec Sep 5, 2023
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 bootstrap.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/bin/bash
#!/bin/zsh
set -eu

export CLEAN=${1:-}
Expand Down Expand Up @@ -31,7 +31,7 @@ git submodule update --init --recursive

if [ ! -f ~/.nvm/nvm.sh ]; then
echo "Nvm not found at ~/.nvm"
exit 1
# exit 1
fi

circuits/cpp/bootstrap.sh
Expand Down
2 changes: 2 additions & 0 deletions yarn-project/aztec-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@
"@aztec/noir-contracts": "workspace:^",
"@aztec/types": "workspace:^",
"commander": "^9.0.0",
"jszip": "^3.10.1",
"lodash.startcase": "^4.4.0",
"node-fetch": "^3.3.2",
"semver": "^7.5.4",
"tslib": "^2.4.0",
"viem": "^1.2.5"
Expand Down
12 changes: 12 additions & 0 deletions yarn-project/aztec-cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
getExampleContractArtifacts,
getTxSender,
prepTx,
unboxContract,
} from './utils.js';

export { cliTestSuite } from './test/cli_test_suite.js';
Expand Down Expand Up @@ -481,6 +482,17 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command {
});

program
.command('unbox')
.description(
'Unboxes an example contract from @aztec/noir-contracts. Copies `noir-libs` dependencies and setup simple frontend for the contract based on the ABI.',
)
.argument('<contractName>', 'Name of the contract to unbox, e.g. "PrivateToken"')
.argument('[localDirectory]', 'name of the local directory to unbox to, defaults to `starter-kit`')
.action(async (contractName, localDirectory) => {
const unboxTo: string = localDirectory ? localDirectory : 'starter-kit';
await unboxContract(contractName, unboxTo, log);

program
.command('get-node-info')
.description('Gets the information of an aztec node at a URL.')
.requiredOption('-u, --rpc-url <string>', 'URL of the Aztec RPC', AZTEC_RPC_HOST || 'http://localhost:8080')
Expand Down
229 changes: 229 additions & 0 deletions yarn-project/aztec-cli/src/unbox.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
// heavily inspired by https://github.com/trufflesuite/truffle/blob/develop/packages/box/lib/utils/unbox.ts
// We download the master branch of the monorepo, and then
// (1) copy "starter-kit" subpackage to the current working directory, into a new subdirectory "starter-kit".
// (2) copy the specified contract from the "noir-contracts" subpackage to into a new subdirectory "starter-kit/noir-contracts",
// (3) copy the specified contract's ABI into the "starter-kit/noir-contracts" subdirectory.
// These will be used by a simple frontend to interact with the contract and deploy to a local sandbox instance of aztec3.
import { LogFn } from '@aztec/foundation/log';

import { promises as fs } from 'fs';
import JSZip from 'jszip';
import fetch from 'node-fetch';
import * as path from 'path';

const GITHUB_OWNER = 'AztecProtocol';
const GITHUB_REPO = 'aztec-packages';
const NOIR_CONTRACTS_PATH = 'yarn-project/noir-contracts/src/contracts';
// const STARTER_KIT_PATH = 'yarn-project/starter-kit';
// before this commit lands, we can't grab from github, so can test with another subpackage like this
const STARTER_KIT_PATH = 'yarn-project/aztec.js';
// for now we just copy the entire noir-libs subpackage, but this should be unnecessary
// when Nargo.toml [requirements] section supports a github URL in addition to relative paths
const NOIR_LIBS_PATH = 'yarn-project/noir-libs';

/**
* Converts a contract name in "upper camel case" to a folder name in snake case.
* @param contractName - The contract name.
* @returns The folder name.
* */
export function contractNameToFolder(contractName: string): string {
return contractName.replace(/[\w]([A-Z])/g, m => m[0] + '_' + m[1]).toLowerCase();
}

/**
*
* @param contractName - The contract name, in upper camel case.
* @param outputPath - The output path, by default this is the current working directory
* @returns The path to the downloaded contract.
*/
export async function downloadContractAndStarterKitFromGithub(
contractName: string = 'PrivateToken',
outputPath: string,
log: LogFn,
): Promise<void> {
// small string conversion, in the ABI the contract name looks like PrivateToken
// but in the repostory it looks like private_token
const contractFolder = `${NOIR_CONTRACTS_PATH}/${contractNameToFolder(contractName)}_contract`;
return await _downloadNoirFilesFromGithub(contractFolder, outputPath, log);
}

/**
*
* @param data - in memory unzipped clone of a github repo
* @param repositoryFolderPath - folder to copy from github repo
* @param localOutputPath - local path to copy to
*/
async function _copyFolderFromGithub(data: JSZip, repositoryFolderPath: string, localOutputPath: string, log: LogFn) {
const repositoryDirectories = Object.values(data.files).filter(file => {
return file.dir && file.name.startsWith(repositoryFolderPath);
});

for (const directory of repositoryDirectories) {
const relativePath = directory.name.replace(repositoryFolderPath, '');
const targetPath = `${localOutputPath}/${relativePath}`;
await fs.mkdir(targetPath, { recursive: true });
}
log(
'copying directories ',
repositoryDirectories.map(dir => dir.name),
);

const starterFiles = Object.values(data.files).filter(file => {
return !file.dir && file.name.startsWith(repositoryFolderPath);
});
// log('copying repository files', starterFiles);

for (const file of starterFiles) {
const relativePath = file.name.replace(repositoryFolderPath, '');
const targetPath = `${localOutputPath}/${relativePath}`;
const content = await file.async('nodebuffer');
await fs.writeFile(targetPath, content);
}
}

/**
* Not flexible at at all, but quick fix to download a noir smart contract from our
* monorepo on github. this will copy over the `yarn-projects/starter-kit` folder in its entirey
* as well as the specfieid directoryPath, which should point to a single noir contract in
* `yarn-projects/noir-contracts/src/contracts/...`
* @param directoryPath - path to the directory in the github repo
* @param outputPath - local path that we will copy the noir contracts and web3 starter kit to
* @returns
*/
async function _downloadNoirFilesFromGithub(directoryPath: string, outputPath: string, log: LogFn): Promise<void> {
const owner = GITHUB_OWNER;
const repo = GITHUB_REPO;
// Step 1: Fetch the ZIP from GitHub, hardcoded to the master branch
const url = `https://github.com/${owner}/${repo}/archive/refs/heads/master.zip`;
const response = await fetch(url);
const buffer = await response.arrayBuffer();

const zip = new JSZip();
const data = await zip.loadAsync(buffer);

// Step 2: copy the '@aztec/starter-kit' subpackage to the output directory
const repoDirectoryPrefix = `${repo}-master/`;

const starterKitPath = `${repoDirectoryPrefix}${STARTER_KIT_PATH}/`;
await _copyFolderFromGithub(data, starterKitPath, outputPath, log);

// TEMPORARY FIX - we also need the `noir-libs` subpackage, which needs to be referenced by
// a relative path in the Nargo.toml file. Copy those over as well.
const noirLibsPath = `${repoDirectoryPrefix}${NOIR_LIBS_PATH}/`;
await _copyFolderFromGithub(data, noirLibsPath, path.join(outputPath, 'src', 'contracts'), log);

// Step 3: copy the noir contracts to the output directory under subdir /src/contracts/
const contractDirectoryPath = `${repoDirectoryPrefix}${directoryPath}/`;

const contractFiles = Object.values(data.files).filter(file => {
return !file.dir && file.name.startsWith(contractDirectoryPath);
});

const contractTargetDirectory = path.join(outputPath, 'src', 'contracts');
await fs.mkdir(contractTargetDirectory, { recursive: true });
// Nargo.toml file needs to be in the root of the contracts directory,
// and noir files in the src/ subdirectory
await fs.mkdir(path.join(contractTargetDirectory, 'src'), { recursive: true });
for (const file of contractFiles) {
const targetPath = path.join(contractTargetDirectory, file.name.replace(contractDirectoryPath, ''));
log(`Copying ${file.name} to ${targetPath}`);

const content = await file.async('nodebuffer');
await fs.writeFile(targetPath, content);
log(`Copied ${file.name} to ${targetPath}`);
}
}

/**
* Sets up the .env file for the starter-kit cloned from the monorepo.
* for standalone front end only react app, only vars prefixed with REACT_APP_ will be available.
* @param outputPath - path to newly created directory
* @param contractAbiFileName - copied as-is from the noir-contracts artifacts
*/
export async function createEnvFile(outputPath: string, contractName: string) {
const envData = {
VITE_CONTRACT_ABI_FILE_NAME: `${contractName}.json`, // copied over by `unbox` command
VITE_CONTRACT_TYPESCRIPT_FILE_NAME: `${contractName}.ts`, // this is generated later by compile command
VITE_SANDBOX_RPC_URL: 'http://localhost:8080', // sandbox default and the `aztec-cli deploy` command default
// two accounts included in the sandbox
ALICE: '0x2e13f0201905944184fc2c09d29fcf0cac07647be171656a275f63d99b819360',
VITE_WALLET_ADDRESS: '0x2e13f0201905944184fc2c09d29fcf0cac07647be171656a275f63d99b819360',
BOB: '0x0d557417a3ce7d7b356a8f15d79a868fd8da2af9c5f4981feb9bcf0b614bd17e',
// hardcoded contract address for the PrivateToken contract in the sandbox,
// needs user update if they deploy something else or change the args
VITE_CONTRACT_ADDRESS: '0x03b030d48607ba8a0562f0f1f82be26c3f091e45e10f74c2d8cebb80d526a69f',
};
const content = Object.entries(envData)
.map(([key, value]) => `${key}=${value}`)
.join('\n');

const envFilePath = path.join(outputPath, '.env'); // Adjust the path as necessary
await fs.writeFile(envFilePath, content);
}

/**
* quick hack to adjust the copied contract Nargo.toml file to point to the location
* of noir-libs in the newly created/copied starter-kit directory
* @param outputPath - relative path where we are copying everything
*/
export async function updateNargoToml(outputPath: string, log: LogFn): Promise<void> {
const nargoTomlPath = path.join(outputPath, 'src', 'contracts', 'Nargo.toml');
const fileContent = await fs.readFile(nargoTomlPath, 'utf-8');
const lines = fileContent.split('\n');
const newLines = lines
.filter(line => line.startsWith('#'))
.map(line => {
if (line.startsWith('aztec')) {
return `aztec = { path = "./noir-aztec" }`;
}
if (line.startsWith('value_note')) {
return `value_note = { path = "./value-note" }`;
}
if (line.startsWith('easy_private_state')) {
return `easy_private_state = { path = "./easy-private-state" }`;
}
return line;
});
const updatedContent = newLines.join('\n');
await fs.writeFile(nargoTomlPath, updatedContent);
log(`Updated Nargo.toml to point to local copy of noir-libs`);
}

/**
* We pin to "workspace:^" in the package.json for subpackages, but we need to replace it with
* an the actual version number in the cloned starter kit
* We modify the copied package.json and pin to the version of the package that was downloaded
* @param outputPath - directory we are unboxing to
* @param log - logger
*/
export async function updatePackageJsonVersions(outputPath: string, log: LogFn): Promise<void> {
const packageJsonPath = path.join(outputPath, 'package.json');
const fileContent = await fs.readFile(packageJsonPath, 'utf-8');
const packageData = JSON.parse(fileContent);

// Check and replace in dependencies
if (packageData.dependencies) {
for (const [key, value] of Object.entries(packageData.dependencies)) {
if (value === 'workspace:^') {
packageData.dependencies[key] = `^${packageData.version}`;
}
}
}

// Check and replace in devDependencies
if (packageData.devDependencies) {
for (const [key, value] of Object.entries(packageData.devDependencies)) {
if (value === 'workspace:^') {
packageData.devDependencies[key] = `^${packageData.version}`;
}
}
}

// Convert the modified object back to a string
const updatedContent = JSON.stringify(packageData, null, 2);

// Write the modified content back to the package.json file
await fs.writeFile(packageJsonPath, updatedContent);

log(`Updated package.json versions to ${packageData.version}`);
}
55 changes: 55 additions & 0 deletions yarn-project/aztec-cli/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,16 @@ import { ContractAbi } from '@aztec/foundation/abi';
import { DebugLogger, LogFn } from '@aztec/foundation/log';

import fs from 'fs';
import * as path from 'path';
import { mnemonicToAccount, privateKeyToAccount } from 'viem/accounts';

import { encodeArgs } from './encoding.js';
import {
createEnvFile,
downloadContractAndStarterKitFromGithub,
updateNargoToml,
updatePackageJsonVersions,
} from './unbox.js';

export { createClient } from './client.js';
/**
Expand Down Expand Up @@ -141,3 +148,51 @@ export async function prepTx(

return { contractAddress, functionArgs, contractAbi };
}

/**
* Unboxes a contract from `@aztec/noir-contracts` and generates a simple frontend.
* Performs the following operations in order:
* 1. Checks if the contract exists in `@aztec/noir-contracts`
* 2. Copies the contract from the `@aztec/noir-contracts` to the current working directory under "starter-kit/"
* This is done via brute force search of the master branch of the monorepo, within the yarn-projects/noir-contracts folder.
* 3. Copies the frontend template from `@aztec/starter-kit` to the current working directory under "starter-kit"
*
* These will be used by a simple next.js/React app in `@aztec/starter-kit` which parses the contract ABI
* and generates a UI to deploy + interact with the contract on a local aztec testnet.
* @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)
* @param log - Logger instance that will output to the CLI
* TODO: add the jest tests
*/
export async function unboxContract(contractName: string, outputDirectoryName: string, log: LogFn) {
const contracts = await import('@aztec/noir-contracts/artifacts');

const contractNames = Object.values(contracts).map(contract => contract.name);
if (!contractNames.includes(contractName)) {
log(
`The noir contract named "${contractName}" was not found in "@aztec/noir-contracts" package. Valid options are:
${contractNames.join('\n\t')}
We recommend "PrivateToken" as a default.`,
);
return;
}
// downloads the selected contract's noir source code into `${outputDirectoryName}/src/contracts`,
// along with the @aztec/starter-kit and @aztec/noir-libs
const starterKitPath = path.join(process.cwd(), outputDirectoryName);
await downloadContractAndStarterKitFromGithub(contractName, starterKitPath, log);
log(`Downloaded 'starter-kit' and ${contractName} from @aztec/noir-contracts. to ${starterKitPath}`);

const chosenContractAbi = Object.values(contracts).filter(contract => contract.name === contractName)[0];
const contractAbiOutputPath = path.join(starterKitPath, 'src', 'artifacts');
fs.mkdir(contractAbiOutputPath, { recursive: true }, err => {
log('error creating artifacts directory', err);
});

const contractAbiFileName = `${contractName}.json`;
fs.writeFileSync(path.join(contractAbiOutputPath, contractAbiFileName), JSON.stringify(chosenContractAbi, null, 4));
log(`copied contract ABI to ${contractAbiOutputPath}`);

await createEnvFile(starterKitPath, contractName);

await updateNargoToml(outputDirectoryName, log);
await updatePackageJsonVersions(outputDirectoryName, log);
}
4 changes: 2 additions & 2 deletions yarn-project/bootstrap.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/bin/bash
#!/bin/zsh
set -eu

# Navigate to script folder
Expand All @@ -10,7 +10,7 @@ if [ "$(uname)" = "Darwin" ]; then
else
\. ~/.nvm/nvm.sh
fi
nvm install
# nvm install

yarn install --immutable

Expand Down
1 change: 1 addition & 0 deletions yarn-project/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"rollup-provider",
"aztec-node",
"sequencer-client",
"starter-kit",
"types",
"world-state",
"yarn-project-base"
Expand Down
11 changes: 11 additions & 0 deletions yarn-project/starter-kit/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:react-hooks/recommended'],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
},
};
Loading