关于 dynamic-midnight-wallet
这是一项通过 Dynamic.xyz 连接 Midnight 钱包并生成 React、Vite 和 TypeScript 应用的技能。内容涵盖 1AM 扩展与嵌入式钱包流程,并提供钱包地址、余额和转账的类型化访问。
主要功能
- 01React + Vite + TypeScript 应用脚手架
- 02Dynamic 钱包连接配置
- 031AM 与嵌入式钱包流程
- 04类型化 Midnight 地址和余额访问
- 05Shielded 与 unshielded 代币转账
适用场景
- 01构建连接 1AM 钱包的 Web 应用
- 02通过社交或电子邮件接入嵌入式 Midnight 钱包
- 03展示 shielded、unshielded 和 DUST 钱包状态
- 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.addressShielded 私有代币池 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],
}}
>
控制面板要求:
- 在 Chains & Networks 中启用 Midnight
- 在 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 Connector | react-wallet-connector/ |
| 合约部署 + 电路 | 1am-wallet/ |
| 代币单位和池 | token-transfers/ |
| Locker / 支付金库 DApp | example-locker-dapp/、example-payment-dapp/ |