Verifiable state manager for React based on o1js & Mina protocol.
# using npm
npm install zk-states
# using yarn
yarn add zk-states
⚠️ IMPORTANT: To approve transactions on Mina Network, Auro wallet extension has to be installed in the browser.
zk-states
requires a web worker in order to execute the heaviest ZK computations. Define a file where the web worker code will run, here we will name it zkStatesWorker.ts
, but you can use whatever name you prefer. The content of your file should look like this:
import { initZKWorker } from "zk-states";
initZKWorker();
That's it for the worker file!
You define a ZK State in the following way:
import { createZKAppWorkerClient, createZKAssert, createZKState } from "zk-states";
// replace './zkStatesWorker.ts` with the path to the previously defined web worker
const worker = new Worker(new URL("./zkStatesWorker.ts", import.meta.url), {
type: "module",
});
const workerClient = createZKAppWorkerClient(worker);
// creating the assertion library, it needs the `workerClient` in order to perform calls to the ZK program
const zkAssert = createZKAssert(workerClient);
interface ZKState {
num: number;
incNum: () => void;
}
const { useInitZKStore, useZKStore, useProof, useIsInitialized } =
createZKState<ZKState>(workerClient, (set) => ({ // zustand state definition https://github.com/pmndrs/zustand
num: 0,
incNum: () =>
set((state) => {
// This assertion checks the requirements of the specified value.
// If these requirements are met, the local state will be updated optimistically
// and the proof generation will be queued in the web worker.
// The failure of the proof generation will roll back to the previous valid state.
zkAssert.numeric.lessThanOrEqual(state.num, 5);
return {
num: state.num + 1,
};
}),
}),
);
ZkStates leverages o1js to enable proof generation in the browser. to enable o1js for the web, we must set the COOP and COEP headers. When using a Vite project we also need to install a plugin to enable topLevelAwait for the web worker.
Open next.config.js
and make sure you add these two configs.
const nextConfig = {
webpack(config) {
config.resolve.alias = {
...config.resolve.alias,
o1js: require('path').resolve('node_modules/o1js')
};
config.experiments = { ...config.experiments, topLevelAwait: true };
return config;
},
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'Cross-Origin-Opener-Policy',
value: 'same-origin',
},
{
key: 'Cross-Origin-Embedder-Policy',
value: 'require-corp',
},
],
},
];
}
};
add vite-plugin-top-level-await
# using npm
npm install vite-plugin-top-level-await
# using yarn
yarn add vite-plugin-top-level-await
After installing the Vite plugin open the vite.config.ts
and add these two entries:
export default defineConfig({
plugins: [
topLevelAwait(),
{
name: "isolation",
configureServer(server) {
server.middlewares.use((_req, res, next) => {
res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
res.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
next();
});
},
},
],
});
By default, the library connects to an already deployed contract on the berkeley testnet. If you want to deploy your own, run the following command from the root directory:
DEPLOYER_PRIVATE_KEY=<your_wallet_private_key> MINA_URL=<graphql_mina_url> yarn deploy:zkapp
MINA_URL
env variable is optional and defaults to https://proxy.berkeley.minaexplorer.com/graphql
The deployment will take some time. If successfull, it will print out the private and public keys of the newly deployed zkApp. Keep the private key secret!
You can use the public key in the library, to connect to your own deployed contract:
const { useZKState } = createZKState(workerClient, () => {...}, "berkeley", "<your_zkapp_address>");
When deploying an application to production, you want to configure your COOP and COEP headers to allow web workers to work properly. Here you can find instructions on how to do the configuration.
If you are using Next.js, you might wanna do the following:
- Add the
coi-serviceworker
script in thepublic/
directory - Write this init script in your main codebase
- Import it in your root component in order to execute it
initZKWorker(testRef?: Window & typeof globalThis): void
Executes the web worker script.
testRef
param is useful when testing in a non browser environment. Do not use it in development or production. Check out our test files to see how it's being used.
⚠️ WARNING: do not import or execute this function in the main thread, but use it as illustrated in the example above!
createZKAppWorkerClient(worker: Worker): ZkAppWorkerClient
Generates a worker client instance that communicates with the provided web worker. ZkAppWorkerClient
is used internally and you don't have to interact with it directly.
This is the main function of the library, as it generates the hooks that allow you to interact with the library from a React component. It accepts the following parameters:
workerClient: ZkAppWorkerClient
- worker client instance generated withcreateZKAppWorkerClient
function.createState: StateCreator<T, [], []>
- Zustand state creator function. Refer to this documentation for more information on how to define global state with Zustand.networkName: MinaNetwork
- specifies which network to connect to. Defaults to"berkeley"
.appPublicKeyBase58: string
- specifies the address of the deployedStatesVerifier
zkApp. Defaults to the address of aStatesVerifier
zkApp we pre-deployed on the Berkeley testnet. In the previous sections you can find instructions on how to deploy your own zkApp.
The function returns an object containing the hooks documented in the next section.
Takes the worker client as a param and returns the assertion library. Assertions are functions that will either return void
when the internal evaluation is successful or will throw a FailedLocalAssert
error if unsuccessful. Assertions are supposed to be called inside actions defined in createZKState
state creator, since those are wrapped with a middleware that will handle the thrown error.
If all the assertions called inside an action succeed, they will be sequentially processed by the web worker, which will generate their proofs. If an assertion fails locally, all the other assertions specified in the action will not be submitted to the web worker and the local state won't be updated. If an assertion succeeds locally, but later fails when being proven in the web worker, the local state will automatically rollback to the latest valid state.
To generate the assertion library:
const zkAssert = createZKAssert(workerClient);
To call an assertion:
// will succeed if `value` is greater than 5 and throw otherwise
zkAssert.numeric.greaterThan(value, 5);
Refer to this section for an example on how to properly call assertions.
Allows you to access the state from a React component. Take a look at Zustand's documentation for usage examples.
Initializes the zk-store library and connects to Auro wallet. This hook is supposed to be called once per application, possibly in a high level component.
Returns the latest valid proof.
Returns a boolean that indicates if the library has been initialized ot not.
Returns a string that shows the current initialization stage.
Returns an array containing a stringified representation of the assertions that are waiting to be proven by the web worker.
Returns a boolean that indicates if the web worker is currently generating a proof.
Returns a boolean that is true
if the latest web worker proof generation failed.
Returns a verify
function that will call the remote proof verification on the Mina blockchain. User will have to approve the transaction through their wallet.
Returns a boolean that is true
if the Auro Wallet extension is installed in the browser.
⚠️ IMPORTANT: The extension has to be installed in order to use the library!