dynamic-midnight-wallet について
Dynamic.xyz経由でMidnightウォレットに接続するReact、Vite、TypeScriptアプリを生成するスキルです。1AM拡張機能と組み込みウォレットの両方を扱い、型付きのアドレス、残高、送金アクセスを提供します。
主な機能
- 01React + Vite + TypeScriptアプリのひな形
- 02Dynamicウォレット接続の設定
- 031AMと組み込みウォレットのフロー
- 04型付きMidnightアドレスと残高へのアクセス
- 05Shieldedとunshieldedのトークン送金
活用シーン
- 011AMウォレットに接続するWebアプリの構築
- 02ソーシャルまたはメール対応の組み込みMidnightウォレット追加
- 03Shielded、unshielded、DUSTの状態表示
- 04NIGHT送金を対応するプールへ振り分け
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ kali-decoder/ midnight-skills --skill dynamic-midnight-walletインストール後、Claude Code で/dynamic-midnight-walletを入力して読み込まれたか確認します。
詳細
Dynamic Midnight Wallet スキル
このスキルでは、Dynamic.xyz(@dynamic-labs/midnight)を通じてMidnightウォレットに接続する、完全なReact + Vite + TypeScriptアプリを生成します。手作業でのwindow.midnight接続処理を、DynamicContextProvider、DynamicWidget、型付きの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ウォレットは単一のアドレスではなく、3つの領域があります。
領域 役割 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を直接使ったコントラクトのデプロイと回路呼び出し →
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+
- Chains & NetworksでMidnightを有効化したDynamic.xyzアカウント
- ダッシュボードから取得した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
3つの領域と残高をすべて表示します。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ウォレットに接続すると、パネルに3つの領域と残高が表示されます。
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)
回路でコイン/暗号化の公開鍵が必要な場合:
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 extension | 鍵の管理、導出、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) エージェント用チェックリスト
ユーザー向けにこのテンプレートを生成する場合:
- Vite
react-tsのひな形を作成するか、このスキルからすべてのファイルを書き込みます -
@dynamic-labs/sdk-react-core、@dynamic-labs/midnight、@dynamic-labs/sdk-api-coreを同じバージョンでインストールします -
.envにVITE_DYNAMIC_ENVIRONMENT_IDを設定します - 拡張機能には
MidnightWalletConnectors(デフォルト)、埋め込みウォレットにはDynamicWaasMidnightConnectorsを使用します - 接続/切断用に
DynamicWidgetを含めます - 入金 UI に未シールド、シールド済み、DUST を表示します
- すべての Midnight 呼び出しを
isMidnightWalletでガードします - ユーザーからスタイルの指定がない限り、CSS は省略します
- Dynamic 接続上でのコントラクトデプロイが必要なユーザーには
1am-wallet/へのリンクを案内します
22) 関連スキル
| 必要なもの | スキル |
|---|---|
| Dynamic を使わない DApp コネクター | react-wallet-connector/ |
| コントラクトのデプロイと回路 | 1am-wallet/ |
| トークン単位とプール | token-transfers/ |
| Locker/ペイメントボールト DApp | example-locker-dapp/、example-payment-dapp/ |