mcpField
D
dynamic-midnight-wallet

dynamic-midnight-wallet

github.com/kali-decoder开发18125 次安装1 天前更新
代码仓库

关于 dynamic-midnight-wallet

这是一项通过 Dynamic.xyz 连接 Midnight 钱包并生成 React、Vite 和 TypeScript 应用的技能。内容涵盖 1AM 扩展与嵌入式钱包流程,并提供钱包地址、余额和转账的类型化访问。

主要功能

  1. 01React + Vite + TypeScript 应用脚手架
  2. 02Dynamic 钱包连接配置
  3. 031AM 与嵌入式钱包流程
  4. 04类型化 Midnight 地址和余额访问
  5. 05Shielded 与 unshielded 代币转账

适用场景

  1. 01构建连接 1AM 钱包的 Web 应用
  2. 02通过社交或电子邮件接入嵌入式 Midnight 钱包
  3. 03展示 shielded、unshielded 和 DUST 钱包状态
  4. 04将 NIGHT 转账路由到对应资金池

安装

复制下面的命令,在终端中运行。

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

安装后在 Claude Code 中输入/dynamic-midnight-wallet确认已加载。

详细介绍

Dynamic Midnight 钱包技能

此技能会生成一个完整的 React + Vite + TypeScript 应用,通过 Dynamic.xyz(@dynamic-labs/midnight)连接 Midnight 钱包。它使用 DynamicContextProvider、DynamicWidget 和类型化的 Midnight 钱包访问器,取代手动编写的 window.midnight 接线代码。

主要参考资料:

  • dynamic.xyz/docs/react/wallets/using-wallets/midnight/using-midnight-wallets — 注入式 1AM 扩展
  • dynamic.xyz/docs/react/wallets/using-wallets/midnight/midnight-embedded-wallets — 社交账号/邮箱 MPC 钱包
  • dynamic.xyz/docs/llms.txt — Dynamic 完整文档索引
  • @dynamic-labs/[email protected] + @dynamic-labs/[email protected](版本需保持一致)

关键架构说明:

  • Midnight 钱包不是单一地址,而是有三个面向:
    面向作用Dynamic API
    Unshielded公开地址/状态wallet.address
    Shielded私有代币池wallet.additionalAddresses(MidnightShielded)
    DUST手续费生成状态wallet.additionalAddresses(MidnightDust)
  • NIGHT 同时存在于 Shielded 和 Unshielded 中——它们属于不同的池,并非重复余额
  • Dynamic 负责连接生命周期、连接器、地址/余额访问器以及发送路由
  • 1AM 扩展(注入式)负责密钥托管、证明生成、签名和提交——Dynamic 通过 @midnight-ntwrk/dapp-connector-api 与其通信
  • 你的应用负责存款 UX——展示 Unshielded 和 Shielded 两种地址;DUST 是生成的,不是存入的
  • sendBalance 根据收款人前缀路由:mn_shield... → Shielded 池,否则路由到 Unshielded 池。不同池之间不能转账
  • 调用 Midnight 方法前,务必先通过 isMidnightWallet(wallet) 检查

与其他技能的关系:

  • 不使用 Dynamic 的原始 DApp Connector → react-wallet-connector/
  • 直接通过 1AM 部署合约 + 调用 circuit → 1am-wallet/
  • 仅使用嵌入式钱包 → 参见本技能 §17(DynamicWaasMidnightConnectors)

1) 项目结构

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) 前提条件

  • Node.js 18+
  • Dynamic.xyz 账号,并在 Chains & Networks 下启用 Midnight
  • 从控制面板获取 Dynamic Environment ID
  • 对于注入式流程:已安装 1AM 钱包浏览器扩展
  • 对于嵌入式流程:在 Embedded Wallets → Security 下启用 Private Key Exports(MPC Midnight 钱包必须启用此项)

3) 脚手架搭建与安装

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]

创建 .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"
  }
}

将所有 @dynamic-labs/* 软件包固定为相同版本。如果安装失败,请检查 npm view @dynamic-labs/midnight version。


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

使用标准的 Vite react-ts 模板配置(与 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

注入式 1AM 扩展流程(默认模板):

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

显示全部三个面向及其余额。不包含 CSS——请添加 Tailwind 或你偏好的样式。

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) 运行

cp .env.example .env
# Paste your Dynamic Environment ID
npm install
npm run dev

打开 http://localhost:5173。点击 DynamicWidget → 连接 1AM 钱包 → 面板显示三个面向及其余额。


15) 检查钱包类型

import { isMidnightWallet } from '@dynamic-labs/midnight';

if (!isMidnightWallet(wallet)) {
  throw new Error('This wallet is not a Midnight wallet');
}

调用 getFormattedBalances()、sendBalance() 或连接器方法前,务必先进行类型收窄。


16) 更丰富的 Shielded 句柄(Connector API)

当 circuits 需要 coin/encryption 公钥时:

const connector = primaryWallet.connector;

const { shieldedAddress, shieldedCoinPublicKey, shieldedEncryptionPublicKey } =
  await connector.getShieldedAddresses();
const { unshieldedAddress } = await connector.getUnshieldedAddress();
const { dustAddress } = await connector.getDustAddress();

在 Dynamic 之上接入 1am-wallet/ 提供者会话时使用这些句柄。


17) 嵌入式钱包(社交 / 邮箱)

在 App.tsx 中替换连接器:

import { DynamicWaasMidnightConnectors } from '@dynamic-labs/midnight';

<DynamicContextProvider
  settings={{
    environmentId,
    walletConnectors: [DynamicWaasMidnightConnectors],
  }}
>

控制面板要求:

  1. 在 Chains & Networks 中启用 Midnight
  2. 在 Embedded Wallets → Security 下启用 Private Key Exports(如果未启用,所有操作都会返回 403)

嵌入式钱包的余额结构略有不同:

const { unshieldedBalance, shieldedTokenCount, dustBalance, dustSyncing } =
  await primaryWallet.getFormattedBalances();

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

仅嵌入式钱包支持的额外功能:registerDust()、signMessage(),以及分步调用 createTransferTransaction → signTransaction → submitTransaction。


18) 发送代币

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

不支持跨池转账。发送方和接收方必须在同一个池中。

对于原始代币数量:

const { shielded, unshielded, dust } = await primaryWallet.getBalances();

19) 所有权边界

层负责内容
Dynamic连接生命周期、连接器、MidnightWallet 对象、地址 / 余额访问器、发送路由
1AM 扩展密钥托管、派生、ZK 证明、签名、提交
你的 DApp存入 / 接收 UX(同时显示未屏蔽和屏蔽状态)、要显示的池、DUST 生成提示

20) 故障排除

症状原因解决方法
小组件中没有 Midnight链未启用在 Dynamic 控制面板 → Chains & Networks 中启用 Midnight
所有钱包方法都返回 403(嵌入式)Private Key Exports 已关闭在 Embedded Wallets → Security 下启用
isMidnightWallet 为 false连接器或链错误使用 MidnightWalletConnectors 或 DynamicWaasMidnightConnectors
缺少屏蔽地址钱包仍在同步连接后重试;对于 WaaS,轮询 dustSyncing
跨池发送失败mn_addr → mn_shield只能在同一池内转账——未屏蔽到未屏蔽,屏蔽到屏蔽
版本不匹配错误@dynamic-labs/* 版本混用将所有 Dynamic 软件包固定为相同版本
DUST 为空NIGHT 尚未注册为未屏蔽的 NIGHT 充值后调用 registerDust()(嵌入式)

21) Agent 检查清单

为用户生成此模板时:

  • 使用 Vite react-ts 脚手架,或根据此技能编写所有文件
  • 安装同一版本的 @dynamic-labs/sdk-react-core、@dynamic-labs/midnight、@dynamic-labs/sdk-api-core
  • 在 .env 中设置 VITE_DYNAMIC_ENVIRONMENT_ID
  • 扩展使用 MidnightWalletConnectors(默认),嵌入式使用 DynamicWaasMidnightConnectors
  • 添加 DynamicWidget 以支持连接 / 断开连接
  • 在存入界面中显示未屏蔽、屏蔽和DUST
  • 使用 isMidnightWallet 为所有 Midnight 调用添加保护
  • 除非用户要求设置样式,否则省略 CSS
  • 如果用户需要在 Dynamic 连接之上部署合约,链接到 1am-wallet/

22) 相关技能

需求技能
不使用 Dynamic 的 DApp Connectorreact-wallet-connector/
合约部署 + 电路1am-wallet/
代币单位和池token-transfers/
Locker / 支付金库 DAppexample-locker-dapp/、example-payment-dapp/

评论

支持 Markdown