build strategy · midnight

Real ZK privacy, five secrets, one build.

Every mega-prompt in this repo uses the same pattern, because it's the only pattern that lets a Lovable account ship a verifiable Midnight demo in one shot.

Why Midnight and not Ethereum?

Midnight is a privacy-first Layer 1 where every smart contract is a triple: a public ledger, a ZK circuit, and a local off-chain component. Circuit parameters are private by default — moving anything to public state requires an explicitdisclose() call, so leaks become compile errors instead of runtime bugs. The preview testnet is funded by a free tDUST faucet; the block explorer, indexer, and Lace wallet are the same tooling you'd use on mainnet. Move to mainnet after the hackathon by flipping VITE_NETWORK_ID.

The recipe

recipe
# 1. In your Lovable project, add five secrets (Settings -> Secrets):
VITE_NETWORK_ID=preview
VITE_INDEXER_URL=https://indexer.preview.midnight.network/api/v4/graphql
VITE_INDEXER_WS_URL=wss://indexer.preview.midnight.network/api/v4/graphql/ws
VITE_PROOF_SERVER_URL=http://localhost:6300
VITE_DEFAULT_CONTRACT=<paste the address printed by your first deploy>

# Optional (only for ideas that pin artefacts to IPFS):
VITE_PINATA_JWT=eyJhbGciOi...

# 2. Install the Lace wallet extension:
open https://www.lace.io/

# 3. Get tNIGHT from the Midnight faucet (pick the network you're building on):
open https://midnight-tmnight-preview.nethermind.dev/   # Preview
open https://midnight-tmnight-preprod.nethermind.dev/   # Preprod
#    Then in Lace click "Generate tDUST" to delegate tNIGHT → tDUST.

# 4. Start the local proof server (one terminal tab, leave running):
docker run -p 6300:6300 midnightntwrk/proof-server:latest midnight-proof-server -v

# 5. Copy a mega-prompt from this repo into Lovable. One paste:
#    - scaffolds the React + Vite app with WASM + top-level-await plugins
#    - writes the Compact contract (with hackathon credit in the header)
#    - wires Lace detection, provider bootstrap, and the witness callback
#    - shows Proving -> Balancing -> Submitting -> Confirmed transaction states
#    - reads public ledger state from the Midnight Indexer
#    - exposes the contract address + explorer link in the UI

# 6. Open the Midnight explorer link. Your demo is provably private, provably on chain.

1. The Compact contract — credit baked in

Every .compact file deployed from a Creative Midnight prompt MUST carry the hackathon credit as a header comment, so provenance lives alongside the ZK verifying key.

contracts/TimestampLog.compact
// contracts/TimestampLog.compact — every contract carries the hackathon credit as a header comment
// Built during the Creative AI & Quantum Hackathon
// organised by StreetKode Fam during Indian Krump Festival 14
pragma language_version 0.23;

import CompactStandardLibrary;

export ledger entry_count: Counter;
export ledger last_message: Opaque<"string">;
export ledger last_author_commitment: Bytes<32>;

witness localSecretKey(): Bytes<32>;

constructor() {
  entry_count.increment(1);
  last_message = disclose("(empty)");
}

export circuit appendEntry(newMessage: Opaque<"string">): [] {
  const sk = localSecretKey();
  const seq = entry_count as Field as Bytes<32>;
  last_author_commitment = disclose(
    persistentHash<Vector<3, Bytes<32>>>([pad(32, "log:author:"), seq, sk])
  );
  last_message = disclose(newMessage);
  entry_count.increment(1);
}

2. Compile & run the proof server

terminal
# One-time toolchain install
curl --proto '=https' --tlsv1.2 -LsSf \
  https://github.com/midnightntwrk/compact/releases/latest/download/compact-installer.sh | sh
source ~/.bashrc
compact update

# Compile — produces ZK proving keys + TypeScript bindings
compact compile contracts/TimestampLog.compact contracts/managed/timestamp-log

# Serve the ZK keys to the browser (FetchZkConfigProvider reads them from /keys and /zkir)
cp -r contracts/managed/timestamp-log/keys ./public/keys
cp -r contracts/managed/timestamp-log/zkir ./public/zkir

# Start the local proof server (all tx submits go through this)
docker run -p 6300:6300 midnightntwrk/proof-server:latest midnight-proof-server -v

3. Lace wallet + provider bootstrap

src/lib/lace.ts
// src/lib/lace.ts — Lace wallet detection + provider bootstrap (client-only)
import { Buffer } from 'buffer'; (globalThis as any).Buffer = Buffer;
import { setNetworkId } from '@midnight-ntwrk/midnight-js-network-id';
import { FetchZkConfigProvider } from '@midnight-ntwrk/midnight-js-fetch-zk-config-provider';
import { httpClientProofProvider } from '@midnight-ntwrk/midnight-js-http-client-proof-provider';
import { indexerPublicDataProvider } from '@midnight-ntwrk/midnight-js-indexer-public-data-provider';
import semver from 'semver';

export async function initProviders() {
  const net = import.meta.env.VITE_NETWORK_ID ?? 'preview';
  setNetworkId(net);
  const lace = await new Promise<any>((res, rej) => {
    const t0 = Date.now();
    const iv = setInterval(() => {
      const w = Object.values((window as any).midnight ?? {}).find((x: any) =>
        x && 'apiVersion' in x && semver.satisfies(x.apiVersion, '4.x'));
      if (w) { clearInterval(iv); res(w); }
      else if (Date.now() - t0 > 5000) { clearInterval(iv); rej(new Error('Install Lace: https://www.lace.io/')); }
    }, 100);
  });
  const api = await lace.connect(net);
  const cfg = await api.getConfiguration();
  const zk = new FetchZkConfigProvider(window.location.origin, fetch.bind(window));
  return {
    connectedAPI: api,
    proofProvider: httpClientProofProvider(cfg.proverServerUri ?? import.meta.env.VITE_PROOF_SERVER_URL, zk),
    publicDataProvider: indexerPublicDataProvider(cfg.indexerUri, cfg.indexerWsUri),
    zkConfigProvider: zk,
  };
}
wallet · lace

Lace ships as a mobile wallet today (iOS / Android, the MetaMask / Phantom equivalent for the Cardano ecosystem) — but only for Cardano. Midnight support on mobile is not shipped yet, so for these demos install the Lace desktop browser extension and switch it to Midnight preview / preprod.

4. Pin artefacts to IPFS (optional)

src/lib/pinata.ts
// src/lib/pinata.ts — pin a Blob to IPFS, then commit the CID via Compact
export async function pinToIPFS(file: Blob, name = "artifact") {
  const fd = new FormData(); fd.append("file", file, name);
  const r = await fetch("https://api.pinata.cloud/pinning/pinFileToIPFS", {
    method: "POST",
    headers: { Authorization: `Bearer ${import.meta.env.VITE_PINATA_JWT}` },
    body: fd,
  });
  return (await r.json()).IpfsHash as string; // the CID — commit this to the ledger
}

Hackathon rules of thumb

  • · One mega-prompt = one build message. Don't iterate the architecture, iterate the UI.
  • · Always show the live Midnight explorer link in the UI — that's your proof.
  • · Design for 30–120s proof latency. Show a Proving state; keep the rest of the UI usable.
  • · Private-witness ideas: never send the 32-byte secret over the network. localStorage only.
  • · Add a "Built during the Creative AI & Quantum Hackathon — StreetKode Fam · Indian Krump Festival 14" line to your footer.