example-locker-dapp について
Midnight Network 上で NIGHT をロックする dApp を構築する開発スキルです。Compact コントラクトを扱う開発者向けで、期限前の引き出しを防ぎ、期限後は受益者だけがトークンを解放できる Next.js テンプレートを提供します。
主な機能
- 01指定時刻まで NIGHT をロック
- 02期限前の引き出しを禁止
- 03受益者の検証後にトークンを解放
- 04ロック残高と状態を照会
- 05実行可能な Next.js テンプレート
活用シーン
- 01定期預金ロックの実装
- 02トークンの権利確定を分割して設定
- 03流動性トークンを期間指定でロック
- 04期限後に解放するエスクローの構築
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ kali-decoder/ midnight-skills --skill example-locker-dappインストール後、Claude Code で/example-locker-dappを入力して読み込まれたか確認します。
詳細
Midnight Network ロッカー DApp
ロッカー dApp は、unlockTime(Unix 秒、Uint<64>)になるまで、シールドされていない NIGHT を Compact コントラクト内に保管します。デプロイヤーを含め、誰も期限前に引き出すことはできません。期限を過ぎると、受取人だけが(witness による証明を通じて)release を呼び出し、トークンを受取先アドレスに送信できます。
実行可能なテンプレート: 完成した Next.js プロジェクト(コントラクト + UI)として templates/locker-dapp/ をコピーします。1AM ウォレットをインストールした後、npm install && npm run compact && npm run dev を実行します。
このスキルで生成されるもの:
contract/— Compact ロッカーボールト + TypeScript witness + コンパイルスクリプトapp/locker/— Next.js クライアント UI(接続、デプロイ、ロック、リリース、ステータス)lib/midnight.ts— ウォレットセッション + パッチ適用済みインデクサープロバイダー(コピー元:references/midnight-session.mdまたはtemplates/locker-dapp/lib/midnight.ts)lib/locker.ts— デプロイ、lockTokens、release、台帳のデコードpublic/zk/locker/— コントラクトビルドから同期した ZK 証明アセット
共有リファレンス(プロバイダーの正規実装 + トラブルシューティング — プロンプト内で重複させないでください):
references/midnight-session.md—createConnectedSession、インデクサーパッチ、デプロイ/呼び出しヘルパーreferences/gotchas.md— preprod でのデプロイのハング、GraphQL のoffset: null、ZK アセットのパスreferences/versions.json— 固定された@midnight-ntwrk/*のバージョン
主なリファレンス:
example-payment-dapp/— プロバイダーの接続、低レベルのデプロイ/呼び出し、インデクサーパッチ1am-wallet/— 1AM ブラウザー拡張機能、ダストを発生させないフローcompact/—blockTimeGte/blockTimeLt、disclose()、台帳 ADTtoken-transfers/— シールドされていない NIGHT の単位(Stars)、UserAddress受取人パターンindexer/— トランザクション後のコントラクト状態のポーリング
主なアーキテクチャ上の注意点:
blockTimeGte(unlockTime)を使用してください。台帳上で生のブロックタイムスタンプを比較しないでください(プライバシー + API 設計)- Unix タイムスタンプには必ず
Uint<64>を使用してください。Uint<16>では現在のエポック値を保持できません createUnprovenDeployTx+submitTxAsyncを使用してください。deployContract()は使用しないでください(preprod でハングします)indexerPublicDataProviderを、パッチ適用済みのqueryContractStateでラップしてください(GraphQL のoffset: nullバグ)- トークン量の単位は Stars です(1 NIGHT = 1_000_000 Stars)。あらゆる箇所で
BigIntを使用してください - 受取人の公開鍵バイト列はロック時にオンチェーンへ保存されます。
beneficiaryKeywitness がリリース権限を証明します
ワークフロー
ユーザーを支援する際は、次の手順に従ってください。
- コントラクト —
locker.compactのコンパイル + witness - プロバイダー —
createConnectedSession(references/midnight-session.mdから取得) - デプロイ — 低レベルのデプロイ、プライベート状態 + コントラクトアドレスの永続化
- ロック —
lockTokens(amount, releaseTime, beneficiaryPkBytes) - インデクサーのポーリング — 台帳から
balance、unlockTime、lockActiveを読み取る - リリース —
blockTimeGte(unlockTime)の後、受取人がrelease(recipient)を呼び出す - UI — カウントダウンを表示し、ロック解除時刻までリリースを無効にする(UX のみ — コントラクトが強制)
1) プロジェクト構成
locker-dapp/
├── package.json
├── next.config.mjs
├── lib/
│ ├── isomorphic-ws-fix.mjs
│ ├── midnight.ts # session, patched provider, hex helpers
│ └── locker.ts # deploy, lock, release, decode state
├── app/
│ ├── layout.tsx
│ └── locker/
│ └── LockerClient.tsx # all wallet + contract UI (client component)
├── contract/
│ ├── package.json
│ ├── src/
│ │ ├── locker.compact
│ │ ├── witnesses.ts
│ │ ├── index.ts # barrel → managed/locker
│ │ └── managed/locker/ # compiler output (gitignored)
│ └── tsconfig.json
└── public/zk/locker/ # synced keys + zkir (gitignored in dev, committed for deploy)
2) 前提条件
node --version # 20+ for Next.js; 22+ if adding vitest contract tests
docker --version # optional local stack
# Compact compiler
curl --proto '=https' --tlsv1.2 -sSf \
https://github.com/midnightntwrk/compact/releases/latest/download/compact-installer.sh | sh
source $HOME/.local/bin/env
ブラウザー:preprod の 1AM ウォレット拡張機能(または Lace フォールバック)。
3) ルート package.json
{
"name": "locker-dapp",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "next dev --webpack",
"build": "npm run sync:assets && next build --webpack",
"compact": "npm run compact --prefix contract",
"sync:assets": "node scripts/sync-zk-assets.mjs",
"postinstall": "npm install --prefix contract"
},
"dependencies": {
"@midnight-ntwrk/compact-runtime": "0.16.0",
"@midnight-ntwrk/ledger-v8": "8.0.3",
"@midnight-ntwrk/midnight-js-contracts": "4.0.4",
"@midnight-ntwrk/midnight-js-fetch-zk-config-provider": "4.0.4",
"@midnight-ntwrk/midnight-js-indexer-public-data-provider": "4.0.4",
"@midnight-ntwrk/midnight-js-network-id": "4.0.4",
"@midnight-ntwrk/midnight-js-types": "4.0.4",
"@midnight-ntwrk/wallet-sdk-address-format": "3.1.0",
"next": "^15.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"typescript": "^5.7.0"
}
}
4) contract/package.json
{
"name": "@locker/contract",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"compact": "compact compile src/locker.compact src/managed/locker"
},
"dependencies": {
"@midnight-ntwrk/compact-runtime": "0.16.0"
}
}
コンパイル:
npm run compact
# → contract/src/managed/locker/{contract,keys,zkir}/
npm run sync:assets
# → public/zk/locker/
5) contract/src/locker.compact
コントラクトインスタンスごとに有効なロックは 1 つです(シンプルなボールト)。並行してロックする場合は複数のコントラクトアドレスをデプロイするか、Map<Bytes<32>, LockRecord> を使って拡張してください(§18 を参照)。
pragma language_version >= 0.23;
import CompactStandardLibrary;
export ledger balance: Uint<128>;
export ledger unlockTime: Uint<64>;
export ledger beneficiary: Bytes<32>;
export ledger lockActive: Boolean;
export ledger totalLocked: Uint<128>;
export ledger totalReleased: Uint<128>;
witness beneficiaryKey(): Bytes<32>;
constructor() {
balance = 0;
unlockTime = 0;
beneficiary = default<Bytes<32>>;
lockActive = false;
totalLocked = 0;
totalReleased = 0;
}
// Deposit unshielded NIGHT and schedule release for beneficiaryPk at releaseTime (Unix seconds).
export circuit lockTokens(
amount: Uint<128>,
releaseTime: Uint<64>,
beneficiaryPk: Bytes<32>
): [] {
assert(!disclose(lockActive), "Vault already has an active lock");
assert(disclose(amount) > 0, "Amount must be positive");
receiveUnshielded(default<Bytes<32>>, disclose(amount));
beneficiary = disclose(beneficiaryPk);
unlockTime = disclose(releaseTime);
balance = disclose(amount);
lockActive = disclose(true);
totalLocked = disclose((totalLocked + amount) as Uint<128>);
}
// Beneficiary releases locked tokens after unlockTime to recipient (unshielded UserAddress).
export circuit release(recipient: UserAddress): [] {
assert(disclose(lockActive), "No active lock");
assert(blockTimeGte(disclose(unlockTime)), "Lock period not finished");
assert(deriveKey(beneficiaryKey()) == beneficiary, "Only beneficiary can release");
const amount = disclose(balance);
assert(amount > 0, "Nothing to release");
sendUnshielded(
default<Bytes<32>>,
amount,
right<ContractAddress, UserAddress>(disclose(recipient))
);
totalReleased = disclose((totalReleased + amount) as Uint<128>);
balance = 0;
lockActive = false;
}
pure circuit deriveKey(sk: Bytes<32>): Bytes<32> {
return persistentHash<Vector<2, Bytes<32>>>([pad(32, "locker:beneficiary:v1"), sk]);
}
時刻 API: blockTimeGte(t) は block_time >= t の場合に true を返します。Unix 秒には Uint<64> を使用してください。台帳に生の block_time を保存しないでください。
6) contract/src/witnesses.ts
import type { WitnessContext } from '@midnight-ntwrk/compact-runtime';
export type LockerPrivateState = {
beneficiarySecretKey: Uint8Array;
};
export const witnesses = {
beneficiaryKey: (context: WitnessContext<LockerPrivateState>) =>
[context.privateState, context.privateState.beneficiarySecretKey] as const,
};
受取人の Zswap シークレットシード(32 バイト)は、プライベート状態に保持する必要があります。自分自身のためにロックする場合は、プライベート状態を自分のシールド済みコインのシークレットで初期化してください。他の人のためにロックする場合は、その人が自身のロッカーをデプロイするか、マルチパーティーフローを使用する必要があります。リリース時に witness はオンチェーンの beneficiary フィールドと一致していなければなりません。
7) contract/src/index.ts
import { CompiledContract } from '@midnight-ntwrk/compact-runtime';
import { fileURLToPath } from 'node:url';
import path from 'node:path';
import { witnesses } from './witnesses.js';
export {
Contract,
ledger,
pureCircuits,
type Ledger,
} from './managed/locker/contract/index.js';
import { Contract } from './managed/locker/contract/index.js';
const currentDir = path.dirname(fileURLToPath(import.meta.url));
export const zkConfigPath = path.resolve(currentDir, 'managed', 'locker');
export const CompiledLockerContract = CompiledContract.make('locker', Contract).pipe(
CompiledContract.withWitnesses(witnesses),
CompiledContract.withCompiledFileAssets(zkConfigPath),
);
8) プロバイダーのセットアップ
example-payment-dapp/SKILL.md §「Provider Setup」からプロバイダーブロック全体をコピーします。
ConnectedSession型createConnectedSession(ZK パス →/zk/locker/)coinPublicKeyToBytescreatePatchedPublicDataProvidercreatePrivateStateProvider
次の箇所のみ変更します。
ZK_ASSET_PATH/FetchZkConfigProviderのベース →/zk/lockerapi.getConfiguration()の後にsetNetworkId(config.networkId)を設定
ウォレットの検出(1AM を優先し、Lace をフォールバックとして使用):
const wallet =
(window as any).midnight?.['1am'] ??
Object.values((window as any).midnight ?? {})[0];
ハードコードされた Lace キーより、Object.values(window.midnight) の列挙を優先してください。react-wallet-connector/ を参照してください。
9) lib/locker.ts
import { createUnprovenDeployTx, submitCallTxAsync } from '@midnight-ntwrk/midnight-js-contracts';
import { ContractState, sampleSigningKey } from '@midnight-ntwrk/compact-runtime';
import { CompiledLockerContract, Contract, ledger } from '../contract/src/index.js';
import type { ConnectedSession } from './midnight.js';
import { fromHex, pollForState } from './midnight.js';
const PRIVATE_STATE_ID = 'LockerPrivateState';
const ZK_ASSET_PATH = '/zk/locker';
function makeCompiledContract() {
return CompiledLockerContract as any;
}
export async function deployLocker(
session: ConnectedSession,
beneficiarySecretKey: Uint8Array,
): Promise<string> {
const initialPrivateState = { beneficiarySecretKey };
const deployTxData = await (createUnprovenDeployTx as any)(
{
zkConfigProvider: session.providers.zkConfigProvider,
walletProvider: session.providers.walletProvider,
},
{
compiledContract: makeCompiledContract(),
args: [],
privateStateId: PRIVATE_STATE_ID,
initialPrivateState,
signingKey: sampleSigningKey(),
},
);
const contractAddress = deployTxData.public.contractAddress;
await (submitTxAsync as any)(session.providers, {
unprovenTx: deployTxData.private.unprovenTx,
});
await session.providers.privateStateProvider.setContractAddress(contractAddress);
await session.providers.privateStateProvider.set(PRIVATE_STATE_ID, initialPrivateState);
await session.providers.privateStateProvider.setSigningKey(
contractAddress,
deployTxData.private.signingKey,
);
return contractAddress;
}
export async function lockTokens(
session: ConnectedSession,
contractAddress: string,
amount: bigint,
releaseTimeUnix: bigint,
beneficiaryPkBytes: Uint8Array,
) {
await (submitCallTxAsync as any)(session.providers, {
compiledContract: makeCompiledContract(),
contractAddress,
circuitId: 'lockTokens',
args: [amount, releaseTimeUnix, { bytes: beneficiaryPkBytes }],
privateStateId: PRIVATE_STATE_ID,
});
}
export async function releaseTokens(
session: ConnectedSession,
contractAddress: string,
recipientBytes: Uint8Array,
) {
await (submitCallTxAsync as any)(session.providers, {
compiledContract: makeCompiledContract(),
contractAddress,
circuitId: 'release',
args: [{ bytes: recipientBytes }],
privateStateId: PRIVATE_STATE_ID,
});
}
export function decodeLockerState(stateHex: string) {
const contractState = ContractState.deserialize(fromHex(stateHex));
const l = ledger(contractState.data);
return {
balance: l.balance as unknown as bigint,
unlockTime: l.unlockTime as unknown as bigint,
beneficiary: l.beneficiary as unknown as Uint8Array,
lockActive: l.lockActive as unknown as boolean,
totalLocked: l.totalLocked as unknown as bigint,
totalReleased: l.totalReleased as unknown as bigint,
};
}
export async function fetchLockerState(queryUrl: string, contractAddress: string) {
const hex = await pollForState(queryUrl, contractAddress);
return decodeLockerState(hex);
}
10) フロントエンド — app/locker/LockerClient.tsx
クライアントコンポーネントのパターン(payment dapp と同じ)。主な UI 状態:
| 状態 | UI |
|---|---|
| ウォレットが未接続 | 接続ボタン |
| 接続済み、コントラクトなし | ロッカーをデプロイ |
| デプロイ済み、ロックなし | ロックフォーム(額、ロック解除日時、受取人 = 自分) |
| 有効なロックあり、ロック解除前 | 残高、ロック解除日、カウントダウンを表示し、リリースボタンは無効 |
| 有効なロックあり、ロック解除後 | リリースボタンを有効化 → 受取人 = 自分の非シールドアドレスのバイト列 |
| リリース済み | 合計を表示し、新しいロックを提案(新しいコントラクトをデプロイするか、コントラクトを拡張) |
'use client';
import { useCallback, useEffect, useMemo, useState } from 'react';
import {
deployLocker,
fetchLockerState,
lockTokens,
releaseTokens,
} from '@/lib/locker';
const STARS_PER_NIGHT = 1_000_000n;
export default function LockerClient() {
const [session, setSession] = useState<ConnectedSession | null>(null);
const [contractAddress, setContractAddress] = useState<string | null>(null);
const [ledger, setLedger] = useState<LockerLedgerView | null>(null);
const [amountNight, setAmountNight] = useState('1');
const [unlockAt, setUnlockAt] = useState(''); // datetime-local input
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
const nowUnix = BigInt(Math.floor(Date.now() / 1000));
const canRelease = ledger?.lockActive && ledger.unlockTime <= nowUnix;
const refresh = useCallback(async () => {
if (!session || !contractAddress) return;
const state = await fetchLockerState(session.config.indexerUri, contractAddress);
setLedger(state);
}, [session, contractAddress]);
useEffect(() => { void refresh(); }, [refresh]);
async function onConnect() {
setError(null);
const wallet =
(window as any).midnight?.['1am'] ??
Object.values((window as any).midnight ?? {})[0];
if (!wallet) throw new Error('Install a Midnight wallet extension');
const api = await wallet.connect('preprod');
const { createConnectedSession } = await import('@/lib/midnight');
setSession(await createConnectedSession(api, '/zk/locker'));
}
async function onDeploy() {
if (!session) return;
setBusy(true);
setError(null);
try {
const beneficiarySecretKey = session.coinPublicKeyBytes; // lock to self
const addr = await deployLocker(session, beneficiarySecretKey);
setContractAddress(addr);
await refresh();
} catch (e) {
setError(String(e));
} finally {
setBusy(false);
}
}
async function onLock() {
if (!session || !contractAddress) return;
setBusy(true);
setError(null);
try {
const amount = BigInt(amountNight) * STARS_PER_NIGHT;
const releaseTime = BigInt(Math.floor(new Date(unlockAt).getTime() / 1000));
await lockTokens(
session,
contractAddress,
amount,
releaseTime,
session.coinPublicKeyBytes,
);
await refresh();
} catch (e) {
setError(String(e));
} finally {
setBusy(false);
}
}
async function onRelease() {
if (!session || !contractAddress) return;
setBusy(true);
setError(null);
try {
const recipientBytes = unshieldedAddressToBytes(session.unshieldedAddress);
await releaseTokens(session, contractAddress, recipientBytes);
await refresh();
} catch (e) {
setError(String(e));
} finally {
setBusy(false);
}
}
return (
<div>
<h1>Token Locker</h1>
{!session ? (
<button type="button" onClick={onConnect} disabled={busy}>Connect Wallet</button>
) : (
<>
<p>Connected: {session.unshieldedAddress}</p>
{!contractAddress ? (
<button type="button" onClick={onDeploy} disabled={busy}>Deploy Locker</button>
) : (
<>
<p>Contract: {contractAddress}</p>
{ledger?.lockActive ? (
<>
<p>Locked: {formatStars(ledger.balance)} Stars</p>
<p>Unlocks: {new Date(Number(ledger.unlockTime) * 1000).toLocaleString()}</p>
<button type="button" onClick={onRelease} disabled={busy || !canRelease}>
Release Tokens
</button>
</>
) : (
<>
<label>
Amount (NIGHT)
<input value={amountNight} onChange={(e) => setAmountNight(e.target.value)} />
</label>
<label>
Unlock at
<input type="datetime-local" value={unlockAt} onChange={(e) => setUnlockAt(e.target.value)} />
</label>
<button type="button" onClick={onLock} disabled={busy}>Lock Tokens</button>
</>
)}
</>
)}
</>
)}
{error ? <p role="alert">{error}</p> : null}
</div>
);
}
function formatStars(n: bigint) {
return n.toString();
}
// Implement: unshieldedAddressToBytes — decode mn_addr_* to 32-byte recipient for circuits
// Never use encodeUserAddress with raw coin pk; use bytes helper from 1am-wallet skill
必要に応じてスタイル(Tailwind/CSS)を追加してください。上記のロジックは、意図的に react-wallet-connector/ のようにスタイルを設定していません。
11) ZK アセットの同期 — scripts/sync-zk-assets.mjs
import { cpSync, mkdirSync, rmSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
const src = join(root, 'contract/src/managed/locker');
const dest = join(root, 'public/zk/locker');
rmSync(dest, { recursive: true, force: true });
mkdirSync(dest, { recursive: true });
for (const dir of ['keys', 'zkir']) {
cpSync(join(src, dir), join(dest, dir), { recursive: true });
}
SDK エラーのデバッグを始める前に、次を確認してください。
http://localhost:3000/zk/locker/keys/lockTokens.prover が 200 を返す必要があります。
12) Next.js の設定
example-payment-dapp/SKILL.md § Next.js と同じです。
- WebSocket シムとしての
lib/isomorphic-ws-fix.mjs asyncWebAssembly、topLevelAwait、isomorphic-wsエイリアスを設定したnext.config.mjs- スクリプトでは
next dev --webpackとnext build --webpackを必ず使用します
13) エンドツーエンドのフロー
1. npm install && npm run compact && npm run sync:assets
2. npm run dev
3. Connect 1AM wallet (preprod, funded with tNIGHT)
4. Deploy locker contract
5. Lock 1 NIGHT until chosen datetime
6. UI shows locked balance + unlock time (poll indexer every 2s after tx)
7. Wait until unlock (or advance clock on local undeployed only)
8. Beneficiary clicks Release → tokens sent to unshielded address
14) トラブルシューティング
| 症状 | 原因 | 解決方法 |
|---|---|---|
Lock period not finished | blockTimeGte が false | オンチェーンの時刻が unlockTime 以上になるまで待ちます。ウォレットのネットワークを確認してください |
Only beneficiary can release | Witness キーが台帳の beneficiary と一致しない | プライベート状態の beneficiarySecretKey から、ロック時に保存されたものと同じ pk が導出される必要があります |
Vault already has an active lock | 単一ロックのコントラクト | 先にリリースするか、別のコントラクトインスタンスをデプロイします |
タイムスタンプの Uint<16> コンパイルエラー | 整数幅が誤っている | すべての Unix 時刻に Uint<64> を使用します |
| デプロイが 30~120 秒間停止する | deployContract() を使用した | createUnprovenDeployTx + submitTxAsync を使用します |
GraphQL の offset: null | デフォルトのインデクサープロバイダー | パッチ適用済みの queryContractState を使用します |
| ZK 404 | アセットが同期されていない | npm run sync:assets |
Invalid character 'm' at position 0 | 受取人のアドレスエンコーディングが誤っている | encodeUserAddress(coinPk) ではなく、回路に { bytes: Uint8Array } を渡します |
| UI でロック解除前にリリースボタンが動作する | UI のみでチェックしている | コントラクトは引き続き拒否します。UX を修正しつつ、オンチェーンの blockTimeGte を信頼してください |
15) エージェントのチェックリスト
ユーザー向けにこの dApp を生成する際は、次の項目を確認してください。
-
lockTokens+release+blockTimeGteを含むlocker.compactを作成する - コントラクトをコンパイルし、ZK アセットを
public/zk/locker/に同期する - パッチ適用済みのインデクサーを使って
createConnectedSessionを接続する(payment dapp から取得) - ロック/リリースには、低レベルのデプロイ +
submitCallTxAsyncを使用する -
beneficiarySecretKeyをプライベート状態に保存し、ロック時にcoinPublicKeyBytesを渡す -
ledger(contractState.data)で台帳をデコードする。生のContractStateは使用しない - 額と Unix タイムスタンプには
BigIntを使用する - WASM 用に Next.js の
--webpackフラグを指定する - 1 NIGHT = 1_000_000 Stars と記載する
16) ユースケースの対応付け
| ユースケース | このテンプレートでの対応方法 |
|---|---|
| 定期預金 | unlockTime までユーザーがロックし、自分自身にリリースします |
| トークンのベスティング | トランシェごとにロッカーをデプロイするか、Map でスケジュールを拡張します(§18) |
| 流動性のロック | releaseTime まで LP 数量をロックし、検証用にコントラクトアドレスを公開します |
| エスクロー | 受益者に相手方の pk を設定し、オフチェーンで条件が満たされた後、期限を過ぎてからリリースします |
| NFT のロック | ここでは扱いません。シールドされたアセットには nft/ スキルとカスタムの receive/send パターンを使用してください |
17) ロック状態の読み取り(インデクサー)
// After lockTokens tx — poll until balance > 0
const state = await fetchLockerState(indexerUrl, contractAddress);
console.log({
locked: state.lockActive,
amount: state.balance,
unlockAt: new Date(Number(state.unlockTime) * 1000).toISOString(),
});
18) 拡張
マルチロック(Map)
単一の lockActive セルを次のように置き換えます。
struct LockRecord {
amount: Uint<128>,
unlockTime: Uint<64>,
beneficiary: Bytes<32>,
active: Boolean,
}
export ledger locks: Map<Bytes<32>, LockRecord>;
export ledger nextLockId: Counter;
release に lockId パラメーターを追加し、lockTokens ごとにカウンターを増分します。
線形ベスティング
unlockTime の値をずらして複数のロッカーコントラクトをデプロイするか、List<VestTranche> を持つ vesting.compact を追加し、blockTimeGte(tranche.unlockTime) を使用する個別の claimTranche(index) 回路を追加します。
React のみのウォレットシェル
まず接続 UI だけが必要な場合は、react-wallet-connector/ でスキャフォールドを作成してから、references/midnight-session.md のプロバイダー配線を追加します。
20) トラブルシューティング
完全な表については references/gotchas.md を参照してください。ロッカー固有のよくある問題:
| 症状 | 修正方法 |
|---|---|
unlockTime のオーバーフロー | Unix 秒には Uint<16> ではなく Uint<64> を使用します |
| 期限前のリリース | blockTimeGte(unlockTime) が成功する必要があります。インデクサーのブロック時刻を確認してください |
| リリース時の受益者が誤っている | Witness の beneficiaryKey は、ロック時に保存されたバイト列と一致する必要があります |
| preprod でデプロイが停止する | deployContract() ではなく、createUnprovenDeployTx と submitTxAsync を使用します |
21) 関連スキル
| 次のステップ | スキル |
|---|---|
| ウォレット接続のみ | react-wallet-connector/ |
| ペイメントボールト(タイムロックなし) | example-payment-dapp/ |
| CLI とテストの完全なリファレンス | example-counter/ |
| NFT のロック | nft/ |
| プライバシー監査 | security/ |