DEV Community

uratmangun
uratmangun

Posted on

Step-by-Step Guide to Building a zkApp with O1js

Zero-knowledge applications (zkApps) enable privacy-preserving decentralized apps on Mina Protocol. This tutorial uses O1js (formerly SnarkyJS), a TypeScript library for building zk-SNARK circuits, to create a simple zkApp.


Prerequisites

  1. Basic knowledge of TypeScript/JavaScript.
  2. Node.js (v18+ recommended).
  3. Familiarity with Mina Protocol concepts (e.g., zk-SNARKs).
  4. Terminal/CLI proficiency.

1. Environment Setup

Install Dependencies

npm install -g zkapp-cli # Mina zkApp CLI tool
npm install -g typescript ts-node # TypeScript tools
Enter fullscreen mode Exit fullscreen mode

2. Initialize Project

Create a new zkApp project using the Mina CLI:

zk create my-zkapp --template simple # Use the "simple" template
cd my-zkapp
npm install
Enter fullscreen mode Exit fullscreen mode

Project Structure

my-zkapp/
├── src/
│   ├── contracts/    # zkApp smart contracts
│   ├── tests/        # Test files
│   └── index.ts      # Main entry (optional)
├── zkapp.config.json # Configuration
└── package.json
Enter fullscreen mode Exit fullscreen mode

3. Write a zkApp Contract

Create src/contracts/NumberUpdate.ts:

import {
  SmartContract,
  State,
  state,
  method,
  PublicKey,
  PrivateKey,
} from 'o1js';

export class NumberUpdate extends SmartContract {
  @state(Field) number = State<Field>(); // On-chain state

  init() {
    super.init();
    this.number.set(Field(0)); // Initialize state
  }

  // Method to update the number with a constraint
  @method updateNumber(newNumber: Field) {
    const currentNumber = this.number.get();
    this.number.assertEquals(currentNumber); // Verify current state
    newNumber.assertLessThan(Field(100)); // Custom constraint: new number < 100
    this.number.set(newNumber); // Update state
  }
}
Enter fullscreen mode Exit fullscreen mode

Key Concepts

  • @state: Declares on-chain state.
  • @method: Defines a zk-SNARK circuit (private computation).
  • Field: A primitive for finite field arithmetic.

4. Compile the Contract

Compile to generate proofs and AVM bytecode:

zk compile src/contracts/NumberUpdate.ts
Enter fullscreen mode Exit fullscreen mode
  • Compilation may take 2-10 minutes (generates zk-SNARK keys).

5. Write Tests

Create src/tests/NumberUpdate.test.ts:

import { Test, expect } from 'zken';
import { NumberUpdate } from '../contracts/NumberUpdate';
import { Field, PrivateKey } from 'o1js';

describe('NumberUpdate', () => {
  let zkApp: NumberUpdate;
  let deployer: PrivateKey;

  beforeAll(async () => {
    deployer = PrivateKey.random(); // Test account
  });

  beforeEach(() => {
    zkApp = new NumberUpdate(deployer.toPublicKey());
  });

  it('updates number correctly', async () => {
    await zkApp.compile(); // Ensure contract is compiled

    // Deploy
    const tx = await Mina.transaction(deployer, () => {
      zkApp.deploy();
      zkApp.updateNumber(Field(42)); // Update to 42
    });
    await tx.prove(); // Generate proof
    await tx.sign([deployer]).send(); // Submit to testnet

    // Verify on-chain state
    expect(zkApp.number.get()).toEqual(Field(42));
  });
});
Enter fullscreen mode Exit fullscreen mode

Run tests:

zk test
Enter fullscreen mode Exit fullscreen mode

6. Deploy to Mina Network

Configure Network

Update zkapp.config.json:

{
  "networks": {
    "berkeley": {
      "url": "https://proxy.berkeley.minaexplorer.com/graphql",
      "keyPath": "./keys/berkeley.json"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Fund Account & Deploy

  1. Get testnet MINA from Mina Faucet.
  2. Deploy:
zk deploy:berkeley src/contracts/NumberUpdate.ts \
  --key-file ./keys/berkeley.json
Enter fullscreen mode Exit fullscreen mode

7. Build a Frontend (React Example)

Install dependencies:

npm install react @mina_ui/core
Enter fullscreen mode Exit fullscreen mode

Example component (src/index.tsx):

import { useState } from 'react';
import { NumberUpdate } from './contracts/NumberUpdate';
import { Mina, PublicKey } from 'o1js';

export default function App() {
  const [number, setNumber] = useState<number>(0);

  const updateNumber = async () => {
    const mina = Mina.connect('https://berkeley.minaexplorer.com');
    const contractAddress = PublicKey.fromBase58('YOUR_DEPLOYED_ADDRESS');
    const contract = new NumberUpdate(contractAddress);

    const tx = await Mina.transaction({ sender: contractAddress }, () => {
      contract.updateNumber(Field(number));
    });
    await tx.prove();
    await tx.send();
  };

  return (
    <div>
      <input type="number" onChange={(e) => setNumber(Number(e.target.value))} />
      <button onClick={updateNumber}>Update Privately</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

8. Advanced Features

Add Privacy with @method.private

@method private validateSecret(secret: Field) {
  Poseidon.hash([secret]).assertEquals(this.account.hash);
}
Enter fullscreen mode Exit fullscreen mode

Gas Optimization

  • Use @method({ gasBudget: 0.1 }) to limit gas costs.
  • Batch proofs using Mina.transactionBatch().

Best Practices

  1. Testing: Cover all circuit branches with unit tests.
  2. Security: Audit constraints to prevent invalid state transitions.
  3. Gas Costs: Optimize complex circuits with @method.runUnchecked.

Conclusion

You’ve built a zkApp that updates a number with privacy guarantees! Expand by:

  • Adding more complex business logic.
  • Integrating with off-chain oracles.
  • Exploring token standards (e.g., zkTokens).

Resources:

Heroku

Build apps, not infrastructure.

Dealing with servers, hardware, and infrastructure can take up your valuable time. Discover the benefits of Heroku, the PaaS of choice for developers since 2007.

Visit Site

Top comments (0)

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay