mcpField
E
example-locker-dapp

example-locker-dapp

github.com/kali-decoder開発18122 回インストール1 日前に更新
リポジトリ

example-locker-dapp について

Midnight Network 上で NIGHT をロックする dApp を構築する開発スキルです。Compact コントラクトを扱う開発者向けで、期限前の引き出しを防ぎ、期限後は受益者だけがトークンを解放できる Next.js テンプレートを提供します。

主な機能

  1. 01指定時刻まで NIGHT をロック
  2. 02期限前の引き出しを禁止
  3. 03受益者の検証後にトークンを解放
  4. 04ロック残高と状態を照会
  5. 05実行可能な Next.js テンプレート

活用シーン

  1. 01定期預金ロックの実装
  2. 02トークンの権利確定を分割して設定
  3. 03流動性トークンを期間指定でロック
  4. 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()、台帳 ADT
  • token-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 を使用してください
  • 受取人の公開鍵バイト列はロック時にオンチェーンへ保存されます。beneficiaryKey witness がリリース権限を証明します

ワークフロー

ユーザーを支援する際は、次の手順に従ってください。

  1. コントラクト — locker.compact のコンパイル + witness
  2. プロバイダー — createConnectedSession(references/midnight-session.md から取得)
  3. デプロイ — 低レベルのデプロイ、プライベート状態 + コントラクトアドレスの永続化
  4. ロック — lockTokens(amount, releaseTime, beneficiaryPkBytes)
  5. インデクサーのポーリング — 台帳から balance、unlockTime、lockActive を読み取る
  6. リリース — blockTimeGte(unlockTime) の後、受取人が release(recipient) を呼び出す
  7. 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/)
  • coinPublicKeyToBytes
  • createPatchedPublicDataProvider
  • createPrivateStateProvider

次の箇所のみ変更します。

  • ZK_ASSET_PATH / FetchZkConfigProvider のベース → /zk/locker
  • api.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 finishedblockTimeGte が falseオンチェーンの時刻が unlockTime 以上になるまで待ちます。ウォレットのネットワークを確認してください
Only beneficiary can releaseWitness キーが台帳の 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/

コメント