A minimal Midnight Network reference demonstrating browser wallet connection (Lace / 1AM) and CLI transaction flows on Preprod.
- Wallet Detection: Auto-detect compatible wallets via
window.midnight - Browser Transfers: Send unshielded tNIGHT through Lace/1AM using
balanceUnsealedTransaction - CLI Transfers: Headless Node.js scripts with mnemonic-derived keys using
balanceUnboundTransaction - State Persistence: Save and restore wallet sync state to avoid re-syncing from genesis
- React 19 + Vite + TypeScript
- Tailwind CSS v4 (dark theme)
@midnight-ntwrk/dapp-connector-apiv4 (wallet integration)@midnight-ntwrk/wallet-sdk-*v3 (CLI wallet SDK)
| Route | Description |
|---|---|
/ |
Home: Balances, addresses, wallet status |
/transfer |
Send an unshielded self-transfer via browser wallet |
- Node.js v22+
- Docker (for proof server)
- A Midnight wallet (1AM or Lace) with Preprod NIGHT tokens
- 24-word test mnemonic for CLI scripts
Create a .env file in the project root:
VITE_INDEXER_HTTP=https://indexer.preprod.midnight.network/api/v4/graphql
VITE_INDEXER_WS=wss://indexer.preprod.midnight.network/api/v4/graphql/ws
VITE_PROOF_SERVER=http://localhost:6300npm installdocker run -p 6300:6300 midnightntwrk/proof-server:8.0.3npm run devThe app runs at http://localhost:5173.
MNEMONIC="word1 word2 ... word24" npx tsx scripts/test-v3-sync-and-transfer.tsThis restores wallet state from .wallet-state/ (or syncs from scratch), waits for sync, then submits a 1-unit unshielded self-transfer.
midnight-dapp-connect/
├── src/
│ ├── pages/
│ │ ├── Home.tsx # Wallet dashboard
│ │ └── Transfer.tsx # Transfer form
│ ├── hooks/
│ │ ├── useWallet.ts # Zustand store + detection
│ │ └── useWalletSubscription.ts
│ ├── lib/
│ │ └── transaction-cli.ts # CLI wallet init + state persistence
│ └── App.tsx
├── scripts/
│ └── test-v3-sync-and-transfer.ts
└── README.md
| Browser | CLI | |
|---|---|---|
| Key custody | Wallet extension (Lace / 1AM) | 24-word mnemonic in script |
| Balancing API | balanceUnsealedTransaction |
balanceUnboundTransaction |
| Transfer API | makeTransfer or manual Intent + UnshieldedOffer |
transferTransaction + signRecipe |
| Proof step | tx.prove() via wallet's proving provider |
Wallet SDK internal proving |
| State sync | Wallet extension handles it | WalletFacade + RxJS push streams |
| State restore | N/A (extension persists) | restoreWalletState() from .wallet-state/ |
| Error | Fix |
|---|---|
Cannot read properties of undefined (reading 'toString') |
Use correct DesiredOutput fields: kind, type, value, recipient |
Invalid character 'm' at position 0 |
Decode Bech32 address to hex before UnshieldedOffer.new |
expected ...proof..., got ...proof-preimage... |
Call tx.prove() before balanceUnsealedTransaction |
BALANCE_FAILED: invalid network ID |
Use 'preprod' in Transaction.fromParts, not 'undeployed' |
Custom error: 192 |
Add signRecipe() step for CLI transferTransaction |
Custom error: 170 |
Wait for isSynced = true before submitting |
| Dust sync timeout | Use restoreWalletState(); save on SIGINT; allow 2h timeout |
Built with @midnight-ntwrk/midnight-js 4.0.4 and Wallet SDK 3.0.0 for the Midnight Preprod network.