Skip to content

Repository files navigation

MinaGuard Monorepo

Online creation checks whether the exact proposal already exists before requesting a signature, and again when assessing a stale transaction. The recovery panel offers View proposal so another owner can approve the existing proposal. Propose and approve recheck their indexed witnesses against fresh vault state after the proposal signature, before building the transaction.

Initial verified-store mismatches show “Vault data isn’t up to date” with Retry / Cancel. Retry restarts online preparation or offline request export from current state and indexed stores; it never bypasses root validation or retries automatically. Network and unrelated errors are not classified as store mismatches.

Prepared transactions are checked against current on-chain state before broadcast (before wallet handoff for Auro). The UI shows “Checking latest vault state…” before Auro handoff, then “Waiting for wallet confirmation…”. Stale transactions stop with an explicit recovery action. See transaction preflight.

MinaGuard is a multisig wallet zkApp for Mina built with o1js, plus a Next.js UI and an Express indexer API.

Packages

  • contracts/ - MinaGuard smart contract, stores, and tests.
  • backend/ - Express + Prisma + PostgreSQL read API and chain indexer.
  • ui/ - Next.js app with Auro wallet integration and on-chain actions.

Key Features

  • Propose -> approve -> execute lifecycle using proposal hash keyed approvals.
  • Transfer, add/remove owner, threshold change, and delegate execution support.
  • Indexed read API for contracts, owners, proposals, approvals, and raw events.
  • Deploy + setup UI flow with session-only zkApp private key usage.
  • Child reservations validate governance bounds and initialized root-parent state; successful child setup clears the consumed reservation hash.
  • Child allocations require initialized recipients bound to the sending parent. Complete child setup before funding; ordinary transfers and external deposits to uninitialized children remain unrecoverable until setup succeeds. See safe child funding.

o1js dependency

The runtime o1js library comes from o1js@3.0.0-mesa.final on npm — the canonical Mesa-network release.

The ui/deps/o1js/ submodule is still required, but only for mina-signer — the ui and offline-cli workspaces import it via file: workspace deps (mina-signer is a Mina-specific browser-side signing package shipped as a subpackage inside the o1js repo). The submodule points at graikos/o1js#develop-3.0 (currently tip a252f8bb); its mina-signer source is byte-identical to what o1js@3.0.0-mesa.final ships in node_modules/o1js/src/mina-signer/, so there's no protocol-level divergence. A follow-up could drop the submodule entirely by switching to the standalone mina-signer npm package.

Development

First-time setup

# Fetch submodule (o1js source, needed for mina-signer browser build)
git submodule update --init

bun install

# Build contracts (required by backend and UI)
bun run --filter contracts build

# Set up the backend environment
cp backend/.env.example backend/.env

# Generate Prisma client
cd backend && bunx prisma generate && cd ..

Lightnet helpers

dev-helpers/lightnet-up.sh starts a standalone Mesa lightnet on the host (ports 8080 GraphQL, 8282 archive, 8181 account manager, 5432 Postgres). It first frees those host ports by stopping any colliding containers (zkao-postgres-dev, local-lightnet-1) and records what it stopped:

./dev-helpers/lightnet-up.sh

dev-helpers/lightnet-down.sh stops lightnet and restarts whatever the up script paused, so you're back where you started:

./dev-helpers/lightnet-down.sh

Once lightnet is running, accounts must have funds before they can submit transactions. Two ways:

  • From the UI — once a wallet is connected on a testnet network, the header shows a "Fund" button that calls the backend's /api/fund route (which in turn drips from the lightnet account manager).
  • From the CLI — add public keys to dev-helpers/.env and run:
    cd dev-helpers && bun run cli.ts lightnet-fund

NOTE: To test with a Ledger device, its public key (corresponding to the account index used) must be funded similarly. Only the public key is needed.

Lightnet resets: lightnet sometimes stops or wipes its chain state without warning. When that happens the backend's indexed view (contracts, proposals, etc.) no longer matches the live chain, so wipe the DB to start clean:

cd backend && bunx prisma db push --force-reset --skip-generate

Then restart lightnet (./dev-helpers/lightnet-up.sh) and the backend.

Running

To run in browser:

# Run UI (from ui/ directory)
cd ui && bun run dev

# Run backend API/indexer (from backend/ directory)
cd backend && bun run dev

# Run contract tests (choose the circuit domain explicitly)
MINA_NETWORK_DOMAIN=testnet bun run --filter contracts test

To run the Electron app:

# Build UI (from ui/ directory; use the target network's domain)
cd ui && NEXT_PUBLIC_MINA_NETWORK=testnet bun run build

# Run backend API/indexer (from backend/ directory)
cd backend && bun run dev

# Run the Electron desktop app
cd desktop && bun run dev

E2E Testing

Full end-to-end tests live in e2e/ and exercise the deploy → propose → approve → execute lifecycle against a real Mina network. See e2e/README.md for setup details.

# Quick start with local lightnet (default)
bun run test:e2e

# Against Mina devnet (requires funded accounts in e2e/.env.devnet)
NETWORK=devnet bun run test:e2e

# Fast UI bundle-export and desktop network checks (also run in CI)
bun run --filter ui test
bun run --filter desktop test

Build

bun run --filter contracts build
bun run --filter backend build
NEXT_PUBLIC_MINA_NETWORK=testnet bun run --filter ui build

CI computes a fingerprint of the tracked circuit inputs. It compiles testnet and mainnet in parallel when their cached hashes are missing, then publishes a minaguard-vk-manifest artifact for the exact source commit. It contains contracts/.vk-hash with testnet, mainnet, and devnet entries; devnet uses the testnet circuit. Release and deploy jobs verify the artifact's commit before using its network hash for the backend, UI, or desktop bundle. Local builds do not need to compile the circuit just to update a committed hash file. The testnet runner proves parent-state paths and, when present, proposal-signing paths in separate steps so each has its own timeout. The mainnet runner proves child reservation and setup. These proof tests still run on every PR check. A VK cache miss recompiles the circuit, including after cache expiry or a merge to a branch that cannot access the PR's cache.

PR Preview Environments

Each PR targeting main gets an isolated preview stack deployed to the Hetzner server via a self-hosted GitHub Actions runner. Preview URLs follow the pattern https://mina-nodes.duckdns.org/preview/<PR_NUMBER>/.

Each stack includes: lightnet, PostgreSQL, backend, frontend, block explorer, and a Caddy reverse proxy.

Manual management

# From repo root
./preview-env/preview.sh up <PR_NUMBER>    # deploy
./preview-env/preview.sh down <PR_NUMBER>  # teardown
./preview-env/preview.sh list              # show active previews

Local development with Docker

You can run the full stack locally without the server's Caddy:

./preview-env/local-preview.sh up 1

Access at https://localhost:10001/preview/1/. In Auro Wallet, set the network URL to https://localhost:10001/preview/1/graphql.

Caddy serves this over HTTPS with a self-signed cert (tls internal) and sets the COOP/COEP headers o1js needs — accept the cert warning on first visit. The CA persists in the caddy-local-data volume so the cert stays stable across restarts.

The helper builds backend, frontend, and explorer sequentially before starting the stack. This avoids the RAM spike from docker compose up -d --build, which can try to build all three images at once.

The first uncached frontend build can take a few minutes because Next.js has to compile the heavy o1js worker bundle. local-preview.sh now prints a periodic heartbeat during long builds so this does not look like a freeze.

Frontend changes do not hot-reload in this Docker preview flow. The container runs a built next start app, so after changing files under ui/, rebuild the frontend image and restart only that service:

PR_NUMBER=1 PREVIEW_PORT=10001 docker compose \
  -f preview-env/docker-compose.preview.yml \
  -f preview-env/docker-compose.local.yml \
  -p local \
  build frontend

PR_NUMBER=1 PREVIEW_PORT=10001 docker compose \
  -f preview-env/docker-compose.preview.yml \
  -f preview-env/docker-compose.local.yml \
  -p local \
  up -d --no-deps frontend

build frontend picks up your latest UI code. up -d --no-deps frontend recreates just the frontend container without restarting backend, lightnet, explorer, or Caddy.

To seed coherent test data, prefer the real on-chain fixture helper over direct DB inserts:

MINA_NETWORK_DOMAIN=testnet bun run dev-helpers/cli.ts lightnet-fixture --main-address <YOUR_WALLET_ADDRESS>

By default this uses the quick-test minimal scenario: 2 vaults, 2 executed proposals per vault, and both vaults ending with your wallet as the sole owner at threshold 1. For broader coverage, you can also use --scenario full.

That command acquires a funded lightnet deployer, generates helper signers, deploys real MinaGuard contracts that include your wallet address as an owner, submits real propose / approve / execute transactions with proofs disabled, and waits for the preview indexer to ingest them. The result is fixture data that stays consistent across chain, backend, and UI.

# Logs
docker compose -p local logs -f            # all services
docker compose -p local logs -f frontend   # frontend only
docker compose -p local logs -f backend    # backend/indexer
docker compose -p local logs -f lightnet   # mina node + archive

# Tear down
./preview-env/local-preview.sh down 1

Develop on the remote server via SSH tunnel

When you want the heavy services (lightnet, backend, frontend dev server) to run on the server but iterate from your laptop's browser + Auro wallet.

On the server, follow First-time setup and Running so lightnet, backend, and the UI dev server are listening on the default localhost:* ports. backend/.env and ui/.env.local need no changes.

On the laptop, open one SSH tunnel that forwards every port the bundle references:

ssh -L 3000:localhost:3000 \
    -L 3001:localhost:3001 \
    -L 8080:localhost:8080 \
    -L 8282:localhost:8282 \
    user@server

Then open http://localhost:3000 in the browser. The frontend bundle has localhost:* baked in for the backend / Mina / archive URLs; both the server-side dev server and the laptop-side browser resolve localhost to themselves, so the URLs work on both ends as long as the tunnel is up.

Notes:

  • Add a custom network in Auro Wallet pointing at http://localhost:8080/graphql and switch to it before connecting.
  • Tunnel dies → the page errors. Run the SSH command inside tmux/screen if you want it sticky.

Architecture

Requests hit the main Caddy (TLS + COOP/COEP headers) which reverse-proxies to a per-preview Caddy container that routes to individual services. COOP/COEP headers are set at the main Caddy level and upstream copies are stripped to prevent duplicates.

Server setup

Preview routes are managed via the Caddy admin API (localhost:2019) — no sudo required. The self-hosted runner only needs Docker access (docker group).

Gotchas

  • SharedArrayBuffer: o1js WASM requires crossOriginIsolated, which needs COOP + COEP headers over HTTPS. Do not add Cross-Origin-Resource-Policy: same-origin — it blocks o1js blob URL sub-workers.
  • Bun workspaces: ui/deps/ must be copied into Dockerfiles because mina-signer is a file: dependency.
  • Minification disabled: SWC/terser mangle BigInt ops used by o1js.
  • Server limits: ~2GB RAM per preview stack, max 2–3 concurrent previews on the 30GB server. Run docker image prune -f periodically.

Lightnet (working image before update 2026/04/13)

# Pull previous working lightnet (non-MESA) docker image
docker pull 'o1labs/mina-local-network@sha256:746190ff2f556f252b7f50215ae60d4a5e786c8adc16f27986e3e35ce6105949' 

# Verify it was pulled
docker inspect 'o1labs/mina-local-network@sha256:746190ff2f556f252b7f50215ae60d4a5e786c8adc16f27986e3e35ce6105949' --format '{{.Id}} {{.RepoTags}}'

# Tag it as a distinct image
docker tag 'o1labs/mina-local-network@sha256:746190ff2f556f252b7f50215ae60d4a5e786c8adc16f27986e3e35ce6105949' o1labs/mina-local-network:known-good

# Add a second tag, the one zk will look for
docker tag 'o1labs/mina-local-network@sha256:746190ff2f556f252b7f50215ae60d4a5e786c8adc16f27986e3e35ce6105949' o1labs/mina-local-network:compatible-latest-lightnet

# Confirm no stale state
zk lightnet stop --clean-up

# Start lightnet WITHOUT pulling latest
zk lightnet start --pull=false

Incremental signing state and offline compatibility

The signing worker saves public owner/approval/nullifier checkpoints locally and fetches only newer event blocks on later operations using the existing backend block filters and offset pagination. It checks the resulting roots against the Mina node and falls back to one full replay on mismatch. Initial sync still needs complete history; cold restoration rehashes saved leaves. Clearing browser storage loses only this optimization, not signing keys. Ranges exceeding the existing pagination cap fail closed; cursor pagination is tracked in issue #143.

Offline requests exported by this UI use version 1 with a complete public store snapshot. Proposals use application-tagged hashes, distinct propose/approve signing messages, and length-prefixed memo commitments (including empty memos). Owner-chain links, vote-nullifier keys, and child configuration hashes also have separate tags. Use matching CLI/UI/backend/desktop builds and new network VKs. Requests and signed responses both use v1 after the pre-release reset. Discard older files: the version number alone does not distinguish them from current files. This breaking change requires fresh vaults and recreated proposals. See the offline audit guide for migration and trust boundaries. Desktop packages the same worker and exporter.

About

MultiSig Wallet for the Mina Network

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages