diff --git a/.circleci/config.yml b/.circleci/config.yml index c8d57a6a382..d745187b5b3 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1133,10 +1133,10 @@ jobs: - *checkout - *setup_env - run: - name: "Build and test" + name: "Build" command: build canary true - run-deployment-canary: + run-deployment-canary-uniswap: docker: - image: aztecprotocol/alpine-build-image resource_class: small @@ -1147,6 +1147,17 @@ jobs: name: "Test" command: spot_run_test_script ./scripts/run_tests canary uniswap_trade_on_l1_from_l2.test.ts canary docker-compose.yml + run-deployment-canary-browser: + docker: + - image: aztecprotocol/alpine-build-image + resource_class: small + steps: + - *checkout + - *setup_env + - run: + name: "Test" + command: spot_run_test_script ./scripts/run_tests canary aztec_js_browser.test.test.ts canary docker-compose.yml + # Repeatable config for defining the workflow below. tag_regex: &tag_regex /^v.*/ defaults: &defaults @@ -1396,7 +1407,13 @@ workflows: - deploy-end <<: *deploy_defaults - - run-deployment-canary: + - run-deployment-canary-uniswap: + requires: + - build-deployment-canary + <<: *deploy_defaults + + - run-deployment-canary-browser: requires: - build-deployment-canary <<: *deploy_defaults + diff --git a/yarn-project/aztec.js/src/index.ts b/yarn-project/aztec.js/src/index.ts index 7205875a981..8126942f497 100644 --- a/yarn-project/aztec.js/src/index.ts +++ b/yarn-project/aztec.js/src/index.ts @@ -19,6 +19,7 @@ export { PackedArguments, PublicKey, PrivateKey, + SyncStatus, Tx, TxExecutionRequest, TxHash, diff --git a/yarn-project/canary/package.json b/yarn-project/canary/package.json index ae9f75e2aea..672e1fe50fd 100644 --- a/yarn-project/canary/package.json +++ b/yarn-project/canary/package.json @@ -26,8 +26,12 @@ "@aztec/noir-contracts": "workspace:^", "@jest/globals": "^29.5.0", "@types/jest": "^29.5.0", + "@types/koa-static": "^4.0.2", "@types/node": "^18.7.23", "jest": "^29.5.0", + "koa": "^2.14.2", + "koa-static": "^5.0.0", + "puppeteer": "^21.1.0", "ts-jest": "^29.1.0", "ts-node": "^10.9.1", "tslib": "^2.4.0", diff --git a/yarn-project/canary/src/aztec_js_browser.test.ts b/yarn-project/canary/src/aztec_js_browser.test.ts new file mode 100644 index 00000000000..c9427ae15fd --- /dev/null +++ b/yarn-project/canary/src/aztec_js_browser.test.ts @@ -0,0 +1,216 @@ +/* eslint-disable no-console */ +import * as AztecJs from '@aztec/aztec.js'; +import { AztecAddress, PrivateKey } from '@aztec/circuits.js'; +import { DebugLogger, createDebugLogger } from '@aztec/foundation/log'; +import { PrivateTokenContractAbi } from '@aztec/noir-contracts/artifacts'; + +import { Server } from 'http'; +import Koa from 'koa'; +import serve from 'koa-static'; +import path, { dirname } from 'path'; +import { Browser, Page, launch } from 'puppeteer'; +import { fileURLToPath } from 'url'; + +declare global { + interface Window { + AztecJs: typeof AztecJs; + } +} + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const PORT = 3000; + +const { SANDBOX_URL } = process.env; + +const conditionalDescribe = () => (SANDBOX_URL ? describe : describe.skip); +const privKey = PrivateKey.random(); + +/** + * This test is a bit of a special case as it's relying on sandbox and web browser and not only on anvil and node.js. + * To run the test, do the following: + * 1) Build the whole repository, + * 2) go to `yarn-project/aztec.js` and build the web packed package with `yarn build:web`, + * 3) start anvil: `anvil`, + * 4) open new terminal and optionally set the more verbose debug level: `DEBUG=aztec:*`, + * 5) go to the sandbox dir `yarn-project/aztec-sandbox` and run `yarn start`, + * 6) open new terminal and export the sandbox URL: `export SANDBOX_URL='http://localhost:8080'`, + * 7) go to `yarn-project/end-to-end` and run the test: `yarn test aztec_js_browser` + * + * NOTE: If you see aztec-sandbox logs spammed with unexpected logs there is probably a chrome process with a webpage + * unexpectedly running in the background. Kill it with `killall chrome` + */ +conditionalDescribe()('e2e_aztec.js_browser', () => { + const initialBalance = 33n; + const transferAmount = 3n; + + let contractAddress: AztecAddress; + + let logger: DebugLogger; + let pageLogger: DebugLogger; + let app: Koa; + let testClient: AztecJs.AztecRPC; + let server: Server; + + let browser: Browser; + let page: Page; + + beforeAll(async () => { + testClient = AztecJs.createAztecRpcClient(SANDBOX_URL!, AztecJs.makeFetch([1, 2, 3], true)); + await AztecJs.waitForSandbox(testClient); + + app = new Koa(); + app.use(serve(path.resolve(__dirname, './web'))); + server = app.listen(PORT, () => { + logger(`Server started at http://localhost:${PORT}`); + }); + + logger = createDebugLogger('aztec:aztec.js:web'); + pageLogger = createDebugLogger('aztec:aztec.js:web:page'); + + browser = await launch({ + executablePath: process.env.CHROME_BIN, + headless: 'new', + args: [ + '--allow-file-access-from-files', + '--no-sandbox', + '--headless', + '--disable-web-security', + '--disable-features=IsolateOrigins', + '--disable-site-isolation-trials', + '--disable-gpu', + '--disable-dev-shm-usage', + '--disk-cache-dir=/dev/null', + ], + }); + page = await browser.newPage(); + page.on('console', msg => { + pageLogger(msg.text()); + }); + page.on('pageerror', err => { + pageLogger.error(err.toString()); + }); + await page.goto(`http://localhost:${PORT}/index.html`); + }, 120_000); + + afterAll(async () => { + await browser.close(); + server.close(); + }); + + it('Loads Aztec.js in the browser', async () => { + const createAccountsExists = await page.evaluate(() => { + const { createAccounts } = window.AztecJs; + return typeof createAccounts === 'function'; + }); + expect(createAccountsExists).toBe(true); + }); + + it('Creates an account', async () => { + const result = await page.evaluate( + async (rpcUrl, privateKeyString) => { + const { PrivateKey, createAztecRpcClient, makeFetch, getUnsafeSchnorrAccount } = window.AztecJs; + const client = createAztecRpcClient(rpcUrl!, makeFetch([1, 2, 3], true)); + const privateKey = PrivateKey.fromString(privateKeyString); + const account = getUnsafeSchnorrAccount(client, privateKey); + await account.waitDeploy(); + const completeAddress = await account.getCompleteAddress(); + const addressString = completeAddress.address.toString(); + console.log(`Created Account: ${addressString}`); + return addressString; + }, + SANDBOX_URL, + privKey.toString(), + ); + const accounts = await testClient.getAccounts(); + const stringAccounts = accounts.map(acc => acc.address.toString()); + expect(stringAccounts.includes(result)).toBeTruthy(); + }, 15_000); + + it('Deploys Private Token contract', async () => { + await deployPrivateTokenContract(); + }, 30_000); + + it("Gets the owner's balance", async () => { + const result = await page.evaluate( + async (rpcUrl, contractAddress, PrivateTokenContractAbi) => { + const { Contract, AztecAddress, createAztecRpcClient, makeFetch } = window.AztecJs; + const client = createAztecRpcClient(rpcUrl!, makeFetch([1, 2, 3], true)); + const owner = (await client.getAccounts())[0].address; + const wallet = await AztecJs.getSandboxAccountsWallet(client); + const contract = await Contract.at(AztecAddress.fromString(contractAddress), PrivateTokenContractAbi, wallet); + const balance = await contract.methods.getBalance(owner).view({ from: owner }); + return balance; + }, + SANDBOX_URL, + (await getPrivateTokenAddress()).toString(), + PrivateTokenContractAbi, + ); + logger('Owner balance:', result); + expect(result).toEqual(initialBalance); + }); + + it('Sends a transfer TX', async () => { + const result = await page.evaluate( + async (rpcUrl, contractAddress, transferAmount, PrivateTokenContractAbi) => { + console.log(`Starting transfer tx`); + const { AztecAddress, Contract, createAztecRpcClient, makeFetch } = window.AztecJs; + const client = createAztecRpcClient(rpcUrl!, makeFetch([1, 2, 3], true)); + const accounts = await client.getAccounts(); + const owner = accounts[0].address; + const receiver = accounts[1].address; + const wallet = await AztecJs.getSandboxAccountsWallet(client); + const contract = await Contract.at(AztecAddress.fromString(contractAddress), PrivateTokenContractAbi, wallet); + await contract.methods.transfer(transferAmount, owner, receiver).send({ origin: owner }).wait(); + console.log(`Transferred ${transferAmount} tokens to new Account`); + return await contract.methods.getBalance(receiver).view({ from: receiver }); + }, + SANDBOX_URL, + (await getPrivateTokenAddress()).toString(), + transferAmount, + PrivateTokenContractAbi, + ); + expect(result).toEqual(transferAmount); + }, 60_000); + + const deployPrivateTokenContract = async () => { + const txHash = await page.evaluate( + async (rpcUrl, privateKeyString, initialBalance, PrivateTokenContractAbi) => { + const { PrivateKey, DeployMethod, createAztecRpcClient, makeFetch, getUnsafeSchnorrAccount } = window.AztecJs; + const client = createAztecRpcClient(rpcUrl!, makeFetch([1, 2, 3], true)); + let accounts = await client.getAccounts(); + if (accounts.length === 0) { + // This test needs an account for deployment. We create one in case there is none available in the RPC server. + const privateKey = PrivateKey.fromString(privateKeyString); + await getUnsafeSchnorrAccount(client, privateKey).waitDeploy(); + accounts = await client.getAccounts(); + } + const owner = accounts[0]; + const tx = new DeployMethod(owner.publicKey, client, PrivateTokenContractAbi, [ + initialBalance, + owner.address, + ]).send(); + await tx.wait(); + const receipt = await tx.getReceipt(); + console.log(`Contract Deployed: ${receipt.contractAddress}`); + return receipt.txHash.toString(); + }, + SANDBOX_URL, + privKey.toString(), + initialBalance, + PrivateTokenContractAbi, + ); + + const txResult = await testClient.getTxReceipt(AztecJs.TxHash.fromString(txHash)); + expect(txResult.status).toEqual(AztecJs.TxStatus.MINED); + contractAddress = txResult.contractAddress!; + }; + + const getPrivateTokenAddress = async () => { + if (!contractAddress) { + await deployPrivateTokenContract(); + } + return contractAddress; + }; +}); diff --git a/yarn-project/yarn.lock b/yarn-project/yarn.lock index df58c2269b0..9a8c84d9a7f 100644 --- a/yarn-project/yarn.lock +++ b/yarn-project/yarn.lock @@ -269,8 +269,12 @@ __metadata: "@jest/globals": ^29.5.0 "@rushstack/eslint-patch": ^1.1.4 "@types/jest": ^29.5.0 + "@types/koa-static": ^4.0.2 "@types/node": ^18.7.23 jest: ^29.5.0 + koa: ^2.14.2 + koa-static: ^5.0.0 + puppeteer: ^21.1.0 ts-jest: ^29.1.0 ts-node: ^10.9.1 tslib: ^2.4.0 @@ -2719,6 +2723,23 @@ __metadata: languageName: node linkType: hard +"@puppeteer/browsers@npm:1.7.0": + version: 1.7.0 + resolution: "@puppeteer/browsers@npm:1.7.0" + dependencies: + debug: 4.3.4 + extract-zip: 2.0.1 + progress: 2.0.3 + proxy-agent: 6.3.0 + tar-fs: 3.0.4 + unbzip2-stream: 1.4.3 + yargs: 17.7.1 + bin: + browsers: lib/cjs/main-cli.js + checksum: 0a2aecc72fb94a8d94246188f94cfaad730d1d372b34df94ca51ff8a94596bf475a0fee162c317a768fa4b2a707bfa8afd582d594958f49e1019effadfe744b6 + languageName: node + linkType: hard + "@rushstack/eslint-patch@npm:^1.1.4, @rushstack/eslint-patch@npm:^1.2.0": version: 1.3.2 resolution: "@rushstack/eslint-patch@npm:1.3.2" @@ -4826,6 +4847,17 @@ __metadata: languageName: node linkType: hard +"chromium-bidi@npm:0.4.20": + version: 0.4.20 + resolution: "chromium-bidi@npm:0.4.20" + dependencies: + mitt: 3.0.1 + peerDependencies: + devtools-protocol: "*" + checksum: 397145b3728948d403dbf087af97b7112988ed3c4cf43286754452a4b88f087f2088e1b3f18fa5974ceecc8200ca004ed258e18b784ecb4c6ab1ed78c1b280b0 + languageName: node + linkType: hard + "ci-info@npm:^3.2.0": version: 3.8.0 resolution: "ci-info@npm:3.8.0" @@ -5508,6 +5540,13 @@ __metadata: languageName: node linkType: hard +"devtools-protocol@npm:0.0.1159816": + version: 0.0.1159816 + resolution: "devtools-protocol@npm:0.0.1159816" + checksum: 24aa985a34a093a7418c955d0398e8a0829bc1f482eb1db55eca0cf09789de8344d0f36177d3e394fc63ec9bf08cf4114fadf3a7f2ab64f95a17ccc53bcf1aea + languageName: node + linkType: hard + "dezalgo@npm:^1.0.4": version: 1.0.4 resolution: "dezalgo@npm:1.0.4" @@ -9350,6 +9389,13 @@ __metadata: languageName: node linkType: hard +"mitt@npm:3.0.1": + version: 3.0.1 + resolution: "mitt@npm:3.0.1" + checksum: b55a489ac9c2949ab166b7f060601d3b6d893a852515ae9eca4e11df01c013876df777ea109317622b5c1c60e8aae252558e33c8c94e14124db38f64a39614b1 + languageName: node + linkType: hard + "mkdirp-classic@npm:^0.5.2": version: 0.5.3 resolution: "mkdirp-classic@npm:0.5.3" @@ -10263,6 +10309,20 @@ __metadata: languageName: node linkType: hard +"puppeteer-core@npm:21.1.0": + version: 21.1.0 + resolution: "puppeteer-core@npm:21.1.0" + dependencies: + "@puppeteer/browsers": 1.7.0 + chromium-bidi: 0.4.20 + cross-fetch: 4.0.0 + debug: 4.3.4 + devtools-protocol: 0.0.1159816 + ws: 8.13.0 + checksum: 1c3f2a2bb6de3ec90808f06f0c3ef769f0854e7df610c16080c1dd5434cedf865f48d5059c5406fa84227aab8bfae7dec789e5701adb800895c7a7ec888cfb04 + languageName: node + linkType: hard + "puppeteer@npm:^20.9.0": version: 20.9.0 resolution: "puppeteer@npm:20.9.0" @@ -10274,6 +10334,17 @@ __metadata: languageName: node linkType: hard +"puppeteer@npm:^21.1.0": + version: 21.1.0 + resolution: "puppeteer@npm:21.1.0" + dependencies: + "@puppeteer/browsers": 1.7.0 + cosmiconfig: 8.2.0 + puppeteer-core: 21.1.0 + checksum: 15e343dd1c048a0ef228aa52a2df2240dc76d543c379fc9246c41a87f226a2e820c957a911d5fed9e0394c5dd56da5fc163b6626e1219a857268e88cc2544163 + languageName: node + linkType: hard + "pure-rand@npm:^6.0.0": version: 6.0.2 resolution: "pure-rand@npm:6.0.2"