| name | dynamic-midnight-wallet |
| description | DISABLED ON PLATFORM — skill file retained for future release. Do not surface in MIDSKILLS index until re-enabled. Scaffold a React + Vite app with Dynamic.xyz Midnight wallet connection via @dynamic-labs/midnight.
|
| author | Kali-Decoder |
| disable-model-invocation | true |
Dynamic Midnight Wallet Skill
This skill generates a complete React + Vite + TypeScript app that connects Midnight wallets through Dynamic.xyz (@dynamic-labs/midnight). It replaces hand-rolled window.midnight wiring with DynamicContextProvider, DynamicWidget, and typed Midnight wallet accessors.
Primary references:
dynamic.xyz/docs/react/wallets/using-wallets/midnight/using-midnight-wallets — injected 1AM extension
dynamic.xyz/docs/react/wallets/using-wallets/midnight/midnight-embedded-wallets — social/email MPC wallets
dynamic.xyz/docs/llms.txt — full Dynamic documentation index
@dynamic-labs/midnight@4.91.2 + @dynamic-labs/sdk-react-core@4.91.2 (pin together)
Key architecture notes:
- A Midnight wallet is not a single address — it has three surfaces:
| Surface | Role | Dynamic API |
|---|
| Unshielded | Public address/state | wallet.address |
| Shielded | Private token pool | wallet.additionalAddresses (MidnightShielded) |
| DUST | Fee-generation state | wallet.additionalAddresses (MidnightDust) |
NIGHT exists as both shielded and unshielded — different pools, not duplicate balances
- Dynamic owns connection lifecycle, connectors, address/balance accessors, send routing
- 1AM extension (injected) owns key custody, proving, signing, submission — Dynamic talks to it via
@midnight-ntwrk/dapp-connector-api
- Your app owns deposit UX — surface both unshielded and shielded addresses; DUST is generated, not deposited to
sendBalance routes by recipient prefix: mn_shield... → shielded pool, otherwise unshielded. No cross-pool transfers
- Always guard with
isMidnightWallet(wallet) before calling Midnight methods
Relationship to other skills:
- Raw DApp Connector without Dynamic →
react-wallet-connector/
- Contract deploy + circuit calls via 1AM directly →
1am-wallet/
- Embedded wallets only → see §17 in this skill (
DynamicWaasMidnightConnectors)
1) Project Structure
my-dynamic-midnight-app/
├── index.html
├── package.json
├── tsconfig.json
├── tsconfig.app.json
├── tsconfig.node.json
├── vite.config.ts
├── .env.example
└── src/
├── main.tsx
├── App.tsx
├── MidnightWalletPanel.tsx
├── hooks/
│ └── useMidnightWallet.ts
└── vite-env.d.ts
2) Prerequisites
- Node.js 18+
- Dynamic.xyz account with Midnight enabled under Chains & Networks
- Dynamic Environment ID from the dashboard
- For injected flow: 1AM wallet browser extension installed
- For embedded flow: enable Private Key Exports under Embedded Wallets → Security (required for MPC Midnight wallets)
3) Scaffold & Install
npm create vite@latest my-dynamic-midnight-app -- --template react-ts
cd my-dynamic-midnight-app
npm install @dynamic-labs/sdk-react-core@4.91.2 @dynamic-labs/midnight@4.91.2 @dynamic-labs/sdk-api-core@4.91.2
Create .env:
VITE_DYNAMIC_ENVIRONMENT_ID=your-environment-id-here
4) package.json
{
"name": "my-dynamic-midnight-app",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"@dynamic-labs/midnight": "4.91.2",
"@dynamic-labs/sdk-api-core": "4.91.2",
"@dynamic-labs/sdk-react-core": "4.91.2",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react": "^4.3.0",
"typescript": "~5.7.0",
"vite": "^6.0.0"
}
}
Pin all @dynamic-labs/* packages to the same version. Check npm view @dynamic-labs/midnight version if install fails.
5) vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
});
6) tsconfig.json / tsconfig.app.json / tsconfig.node.json
Use the standard Vite react-ts template configs (same as react-wallet-connector/SKILL.md §6–8).
7) src/vite-env.d.ts
interface ImportMetaEnv {
readonly VITE_DYNAMIC_ENVIRONMENT_ID: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
8) src/main.tsx
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.tsx';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
);
9) src/App.tsx
Injected 1AM extension flow (default template):
import { DynamicContextProvider, DynamicWidget } from '@dynamic-labs/sdk-react-core';
import { MidnightWalletConnectors } from '@dynamic-labs/midnight';
import MidnightWalletPanel from './MidnightWalletPanel';
const environmentId = import.meta.env.VITE_DYNAMIC_ENVIRONMENT_ID;
if (!environmentId) {
throw new Error('Set VITE_DYNAMIC_ENVIRONMENT_ID in .env');
}
export default function App() {
return (
<DynamicContextProvider
settings={{
environmentId,
walletConnectors: [MidnightWalletConnectors],
}}
>
<header>
<h1>Midnight + Dynamic</h1>
<DynamicWidget />
</header>
<main>
<MidnightWalletPanel />
</main>
</DynamicContextProvider>
);
}
10) src/hooks/useMidnightWallet.ts
import { useDynamicContext } from '@dynamic-labs/sdk-react-core';
import { isMidnightWallet } from '@dynamic-labs/midnight';
import { WalletAddressType } from '@dynamic-labs/sdk-api-core';
export function useMidnightWallet() {
const { primaryWallet } = useDynamicContext();
if (!primaryWallet || !isMidnightWallet(primaryWallet)) {
return { wallet: null, isConnected: false as const };
}
const shieldedAddress = primaryWallet.additionalAddresses?.find(
(a) => a.type === WalletAddressType.MidnightShielded,
)?.address;
const dustAddress = primaryWallet.additionalAddresses?.find(
(a) => a.type === WalletAddressType.MidnightDust,
)?.address;
return {
wallet: primaryWallet,
isConnected: true as const,
unshieldedAddress: primaryWallet.address,
shieldedAddress,
dustAddress,
};
}
11) src/MidnightWalletPanel.tsx
Displays all three surfaces and balances. No CSS — add Tailwind or your preferred styling.
import { useCallback, useEffect, useState } from 'react';
import { useMidnightWallet } from './hooks/useMidnightWallet';
type Balances = {
shieldedBalance?: string;
unshieldedBalance?: string;
dustBalance?: { balance: string; cap: string };
};
export default function MidnightWalletPanel() {
const { wallet, isConnected, unshieldedAddress, shieldedAddress, dustAddress } =
useMidnightWallet();
const [balances, setBalances] = useState<Balances | null>(null);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const refreshBalances = useCallback(async () => {
if (!wallet) return;
setLoading(true);
setError(null);
try {
const formatted = await wallet.getFormattedBalances();
setBalances({
shieldedBalance: formatted.shieldedBalance,
unshieldedBalance: formatted.unshieldedBalance,
dustBalance: formatted.dustBalance,
});
} catch (e) {
setError(String(e));
} finally {
setLoading(false);
}
}, [wallet]);
useEffect(() => {
void refreshBalances();
}, [refreshBalances]);
if (!isConnected || !wallet) {
return <p>Connect your Midnight wallet with the widget above.</p>;
}
return (
<div>
<h2>Wallet Surfaces</h2>
<dl>
<dt>Unshielded (public)</dt>
<dd title={unshieldedAddress}>{unshieldedAddress}</dd>
<dt>Shielded (private)</dt>
<dd title={shieldedAddress ?? ''}>{shieldedAddress ?? '—'}</dd>
<dt>DUST (fees)</dt>
<dd title={dustAddress ?? ''}>{dustAddress ?? '—'}</dd>
</dl>
<h2>Balances</h2>
{loading ? <p>Loading…</p> : null}
{error ? <p role="alert">{error}</p> : null}
{balances ? (
<ul>
<li>Unshielded NIGHT: {balances.unshieldedBalance ?? '0'}</li>
<li>Shielded NIGHT: {balances.shieldedBalance ?? '0'}</li>
<li>
DUST:{' '}
{balances.dustBalance
? `${balances.dustBalance.balance} / cap ${balances.dustBalance.cap}`
: '—'}
</li>
</ul>
) : null}
<button type="button" onClick={() => void refreshBalances()}>
Refresh Balances
</button>
<h2>Deposit instructions</h2>
<p>
Send to the <strong>unshielded</strong> address for public NIGHT, or the{' '}
<strong>shielded</strong> address for private NIGHT. DUST is generated from
registered unshielded NIGHT — it is not deposited to directly.
</p>
</div>
);
}
12) index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Midnight + Dynamic</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
13) .env.example
VITE_DYNAMIC_ENVIRONMENT_ID=
14) Run
cp .env.example .env
npm install
npm run dev
Open http://localhost:5173. Click DynamicWidget → connect 1AM wallet → panel shows three surfaces and balances.
15) Checking Wallet Type
import { isMidnightWallet } from '@dynamic-labs/midnight';
if (!isMidnightWallet(wallet)) {
throw new Error('This wallet is not a Midnight wallet');
}
Always narrow before calling getFormattedBalances(), sendBalance(), or connector methods.
16) Richer Shielded Handles (Connector API)
When circuits need coin/encryption public keys:
const connector = primaryWallet.connector;
const { shieldedAddress, shieldedCoinPublicKey, shieldedEncryptionPublicKey } =
await connector.getShieldedAddresses();
const { unshieldedAddress } = await connector.getUnshieldedAddress();
const { dustAddress } = await connector.getDustAddress();
Use these when wiring 1am-wallet/ provider sessions on top of Dynamic.
17) Embedded Wallets (Social / Email)
Swap connector in App.tsx:
import { DynamicWaasMidnightConnectors } from '@dynamic-labs/midnight';
<DynamicContextProvider
settings={{
environmentId,
walletConnectors: [DynamicWaasMidnightConnectors],
}}
>
Dashboard requirements:
- Enable Midnight under Chains & Networks
- Enable Private Key Exports under Embedded Wallets → Security (403 on all ops if disabled)
Embedded balance shape differs slightly:
const { unshieldedBalance, shieldedTokenCount, dustBalance, dustSyncing } =
await primaryWallet.getFormattedBalances();
if (dustSyncing) {
setTimeout(() => void refreshBalances(), 2500);
}
Embedded-only extras: registerDust(), signMessage(), step-by-step createTransferTransaction → signTransaction → submitTransaction.
18) Sending Tokens
await primaryWallet.sendBalance({
toAddress: 'mn_shield...',
amount: '1.5',
});
Cross-pool transfers are not supported. Sender and recipient must be in the same pool.
For raw per-token amounts:
const { shielded, unshielded, dust } = await primaryWallet.getBalances();
19) Ownership Boundaries
| Layer | Owns |
|---|
| Dynamic | Connection lifecycle, connectors, MidnightWallet object, address/balance accessors, send routing |
| 1AM extension | Key custody, derivation, ZK proving, signing, submission |
| Your DApp | Deposit/receive UX (show unshielded and shielded), which pool to display, DUST generation messaging |
20) Troubleshooting
| Symptom | Cause | Fix |
|---|
| Midnight not in widget | Chain disabled | Enable Midnight in Dynamic dashboard → Chains & Networks |
| 403 on all wallet methods (embedded) | Private Key Exports off | Enable under Embedded Wallets → Security |
isMidnightWallet false | Wrong connector or chain | Use MidnightWalletConnectors or DynamicWaasMidnightConnectors |
| Shielded address missing | Wallet still syncing | Retry after connect; poll dustSyncing for WaaS |
| Send fails cross-pool | mn_addr → mn_shield | Same pool only — unshielded to unshielded, shielded to shielded |
| Version mismatch errors | Mixed @dynamic-labs/* versions | Pin all Dynamic packages to same version |
| Empty DUST | NIGHT not registered | Call registerDust() after funding unshielded NIGHT (embedded) |
21) Agent Checklist
When generating this template for a user:
22) Related Skills
| Need | Skill |
|---|
| DApp Connector without Dynamic | react-wallet-connector/ |
| Contract deploy + circuits | 1am-wallet/ |
| Token units & pools | token-transfers/ |
| Locker / payment vault dApps | example-locker-dapp/, example-payment-dapp/ |