Skip to content

Commit

Permalink
feat: PXE in the browser (#10353)
Browse files Browse the repository at this point in the history
PXE running in a browser via a vite app. In order to achieve this,
several things had to be implemented/altered:

- Original LMDB storage was not compatible with the browser. Also its
very nature made most get operations synchronous, which is pretty weird
for a DB. A new `AztecAsyncKVStore` has been created, offering *almost*
the same interface but with async methods for everything. This store is
backed by IndexedDB, which is compatible with most modern browsers. The
LMDB backed store has been updated to also support the async interface,
so now `PxeKvDatabase` can run with either.
- In order to support testing in the browser, `@aztec/kv-store` tests
have been moved from jest to mocha
- Most imports of `fs/promises` have been replaced with `import {
promises as fs } from 'fs';` which can be polyfilled by
[vite-plugin-node-polyfills](https://www.npmjs.com/package/vite-plugin-node-polyfills).
- Several packages (such as `simulator`) have been split to allow
importing the minimal set of APIs required for PXE to do its client side
thing, avoiding unneeded (and browser-incompatible) APIs and files.
- PXE package now generates a static info file (version and name) at
compilation time, to avoid reading it from the package.json file (which
cannot be done in the browser). A bundler was considered, but this is
simpler and more flexible.
- A new flag has been added to the sandbox, so it can run without a PXE
(`--sandbox.noPXE`). This is very useful to deploy a "network in a box"
that can be used to test apps that provide their own PXE implementation.
- The vite box is very rough and mostly mimics what the `react` one
does, but shows how a hypothetical dapp could be bundled.

---------

Co-authored-by: Alex Gherghisan <[email protected]>
  • Loading branch information
Thunkar and alexghr authored Dec 9, 2024
1 parent 7fa8f84 commit 676f673
Show file tree
Hide file tree
Showing 158 changed files with 7,010 additions and 1,869 deletions.
1 change: 0 additions & 1 deletion avm-transpiler/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 27 additions & 0 deletions boxes/boxes/vite/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# 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?

artifacts/*
codegenCache.json
50 changes: 50 additions & 0 deletions boxes/boxes/vite/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# React + TypeScript + Vite

This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.

Currently, two official plugins are available:

- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh

## Expanding the ESLint configuration

If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:

- Configure the top-level `parserOptions` property like this:

```js
export default tseslint.config({
languageOptions: {
// other options...
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
},
})
```

- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
- Optionally add `...tseslint.configs.stylisticTypeChecked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:

```js
// eslint.config.js
import react from 'eslint-plugin-react'

export default tseslint.config({
// Set the react version
settings: { react: { version: '18.3' } },
plugins: {
// Add the react plugin
react,
},
rules: {
// other rules...
// Enable its recommended rules
...react.configs.recommended.rules,
...react.configs['jsx-runtime'].rules,
},
})
```
28 changes: 28 additions & 0 deletions boxes/boxes/vite/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'

export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
)
25 changes: 25 additions & 0 deletions boxes/boxes/vite/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Private Token Noir Smart Contract</title>
<style>
#root {
width: 100%;
text-align: center;
}

body {
display: flex;
place-items: center;
background: linear-gradient(#f6fbfc, #d8d4e7);
min-height: 100vh;
}
</style>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
48 changes: 48 additions & 0 deletions boxes/boxes/vite/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"name": "vite",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"compile": "cd src/contracts && ${AZTEC_NARGO:-aztec-nargo} compile --silence-warnings",
"codegen": "${AZTEC_BUILDER:-aztec} codegen src/contracts/target -o artifacts",
"clean": "rm -rf ./dist .tsbuildinfo ./artifacts ./src/contracts/target",
"prep": "yarn clean && yarn compile && yarn codegen",
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@aztec/accounts": "portal:../../../yarn-project/accounts",
"@aztec/aztec.js": "portal:../../../yarn-project/aztec.js",
"@aztec/circuit-types": "portal:../../../yarn-project/circuit-types",
"@aztec/key-store": "link:../../../yarn-project/key-store",
"@aztec/kv-store": "portal:../../../yarn-project/kv-store",
"@aztec/pxe": "link:../../../yarn-project/pxe",
"@noir-lang/acvm_js": "link:../../../noir/packages/acvm_js",
"@noir-lang/noirc_abi": "link:../../../noir/packages/noirc_abi",
"buffer": "^6.0.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-toastify": "^10.0.6"
},
"devDependencies": {
"@eslint/js": "^9.13.0",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react-swc": "^3.5.0",
"eslint": "^9.13.0",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.14",
"globals": "^15.11.0",
"memfs": "^4.14.0",
"node-stdlib-browser": "^1.3.0",
"typescript": "~5.6.2",
"typescript-eslint": "^8.11.0",
"vite": "^5.4.10",
"vite-plugin-externalize-deps": "^0.8.0",
"vite-plugin-node-polyfills": "^0.22.0",
"vite-plugin-top-level-await": "^1.4.4"
}
}
42 changes: 42 additions & 0 deletions boxes/boxes/vite/src/App.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}

.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}

@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}

.card {
padding: 2em;
}

.read-the-docs {
color: #888;
}
35 changes: 35 additions & 0 deletions boxes/boxes/vite/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { ToastContainer } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
import "./App.css";
import { Home } from "./pages/home";
import { useEffect, useState } from "react";
import initACVM from "@noir-lang/acvm_js/web/acvm_js";
import initABI from "@noir-lang/noirc_abi/web/noirc_abi_wasm";
import acvmURL from "@noir-lang/acvm_js/web/acvm_js_bg.wasm?url";
import abiURL from "@noir-lang/noirc_abi/web/noirc_abi_wasm_bg.wasm?url";

const InitWasm = ({ children }: any) => {
const [init, setInit] = useState(false);
useEffect(() => {
(async () => {
await Promise.all([
initACVM(new URL(acvmURL, import.meta.url).toString()),
initABI(new URL(abiURL, import.meta.url).toString()),
]);
setInit(true);
})();
}, []);

return <div>{init && children}</div>;
};

function App() {
return (
<InitWasm>
<Home />
<ToastContainer />
</InitWasm>
);
}

export default App;
101 changes: 101 additions & 0 deletions boxes/boxes/vite/src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import {
AztecNode,
Fr,
createDebugLogger,
deriveMasterIncomingViewingSecretKey,
} from "@aztec/aztec.js";
import { BoxReactContractArtifact } from "../artifacts/BoxReact";
import { AccountManager } from "@aztec/aztec.js/account";
import { SingleKeyAccountContract } from "@aztec/accounts/single_key";
import { createAztecNodeClient } from "@aztec/aztec.js";
import { PXEService } from "@aztec/pxe/service";
import { PXEServiceConfig, getPXEServiceConfig } from "@aztec/pxe/config";
import { KVPxeDatabase } from "@aztec/pxe/database";
import { TestPrivateKernelProver } from "@aztec/pxe/kernel_prover";
import { KeyStore } from "@aztec/key-store";
import { PrivateKernelProver } from "@aztec/circuit-types";
import { L2TipsStore } from "@aztec/kv-store/stores";
import { createStore } from "@aztec/kv-store/indexeddb";

const SECRET_KEY = Fr.random();

export class PrivateEnv {
pxe;
accountContract;
account: AccountManager;

constructor(
private secretKey: Fr,
private nodeURL: string,
) {}

async init() {
const config = getPXEServiceConfig();
config.dataDirectory = "pxe";
const aztecNode = await createAztecNodeClient(this.nodeURL);
const proofCreator = new TestPrivateKernelProver();
this.pxe = await this.createPXEService(aztecNode, config, proofCreator);
const encryptionPrivateKey = deriveMasterIncomingViewingSecretKey(
this.secretKey,
);
this.accountContract = new SingleKeyAccountContract(encryptionPrivateKey);
this.account = new AccountManager(
this.pxe,
this.secretKey,
this.accountContract,
);
}

async createPXEService(
aztecNode: AztecNode,
config: PXEServiceConfig,
proofCreator?: PrivateKernelProver,
) {
const l1Contracts = await aztecNode.getL1ContractAddresses();
const configWithContracts = {
...config,
l1Contracts,
} as PXEServiceConfig;

const store = await createStore(
"pxe_data",
configWithContracts,
createDebugLogger("aztec:pxe:data:indexeddb"),
);

const keyStore = new KeyStore(store);

const db = await KVPxeDatabase.create(store);
const tips = new L2TipsStore(store, "pxe");

const server = new PXEService(
keyStore,
aztecNode,
db,
tips,
proofCreator,
config,
);
await server.start();
return server;
}

async getWallet() {
// taking advantage that register is no-op if already registered
return await this.account.register();
}
}

export const deployerEnv = new PrivateEnv(
SECRET_KEY,
process.env.PXE_URL || "http://localhost:8080",
);

const IGNORE_FUNCTIONS = [
"constructor",
"compute_note_hash_and_optionally_a_nullifier",
"sync_notes",
];
export const filteredInterface = BoxReactContractArtifact.functions.filter(
(f) => !IGNORE_FUNCTIONS.includes(f.name),
);
9 changes: 9 additions & 0 deletions boxes/boxes/vite/src/contracts/Nargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[package]
name = "boxreact"
authors = [""]
compiler_version = ">=0.18.0"
type = "contract"

[dependencies]
aztec = { path = "../../../../../noir-projects/aztec-nr/aztec" }
value_note = { path = "../../../../../noir-projects/aztec-nr/value-note" }
Loading

0 comments on commit 676f673

Please sign in to comment.