💃 Dance & Choreography · authenticity verification

Dance Move Provenance

Verify authenticity and origin of iconic dance moves onchain, gaslessly.

Lace wallet + tDUST· wallet UX
Section · Onchain

The primitive.

full primer →

The onchain primitive runs at the right moment in the flow and surfaces a clear, verifiable result that choreographers can act on without web3 jargon.

Why this primitiveThis idea fits Lace wallet + tDUST because wallet UX is exactly what a authenticity verification demo needs: detect `window.midnight`, call `initialAPI.connect('preview')`, let Lace balance the transaction with tDUST and submit i...

Kernel
detect `window.midnight`, call `initialAPI.connect('preview')`, let Lace balance the transaction with tDUST and submit it via `midnightProvider.submitTx`
Drives the UI as
a 'Connected via Lace' status pill, the shielded address, and a Proving → Submitting → Confirmed transaction trail
Appendix · Secrets

Required keys.

VITE_NETWORK_ID
`preview` (or `preprod`). Matches Lace's network setting.
open ↗
VITE_INDEXER_URL
Midnight GraphQL Indexer for public ledger reads.
open ↗
VITE_INDEXER_WS_URL
WebSocket endpoint for realtime ledger subscriptions.
open ↗
VITE_PROOF_SERVER_URL
Local Docker proof server (port 6300) — required for tx submits.
open ↗
VITE_DEFAULT_CONTRACT
Pre-deployed contract address so users skip the deploy step.
open ↗

Add these in your Lovable project under Settings → Secrets before pasting the prompt below.

Appendix · Mega-prompt

The build prompt.

Paste into a fresh Lovable project. Make sure all five secrets above are set first. read the build strategy →

The prompt below includes a self-contained Connect-Lace step (DApp Connector v4) — no extra setup needed on the target project.

Build "Dance Move Provenance" in ONE Lovable message. Single-page Midnight ZK demo.

CONCEPT
Verify authenticity and origin of iconic dance moves onchain, gaslessly.
Discipline: Dance & Choreography (authenticity verification).
Onchain primitive: Lace wallet + tDUST (wallet UX). Why this primitive: This idea fits Lace wallet + tDUST because wallet UX is exactly what a authenticity verification demo needs: detect `window.midnight`, call `initialAPI.connect('preview')`, let Lace balance the transaction with tDUST and submit i...

5-CREDIT BUDGET (HARD LIMIT):
- ONE single-page Vite + React app. No router, no Lovable Cloud, no database, no server-side auth.
- ONE Compact contract, ≤80 lines, deployed to Midnight preview testnet.
- Lace wallet is the auth + tx layer. `window.midnight` is polled; the shielded address is the identity.
- A locally-run proof server (Docker port 6300) is REQUIRED for any tx submit; the UI must show a
  "Proving… this can take 30–120s" state and stay usable while proofs generate.
- Pinata / IPFS only if the idea genuinely stores a file or artefact — then the CID is committed on-chain.
- At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY if AI is part of the idea).
- Skip tests, skip CI, skip docs pages. Ship the demo, nothing else.

STACK
- React + Vite single page (index route only).
- Midnight preview testnet. Compact language 0.23. MidnightJS SDK 4.1.1.
- Lace wallet is the sole auth surface — no Privy, no MetaMask, no OAuth.
- Local proof server (Docker port 6300) does all ZK proving. The UI shows Proving state.
- No SSR. All MidnightJS imports live behind `<ClientOnly>` + `useEffect`.

PACKAGES (all pinned to the versions Midnight ships together):
- @midnight-ntwrk/dapp-connector-api@4.0.1
- @midnight-ntwrk/midnight-js-contracts@4.1.1
- @midnight-ntwrk/midnight-js-types@4.1.1
- @midnight-ntwrk/midnight-js-protocol@4.1.1
- @midnight-ntwrk/midnight-js-network-id@4.1.1
- @midnight-ntwrk/midnight-js-fetch-zk-config-provider@4.1.1
- @midnight-ntwrk/midnight-js-http-client-proof-provider@4.1.1
- @midnight-ntwrk/midnight-js-indexer-public-data-provider@4.1.1
- @midnight-ntwrk/midnight-js-utils@4.1.1
- @midnight-ntwrk/compact-runtime@0.16.0
- rxjs fp-ts semver buffer pino
- vite-plugin-wasm  vite-plugin-top-level-await  (dev)

COMPACT TOOLCHAIN (one-time setup — the human runs this in a terminal, not Lovable):
```bash
curl --proto '=https' --tlsv1.2 -LsSf \
  https://github.com/midnightntwrk/compact/releases/latest/download/compact-installer.sh | sh
source ~/.bashrc && compact update
compact compile contracts/YourContract.compact contracts/managed/your-contract
cp -r contracts/managed/your-contract/keys public/keys
cp -r contracts/managed/your-contract/zkir public/zkir
docker run -p 6300:6300 midnightntwrk/proof-server:latest midnight-proof-server -v
```

VITE CONFIG (vite.config.ts) — WASM + top-level await are MANDATORY for MidnightJS:
```ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import wasm from 'vite-plugin-wasm';
import topLevelAwait from 'vite-plugin-top-level-await';
export default defineConfig({
  build: { target: 'esnext', commonjsOptions: { transformMixedEsModules: true, extensions: ['.js','.cjs'] } },
  plugins: [react(), wasm(), topLevelAwait()],
  optimizeDeps: {
    esbuildOptions: { target: 'esnext', supported: { 'top-level-await': true } },
    include: ['@midnight-ntwrk/compact-runtime'],
    exclude: ['@midnight-ntwrk/onchain-runtime-v3',
              '@midnight-ntwrk/onchain-runtime-v3/midnight_onchain_runtime_wasm_bg.wasm'],
  },
});
```

SSR RULE: never import a `@midnight-ntwrk/*` package at module scope of a route file — it uses
Node Buffer + browser globals + WASM top-level await and crashes SSR. Load providers behind
`useEffect` or a dynamic `import()` inside a `<ClientOnly>` boundary.

WALLET DETECT (src/lib/lace.ts) — poll window.midnight up to 5s:
```ts
import type { InitialAPI } from '@midnight-ntwrk/dapp-connector-api';
import semver from 'semver';
export async function waitForLace(timeoutMs = 5000): Promise<InitialAPI> {
  return new Promise((resolve, reject) => {
    const start = Date.now();
    const t = setInterval(() => {
      const m = (window as any).midnight ?? {};
      const w = Object.values(m).find((x: any) =>
        x && typeof x === 'object' && 'apiVersion' in x &&
        semver.satisfies(x.apiVersion, '4.x')) as InitialAPI | undefined;
      if (w) { clearInterval(t); resolve(w); return; }
      if (Date.now() - start > timeoutMs) { clearInterval(t);
        reject(new Error('Lace Midnight wallet not found. Install it: https://www.lace.io/')); }
    }, 100);
  });
}
```

BUFFER POLYFILL (src/main.tsx, MUST be the very first line):
```ts
import { Buffer } from 'buffer'; (globalThis as any).Buffer = Buffer;
```

PROVIDERS (src/lib/providers.ts) — chain Lace + proof server + indexer:
```ts
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 { waitForLace } from './lace';

export async function initProviders() {
  setNetworkId(import.meta.env.VITE_NETWORK_ID ?? 'preview');
  const lace = await waitForLace();
  const connectedAPI = await lace.connect(import.meta.env.VITE_NETWORK_ID ?? 'preview');
  const cfg = await connectedAPI.getConfiguration();
  const zk = new FetchZkConfigProvider(window.location.origin, fetch.bind(window));
  return {
    connectedAPI,
    zkConfigProvider: zk,
    proofProvider: httpClientProofProvider(cfg.proverServerUri ?? import.meta.env.VITE_PROOF_SERVER_URL, zk),
    publicDataProvider: indexerPublicDataProvider(cfg.indexerUri, cfg.indexerWsUri),
  };
}
```

READ-ONLY LEDGER FETCH (no wallet needed — great for public feeds):
```ts
const INDEXER = import.meta.env.VITE_INDEXER_URL;
export async function readLedger(address: string) {
  const r = await fetch(INDEXER, {
    method: 'POST', headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      query: `query($a:HexEncoded!){ contractAction(address:$a){ state } }`,
      variables: { address },
    }),
  });
  return (await r.json()).data?.contractAction?.state as string | null;
}
```

CONTRACT
```compact
// contracts/DanceMoveProvenance.compact
// Verify authenticity and origin of iconic dance moves onchain, gaslessly.
// Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14
pragma language_version 0.23;

import CompactStandardLibrary;

// Public ledger state — visible to everyone via the Indexer
export ledger entry_count: Counter;
export ledger last_message: Opaque<"string">;
export ledger last_author_commitment: Bytes<32>;

// Private callback wired from TypeScript; the returned bytes never touch chain
witness localSecretKey(): Bytes<32>;

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

export circuit authorCommitment(sk: Bytes<32>, seq: Bytes<32>): Bytes<32> {
  return persistentHash<Vector<3, Bytes<32>>>(
    [pad(32, "dance_move_provenance:author:"), seq, sk]
  );
}

export circuit appendEntry(newMessage: Opaque<"string">): [] {
  const sk = localSecretKey();
  const seq = entry_count as Field as Bytes<32>;
  last_author_commitment = disclose(authorCommitment(sk, seq));
  last_message = disclose(newMessage);      // disclose is REQUIRED before writing to ledger
  entry_count.increment(1);
}
```

FRONTEND FLOW — Lace is the whole auth story
1. Landing page shows one button: "Connect Lace". No email, no password, no OAuth.
2. On click: `initProviders()` → shielded address becomes the identity.
3. tDUST balance strip: `const bal = await connectedAPI.balanceAndProofOfBalance();` render it.
4. Every authenticity verification action = `contract.callTx.appendEntry(witnesses, payload)`. Show status:
   `Proving → Balancing (Lace adds tDUST fees) → Submitting → Confirmed` with a Midnight explorer link.
5. If `waitForLace` rejects, render the exact install URL for Lace with a friendly nudge —
   no fake "click here to install" placeholder. This is the single biggest onboarding drop-off.

RED FLAGS — DO NOT ATTEMPT:
- No bridging to Ethereum / any EVM chain. Midnight is a standalone L1; there is no bridge.
- No oracle / external HTTP data inside a circuit. Circuits are bounded and cannot do I/O.
- No sub-second finality UX. Proofs for k=14 circuits take 30–120s — build for that latency.
- No recursion in Compact. Loops must be bounded by compile-time constants.
- No SSR. MidnightJS uses browser globals + WASM + top-level-await; ClientOnly is mandatory.

REQUIRED SECRETS (Lovable → Project Settings → Secrets):
- VITE_NETWORK_ID           preview   (Midnight preview testnet; set to `preprod` for the more stable network)
- 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   (run `docker run -p 6300:6300 midnightntwrk/proof-server:latest midnight-proof-server -v`)
- VITE_DEFAULT_CONTRACT     the hex address printed by your first deploy — paste it here so users skip the deploy step

Faucets: Preview  https://midnight-tmnight-preview.nethermind.dev/  ·  Preprod  https://midnight-tmnight-preprod.nethermind.dev/  (dispense tNIGHT — generate tDUST in Lace)
Explorer: Preview  https://preview.midnightexplorer.com/  ·  Preprod  https://preprod.midnightexplorer.com/
Docs:    https://docs.midnight.network/llms-full.txt

CREDIT (must appear in UI footer AND as a header comment on every Compact contract):
Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14

--- BEGIN: Connect-Lace boilerplate (self-contained, DApp Connector v4) ---

Add a working "Connect Lace" button to the primary page of the app. This
section is self-contained — do NOT install any @midnight-ntwrk/* package for
the connect step; the Lace browser extension injects everything you need at
`window.midnight`. Follow the DApp Connector API v4 exactly as written below.

HARD RULES
- Do NOT call `enable()` or `state()` — DApp Connector v4.0 removed them.
- Do NOT read `window.midnight` at module scope or during SSR — only inside
  `useEffect`. Gate the panel render until after hydration.
- Do NOT hardcode the network — try Preview, Preprod, and Mainnet in order
  and use whichever `connect(networkId)` accepts.
- Reading a shielded address is a permission-only handshake — no signing,
  no funds moved.

CREATE FILE: src/lib/use-midnight-wallet.ts
```ts
import { useCallback, useEffect, useState } from "react";

export type WalletStatus =
  | "idle" | "detecting" | "ready" | "connecting" | "connected" | "error";

type Connector = {
  apiVersion: string;
  name?: string;
  connect: (networkId: string) => Promise<ConnectedApi>;
  isEnabled?: () => Promise<boolean>;
};

type ConnectedApi = {
  getShieldedAddresses?: () => Promise<string[] | Record<string, string>>;
  getUnshieldedAddress?: () => Promise<string>;
  getDustAddress?: () => Promise<string>;
  getConfiguration?: () => Promise<{
    indexerUri?: string; indexerWsUri?: string; proverServerUri?: string;
  }>;
};

function pickConnector(): Connector | null {
  if (typeof window === "undefined") return null;
  const m = (window as unknown as { midnight?: Record<string, Connector> }).midnight;
  if (!m) return null;
  for (const v of Object.values(m)) {
    if (v && typeof v === "object" && "apiVersion" in v && /^4\./.test(String(v.apiVersion))) {
      return v as Connector;
    }
  }
  const first = Object.values(m)[0];
  return first && "apiVersion" in first ? (first as Connector) : null;
}

function inferNetwork(addr: string): string {
  const m = addr.match(/^mn_(?:shield-)?addr_([a-z0-9]+?)1/i);
  if (!m) return "unknown";
  const s = m[1].toLowerCase();
  if (s === "test") return "preprod";
  if (s === "undeployed") return "preview";
  return s;
}

export function useMidnightWallet() {
  const [status, setStatus] = useState<WalletStatus>("idle");
  const [address, setAddress] = useState<string | null>(null);
  const [apiVersion, setApiVersion] = useState<string | null>(null);
  const [network, setNetwork] = useState<string | null>(null);
  const [error, setError] = useState<string | null>(null);
  const [tick, setTick] = useState(0);

  useEffect(() => {
    if (typeof window === "undefined") return;
    setStatus((p) => (p === "connected" ? p : "detecting"));
    setError(null);
    const t0 = Date.now();
    const iv = window.setInterval(() => {
      const c = pickConnector();
      if (c) {
        window.clearInterval(iv);
        setApiVersion(c.apiVersion);
        setStatus((p) => (p === "connected" ? p : "ready"));
        if (!/^4\./.test(c.apiVersion)) {
          setStatus("error");
          setError(`Lace connector ${c.apiVersion} is not compatible. Update Lace.`);
        }
      } else if (Date.now() - t0 > 5000) {
        window.clearInterval(iv);
        setStatus("error");
        setError("No Midnight wallet detected. Install Lace from lace.io.");
      }
    }, 100);
    return () => window.clearInterval(iv);
  }, [tick]);

  const connect = useCallback(async () => {
    try {
      setError(null);
      setStatus("connecting");
      const c = pickConnector();
      if (!c) throw new Error("No Midnight wallet detected.");
      const preferred = (import.meta.env.VITE_NETWORK_ID as string) || "preprod";
      const candidates = Array.from(new Set([preferred, "preview", "preprod", "mainnet"]));
      let api: ConnectedApi | null = null;
      let used: string | null = null;
      let mismatch: unknown = null;
      for (const n of candidates) {
        try { api = await c.connect(n); used = n; break; }
        catch (e) {
          const msg = e instanceof Error ? e.message : String(e);
          if (/network|mismatch/i.test(msg)) { mismatch = e; continue; }
          throw e;
        }
      }
      if (!api || !used) {
        throw new Error(
          mismatch
            ? "Lace is on a different network than this app supports. Switch Lace to Preview or Preprod and retry."
            : "Failed to connect to Lace.",
        );
      }
      let addr: string | null = null;
      if (typeof api.getShieldedAddresses === "function") {
        try {
          const s = await api.getShieldedAddresses();
          if (Array.isArray(s)) addr = s[0] ?? null;
          else if (s && typeof s === "object") addr = Object.values(s)[0] ?? null;
        } catch {}
      }
      if (!addr && typeof api.getUnshieldedAddress === "function") {
        try { addr = await api.getUnshieldedAddress(); } catch {}
      }
      if (!addr) throw new Error("Connected but couldn't read an address. Update Lace.");
      setAddress(addr);
      setNetwork(used ?? inferNetwork(addr));
      setApiVersion(c.apiVersion);
      setStatus("connected");
    } catch (e) {
      setError(e instanceof Error ? e.message : String(e));
      setStatus("error");
    }
  }, []);

  return {
    status, address, apiVersion, network, error,
    connect,
    disconnect: () => { setAddress(null); setNetwork(null); setStatus("ready"); setError(null); },
    redetect: () => setTick((n) => n + 1),
  };
}
```

CREATE FILE: src/components/WalletConnectPanel.tsx
```tsx
import { useEffect, useState } from "react";
import { useMidnightWallet } from "@/lib/use-midnight-wallet";

function truncate(a: string, h = 12, t = 8) {
  return a.length <= h + t + 1 ? a : `${a.slice(0, h)}…${a.slice(-t)}`;
}

export function WalletConnectPanel({ expectedNetwork = "preprod" }: { expectedNetwork?: string }) {
  const [hydrated, setHydrated] = useState(false);
  useEffect(() => setHydrated(true), []);
  const w = useMidnightWallet();
  const [copied, setCopied] = useState(false);
  useEffect(() => {
    if (!copied) return;
    const t = setTimeout(() => setCopied(false), 1400);
    return () => clearTimeout(t);
  }, [copied]);

  if (!hydrated) {
    return (
      <div className="p-5 border rounded-md">
        <div className="text-xs uppercase tracking-widest">connect lace</div>
        <div className="mt-3 h-10 bg-muted animate-pulse rounded" />
      </div>
    );
  }

  const wrong = w.status === "connected" && w.network && w.network !== "unknown" && w.network !== expectedNetwork;

  return (
    <div className="p-5 border rounded-md space-y-3">
      <div className="flex items-center justify-between gap-3">
        <span className="text-xs uppercase tracking-widest">connect lace</span>
        {w.apiVersion && <span className="text-[10px] font-mono opacity-60">connector v{w.apiVersion}</span>}
      </div>

      {w.status === "detecting" && <p className="text-sm opacity-70">Detecting Midnight wallet…</p>}

      {w.status === "ready" && (
        <div className="flex items-center gap-3 flex-wrap">
          <button onClick={() => void w.connect()}
            className="px-4 py-2 bg-primary text-primary-foreground text-xs font-semibold uppercase tracking-wider rounded">
            Connect wallet
          </button>
          <span className="text-xs opacity-70">Reads your shielded address — no signing, no funds moved.</span>
        </div>
      )}

      {w.status === "connecting" && <p className="text-sm opacity-70">Approve the connection in Lace…</p>}

      {w.status === "connected" && w.address && (
        <div className="space-y-2">
          <div className="text-[10px] uppercase tracking-widest opacity-60">shielded address</div>
          <div className="flex items-center gap-2 flex-wrap">
            <code className="font-mono text-xs break-all">{truncate(w.address, 16, 12)}</code>
            <button onClick={() => { void navigator.clipboard.writeText(w.address ?? ""); setCopied(true); }}
              className="text-[10px] uppercase tracking-widest text-primary">
              {copied ? "copied" : "copy"}
            </button>
          </div>
          <div className="flex items-center gap-4 text-[11px] flex-wrap">
            <span>network · <span className="font-mono">{w.network}</span></span>
            <button onClick={w.disconnect} className="text-[10px] uppercase tracking-widest opacity-60">disconnect</button>
          </div>
          {wrong && (
            <p className="text-[12px] opacity-80">
              Lace is on <span className="font-mono">{w.network}</span> but this app expects{" "}
              <span className="font-mono">{expectedNetwork}</span>. Switch networks inside Lace.
            </p>
          )}
        </div>
      )}

      {w.status === "error" && (
        <div className="space-y-2">
          <p className="text-sm opacity-80">{w.error ?? "Something went wrong."}</p>
          <div className="flex gap-3 flex-wrap">
            <button onClick={w.redetect} className="px-3 py-2 border text-[10px] uppercase tracking-widest rounded">Retry</button>
            <a href="https://www.lace.io/" target="_blank" rel="noreferrer"
              className="px-3 py-2 border text-[10px] uppercase tracking-widest rounded">Install Lace ↗</a>
          </div>
        </div>
      )}
    </div>
  );
}
```

MOUNT on the primary page (e.g. src/routes/index.tsx or wherever the main
demo lives):
```tsx
import { WalletConnectPanel } from "@/components/WalletConnectPanel";

// Inside your JSX:
<WalletConnectPanel expectedNetwork={import.meta.env.VITE_NETWORK_ID || "preprod"} />
```

PREREQUISITES to tell the end user in your UI copy:
1. Install Lace from https://www.lace.io/ (desktop browser extension).
2. Switch Lace to Midnight Preview or Preprod.
3. Get tNIGHT from the matching faucet, then click "Generate tDUST" in Lace
   to delegate — deploys and shielded writes spend tDUST, not tNIGHT.

--- END: Connect-Lace boilerplate ---
Appendix · Market

Market sizing.

TAM
$5B
dance IP protection
SAM
$550M
digital dance authenticity tools
SOM
$28M
verified dance creators

Indicative figures for hackathon pitches — refine with your own research before raising.

See also

Adjacent entries.