DanceMove Provenance
Trace and display the lineage and remix history of dance moves on an immutable ledger.
The primitive.
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 Compact ZK contract because onchain logic is exactly what a creative lineage demo needs: a Compact `.compact` contract compiled to ZK proving/verifying keys and deployed to the Midnight preview testnet, with p...
Required keys.
Add these in your Lovable project under Settings → Secrets before pasting the prompt below.
The build prompt.
PreviewFastest to demo. Testnet resets often. Faucet: nethermind.dev preview.
Paste into a fresh Lovable project. Make sure the secrets for this target are set first. read the build strategy →
The prompt is self-contained: Connect-Lace (DApp Connector v4), a real scripts/ folder with deploy-midnight.mjs, an in-app one-time setup panel, and — for Undeployed — the exact steps to fund Lace from the local genesis wallet.
VERSION SOURCE OF TRUTH (paste this block at the top of every generated README and every setup panel):
The official Midnight Support Matrix is the source of truth for all versions used below:
https://docs.midnight.network/relnotes/support-matrix
Snapshot as of 2026-07-23:
- Midnight Node: 1.0.1 (Preview) / 1.0.0 (Preprod) / 1.0.0 (Mainnet)
- Midnight Indexer: 4.3.3
- Proof server (public networks): 8.1.0
- Midnight.js packages: 4.1.1
- Wallet SDK: 1.2.0
- DApp Connector API: 4.0.1
- testkit-js: 4.1.1
- Compact toolchain: 0.31.1 (pragma language_version 0.23)
- Compact runtime: 0.16.0
- On-chain runtime: 3.0.0
- Local Undeployed stack images: proof-server:8.0.3, midnight-node:0.22.5, indexer-standalone:4.0.2
If the matrix and this prompt disagree, the matrix wins. Re-check the matrix before installing any pinned package.
Build "DanceMove Provenance" in ONE Lovable message. Single-page Midnight ZK demo.
TARGET NETWORK: **Preview testnet** (VITE_NETWORK_ID = `preview`)
This is one of FOUR variants of the same idea — Preview / Preprod / Undeployed / Mainnet. Only the network
config, secrets, signing surface, and disclaimers differ. Contract + UI + Lace flow are otherwise identical.
CONCEPT
Trace and display the lineage and remix history of dance moves on an immutable ledger.
Discipline: Dance & Choreography (creative lineage).
Onchain primitive: Compact ZK contract (onchain logic). Why this primitive: This idea fits Compact ZK contract because onchain logic is exactly what a creative lineage demo needs: a Compact `.compact` contract compiled to ZK proving/verifying keys and deployed to the Midnight preview testnet, with p...
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 Midnight Support Matrix row; re-check the matrix before installing):
- @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
- @midnight-ntwrk/wallet-sdk@1.2.0
- @midnight-ntwrk/wallet-sdk-address-format@1.0.0 (pin to the wallet-sdk release)
- @midnight-ntwrk/wallet-sdk-hd@3.1.0-beta.1 (pin to the wallet-sdk release)
- @midnight-ntwrk/testkit-js@4.1.1 (for Node deploy / faucet scripts)
- @midnight-ntwrk/ledger-v8@8.1.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
# Bake artefact copy into `bun run compile` from day one — the browser drifts silently otherwise.
mkdir -p public/contract && cp -r contracts/managed/your-contract/keys \
contracts/managed/your-contract/zkir contracts/managed/your-contract/contract public/contract/
# Proof server: PIN THE TAG. For public networks use the matrix tag (8.1.0); for local Undeployed use 8.0.3.
docker run -p 6300:6300 midnightntwrk/proof-server:8.1.0 midnight-proof-server -v
# Lifecycle:
# docker ps — confirm 6300:6300 mapping
# docker logs -f <container> — tail proofs
# docker stop / docker start <container> — pause / resume (image cached)
# Gate: "Cannot connect to the Docker daemon at unix:///var/run/docker.sock"
# = Docker Desktop / colima is NOT started. Start it, wait for the whale icon, retry.
```
macOS prerequisites (run BEFORE the block above):
```bash
# Docker Desktop (Apple Silicon or Intel):
brew install --cask docker # or download from docker.com/products/docker-desktop
open -a Docker # wait for the whale icon in the menu bar to go steady
# Node.js LTS (for bun scripts + npx):
brew install node # or nvm install --lts
```
Apple Silicon note: proof-server image is multi-arch — no --platform flag needed. If Docker
Desktop stalls at "Starting", quit + reopen; if that fails, reset to factory defaults from
the Troubleshoot menu. Copy-button walkthrough: https://midnightprompts.lovable.app/proof-server#docker-setup
WALLET DERIVATION — use midnight-wallet-cli (fastest, recommended by Midnight dev-rel):
Do NOT hand-roll seed → bech32 derivation with wallet-sdk + address encoders. Ship the community CLI as the one-liner path, keep a scripts/derive-unshielded-address.mjs offline fallback.
```bash
npm i -g midnight-wallet-cli
# --seed = 64-char HEX master seed (32 bytes), NOT a BIP-39 mnemonic
mn address --seed <64-hex-master-seed> --network preprod
# → mn_addr_preprod1… (paste into the preprod faucet)
mn balance <mn_addr_preprod1…> --network preprod
```
Rules:
- `--network` accepts `preprod`, `preview`, `undeployed`, `mainnet`.
- Prints the UNSHIELDED bech32 address the faucet needs (`mn_addr_…`). Shielded (`mn_shield-addr_…`) is a different identity — the faucet rejects it.
- Never accept a user's seed / mnemonic in chat or logs. Run this locally only.
- Same output Lace produces from the same seed → safe to fund the printed address.
References: https://www.npmjs.com/package/midnight-wallet-cli · https://github.com/nel349/midnight-wallet-cli
EXPERIMENTAL DAPP DISCLAIMER (MANDATORY on ALL networks, non-negotiable on Mainnet):
This dapp is a hackathon artefact — vibe-coded, not audited, not reviewed by security professionals.
Every generated project MUST ship both of these UI + docs surfaces:
1. README.md — top-of-file block, verbatim:
```markdown
> ⚠️ **Experimental / vibe-coded — not audited.**
> This dapp was built in a hackathon sprint with AI assistance. Contract logic, key handling,
> and UI have NOT been reviewed by security professionals. Do not deposit funds you cannot
> afford to lose. On Mainnet, use only as a proof-of-deploy bragging right. Prefer the
> Undeployed → Preprod → Preview dry-run path before touching Mainnet.
```
2. Persistent in-app top banner — create `src/components/ExperimentalBanner.tsx` and mount it
in the app root (above `<Outlet />` on TanStack Start, or at the top of the single page on plain Vite):
```tsx
// src/components/ExperimentalBanner.tsx
import { useEffect, useState } from 'react';
const VARIANTS = {
mainnet: { bg: 'bg-red-600', fg: 'text-white', msg: 'MAINNET · vibe-coded experiment — funds at risk, no audit. Use only for bragging-right proof-of-deploy.', dismissible: false },
preview: { bg: 'bg-amber-500', fg: 'text-black', msg: 'Testnet (Preview) · experimental hackathon build. Not audited.', dismissible: true },
preprod: { bg: 'bg-amber-500', fg: 'text-black', msg: 'Testnet (Preprod) · experimental hackathon build. Not audited.', dismissible: true },
undeployed: { bg: 'bg-slate-700', fg: 'text-slate-100', msg: 'Local dev chain (Undeployed) · not real value. Experimental build.', dismissible: true },
} as const;
export function ExperimentalBanner() {
const net = (import.meta.env.VITE_NETWORK_ID ?? 'preview') as keyof typeof VARIANTS;
const v = VARIANTS[net] ?? VARIANTS.preview;
const key = `experimental-banner-dismissed-${net}`;
const [hidden, setHidden] = useState(false);
useEffect(() => {
if (v.dismissible && sessionStorage.getItem(key) === '1') setHidden(true);
}, [key, v.dismissible]);
if (hidden) return null;
return (
<div role="alert" className={`${v.bg} ${v.fg} px-4 py-2 text-center text-sm font-semibold sticky top-0 z-50 flex items-center justify-center gap-3`}>
<span>⚠️ {v.msg}</span>
{v.dismissible && (
<button
type="button"
className="underline opacity-80 hover:opacity-100"
onClick={() => { sessionStorage.setItem(key, '1'); setHidden(true); }}
>
dismiss
</button>
)}
</div>
);
}
```
Rules:
- Mainnet banner is **NON-DISMISSIBLE**. Do not add an escape hatch.
- Preview/Preprod/Undeployed banners dismiss per session (sessionStorage), not permanently.
- Never remove the banner "because it looks better" — it is a safety surface, not a style choice.
- On Mainnet, also render a "no audit · experimental" chip next to every write button.
SCRIPTS FOLDER — MANDATORY (all variants):
Every generated app MUST contain a real `scripts/` folder. Do NOT reference
any script from README.md that does not exist on disk — regenerate the README
from the actual `scripts/` contents at the end.
CREATE FILE: scripts/deploy-midnight.mjs — every gotcha baked in. Nine rules,
memorize them:
① Force `ttlOneHour()` in BOTH `walletProvider.balanceTx` AND
`midnightProvider.balanceTx`. The contracts SDK calls balanceTx without a
TTL; the dust wallet then crashes with
`undefined is not an object (evaluating 'arg0.getTime')`.
② `ZK_CONFIG_PATH = path.resolve(__dirname, '..', 'contracts/managed/<name>')`.
Missing `..` → ENOENT on `scripts/contracts/managed/…`.
③ Standalone genesis funds seed `0x000…0002` (SECOND slot, not the first).
Wrong seed → `Insufficient Funds: could not balance dust`.
④ Password for privateStoragePasswordProvider needs ≥3 of
{upper, lower, digit, symbol}. `Choreo-Kits-Local-2026!` passes; a
lowercase-only password fails with "Found: 2".
⑤ `await new Promise(r => setTimeout(r, 15000))` after `wallet.start()` so
the wallet sees the genesis balance before you deploy.
⑥ Adapter must inject TTL — contracts SDK calls `balanceTx` with no TTL.
⑦ Provide an explicit witness object `{ localSecretKey: (ctx) => [ctx, key] }`
on the `Contract` instance. `withVacantWitnesses` does NOT satisfy a
contract that declares any witnesses.
⑧ Retry `deployContract` up to 8× with a 10 s backoff AND a FRESH
`privateStateId` per attempt — the wallet-sync race is real.
⑨ `initialPrivateState: { localSecretKey: <32-byte Uint8Array> }` is
REQUIRED or the constructor throws
`does not contain a function-valued field named localSecretKey`.
```js
// Local Node ESM deploy script. Runs on the developer's machine — never in
// the browser or a Cloudflare Worker. Requires the local proof server + a
// running node + indexer (Undeployed) OR a funded Lace on preview/preprod.
//
// VITE_NETWORK_ID=undeployed bun scripts/deploy-midnight.mjs
// VITE_NETWORK_ID=preprod bun scripts/deploy-midnight.mjs
//
// Writes src/data/midnight-contract.<network>.json so the app hydrates.
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { setNetworkId, NetworkId } from '@midnight-ntwrk/midnight-js-network-id';
import { deployContract } from '@midnight-ntwrk/midnight-js-contracts';
import { httpClientProofProvider } from '@midnight-ntwrk/midnight-js-http-client-proof-provider';
import { indexerPublicDataProvider } from '@midnight-ntwrk/midnight-js-indexer-public-data-provider';
import { NodeZkConfigProvider } from '@midnight-ntwrk/midnight-js-node-zk-config-provider';
import { levelPrivateStateProvider } from '@midnight-ntwrk/midnight-js-level-private-state-provider';
import { ttlOneHour } from '@midnight-ntwrk/midnight-js-utils'; // ← ①
import { WalletBuilder } from '@midnight-ntwrk/wallet';
import { Contract } from '../public/contract/contract/index.cjs';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const NET = process.env.VITE_NETWORK_ID ?? 'undeployed';
// Map VITE_NETWORK_ID → NetworkId. Preview reuses Undeployed. Use ONE across encoders.
const NETWORK_ID = ({
undeployed: NetworkId.Undeployed,
preview: NetworkId.Undeployed,
preprod: NetworkId.TestNet,
mainnet: NetworkId.MainNet,
})[NET];
setNetworkId(NETWORK_ID);
const contractName = process.env.MIDNIGHT_CONTRACT ?? 'timestamp-log';
// ② Resolve ZK config from PROJECT ROOT, not scripts/
const ZK_CONFIG_PATH = path.resolve(__dirname, '..', 'contracts', 'managed', contractName);
if (!fs.existsSync(ZK_CONFIG_PATH)) {
console.error(`Missing ${ZK_CONFIG_PATH}. Run: bun run midnight:compile`);
process.exit(1);
}
// ③ Genesis-funded seed on Undeployed (…0002, NOT …0001). On preview/preprod
// the human uses their Lace wallet — swap this for a headless wallet seed
// provided via MIDNIGHT_WALLET_SEED shell env (never in code).
const SEED = NET === 'undeployed'
? '0000000000000000000000000000000000000000000000000000000000000002'
: process.env.MIDNIGHT_WALLET_SEED;
if (!SEED) { console.error('Set MIDNIGHT_WALLET_SEED for non-undeployed deploys'); process.exit(1); }
// ④ ≥3 character classes
const PRIVATE_STORAGE_PASSWORD = 'Midnight-Local-Dev-2026!';
const deployerSecret = crypto.getRandomValues(new Uint8Array(32));
const wallet = await WalletBuilder.buildFromSeed(
process.env.VITE_INDEXER_URL,
process.env.VITE_INDEXER_WS_URL,
process.env.VITE_PROOF_SERVER_URL,
process.env.VITE_NODE_WS ?? 'ws://localhost:9944',
SEED,
NETWORK_ID,
);
wallet.start();
await new Promise(r => setTimeout(r, 15000)); // ⑤
const baseProviders = {
privateStateProvider: levelPrivateStateProvider({ privateStateStoreName: 'midnight-priv' }),
publicDataProvider: indexerPublicDataProvider(process.env.VITE_INDEXER_URL, process.env.VITE_INDEXER_WS_URL),
zkConfigProvider: new NodeZkConfigProvider(ZK_CONFIG_PATH),
proofProvider: httpClientProofProvider(process.env.VITE_PROOF_SERVER_URL),
privateStoragePasswordProvider: { get: async () => PRIVATE_STORAGE_PASSWORD },
walletProvider: {
coinPublicKey: wallet.state().coinPublicKey,
// ⑥ TTL injected here — contracts SDK calls this without one
balanceTx: (tx, newCoins) => wallet.balanceTransaction(tx, newCoins, ttlOneHour()),
},
midnightProvider: {
submitTx: (tx) => wallet.submitTransaction(tx),
balanceTx: (tx, newCoins) => wallet.balanceTransaction(tx, newCoins, ttlOneHour()),
},
};
// ⑦ Explicit witness — do NOT use withVacantWitnesses when the contract declares any
const contractInstance = new Contract({
localSecretKey: (ctx) => [ctx, deployerSecret],
});
// ⑧ Retry loop with a fresh privateStateId every attempt
let deployed;
for (let i = 0; i < 8; i++) {
try {
deployed = await deployContract(
{ ...baseProviders, privateStateId: `deploy-${Date.now()}-${i}` },
{
contract: contractInstance,
initialPrivateState: { localSecretKey: deployerSecret }, // ⑨
},
);
break;
} catch (e) {
if (i === 7) throw e;
console.warn(`Deploy attempt ${i + 1} failed: ${e.message}. Retrying in 10s…`);
await new Promise(r => setTimeout(r, 10000));
}
}
const address = deployed.deployTxData.public.contractAddress;
const out = {
network: NET,
address,
deployTx: deployed.deployTxData.public.txHash,
deployedAt: new Date().toISOString(),
};
const outPath = `src/data/midnight-contract.${NET}.json`;
fs.mkdirSync(path.dirname(outPath), { recursive: true });
fs.writeFileSync(outPath, JSON.stringify(out, null, 2));
console.log(`✓ wrote ${outPath}`);
console.log(` address: ${address}`);
console.log(` paste into VITE_DEFAULT_CONTRACT`);
process.exit(0);
```
CREATE FILE: scripts/check-midnight-wallet.mjs — a "wallet doctor" that reads
`MIDNIGHT_WALLET_SEED` from the shell env (NEVER accept a seed phrase in chat)
and prints ONLY public addresses + tDUST balance. Refuse to log the seed.
CREATE FILE (Undeployed variants only): scripts/fund-lace.sh — one-shot Lace
funder that clones midnightntwrk/midnight-local-dev and launches its
interactive faucet CLI. See FUND LACE ON UNDEPLOYED below for the exact
contents and menu-option-2 walkthrough. Wire it up as `bun run midnight:fund`.
CREATE FILE: scripts/README.md — list every script (`deploy-midnight.mjs`,
`check-midnight-wallet.mjs`, `midnight-standalone.mjs`, and for Undeployed
also `fund-lace.sh`), its inputs, and when to run it. The Undeployed section
of README.md MUST spell out the funding flow verbatim: run `bun run midnight:fund`,
choose menu option 2 ("Fund accounts by public key"), paste the Lace UNSHIELDED
address (`mn_addr_undeployed1…`), receive 50,000 tNIGHT, tap "Generate tDUST"
in Lace, wait one block. If you add or remove a script, update this file in
the same commit.
DEPENDENCIES the deploy script needs (bun add BEFORE first run — Node ESM
scripts are NOT bundled by Vite; every import must be a real dep). These align
to the Midnight Support Matrix and the active Lovable Midnight skill:
bun add @midnight-ntwrk/midnight-js-contracts@4.1.1 \
@midnight-ntwrk/midnight-js-network-id@4.1.1 \
@midnight-ntwrk/midnight-js-types@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-node-zk-config-provider@4.1.1 \
@midnight-ntwrk/midnight-js-level-private-state-provider@4.1.1 \
@midnight-ntwrk/midnight-js-utils@4.1.1 \
@midnight-ntwrk/wallet@4.0.0 \
@midnight-ntwrk/wallet-sdk@1.2.0 \
@midnight-ntwrk/wallet-sdk-address-format@1.0.0 \
@midnight-ntwrk/wallet-sdk-hd@3.1.0-beta.1 \
@midnight-ntwrk/testkit-js@4.1.1 \
@midnight-ntwrk/ledger-v8@8.1.0 \
bip39
Add a `compile` script to package.json that CHAINS compile → artefact copy →
docker up → deploy so the human runs a single command:
"scripts": {
"midnight:compile": "compact compile contracts/YourContract.compact contracts/managed/your-contract",
"midnight:artefacts": "rm -rf public/contract && mkdir -p public/contract && cp -r contracts/managed/your-contract/keys contracts/managed/your-contract/zkir contracts/managed/your-contract/contract public/contract/",
"midnight:up": "bun scripts/midnight-standalone.mjs up",
"midnight:fund": "bash scripts/fund-lace.sh",
"midnight:deploy": "bun scripts/deploy-midnight.mjs",
"compile": "bun midnight:compile && bun midnight:artefacts && bun midnight:up && bun midnight:deploy"
}
For the Undeployed variant ALSO create scripts/midnight-standalone.mjs — a
thin wrapper around `docker compose` that writes
`.midnight/standalone.docker-compose.yml` (see the canonical yaml in the
LOCAL STACK SETUP block above, tagged proof-server:8.0.3, midnight-node:0.22.5,
indexer-standalone:4.0.2), brings up node + indexer + proof-server, and polls
readiness. See https://midnightprompts.lovable.app/undeployed for a reference
implementation that can be copied verbatim.
VITE CONFIG (vite.config.ts) — WASM + top-level await are MANDATORY for MidnightJS.
Use the `noDiscovery` + explicit-include/exclude shape below. Do NOT put
`@midnight-ntwrk/compact-runtime` in `include` — dep-pre-bundling then crawls the WASM graph
and blocks the client entry for MINUTES on `/.vite/deps/react.js` (blank dev page).
```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';
import type { Plugin } from 'vite';
// On TanStack Start, restrict top-level-await to the CLIENT env — applied to the SSR
// bundle it crashes workerd with "Identifier '__tla' has already been declared".
function clientTopLevelAwait(): Plugin {
return { ...topLevelAwait(), applyToEnvironment: (env) => env.name === 'client' };
}
export default defineConfig({
build: { target: 'esnext', commonjsOptions: { transformMixedEsModules: true, defaultIsModuleExports: 'auto' } },
plugins: [react(), wasm(), clientTopLevelAwait()],
resolve: { conditions: ['browser', 'import', 'default'] },
ssr: { resolve: { conditions: ['browser', 'node', 'import', 'default'] } },
optimizeDeps: {
noDiscovery: true,
esbuildOptions: { target: 'esnext', supported: { 'top-level-await': true } },
include: [
'react', 'react-dom', 'react-dom/client',
'react/jsx-runtime', 'react/jsx-dev-runtime',
'buffer', 'object-inspect', 'cross-fetch', '@subsquid/scale-codec',
],
exclude: [
'@midnight-ntwrk/compact-runtime',
'@midnight-ntwrk/onchain-runtime-v3',
'@midnight-ntwrk/onchain-runtime-v3/midnight_onchain_runtime_wasm_bg.wasm',
'@midnight-ntwrk/midnight-js-contracts',
'@midnight-ntwrk/midnight-js-http-client-proof-provider',
'@midnight-ntwrk/midnight-js-indexer-public-data-provider',
'@midnight-ntwrk/midnight-js-node-zk-config-provider',
'@midnight-ntwrk/midnight-js-level-private-state-provider',
'@midnight-ntwrk/midnight-js-network-id',
'@midnight-ntwrk/midnight-js-utils',
'@midnight-ntwrk/wallet',
'@midnight-ntwrk/wallet-sdk-hd',
],
},
});
```
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.
TANSTACK START SSR STUB (Cloudflare Worker target) — MANDATORY when publishing. Keep nitro
ENABLED (do NOT set `nitro: false`; that splits SSR into chunks the Worker can't resolve).
Add a Vite plugin that swaps every `@midnight-ntwrk/*` import AND your client contract module
(e.g. `src/lib/contract.ts`) to inert stubs during the SSR pass — otherwise the SSR crawler
still walks the WASM graph even for `ssr: false` routes and dies with `MISSING_EXPORT`.
```ts
import path from 'node:path';
function midnightSsrStub(): Plugin {
const wasmStub = path.resolve('src/lib/midnight-ssr-stub.ts');
const contractStub = path.resolve('src/lib/contract.ssr-stub.ts');
const contractReal = path.resolve('src/lib/contract.ts');
return {
name: 'midnight-ssr-stub', enforce: 'pre',
async resolveId(id, importer, options) {
if (!options?.ssr) return;
if (id.startsWith('@midnight-ntwrk/')) return wasmStub;
const resolved = await this.resolve(id, importer, { ...options, skipSelf: true });
if (resolved && resolved.id === contractReal) return contractStub;
return resolved;
},
};
}
// Add to plugins BEFORE react(): [midnightSsrStub(), react(), wasm(), clientTopLevelAwait()]
// Ship matching empty stubs: src/lib/midnight-ssr-stub.ts (`export default {}`) and
// src/lib/contract.ssr-stub.ts that re-exports inert stand-ins for every symbol the
// route imports (publishKit, decodeChainState, KitPayload, loadContractModule, etc.).
```
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;
}
```
ASYNC BUFFER CLIENT ENTRY (TanStack Start) — replaces the module-scope Buffer polyfill.
Vite dep pre-bundling crawls the heavy Midnight WASM graph and can hang the client entry for
minutes on `/.vite/deps/react.js` (blank dev page). Fix: a custom `src/client.tsx` that polyfills
Buffer ASYNCHRONOUSLY, AFTER the lightweight hydrate path is resolved. Module-scope
`globalThis.Buffer = Buffer` races hydration when the optimizer is still crawling.
```tsx
// src/client.tsx
import { hydrateRoot } from 'react-dom/client';
import { Buffer } from 'buffer';
async function start() {
(globalThis as any).Buffer = Buffer;
const { StartClient } = await import('@tanstack/react-start/client');
hydrateRoot(document, <StartClient />);
}
start();
```
Wire it in `vite.config.ts`:
```ts
tanstackStart: { client: { entry: 'client' } },
```
Keep SSR ON the shell route so the header renders in <2s; gate only Midnight-heavy widgets
(wallet, contract writes) behind `<ClientOnly>` and dynamic `import()` inside `useEffect`.
SIGNING STRATEGY — Undeployed vs Preview/Preprod (hard-won lessons from Choreo Kits + ChoreoCrowd Fund):
Mental model — burn this in before writing a line of code:
```text
Undeployed: UI → POST /api/append-entry → genesis wallet (server) → chain
Other nets: UI → LaceWalletProvider → Lace signs in browser → chain
Reads (all): fetchPublicContractLedger via indexer (no wallet needed)
```
| Mode | Signing | UI wallet |
| --- | --- | --- |
| `undeployed` | **Server-side** genesis wallet via `/api/append-entry` (or `/api/mint`, name it after your circuit) | Lace optional / limited |
| `preview` / `preprod` | Lace `publishKit` / `callTx` in the browser | Full Lace flow |
| `mainnet` | Lace only — NEVER server-side | Full Lace flow, user-initiated |
**Lace CANNOT sign on Undeployed.** Per Midnight docs, Lace cannot balance/sign on the local
`undeployed` chain (only Preview/Preprod/Mainnet). Symptoms: ZK proof completes, Lace's "Prove
transaction" dialog spins forever OR submit fails with `Unexpected error submitting scoped
transaction '<unnamed>': Error` — even with tDUST funded.
Fix on Undeployed: route every write through a TanStack server route
(`src/routes/api/append-entry.ts` → `src/lib/append-entry.server.ts`) that reuses the same
`WalletBuilder` + genesis seed `…0002` as `scripts/deploy-midnight.mjs`. Cache the wallet +
providers in a module-scope `ctxPromise` so the first call warms them and subsequent calls are
fast. Frontend detects `import.meta.env.VITE_NETWORK_ID === "undeployed"` and POSTs
`{ contractAddress, ...fields }` to `/api/append-entry` instead of calling Lace. Skip the
Lace-connect and tDUST-balance guards on Undeployed entirely.
SHARED CONSTANTS — MUST stay identical across deploy script AND server-append route.
Mismatch = `RpcError 1010: Invalid Transaction: Custom error: 117` (see RED FLAGS below).
```ts
// src/lib/midnight-shared.ts — import from BOTH deploy script and server route
export const GENESIS_SEED = '0000000000000000000000000000000000000000000000000000000000000002';
export const PRIVATE_STATE_STORE = 'my-app-priv'; // pick one name, use it EVERYWHERE
```
Then in `scripts/deploy-midnight.mjs` AND `src/lib/append-entry.server.ts`:
```ts
import { GENESIS_SEED, PRIVATE_STATE_STORE } from '@/lib/midnight-shared';
initializeMidnightProviders({ privateStateStoreName: PRIVATE_STATE_STORE, /* … */ });
```
Why: `findDeployedContract` reads/writes the deployer's signing key in a LevelDB store keyed by
`privateStateStoreName` + contract address. A mismatched store name → no key found → SDK samples
a FRESH signing key → on-chain contract authority does not match → chain rejects with error 117.
Debug tip: log the first/last 8 chars of `deployed.deployTxData.private.signingKey` on both
deploy and append; they must match byte-for-byte.
LEDGER READ CALL SHAPE — pass the INNER state, never the wrapper:
```ts
// After getPublicStates():
const { contractState } = await getPublicStates(publicDataProvider, address);
const onChain = ledger(contractState.data); // ← .data, not contractState
// Right after a successful callTx:
const onChain = ledger(result.public.nextContractState);
```
Passing the raw `ContractState` wrapper throws `expected instance of ChargedState` from the
compiled contract helpers.
RECOVERY AFTER DOCKER RESET — the address in `src/data/midnight-contract.undeployed.json` is
invalidated with the chain state. Do this every time:
```bash
bun run midnight:down
bun run midnight:up
bun run midnight:deploy # refreshes midnight-contract.undeployed.json
# restart the dev server so it re-imports the JSON
```
Also invalidate the server route's `ctxPromise` cache whenever the contract address changes
(re-read the JSON on each request, or re-init when `address !== cachedAddress`) — otherwise the
2nd+ append silently uses the previous contract and fails with error 117 or a stale-state error.
VITE optimizeDeps — the server-append path pulls Node-only deps transitively. Add them to
`optimizeDeps.exclude` in `vite.config.ts` or the dev server hangs on "Loading …":
```ts
optimizeDeps: {
exclude: [
// … existing @midnight-ntwrk/* entries …
'@midnight-ntwrk/testkit-js',
'pino',
'ws',
'ssh2',
'cpu-features',
],
}
```
Cloudflare build: add `src/lib/append-entry.server.ts` → `src/lib/append-entry.ssr-stub.ts` to
the `midnightSsrStub()` swap list. The stub just returns 500 with a clear "dev-only" message —
the published Worker cannot reach the local Docker stack anyway. Gate the stub on
`command === "build"` so dev SSR still loads real Midnight libs for the API route.
UX NOTE — a disabled "Prove & submit" button is almost always empty form fields, not a wallet
bug. Show a tooltip on hover ("Fill in {project name} and {amount} to enable") so the user
doesn't chase a phantom Lace / tDUST problem.
NFT / MARKETPLACE LEDGER DESIGN — HARD-WON LESSONS (`zealymidnight` / MoveNft rail)
Apply this whenever the idea mints tokens, tickets, licences, certificates, receipts, or resells anything.
1. PUBLIC LEDGER MAPS ARE INSERT-ONLY / APPEND-ONLY IN v1. THIS IS THE BIG ONE.
Writing to a key that ALREADY exists makes the dust fee balancer panic on the NEXT callTx:
`RuntimeError: unreachable` inside `wasm.transaction_feesWithMargin` / `transaction_merge`.
The first tx "succeeds", the second one dies and the whole rail looks broken.
Model every mutation as a NEW key:
```compact
export ledger tokens: Map<Bytes<32>, Bytes<32>>; // tokenId -> metadata/CID hash (insert once)
export ledger listings: Map<Bytes<32>, Field>; // tokenId -> price (insert once)
export ledger sales: Map<Bytes<32>, Bytes<32>>; // saleId -> buyer commitment (append only)
```
`mint` inserts into `tokens`; `listSale` inserts into `listings`; `buy` appends to `sales`
under a FRESH random `saleId` (`crypto.getRandomValues(new Uint8Array(32))`) — it never
rewrites `tokens[tokenId]`. Current ownership is derived off-chain: replay `sales` from the
indexer, or mirror it in the server JSON (`src/data/<app>-owners.json`). The chain stays the
audit log; the mirror is the index.
2. DO NOT PORT ERC-721 INTO COMPACT. `_owners[tokenId] = newOwner` is EXACTLY the overwrite in
rule 1. There is no ERC-721 on Midnight. Ownership transfer = append a sale row.
3. `list` IS A RESERVED COMPACT KEYWORD. `export circuit list(...)` fails to compile; worse, if
you compile as `listSale` and call `callTx.list(...)` from TypeScript you get an undefined
entry point and a silent no-op. Name the circuit `listSale` and call `callTx.listSale(...)`.
Verify the entry point that actually landed via the indexer:
`contractAction(address:$a){ ... on ContractCall { entryPoint transaction { hash } } }`.
4. ONE SHARED OWNER-PK HELPER. Put it in `src/lib/midnight-shared.ts` and import it in BOTH the
server routes and every script — never re-derive inline:
```ts
export const ownerPk = (label: string) =>
sha256(new TextEncoder().encode(\`myapp:owner:v1:\${label}\`)); // domain ≤ 32 UTF-8 bytes
```
Two derivations = the diagnostics disagree with the chain and you chase a phantom "not owner".
5. NO CROSS-CONTRACT CALLS IN COMPACT v1. A marketplace `buy` cannot move mUSDC inside the NFT
contract. Sequence it in ONE server handler: token `faucet`/`transfer` tx, then the NFT `buy`
tx, then return BOTH tx hashes. Label it in the UI: "demo-only atomicity — two transactions".
6. FRESH WALLET PER SERVER CALL, `await wallet.close()` / `stop()` in a `finally`, and never reuse
one wallet across two contract families (token + NFT) in the same request. Working rail order:
`mint → listSale → faucet → pay → buy`.
7. CONTRACT ADDRESS RESOLUTION ORDER: deploy JSON FIRST (`src/data/midnight-contract.undeployed.json`),
`import.meta.env.VITE_DEFAULT_CONTRACT` only as fallback. Vite bakes env at build time and
yesterday's address survives a redeploy — you get `Couldn't find template` on every write.
8. REDEPLOY CHECKLIST AFTER ANY `.compact` CHANGE (skip a step and you burn an hour):
```bash
compact compile contracts/MyContract.compact contracts/managed/my-contract
cp -r contracts/managed/my-contract/{keys,zkir,contract} public/contract/
rm -rf midnight-level-db .midnight # stale verifier keys = RpcError 196
bun run midnight:deploy
# restart the dev server (server routes cache ctxPromise)
```
NEVER mix freshly compiled verifier keys with an old LevelDB.
9. ONE EXCLUSIVE STACK OWNER. Add `scripts/e2e.mjs` that runs the whole rail and prints a single
final line `E2E_OK <mintTx> <listTx> <buyTx>`. Do NOT run two agents/terminals against one
Docker stack or one LevelDB. Never `pkill -f node`/`bun` broadly — you kill the dev server and
the deploy mid-flight. Tail prove logs with `… | tee /tmp/prove.log`, NOT `| head`/`| awk`:
the closed pipe SIGPIPEs the proving process and the tx dies at 90%.
10. PINATA / IPFS SECRETS ARE SERVER-ONLY: `PINATA_JWT`. Never `VITE_PINATA_*` — that ships the
JWT to every browser.
11. INSERT-ONLY APPLIES TO TOKEN CONTRACTS TOO. An mUSDC-style contract that does
`balances[from] = ...; balances[to] = ...` reproduces the SAME fee-balancer panic — it just
surfaces later, on settle/claim instead of mint. Safe shapes:
```compact
export ledger credits: Map<Bytes<32>, Field>; // nonce -> amount (insert once)
export ledger credit_to: Map<Bytes<32>, Bytes<32>>; // nonce -> payee pk (insert once)
export ledger faucet_claimed: Set<Bytes<32>>; // one claim per pk
export ledger spent_nonces: Set<Bytes<32>>; // replay protection
```
A balance is a fold over `credits` filtered by `credit_to`, computed off-chain.
12. MULTI-CONTRACT TOPOLOGY (one genesis wallet, one witness domain per contract):
| key | source | circuits | witness domain |
|--------------|-----------------------|---------------------------------------------|---------------------|
| moveRegistry | `MoveRegistry.compact`| `appendEntry` | `abodc:author:v1` |
| moveNft | `MoveNft.compact` | `mint`,`listSale`,`buy`,`cancel`,`transfer` | `movenft:minter:v1` |
| mandateVault | `MandateVault.compact`| `anchorMandate` | `ap2:buyer:v1` |
| orderLedger | `OrderLedger.compact` | `recordOrder` | `ucp:merchant:v1` |
| midnightUsdc | `MidnightUSDC.compact`| `faucet`,`transfer` | `musdc:signer:v1` |
Domain separators are NEVER reused across contracts and must be <= 32 UTF-8 bytes. Keep ONE
registry (`src/lib/contracts.ts` -> `CONTRACTS`) and derive every user-facing count from
`CONTRACTS.length` — a hardcoded "four deployed contracts" heading goes stale immediately.
13. NEVER CACHE A WALLET PROVIDER ACROSS HTTP REQUESTS. Open -> `callTx` -> `stop()` in a
`finally`, one wallet per request (`withMusdc` / `withMoveNft` / `append-entry.server.ts`).
A module-level `ctxPromise` holding a `MidnightWalletProvider` keeps LevelDB open and the
NEXT contract family's call dies with `SubmissionError`.
14. ONE MIDNIGHT WRITE AT A TIME. A `busy` flag disabling the write button in the UI, and exactly
ONE process owning the Docker stack / LevelDB in ops. Parallel clicks or parallel agents give
`Database failed to open`.
15. SOFT-FAIL SECONDARY APPENDS. When an action does a primary token write PLUS a secondary
registry `appendEntry`, wrap the append in its own try/catch — it must never fail the whole
action. The token transfer is the receipt. Instrument the two legs separately when debugging,
otherwise you get "Claim failed" in the UI while the transfer is already on chain.
16. RPCERROR 117 RECOVERY CHECKLIST — IN ORDER. `SubmissionError` / `FiberFailure` usually WRAP
`RpcError 1010: Invalid Transaction: Custom error: 117`. Do NOT retry the same click:
```bash
# 1. stop the dev server
rm -rf midnight-level-db .midnight # 2. wipe local private state
docker compose -p <project> -f docker-compose.yml up -d # 3. recreate node/indexer/proof
bun run midnight:deploy # 4. FULL deploy, not one contract
bun scripts/debug-<token>-transfer.mjs # 5. verify twice: expect TWO OK lines
bun run dev # 6. restart, hard-refresh, ONE action
```
| code | action |
|------|--------|
| 117 | run the checklist above; never keep retrying the same UI click |
| 104 | wipe LevelDB + full deploy |
| 196 | recompile artefacts + wipe + full deploy |
| UI rows stuck "Pending" | indexer miss or pre-wipe hash in `localStorage` — offer Refresh, then Clear |
17. HUMANIZE RECOVERABLE RPC ERRORS. Map 117 / 104 / 196 to plain-language copy naming the
recovery action; never leak a `FiberFailure` stack into the UI. Skip optional external
verifiers (e.g. an ERC-1271 check) when the network is Undeployed or the secret is absent,
instead of surfacing `missing_secret: <KEY>` to the user.
NFT-RAIL FAILURE MODES
| Symptom | Cause | Fix |
|--------------------------------------------------------------------|----------------------------------------------|------------------------------------------------------------|
| `unreachable` in `transaction_feesWithMargin` / `transaction_merge` | Overwrote an existing ledger map key | Insert-only design (rule 1); append sales rows |
| `callTx.list is not a function` / entry point missing | `list` is reserved; artefacts say `listSale` | Rename circuit + call site to `listSale` |
| `not owner` on a token you just minted | Two different owner-PK derivations | ONE `ownerPk()` helper imported everywhere (rule 4) |
| `RpcError 1010 … Custom error: 117` | Stale private state / store-name drift | Share `PRIVATE_STATE_STORE`; wipe LevelDB; redeploy |
| `RpcError … 104` | Dirty / half-written LevelDB | `rm -rf midnight-level-db .midnight`, redeploy |
| `RpcError … 196` | Verifier key mismatch (recompiled contract) | Full redeploy checklist (rule 8) |
| Prove stalls ~90% then process exits | SIGPIPE from `| head` on the prove log | `tee` to a file instead |
| `SubmissionError` on the next `callTx` | Wallet provider cached across HTTP requests | One wallet per request, `stop()` in `finally` (rule 13) |
| 117 persists after a single-contract redeploy | Partial redeploy on top of dirty private state| Full `midnight:deploy`, not one contract (rule 16) |
| UI says "Claim failed" but the transfer IS on chain | Secondary registry append threw after transfer| Soft-fail the append (rule 15) |
| `Database failed to open` | Parallel writes / parallel agents on one LevelDB| UI busy flag + one exclusive stack owner (rule 14) |
| Old branding or old UX on `:8080` | Dev server serving a different working tree | Check cwd / which tree Vite serves before debugging the UI |
| `bun <<'EOF'` prints help and runs nothing | Bun does not read a program from stdin | `bun scripts/foo.mjs` |
| Containers orphaned after moving the repo | Compose workdir vanished | Recreate with an explicit `-p <project>` name |
| Ledger panel rows stuck "Pending" after a chain wipe | Pre-wipe tx hashes cached in `localStorage` | Offer Refresh + Clear; they are not live stuck transactions |
VERIFICATION HONESTY + SDK PROVIDER GOTCHAS (from a public code review of `ipsmidnight`):
VERIFICATION (applies to every anchoring / attestation / receipt feature)
- **Ledger membership is the only proof.** Reading `commitments.member(commitment)` (or the
equivalent set/map lookup) from the DEPLOYED contract's public state is verification. A stored
tx hash, a "confirmed" status column, or a green badge derived from your own database is NOT.
If the UI says "verified", the code path behind it must have read chain state in that request.
- **Persist the salt** alongside every salted commitment. A commitment whose salt was never
stored can never be re-derived, which makes the anchor permanently unverifiable — and no
reviewer accepts "trust the row".
- **Minimise claims.** Never put raw PII (name, full DOB, document body) in a credential or
on-chain payload whose only job is to prove a predicate. Derive the predicate (`over18`,
`isLicensed`, `digestMatches`) and ship that.
- **Label what you did NOT check.** Simulated artefacts, unresolvable DIDs, and unverified
signatures render as *unverifiable / not checked* — never as pass. Reject simulated artefacts
outright inside signature-check paths. Decoding a JWT is not verifying it.
STATUS VOCABULARY (submitted ≠ verified)
- A transaction that landed on-chain is **`anchored`**, not `verified`. Until an explicit
read-only membership check runs against the contract's public state, label the row
**"anchored · not re-checked"**. Only that check promotes it to `verified`.
- Derive the status **dot colour AND the badge from ONE shared tone helper**. Two independent
mappings drift, and a healthy `anchored` row then renders amber/red — which reads as a failure.
- Once a record is anchored, demote the write action to secondary (`Re-anchor`, outline) and
promote the verification action to primary. The next useful click is verification, not a resubmit.
- A long-running operation's failure must survive the toast: persist the job's state and log tail
and auto-expand it, plus a copy-log button.
LONG-OPERATION ROW LAYOUT (mobile-first)
- Single column on mobile: **metadata first**, then full-width, equal-width action buttons in a
2-up grid. Side-by-side buttons squeeze tx hashes and block numbers into hard-wrapped noise.
- Step timelines and log tails span the **full card width** — never nested inside the metadata column.
- `truncate` record/bundle titles in queue lists so one long title cannot stretch the layout.
SDK v4 PROVIDER GOTCHAS
- `privateStoragePasswordProvider` has a **minimum length**; a short password throws during
wallet construction with an unrelated-looking error. Read a long value from env.
- `httpClientProofProvider(url, zkConfigProvider)` must receive the **same**
`NodeZkConfigProvider(CONTRACT_DIR)` instance you pass as `zkConfigProvider`. Omit it and the
proof server answers `400 Bad Request` on `/check`.
- The genesis wallet must be fully synced BEFORE deploy, and resynced after any chain-data volume
rebuild.
KEY + STATE HYGIENE
- `.gitignore` must cover `.env`, the LevelDB private-state directory (`midnight-level-db/`), and
any `*.tgz` toolchain bundle. Untrack them if they already landed in a commit.
- Dev seeds, storage passwords, and contract addresses come from `process.env` with a documented
fallback — never hardcoded literals inside `scripts/*.mjs`.
TX-HASH PERSISTENCE — the indexer exposes contract STATE, not a list of transaction IDs.
If you want a feed showing tx hashes, persist them client-side after the mint.
Best practice from Choreo Kits:
- Define your payload type with an optional `txId?: string` from the start; keep the canonical
type in ONE browser-safe module and re-export it — do not redefine in multiple components.
- Write feed entries to `localStorage` AFTER the mint succeeds, attaching the `txId` returned
by the mint path:
- Undeployed: `txId` comes from the `/api/mint` response body.
- Preview / Preprod: `txId` comes from Lace `publishKit`.
- Render the full `tx: {hash}` in the feed; label sources (`chain` when read from the indexer,
`local` when read from localStorage).
- Dedupe by `publishedAt` and prefer the local row that already has `txId` when the indexer
catches up (usually a few seconds later).
FRONTEND STANDARDS (Lovable-agent rules — non-negotiable, apply BEFORE writing any component):
1. DESIGN SYSTEM — semantic tokens only, no hardcoded colors in components.
- Define ALL colors, gradients, shadows, radii as HSL CSS variables in \`src/index.css\`
under \`:root\` and \`.dark\`, then map them in \`tailwind.config.ts\` under
\`theme.extend.colors\` (\`background\`, \`foreground\`, \`primary\`, \`accent\`,
\`muted\`, \`card\`, \`border\`, \`ring\`, plus idea-specific accents).
- BANNED in components: \`text-white\`, \`text-black\`, \`bg-black\`, \`bg-white\`,
\`bg-[#...]\`, arbitrary hex, inline \`style={{ color: '#...' }}\`. Use
\`text-foreground\`, \`bg-background\`, \`bg-primary text-primary-foreground\`,
\`border-border\`, etc. Custom gradients belong in \`@layer utilities\`.
- Reject generic AI aesthetics: no default Inter/Poppins body paired with a
purple/indigo gradient on white unless the idea explicitly asks for it.
Commit to ONE distinctive direction that matches the theme (Music / Dance /
Film / Fashion / Writing / etc.) — pick a typography pair (heading + body)
from Google Fonts loaded in \`index.html\`, and one accent hue. Dark-mode-first
(Midnight brand), but ship a working light-mode token set too.
2. SHADCN/UI PRIMITIVES — use \`@/components/ui/*\` for Button, Card, Dialog, Tabs,
Toast, Input, Badge. Customize via \`cva\` variants in the primitive file, never
by slapping hardcoded utility classes on the call site. Small, focused
components live in \`src/components/\`; hooks in \`src/hooks/\`. No file over
~200 lines — split.
3. ASYNC UX — proving takes 30–120 s; the UI must stay alive.
- Stream a status pill: \`Proving → Balancing → Submitting → Confirmed\` (or
\`Error\`), with a determinate label AND an \`aria-live="polite"\` region.
Never a bare spinner.
- On success, render the Midnight explorer link + a Copy-address button.
- On failure, render the exact error text with a Copy-error button — no
silent \`console.error\`. Toasts via \`useToast\` from \`@/components/ui/use-toast\`.
Never \`alert()\`.
- Every async view has loading + empty + error branches. No unhandled promise
rejections. Every \`await\` is wrapped in try/catch OR surfaced through an
error boundary.
4. SEO + HEAD METADATA — set real values in \`index.html\`, not "Lovable App".
- \`<title>\` ≤60 chars, keyword-first. \`<meta name="description">\` ≤160 chars.
- Exactly ONE \`<h1>\`. Use \`<main>\`, \`<section>\`, \`<article>\`, \`<nav>\`, \`<footer>\`.
- \`alt\` on every image. \`loading="lazy"\` on below-the-fold images.
- JSON-LD \`WebApplication\` block in \`<head>\`, canonical tag, responsive
\`<meta name="viewport">\`.
- OpenGraph: \`og:title\`, \`og:description\`, \`og:type=website\`,
\`twitter:card=summary_large_image\`. Skip \`og:image\` unless the demo
produces a real cover.
5. ACCESSIBILITY + RESPONSIVE — mobile-first.
- Every interactive element is keyboard-reachable with a visible focus ring
(via the \`ring\` token). Buttons have \`aria-label\` when icon-only.
- Test at 375 px first. Wrap long hashes, addresses, and CIDs with
\`break-all\` inside a \`min-w-0\` flex child so nothing horizontally scrolls.
- Stack CTA buttons full-width on mobile; row on \`sm:\` and up.
6. STATE + STORAGE — the 5-credit budget forbids Lovable Cloud.
- The 32-byte witness secret lives in \`localStorage\` base64-encoded. NEVER
POST it anywhere. Warn the user in-app that clearing storage revokes proofs.
- Contract address + deploy tx hash cached in \`localStorage\` under a
namespaced key so the app boots straight into the last deployment.
- React Query is fine for Indexer reads. No Redux / Zustand / Jotai.
7. LOVABLE-AGENT WORKFLOW (rules for the coding assistant, not the end user).
- Prefer search-replace over full-file rewrites. Only change what was asked.
- Verify with build output before claiming done — no "should work" hand-waves.
- When the user reports a bug, reproduce first: read console logs, network
requests, and DOM state before proposing a fix.
- Do NOT introduce new deps for anything the current stack already solves.
8. STATUS QUERIES MUST DEGRADE, NEVER THROW (\`ipsmidnight\` lesson).
- Any function that reports stack/host/infra status must return an
\`{ state: "unconfigured", reason }\` result when its secret or env var is
missing. A throw inside a loader or status query blanks the whole page and
hides the one instruction the user needs ("add the secret").
- Never render a green "ready" pill from a stored row alone. Re-probe the
real resource (host API, RPC, contract state) and downgrade the row when
the probe fails — external infra gets destroyed outside your app.
- Humanize infra errors in the UI: name the component, the measured fact,
and the next action. No raw stack traces in the happy path.
PRIVATE STATE PROVIDER (browser) — DO NOT ship `levelPrivateStateProvider` to the browser:
`levelPrivateStateProvider` pulls in `browser-level` → `abstract-level`, whose CJS/ESM interop breaks
under production Rollup. The published site will show a black screen with
`TypeError: Class extends value undefined is not a constructor or null` from `browser-level-*.js`.
Instead ship a tiny localStorage-backed `PrivateStateProvider<string, unknown>` from day one:
- Key layout: `<prefix>:<coinPubKey>:contracts:<contractAddress>:states:<privateStateId>`
and `<prefix>:<coinPubKey>:signing:<address>`.
- JSON-encode `Uint8Array` as `{ __type: "Uint8Array", data: [...] }` and reverse on read.
- Implement `setContractAddress`, `get/set/remove/clear`, `get/set/removeSigningKey`, `clearSigningKeys`;
stub `exportPrivateStates` / `importPrivateStates` / `exportSigningKeys` / `importSigningKeys`.
Node deploy scripts CAN keep using `levelPrivateStateProvider` — the ban is browser-only.
Reference: https://midnightprompts.lovable.app/known-issues
TANSTACK START COMPATIBILITY (if Lovable generates a TanStack Start app instead of a classic Vite SPA):
The same "no SSR for the write path" rule applies. But TanStack Start SSR-renders every route by default,
so you MUST add the Cloudflare-Worker-safe SSR stubbing that the published site requires:
1. Keep Nitro ENABLED. Never set `nitro: false` — it splits the SSR bundle into chunks the Worker runtime cannot resolve.
2. Restrict `vite-plugin-top-level-await` to the client environment:
```ts
function clientTopLevelAwait(): Plugin {
return { ...topLevelAwait(), applyToEnvironment: (env) => env.name === 'client' };
}
```
3. Stub every Midnight package AND the client contract module during the SSR pass:
```ts
function midnightSsrStub(): Plugin {
const wasmStub = path.resolve('src/lib/midnight-ssr-stub.ts');
const contractStub = path.resolve('src/lib/contract.ssr-stub.ts');
const contractReal = path.resolve('src/lib/contract.ts');
return {
name: 'midnight-ssr-stub',
enforce: 'pre',
async resolveId(id, importer, options) {
if (!options?.ssr) return;
if (id.startsWith('@midnight-ntwrk/')) return wasmStub;
const resolved = await this.resolve(id, importer, { ...options, skipSelf: true });
if (resolved && resolved.id === contractReal) return contractStub;
return resolved;
},
};
}
```
Ship `src/lib/midnight-ssr-stub.ts` as `export default {}` and a matching `src/lib/contract.ssr-stub.ts`.
4. Mark every Midnight route `ssr: false`. Never import `@midnight-ntwrk/*` at module scope of a route file.
5. Do NOT use `browser-level` in the browser bundle. Use a localStorage-backed PrivateStateProvider.
Test the production build + Publish → Update on day one. Preview runs on Vite dev; published runs on workerd/Nitro/Rollup,
and the failure modes are invisible in preview. Reference: https://midnightprompts.lovable.app/known-issues
CONTRACT
```compact
// contracts/DanceMoveProvenance.compact
// Trace and display the lineage and remix history of dance moves on an immutable ledger.
// 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, "dancemove_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
1. Land on page → 'Connect Lace' button → poll `window.midnight` → `connect('preview')`.
2. Show shielded address, tDUST reminder ("Get testnet DUST → {VITE_FAUCET_URL}"), and Deploy button.
3. On Deploy: import `deployContract` from `@midnight-ntwrk/midnight-js-contracts`, pass witnesses
({ localSecretKey: async () => sk }) where `sk` is a 32-byte value persisted in localStorage.
4. Show a "Proving…" spinner for up to 120s while the local proof server crunches.
5. On success, render the deployed address + a Midnight explorer link and persist it to
`src/data/midnight-contract.json` so the app boots straight into it next time.
6. The "creative lineage" action calls `appendEntry(payload)` — same flow: prove, submit, refresh feed.
IN-APP SETUP PANEL — MANDATORY (render on the primary page):
Create a <SetupInstructions /> React component and mount it ABOVE the demo
and ABOVE the Connect-Lace panel. It must:
- Be collapsible; persist dismissed state under localStorage key
"setup-dismissed-preview".
- Render the numbered steps below verbatim (copy-paste-friendly code blocks
with a copy button on each shell command).
- Show a small "show setup again" link in the page footer so users can
reopen it after dismissing.
- Copy is prescriptive — do not ship an empty stub or a TODO placeholder.
1. Install the Lace wallet → https://www.lace.io/
2. Switch Lace to Midnight Preview
3. Get tNIGHT from the faucet, then click Generate tDUST in Lace
→ https://midnight-tmnight-preview.nethermind.dev/
4. Start the proof server (public networks use the matrix tag; local Undeployed uses 8.0.3):
docker run -p 6300:6300 midnightntwrk/proof-server:8.1.0 midnight-proof-server -v
5. Deploy the contract:
VITE_NETWORK_ID=preview bun scripts/deploy-midnight.mjs
6. Paste the printed hex address into VITE_DEFAULT_CONTRACT and reload.
Also add a one-line "powered by" reference in the footer linking to
https://midnightprompts.lovable.app so end users can browse the full
network variants + preflight tools.
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 recursion in Compact. Loops must be bounded by compile-time constants.
- No sub-second finality UX. Proofs for k=14 circuits take 30–120s — build for that latency.
- No SSR for the write path. MidnightJS uses `window`, `Buffer`, and WASM top-level-await;
load every `@midnight-ntwrk/*` behind `<ClientOnly>` + `useEffect`. On TanStack Start, keep
nitro ENABLED and stub Midnight packages in the SSR pass (see `midnightSsrStub()`).
- Do NOT set `nitro: false` on TanStack Start to 'escape SSR'. That splits the SSR output into
chunks (`assets/server-*.js` importing `assets/react-*.js`) that the Cloudflare Worker cannot
resolve at runtime — you get `Error: No such module "assets/react"` on every request. Keep
nitro on and use the `midnightSsrStub()` swap instead.
- Do NOT sign Undeployed writes with Lace. Lace cannot balance/sign on the local `undeployed`
chain — the proof completes but submit fails silently. Route every write through a server
`/api/append-entry` (or `/api/mint`) route that reuses the genesis seed (see SIGNING STRATEGY block).
- Do NOT let `privateStateStoreName` drift between `scripts/deploy-midnight.mjs` and the
server-append route. Mismatch → `findDeployedContract` samples a FRESH signing key → chain
rejects with `RpcError 1010: Invalid Transaction: Custom error: 117`. Import ONE shared
constant (e.g. `PRIVATE_STATE_STORE` from `src/lib/midnight-shared.ts`) in BOTH files. Debug
by logging the first/last 8 chars of the signing key on each side — they must match.
- Do NOT pass the raw `ContractState` wrapper to `ledger()`. Symptom:
`expected instance of ChargedState`. Pass `contractState.data` (from `getPublicStates`) or
`result.public.nextContractState` (after a successful `callTx`).
- Do NOT skip re-deploying after `midnight:down` / `midnight:up`. The chain state is wiped and
the address in `src/data/midnight-contract.undeployed.json` is dead. Always run
`bun run midnight:deploy` and restart the dev server, or invalidate the server route's
`ctxPromise` cache when the JSON changes — otherwise the 2nd+ append silently targets the
previous contract and fails with a stale-state or 117 error.
- Do NOT diagnose a disabled 'Prove & submit' / 'Mint' button as a wallet or chain bug before
checking the form. In 90% of cases `canFund` / `canMint` just needs both fields non-empty.
Ship a tooltip that names the missing field so no one loses an hour on this.
- Do NOT omit `@midnight-ntwrk/testkit-js`, `pino`, `ws`, `ssh2`, `cpu-features` from
`optimizeDeps.exclude` when using the server-append pattern. Rolldown/Vite tries to
pre-bundle those Node-only transitive deps and the dev server hangs indefinitely on the
'Loading …' fallback.
- Do NOT ship Mainnet without the persistent red risk banner AND the README disclaimer at the top
of `README.md`. Mainnet handles REAL value — this codebase is vibe-coded / unaudited / hackathon-grade.
- Do NOT route Mainnet writes through a server `/api/mint`. There is no genesis wallet on Mainnet;
signing MUST be Lace-only, initiated by the user.
- Do NOT prompt users for NIGHT seed/recovery phrases. On Mainnet, funds arrive via a withdrawal from
an official exchange partner (https://midnight.network/night?tag=exchange) directly to the Lace
unshielded address. Never accept a phrase in chat, form, screenshot, or issue tracker.
- Do NOT ship `levelPrivateStateProvider` to the browser. Its `browser-level` → `abstract-level`
chain breaks under production Rollup with `Class extends value undefined is not a constructor or null`.
Use a `localStorage`-backed PrivateStateProvider in the browser; keep `levelPrivateStateProvider`
only in Node deploy scripts.
- No deploying from a Cloudflare Worker / TanStack server function. Deploys are a local `bun`
script only — they need Docker, the proof server, and localhost.
- Do NOT use `midnightntwrk/midnight-node:latest` (tag often missing) or the partner-chain 2.x
tags (need Cardano follower + Postgres). For public networks pin the matrix tags: proof-server:8.1.0,
midnight-node:1.0.1 (Preview), indexer:4.3.3. For local Undeployed use the local-dev triple:
proof-server:8.0.3, midnight-node:0.22.5, indexer-standalone:4.0.2.
- Do NOT accept a user's recovery phrase in chat. Ship `scripts/check-midnight-wallet.mjs` that
reads `MIDNIGHT_WALLET_SEED` from their shell env and prints only PUBLIC addresses.
- Do NOT derive shielded and unshielded addresses through different `NetworkId` values — use ONE
`NetworkId` across both encoders and validate the emitted bech32 prefix before writing `.env`
- Do NOT overwrite an existing key in a public ledger Map. The dust fee balancer panics on the NEXT
callTx (`transaction_feesWithMargin` / `transaction_merge` unreachable). Insert-only / append-only
in v1 — see NFT / MARKETPLACE LEDGER DESIGN.
- Do NOT port ERC-721 into Compact. There is no ERC-721 on Midnight; `_owners[tokenId] = newOwner`
is exactly the forbidden overwrite. Ownership transfer = append a new sale row + off-chain mirror.
- Do NOT run two agents / terminals against ONE Docker stack or ONE LevelDB, and never `pkill -f`
broad patterns. One exclusive stack owner; `tee` prove logs instead of piping to `head`/`awk`
(SIGPIPE kills the prove at ~90%).
- Do NOT cache a `MidnightWalletProvider` (or any wallet) across HTTP requests. Open → `callTx`
→ `stop()` in a `finally`, one wallet per request — a cached one holds LevelDB open and the next
contract family's call dies with `SubmissionError`.
- Do NOT hardcode a contract count in user-facing copy. Derive it from the contract registry
(`CONTRACTS.length`) so adding a contract cannot make the UI lie.
- Do NOT keep re-clicking a failing write through an `RpcError 117`. Run the ordered recovery
checklist (wipe LevelDB → recreate containers → FULL deploy → restart dev → hard-refresh).
- Do NOT install Midnight packages with `@latest` or a caret range on a runner/CI machine.
One unpublished transitive alpha (`compact-js@2.5.3` → `ledger-v9@^0.1.0-alpha.1`) breaks every
install with `ETARGET / notarget`. Pin exactly; `compact-js@2.5.1` resolves cleanly.
- Do NOT retry a failed toolchain install without first deleting the stale `package-lock.json`
and `node_modules` — the bad resolution is cached and every retry reproduces the failure.
- Do NOT read a silent, stalled `npm i` as a hang. It is an OOM kill (empty log tail). Give the
machine 4 vCPU / 4 GB, install in sequential groups, heartbeat every ~30s, and surface the
host machine events + exit code.
- Do NOT show a detached job's only error in a toast. Persist the failed job state + log tail
(auto-expanded) with a copy-log button.
- Do NOT call a submitted transaction 'verified'. On-chain = `anchored`; only a read-only ledger
membership check earns `verified`. And never compute status colour in two places — one shared
tone helper feeds both the dot and the badge.
- Do NOT leave a previous chain's branding, token names, or explorer links in user-visible strings
after migrating a demo to Midnight.
REQUIRED SECRETS (Lovable → Project Settings → Secrets) — **PREVIEW** target:
- 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 (run the matrix proof server: `docker run -p 6300:6300 midnightntwrk/proof-server:8.1.0 midnight-proof-server -v`)
- VITE_DEFAULT_CONTRACT hex address printed by your first deploy — paste it here so users skip the deploy step
tNIGHT ≠ tDUST — the #1 support question. Faucet dispenses tNIGHT; deploys spend tDUST. Every user hits this once:
1. Copy your UNSHIELDED address (`mn_addr_undeployed1…` on Preview; Lace labels the network "Preview").
2. Paste into https://midnight-tmnight-preview.nethermind.dev/ → Request → tNIGHT arrives.
3. In Lace, click "Generate tDUST" to delegate tNIGHT → tDUST appears after a block.
4. Only NOW can you deploy — the deploy script errors with `Insufficient Funds: could not balance dust` otherwise.
Explorer: https://preview.midnightexplorer.com/
Notes: Preview is the fastest network to demo on but resets frequently. Best for iterative dev + hackathon judges.
If you don't want to babysit the faucet, use the **Undeployed** variant of this prompt instead.
FURTHER REFERENCE (community skills registry — browsable, per-primitive scaffolds):
- Site: https://midnight-skills.netlify.app
- Source: https://github.com/Kali-Decoder/Midnight-skills
- This app's Undeployed quick-start: https://midnightprompts.lovable.app/undeployed
- This app's preflight checks: https://midnightprompts.lovable.app/undeployed-preflight
- `compact` — Compact 0.23 language deep-dive, ledger vs witness, disclose(), Merkle patterns
- `react-wallet-connector` — full DApp Connector API scaffold (enumerate window.midnight by UUID)
- `midnight-environment-setup` — Compact compiler + Docker + proof server bring-up
- `indexer` — public data provider + GraphQL patterns for read-only ledger views
- `example-locker-dapp` — timelock vault reference (blockTimeGte, receive/sendUnshielded)
- `example-counter` — smallest end-to-end Compact + MidnightJS reference
- Fly.io hosting (four-app topology + failure-mode table): https://midnightprompts.lovable.app/undeployed#flyio
- Signing strategy (Undeployed uses server /api/mint; Preview/Preprod uses Lace publishKit): see SIGNING STRATEGY block above
If the target Lovable session is on this workspace, those six skills are already active. Otherwise, drop
`.agents/skills/<name>/SKILL.md` into your project from the repo above and run `skills--apply_draft`.
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.
- Style the button with shadcn \`Button\` + semantic tokens (\`bg-primary\`,
\`text-primary-foreground\`) — no hardcoded colors, see FRONTEND STANDARDS §1.
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;
}
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"));
} 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", "undeployed", "mainnet"]));
let api: ConnectedApi | null = null;
let used: string | null = null;
for (const n of candidates) {
try { api = await c.connect(n); used = n; break; } catch {}
}
if (!api || !used) throw new Error("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.");
setAddress(addr);
setNetwork(used);
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) };
}
```
MOUNT on the primary page and gate render until hydrated. See
https://midnightprompts.lovable.app for the full component reference.
--- END: Connect-Lace boilerplate ---
Market sizing.
Indicative figures for hackathon pitches — refine with your own research before raising.