From 01513a9dbc3679f01536442bf1295a419f2c7140 Mon Sep 17 00:00:00 2001 From: Dan Lee Date: Mon, 18 Sep 2023 18:51:54 -0700 Subject: [PATCH 01/15] copy a test contract with same webapp --- yarn-project/boxes/blank/.eslintrc.cjs | 61 +++++++ yarn-project/boxes/blank/.gitignore | 24 +++ yarn-project/boxes/blank/.prettierrc.json | 6 + yarn-project/boxes/blank/README.md | 95 ++++++++++ yarn-project/boxes/blank/package.json | 101 +++++++++++ .../boxes/blank/src/@types/index.d.ts | 4 + .../contract_function_form.module.scss | 67 ++++++++ .../app/components/contract_function_form.tsx | 162 ++++++++++++++++++ .../blank/src/app/components/copy.module.scss | 6 + .../boxes/blank/src/app/components/copy.tsx | 28 +++ .../src/app/components/dropdown.module.scss | 68 ++++++++ .../blank/src/app/components/dropdown.tsx | 84 +++++++++ .../boxes/blank/src/app/components/index.ts | 2 + .../src/app/components/popup.module.scss | 27 +++ .../boxes/blank/src/app/components/popup.tsx | 35 ++++ .../src/app/components/select.module.scss | 65 +++++++ .../boxes/blank/src/app/components/select.tsx | 82 +++++++++ .../boxes/blank/src/app/components/terms.tsx | 10 ++ .../components/wallet_dropdown.module.scss | 9 + .../src/app/components/wallet_dropdown.tsx | 60 +++++++ .../boxes/blank/src/app/contract.module.scss | 53 ++++++ yarn-project/boxes/blank/src/app/contract.tsx | 153 +++++++++++++++++ .../boxes/blank/src/app/home.module.scss | 28 +++ yarn-project/boxes/blank/src/app/home.tsx | 91 ++++++++++ yarn-project/boxes/blank/src/app/index.css | 86 ++++++++++ yarn-project/boxes/blank/src/app/index.html | 13 ++ yarn-project/boxes/blank/src/app/index.tsx | 11 ++ .../boxes/blank/src/artifacts/test.ts | 80 +++++++++ .../blank/src/artifacts/test_contract.json | 51 ++++++ yarn-project/boxes/blank/src/config.ts | 16 ++ .../boxes/blank/src/contracts/Nargo.toml | 8 + .../blank/src/contracts/src/interface.nr | 51 ++++++ .../boxes/blank/src/contracts/src/main.nr | 20 +++ .../src/scripts/call_contract_function.ts | 30 ++++ .../blank/src/scripts/deploy_contract.ts | 22 +++ yarn-project/boxes/blank/src/scripts/index.ts | 4 + yarn-project/boxes/blank/src/scripts/util.ts | 37 ++++ .../src/scripts/view_contract_function.ts | 20 +++ .../src/tests/blank_contract.frontend.test.ts | 143 ++++++++++++++++ yarn-project/boxes/blank/src/typings.d.ts | 4 + yarn-project/boxes/blank/webpack.config.js | 119 +++++++++++++ 41 files changed, 2036 insertions(+) create mode 100644 yarn-project/boxes/blank/.eslintrc.cjs create mode 100644 yarn-project/boxes/blank/.gitignore create mode 100644 yarn-project/boxes/blank/.prettierrc.json create mode 100644 yarn-project/boxes/blank/README.md create mode 100644 yarn-project/boxes/blank/package.json create mode 100644 yarn-project/boxes/blank/src/@types/index.d.ts create mode 100644 yarn-project/boxes/blank/src/app/components/contract_function_form.module.scss create mode 100644 yarn-project/boxes/blank/src/app/components/contract_function_form.tsx create mode 100644 yarn-project/boxes/blank/src/app/components/copy.module.scss create mode 100644 yarn-project/boxes/blank/src/app/components/copy.tsx create mode 100644 yarn-project/boxes/blank/src/app/components/dropdown.module.scss create mode 100644 yarn-project/boxes/blank/src/app/components/dropdown.tsx create mode 100644 yarn-project/boxes/blank/src/app/components/index.ts create mode 100644 yarn-project/boxes/blank/src/app/components/popup.module.scss create mode 100644 yarn-project/boxes/blank/src/app/components/popup.tsx create mode 100644 yarn-project/boxes/blank/src/app/components/select.module.scss create mode 100644 yarn-project/boxes/blank/src/app/components/select.tsx create mode 100644 yarn-project/boxes/blank/src/app/components/terms.tsx create mode 100644 yarn-project/boxes/blank/src/app/components/wallet_dropdown.module.scss create mode 100644 yarn-project/boxes/blank/src/app/components/wallet_dropdown.tsx create mode 100644 yarn-project/boxes/blank/src/app/contract.module.scss create mode 100644 yarn-project/boxes/blank/src/app/contract.tsx create mode 100644 yarn-project/boxes/blank/src/app/home.module.scss create mode 100644 yarn-project/boxes/blank/src/app/home.tsx create mode 100644 yarn-project/boxes/blank/src/app/index.css create mode 100644 yarn-project/boxes/blank/src/app/index.html create mode 100644 yarn-project/boxes/blank/src/app/index.tsx create mode 100644 yarn-project/boxes/blank/src/artifacts/test.ts create mode 100644 yarn-project/boxes/blank/src/artifacts/test_contract.json create mode 100644 yarn-project/boxes/blank/src/config.ts create mode 100644 yarn-project/boxes/blank/src/contracts/Nargo.toml create mode 100644 yarn-project/boxes/blank/src/contracts/src/interface.nr create mode 100644 yarn-project/boxes/blank/src/contracts/src/main.nr create mode 100644 yarn-project/boxes/blank/src/scripts/call_contract_function.ts create mode 100644 yarn-project/boxes/blank/src/scripts/deploy_contract.ts create mode 100644 yarn-project/boxes/blank/src/scripts/index.ts create mode 100644 yarn-project/boxes/blank/src/scripts/util.ts create mode 100644 yarn-project/boxes/blank/src/scripts/view_contract_function.ts create mode 100644 yarn-project/boxes/blank/src/tests/blank_contract.frontend.test.ts create mode 100644 yarn-project/boxes/blank/src/typings.d.ts create mode 100644 yarn-project/boxes/blank/webpack.config.js diff --git a/yarn-project/boxes/blank/.eslintrc.cjs b/yarn-project/boxes/blank/.eslintrc.cjs new file mode 100644 index 00000000000..93359038995 --- /dev/null +++ b/yarn-project/boxes/blank/.eslintrc.cjs @@ -0,0 +1,61 @@ +module.exports = { + root: true, + env: { browser: true, es2020: true }, + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:react-hooks/recommended', + 'plugin:import/recommended', + 'plugin:import/typescript', + 'prettier', + ], + settings: { + 'import/resolver': { + typescript: true, + node: true, + }, + }, + ignorePatterns: ['dist', '.eslintrc.cjs'], + parser: '@typescript-eslint/parser', + plugins: ['react-refresh'], + overrides: [ + { + files: ['*.ts', '*.tsx'], + parserOptions: { + // hacky workaround for CI not having the same tsconfig setup + project: true, + }, + }, + ], + rules: { + 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-non-null-assertion': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-empty-function': 'off', + '@typescript-eslint/await-thenable': 'error', + '@typescript-eslint/no-floating-promises': 2, + '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }], + 'require-await': 2, + 'no-console': 'warn', + 'no-constant-condition': 'off', + camelcase: 2, + 'no-restricted-imports': [ + 'error', + { + patterns: [ + { + group: ['client-dest'], + message: "Fix this absolute garbage import. It's your duty to solve it before it spreads.", + }, + { + group: ['dest'], + message: 'You should not be importing from a build directory. Did you accidentally do a relative import?', + }, + ], + }, + ], + 'import/no-unresolved': 'error', + 'import/no-extraneous-dependencies': 'error', + }, +}; diff --git a/yarn-project/boxes/blank/.gitignore b/yarn-project/boxes/blank/.gitignore new file mode 100644 index 00000000000..a547bf36d8d --- /dev/null +++ b/yarn-project/boxes/blank/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/yarn-project/boxes/blank/.prettierrc.json b/yarn-project/boxes/blank/.prettierrc.json new file mode 100644 index 00000000000..7c3bbec6848 --- /dev/null +++ b/yarn-project/boxes/blank/.prettierrc.json @@ -0,0 +1,6 @@ +{ + "singleQuote": true, + "trailingComma": "all", + "printWidth": 120, + "arrowParens": "avoid" +} diff --git a/yarn-project/boxes/blank/README.md b/yarn-project/boxes/blank/README.md new file mode 100644 index 00000000000..272fc2cdfbc --- /dev/null +++ b/yarn-project/boxes/blank/README.md @@ -0,0 +1,95 @@ +This is a minimal [Aztec](https://aztec.network/) Noir smart contract and frontend bootstrapped with [`aztec-cli unbox`](https://github.com/AztecProtocol/aztec-packages/tree/master/yarn-project/cli). It is recommended you use the `aztec-cli unbox PrivateToken` command so that the repository is copied with needed modifications from the monorepo subpackage. + +Some contract specific settings for `PrivateToken` are in a [config](src/config.ts) will require manual updates depending on your changes to the source code. `aztec-cli` can be installed with `npm i -g @aztec/cli`, if you don't have it already. + +## Setup + +Dependencies can be installed from the root of the package: + +```bash +yarn +yarn install:noir +yarn install:sandbox +yarn build +``` + +In addition to the usual javascript dependencies, this project requires `nargo` (package manager) and `noir` (Aztec ZK smart contract language) in addition to `@aztec/aztec-cli`. + +The former are installed within `yarn install:noir` which executes + +```bash +curl -L https://raw.githubusercontent.com/noir-lang/noirup/main/install | bash + +noirup -v aztec +``` + +This sandbox requires [Docker](https://www.docker.com/) to be installed _and running_ locally. In the event the image needs updating, you can run `yarn install:sandbox` which executes + +```bash +docker pull aztecprotocol/aztec-sandbox:latest +``` + +## Getting started + +After `yarn build` has run,`yarn start:sandbox` in one terminal will launch a local instance of the Aztec sandbox via Docker Compose and `yarn start:dev` will launch a frontend app for deploying and interacting with the PrivateToken contract. + +At this point, [http://localhost:5173](http://localhost:5173) should provide a minimal smart contract frontend. + +This folder should have the following directory structure: + +``` +|— README.md +|— package.json +|— src + |-config.ts - PrivateToken specific configuration for the frontend. + | You may need to update this if you modify the contract functions. + |— app + |— [frontend React .tsx code files] + |- scripts + |- [helpers for frontend to interact with contract on the sandbox] + |— contracts + |— src + | The Noir smart contract source files are here. + |— main.nr - the cloned noir contract, your starting point + |- interface.nr - autogenerated from main.nr when you compile + |— Nargo.toml [Noir build file, includes Aztec smart contract dependencies] + |— artifacts + | These are both generated from `contracts/` by the compile command + |— private_token_contract.json + |— private_token.ts + |— tests + | A simple end2end test deploying and testing the PrivateToken on a local sandbox + | using the front end helper methods in app/scripts/ + | The test requires the sandbox and anvil to be running (yarn start:sandbox). + |- privatetoken.test.ts +``` + +Most relevant to you is likely `src/contracts/main.nr` (and the build config `src/contracts/Nargo.toml`). This contains the example PrivateToken logic that the frontend interacts with and is a good place to start writing Noir. + +The `src/artifacts` folder can be re-generated from the command line with `yarn compile` which is an alias for + +```bash +aztec-cli compile src/contracts --outdir ../artifacts --typescript ../artifacts +``` + +This will generate a [Contract ABI](https://www.alchemy.com/overviews/what-is-an-abi-of-a-smart-contract-examples-and-usage) and TypeScript class for the Aztec smart contract in `src/contracts/main.nr`, which the frontend uses to generate the UI. + +Note: the `compile` command seems to generate a Typescript file which needs a single change - + +``` +import PrivateTokenContractAbiJson from 'PrivateToken.json' assert { type: 'json' }; +// need to update the relative import to +import PrivateTokenContractAbiJson from './PrivateToken.json' assert { type: 'json' }; +``` + +After compiling, you can re-deploy the upated noir smart contract from the web UI. The function interaction forms are generated from parsing the ContractABI, so they should update automatically after you recompile. + +## Learn More + +To learn more about Noir Smart Contract development, take a look at the following resources: + +- [Awesome Noir](https://github.com/noir-lang/awesome-noir) - learn about the Noir programming language. + +## Deploy on Aztec3 + +Coming Soon :) diff --git a/yarn-project/boxes/blank/package.json b/yarn-project/boxes/blank/package.json new file mode 100644 index 00000000000..9a2b3c1d65b --- /dev/null +++ b/yarn-project/boxes/blank/package.json @@ -0,0 +1,101 @@ +{ + "name": "blank-contract", + "private": true, + "version": "0.1.0", + "type": "module", + "main": "./dest/index.js", + "scripts": { + "build": "yarn clean && webpack", + "install:noir": "curl -L https://raw.githubusercontent.com/noir-lang/noirup/main/install | bash noirup -v aztec", + "install:sandbox": "docker pull aztecprotocol/aztec-sandbox:latest", + "clean": "rm -rf ./dest .tsbuildinfo", + "start": "serve -p 3000 ./dest", + "start:dev": "webpack serve --mode=development", + "start:sandbox": "SANDBOX_VERSION=latest /bin/bash -c \"$(curl -fsSL 'https://sandbox.aztec.network')\" ", + "formatting": "prettier --check ./src && eslint ./src", + "formatting:fix": "prettier -w ./src", + "compile": "aztec-cli compile src/contracts --outdir ../artifacts --typescript ../artifacts", + "test": "NODE_NO_WARNINGS=1 node --experimental-vm-modules $(yarn bin jest) --runInBand", + "test:integration": "concurrently -k -s first -c reset,dim -n test,anvil \"yarn test\" \"anvil\"" + }, + "jest": { + "preset": "ts-jest/presets/default-esm", + "globals": { + "ts-jest": { + "useESM": true + } + }, + "transform": { + "^.+\\.(ts|tsx)$": "ts-jest" + }, + "moduleNameMapper": { + "^(\\.{1,2}/.*)\\.js$": "$1" + }, + "testRegex": "./src/.*\\.test\\.ts$", + "rootDir": "./src" + }, + "dependencies": { + "@aztec/aztec-ui": "^0.1.14", + "@aztec/aztec.js": "workspace:^", + "@aztec/circuits.js": "workspace:^", + "@aztec/cli": "workspace:^", + "@aztec/foundation": "workspace:^", + "classnames": "^2.3.2", + "formik": "^2.4.3", + "node-sass": "^9.0.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "sass-loader": "^13.3.2", + "serve": "^14.2.1", + "yup": "^1.2.0" + }, + "devDependencies": { + "@types/node": "^20.5.9", + "@types/react": "^18.2.15", + "@types/react-dom": "^18.2.7", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", + "autoprefixer": "^10.4.15", + "copy-webpack-plugin": "^11.0.0", + "css-loader": "^6.8.1", + "eslint": "^8.45.0", + "eslint-import-resolver-typescript": "^3.5.5", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.3", + "jest": "^29.6.4", + "postcss": "^8.4.29", + "postcss-loader": "^7.3.3", + "prettier": "^3.0.3", + "resolve-typescript-plugin": "^2.0.1", + "stream-browserify": "^3.0.0", + "style-loader": "^3.3.3", + "ts-jest": "^29.1.0", + "ts-loader": "^9.4.4", + "ts-node": "^10.9.1", + "tty-browserify": "^0.0.1", + "typescript": "^5.0.4", + "util": "^0.12.5", + "webpack": "^5.88.2", + "webpack-cli": "^5.1.4", + "webpack-dev-server": "^4.15.1" + }, + "browserslist": { + "production": [ + ">0.5%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "files": [ + "dest", + "src", + "!*.test.*" + ], + "types": "./dest/index.d.ts" +} diff --git a/yarn-project/boxes/blank/src/@types/index.d.ts b/yarn-project/boxes/blank/src/@types/index.d.ts new file mode 100644 index 00000000000..091d25e2101 --- /dev/null +++ b/yarn-project/boxes/blank/src/@types/index.d.ts @@ -0,0 +1,4 @@ +declare module '*.svg' { + const content: any; + export default content; +} diff --git a/yarn-project/boxes/blank/src/app/components/contract_function_form.module.scss b/yarn-project/boxes/blank/src/app/components/contract_function_form.module.scss new file mode 100644 index 00000000000..f28047b581a --- /dev/null +++ b/yarn-project/boxes/blank/src/app/components/contract_function_form.module.scss @@ -0,0 +1,67 @@ +.input { + border: none; + outline-width: 0; + outline-color: rgba(0, 0, 0, 0); + padding: 2px 20px 0 20px; + width: 100%; + height: 45px; + color: #000; + border: 1px solid rgba(0, 0, 0, 0); + font-size: 16px; + text-align: left; + font-weight: 400; + border-radius: 10px; + text-align: left; + text-overflow: ellipsis; + transition: box-shadow .2s; + box-shadow: 0px 4px 10px rgba(0, 0, 0, .1); + background-color: white; + -webkit-appearance: none; + + + &:disabled { + color: #4a4a4a; + background-color: rgba(239, 239, 239, 0.3); + background: radial-gradient(rgba(239, 239, 239, 0.3), rgba(239, 239, 239, 0.3)); + -webkit-text-fill-color: #4a4a4a; + cursor: not-allowed; + } +} + +.label { + font-weight: 450; + font-size: 18px; + display: flex; + width: 100%; + flex-direction: column; + text-align: left; + margin-bottom: 15px; + justify-content: space-between; +} + +.inputWrapper { + width: 100%; + display: flex; + gap: 15px; +} + +.field { + display: flex; + justify-content: start; + flex-direction: column; + align-items: flex-start; +} + +.content { + display: flex; + justify-content: space-between; + flex-direction: column; + margin: 30px; + width: 450px; + gap: 30px; +} + +.actionButton { + width: 100%; + align-self: center; +} \ No newline at end of file diff --git a/yarn-project/boxes/blank/src/app/components/contract_function_form.tsx b/yarn-project/boxes/blank/src/app/components/contract_function_form.tsx new file mode 100644 index 00000000000..6daff23f4ac --- /dev/null +++ b/yarn-project/boxes/blank/src/app/components/contract_function_form.tsx @@ -0,0 +1,162 @@ +import { Button, Card, CardTheme, Loader } from '@aztec/aztec-ui'; +import { AztecAddress, CompleteAddress, Fr } from '@aztec/aztec.js'; +import { ContractAbi, FunctionAbi } from '@aztec/foundation/abi'; +import { useFormik } from 'formik'; +import * as Yup from 'yup'; +import { CONTRACT_ADDRESS_PARAM_NAMES, DEFAULT_PUBLIC_ADDRESS, rpcClient } from '../../config.js'; +import { callContractFunction, deployContract, viewContractFunction } from '../../scripts/index.js'; +import { convertArgs } from '../../scripts/util.js'; +import styles from './contract_function_form.module.scss'; + +type NoirFunctionYupSchema = { + // hack: add `any` at the end to get the array schema to typecheck + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [key: string]: Yup.NumberSchema | Yup.ArraySchema | Yup.BooleanSchema | any; +}; + +type NoirFunctionFormValues = { + [key: string]: string | number | number[] | boolean; +}; + +function generateYupSchema(functionAbi: FunctionAbi) { + const parameterSchema: NoirFunctionYupSchema = {}; + const initialValues: NoirFunctionFormValues = {}; + for (const param of functionAbi.parameters) { + if (CONTRACT_ADDRESS_PARAM_NAMES.includes(param.name)) { + // these are hex strings instead, but yup doesn't support bigint so we convert back to bigint on execution + parameterSchema[param.name] = Yup.string().required(); + initialValues[param.name] = DEFAULT_PUBLIC_ADDRESS; + continue; + } + switch (param.type.kind) { + case 'field': + parameterSchema[param.name] = Yup.number().required(); + initialValues[param.name] = 100; + break; + // not really needed for private token, since we hide the nullifier helper method which has the array input + case 'array': + // eslint-disable-next-line no-case-declarations + const arrayLength = param.type.length; + parameterSchema[param.name] = Yup.array() + .of(Yup.number()) + .min(arrayLength) + .max(arrayLength) + .transform(function (value: number[], originalValue: string) { + if (typeof originalValue === 'string') { + return originalValue.split(',').map(Number); + } + return value; + }); + initialValues[param.name] = Array(arrayLength).fill( + CONTRACT_ADDRESS_PARAM_NAMES.includes(param.name) ? DEFAULT_PUBLIC_ADDRESS : 200, + ); + break; + case 'boolean': + parameterSchema[param.name] = Yup.boolean().required(); + initialValues[param.name] = false; + break; + } + } + return { validationSchema: Yup.object().shape(parameterSchema), initialValues }; +} + +async function handleFunctionCall( + contractAddress: AztecAddress | undefined, + contractAbi: ContractAbi, + functionName: string, + args: any, + wallet: CompleteAddress, +) { + const functionAbi = contractAbi.functions.find(f => f.name === functionName)!; + const typedArgs: any[] = convertArgs(functionAbi, args); + + if (functionName === 'constructor' && !!wallet) { + if (functionAbi === undefined) { + throw new Error('Cannot find constructor in the ABI.'); + } + // hack: addresses are stored as string in the form to avoid bigint compatibility issues with formik + // convert those back to bigints before sending + + // for now, dont let user change the salt. requires some change to the form generation if we want to let user choose one + // since everything is currently based on parsing the contractABI, and the salt parameter is not present there + const salt = Fr.random(); + return await deployContract(wallet, contractAbi, typedArgs, salt, rpcClient); + } + + if (functionAbi.functionType === 'unconstrained') { + return await viewContractFunction(contractAddress!, contractAbi, functionName, typedArgs, rpcClient, wallet); + } else { + return await callContractFunction(contractAddress!, contractAbi, functionName, typedArgs, rpcClient, wallet); + } +} + +interface ContractFunctionFormProps { + wallet: CompleteAddress; + contractAddress?: AztecAddress; + contractAbi: ContractAbi; + functionAbi: FunctionAbi; + title?: string; + buttonText?: string; + isLoading: boolean; + disabled: boolean; + onSubmit: () => void; + onSuccess: (result: any) => void; + onError: (msg: string) => void; +} + +export function ContractFunctionForm({ + wallet, + contractAddress, + contractAbi, + functionAbi, + buttonText = 'Submit', + isLoading, + disabled, + onSubmit, + onSuccess, + onError, +}: ContractFunctionFormProps) { + const { validationSchema, initialValues } = generateYupSchema(functionAbi); + const formik = useFormik({ + initialValues: initialValues, + validationSchema: validationSchema, + onSubmit: async (values: any) => { + onSubmit(); + try { + const result = await handleFunctionCall(contractAddress, contractAbi, functionAbi.name, values, wallet); + onSuccess(result); + } catch (e: any) { + onError(e.message); + } + }, + }); + + return ( +
+ {functionAbi.parameters.map(input => ( +
+ + + {formik.touched[input.name] && formik.errors[input.name] && ( +
{formik.errors[input.name]?.toString()}
+ )} +
+ ))} + {isLoading ? ( + + ) : ( +