mcpField
D
dynamic-midnight-wallet

dynamic-midnight-wallet

github.com/kali-decoderDevelopment18125 installsUpdated 1d ago
Repository

About dynamic-midnight-wallet

A skill that generates React, Vite, and TypeScript apps for connecting Midnight wallets through Dynamic.xyz. It covers both the 1AM extension and embedded-wallet flows, with typed access to wallet addresses, balances, and transfers.

Key features

  1. 01React + Vite + TypeScript app scaffolding
  2. 02Dynamic wallet connection setup
  3. 031AM and embedded-wallet flows
  4. 04Typed Midnight address and balance access
  5. 05Shielded and unshielded token transfers

Use cases

  1. 01Building a web app that connects a 1AM wallet
  2. 02Adding embedded Midnight wallets using social or email access
  3. 03Displaying shielded, unshielded, and DUST wallet state
  4. 04Routing NIGHT transfers to the matching pool

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/kali-decoder/midnight-skills --skill dynamic-midnight-wallet

After installing, type/dynamic-midnight-walletin Claude Code to confirm it loaded.

Details

From the project README

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/[email protected] + @dynamic-labs/[email protected] (pin together)

Key architecture notes:

  • A Midnight wallet is not a single address — it has three surfaces:
    SurfaceRoleDynamic API
    UnshieldedPublic address/statewallet.address
    ShieldedPrivate token poolwallet.additionalAddresses (MidnightShielded)
    DUSTFee-generation statewallet.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/[email protected] @dynamic-labs/[email protected] @dynamic-labs/[email protected]

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

/// <reference types="vite/client" />

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
# Paste your Dynamic Environment ID
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:

  1. Enable Midnight under Chains & Networks
  2. 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();

// Poll until DUST sync completes (WaaS only)
if (dustSyncing) {
  setTimeout(() => void refreshBalances(), 2500);
}

Embedded-only extras: registerDust(), signMessage(), step-by-step createTransferTransaction → signTransaction → submitTransaction.


18) Sending Tokens

// Routes by recipient prefix — mn_shield... = shielded, else unshielded
await primaryWallet.sendBalance({
  toAddress: 'mn_shield...',
  amount: '1.5', // human-readable NIGHT for extension wallets
});

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

LayerOwns
DynamicConnection lifecycle, connectors, MidnightWallet object, address/balance accessors, send routing
1AM extensionKey custody, derivation, ZK proving, signing, submission
Your DAppDeposit/receive UX (show unshielded and shielded), which pool to display, DUST generation messaging

20) Troubleshooting

SymptomCauseFix
Midnight not in widgetChain disabledEnable Midnight in Dynamic dashboard → Chains & Networks
403 on all wallet methods (embedded)Private Key Exports offEnable under Embedded Wallets → Security
isMidnightWallet falseWrong connector or chainUse MidnightWalletConnectors or DynamicWaasMidnightConnectors
Shielded address missingWallet still syncingRetry after connect; poll dustSyncing for WaaS
Send fails cross-poolmn_addr → mn_shieldSame pool only — unshielded to unshielded, shielded to shielded
Version mismatch errorsMixed @dynamic-labs/* versionsPin all Dynamic packages to same version
Empty DUSTNIGHT not registeredCall registerDust() after funding unshielded NIGHT (embedded)

21) Agent Checklist

When generating this template for a user:

  • Scaffold Vite react-ts or write all files from this skill
  • Install @dynamic-labs/sdk-react-core, @dynamic-labs/midnight, @dynamic-labs/sdk-api-core at same version
  • Set VITE_DYNAMIC_ENVIRONMENT_ID in .env
  • Use MidnightWalletConnectors for extension (default) or DynamicWaasMidnightConnectors for embedded
  • Include DynamicWidget for connect/disconnect
  • Surface unshielded, shielded, and DUST in deposit UI
  • Guard all Midnight calls with isMidnightWallet
  • Omit CSS unless user requests styling
  • Link to 1am-wallet/ if user needs contract deploy on top of Dynamic connection

22) Related Skills

NeedSkill
DApp Connector without Dynamicreact-wallet-connector/
Contract deploy + circuits1am-wallet/
Token units & poolstoken-transfers/
Locker / payment vault dAppsexample-locker-dapp/, example-payment-dapp/

Comments

Markdown supported