example-locker-dapp
Skill18 starsChanged 16 days agoMITView on GitHub
Build a time-lock vault dApp on Midnight Network — users lock unshielded NIGHT tokens in a Compact smart contract until a Unix timestamp, then the beneficiary releases them. Covers locker.compact (blockTimeGte unlock), Next.js frontend, 1AM wallet integration, low-level deploy/call, indexer polling, and ZK asset hosting. Use for token vesting, liquidity locks, escrow-style lockups, or any "lock until date" dApp on Midnight. Triggers: locker dApp, token lock, time vault, vesting schedule, lockup, release tokens, blockTime deadline, receiveUnshielded/sendUnshielded vault. Also use when debugging unlock timing, beneficiary auth, or extending the payment-dapp wallet/provider pattern.
- Installs packages
In this repository36 other files it gives its agents.
- AGENTS.mdAGENTS.md
- 1am-walletSkill
- android-example-votingSkill
- ankr-midnightSkill
- compactSkill
- dynamic-midnight-walletSkill
- example-counterSkill
- example-hello-worldSkill
- example-leaderboard-dappSkill
- example-passport-dappSkill
- example-payment-dappSkill
- example-private-party-dappSkill
- example-private-reserve-auction-dappSkill
- example-zk-loan-applicationSkill
- gsap-coreSkill
- gsap-utilsSkill
- indexerSkill
- midnight-consensusSkill
- midnight-cryptographySkill
- midnight-environment-setupSkill
- midnight-jsSkill
- midnight-onchain-logicSkill
- midnight-p2p-networkingSkill
- midnight-passportSkill
- midnight-rpcSkill
- midnightskillSkill
- midnight-storageSkill
- midnight-transactionsSkill
- multinetworkSkill
- nftSkill
- react-wallet-connectorSkill
- midnight-securitySkill
- testingSkill
- token-transfersSkill
- why-midnightSkill
- zk-vs-fhe-vs-mpcSkill
The file
We split this skill into the rules it gives an agent
10 rules. About 0 are common, 2 are distinctive, and 8 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
8
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 40
Use blockTimeGte(unlockTime) — never compare raw block timestamps on ledger (privacy + API design)
- Very specific: line 41
Always use Uint<64> for Unix timestamps — Uint<16> cannot hold current epoch values
- Very specific: line 42
Use createUnprovenDeployTx + submitTxAsync — not deployContract() (hangs on preprod)
- Distinctive: line 51
When helping the user, follow this sequence:
- Very specific: line 308
Prefer enumerating Object.values(window.midnight) over hardcoded Lace keys — see react-wallet-connector/.
- Very specific: line 575
Add styling (Tailwind/CSS) as needed — logic above is intentionally unstyled like react-wallet-connector/.
- Very specific: line 597
Verify before debugging SDK errors: http://localhost:3000/zk/locker/keys/lockTokens.prover must return 200.
- Distinctive: line 644
When generating this dApp for a user:
- Very specific: line 702
Add lockId parameter to release.
- Very specific: line 710
If the user only needs connect UI first, scaffold with react-wallet-connector/ then add provider wiring from references/midnight-session.md.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Midnight Network Locker DApp
A locker dApp holds unshielded NIGHT in a Compact contract until unlockTime (Unix seconds, Uint<64>). No one — including the deployer — can withdraw early. After the deadline, only the beneficiary (proven via witness) can call release and send tokens to a recipient address.
Runnable template: Copy templates/locker-dapp/ for a complete Next.js project (contract + UI). Run npm install && npm run compact && npm run dev after installing the 1AM wallet.
What this skill produces:
contract/— Compact locker vault + TypeScript witnesses + compile scriptsapp/locker/— Next.js client UI (connect, deploy, lock, release, status)lib/midnight.ts— wallet session + patched indexer provider (copy fromreferences/midnight-session.mdortemplates/locker-dapp/lib/midnight.ts)lib/locker.ts— deploy,lockTokens,release, ledger decodepublic/zk/locker/— ZK proving assets synced from contract build
Shared references (canonical provider + troubleshooting — do not duplicate in prompts):
references/midnight-session.md—createConnectedSession, indexer patch, deploy/call helpersreferences/gotchas.md— preprod deploy hangs, GraphQLoffset: null, ZK asset pathsreferences/versions.json— pinned@midnight-ntwrk/*versions
Primary references:
example-payment-dapp/— provider wiring, low-level deploy/call, indexer patch1am-wallet/— 1AM browser extension, dust-free flowcompact/—blockTimeGte/blockTimeLt,disclose(), ledger ADTstoken-transfers/— unshielded NIGHT units (Stars),UserAddressrecipient patternindexer/— poll contract state after transactions
Key architecture notes:
- Use
blockTimeGte(unlockTime)— never compare raw block timestamps on ledger (privacy + API design) - Always use
Uint<64>for Unix timestamps —Uint<16>cannot hold current epoch values - Use
createUnprovenDeployTx+submitTxAsync— notdeployContract()(hangs on preprod) - Wrap
indexerPublicDataProviderwith the patchedqueryContractState(GraphQLoffset: nullbug) - Token amounts are in Stars (1 NIGHT = 1_000_000 Stars); use
BigInteverywhere - Beneficiary public key bytes are stored on-chain at lock time;
beneficiaryKeywitness proves release authority
Workflow
When helping the user, follow this sequence:
- Contract —
locker.compactcompile + witnesses - Providers —
createConnectedSession(fromreferences/midnight-session.md) - Deploy — low-level deploy, persist private state + contract address
- Lock —
lockTokens(amount, releaseTime, beneficiaryPkBytes) - Poll indexer — read
balance,unlockTime,lockActivefrom ledger - Release — after
blockTimeGte(unlockTime), beneficiary callsrelease(recipient) - UI — show countdown, disable release until unlock time (UX only — contract enforces)
1) Project Structure
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) Prerequisites
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/envBrowser: 1AM wallet extension on preprod (or Lace fallback).
3) Root 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"
}
}Compile:
npm run compact
# → contract/src/managed/locker/{contract,keys,zkir}/
npm run sync:assets
# → public/zk/locker/5) contract/src/locker.compact
Single active lock per contract instance (simple vault). Deploy multiple contract addresses for parallel locks, or extend with Map<Bytes<32>, LockRecord> (see §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]);
}Time API: blockTimeGte(t) returns true when block_time >= t. Use Uint<64> Unix seconds. Do not store raw block_time on ledger.
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,
};The beneficiary's Zswap secret seed (32 bytes) must live in private state. When locking for yourself, initialize private state with your shielded coin secret. When locking for someone else, they must deploy their own locker or use a multi-party flow — the witness must match the on-chain beneficiary field at release.
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) Provider Setup
Copy the full provider block from example-payment-dapp/SKILL.md § "Provider Setup":
ConnectedSessiontypecreateConnectedSession(ZK path →/zk/locker/)coinPublicKeyToBytescreatePatchedPublicDataProvidercreatePrivateStateProvider
Change only:
ZK_ASSET_PATH/FetchZkConfigProviderbase →/zk/lockersetNetworkId(config.networkId)afterapi.getConfiguration()
Wallet detection (1AM first, Lace fallback):
const wallet =
(window as any).midnight?.['1am'] ??
Object.values((window as any).midnight ?? {})[0];Prefer enumerating Object.values(window.midnight) over hardcoded Lace keys — see 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) Frontend — app/locker/LockerClient.tsx
Client component pattern (same as payment dapp). Core UI states:
| State | UI |
| Wallet disconnected | Connect button |
| Connected, no contract | Deploy locker |
| Deployed, no lock | Lock form (amount, unlock datetime, beneficiary = self) |
| Active lock, before unlock | Show balance, unlock date, countdown, disabled Release |
| Active lock, after unlock | Enabled Release → recipient = own unshielded address bytes |
| Released | Show totals, offer new lock (deploy new contract or extend contract) |
'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 skillAdd styling (Tailwind/CSS) as needed — logic above is intentionally unstyled like react-wallet-connector/.
11) ZK Asset Sync — 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 });
}Verify before debugging SDK errors:
http://localhost:3000/zk/locker/keys/lockTokens.prover must return 200.
12) Next.js Config
Same as example-payment-dapp/SKILL.md § Next.js:
lib/isomorphic-ws-fix.mjsWebSocket shimnext.config.mjswithasyncWebAssembly,topLevelAwait,isomorphic-wsalias- Scripts must use
next dev --webpackandnext build --webpack
13) End-to-End Flow
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 address14) Troubleshooting
| Symptom | Cause | Fix |
Lock period not finished | blockTimeGte false | Wait until on-chain time ≥ unlockTime; check wallet network |
Only beneficiary can release | Witness key ≠ ledger beneficiary | Private state beneficiarySecretKey must derive same pk stored at lock |
Vault already has an active lock | Single-lock contract | Release first, or deploy another contract instance |
Uint<16> compile error on timestamp | Wrong integer width | Use Uint<64> for all Unix times |
| Deploy hangs 30–120s | Used deployContract() | Use createUnprovenDeployTx + submitTxAsync |
GraphQL offset: null | Default indexer provider | Use patched queryContractState |
| ZK 404 | Assets not synced | npm run sync:assets |
Invalid character 'm' at position 0 | Wrong address encoding for recipient | Pass { bytes: Uint8Array } to circuits, not encodeUserAddress(coinPk) |
| Release button works before time in UI | UI-only check | Contract still rejects — fix UX but trust on-chain blockTimeGte |
15) Agent Checklist
When generating this dApp for a user:
- Create
locker.compactwithlockTokens+release+blockTimeGte - Compile contract; sync ZK assets to
public/zk/locker/ - Wire
createConnectedSessionwith patched indexer (from payment dapp) - Use low-level deploy +
submitCallTxAsyncfor lock/release - Store
beneficiarySecretKeyin private state; passcoinPublicKeyBytesat lock - Decode ledger with
ledger(contractState.data)— not rawContractState - Use
BigIntfor amounts and Unix timestamps - Next.js
--webpackflag for WASM - Document: 1 NIGHT = 1_000_000 Stars
16) Use Case Mapping
| Use case | How this template maps |
| Fixed deposit | User locks until unlockTime, releases to self |
| Token vesting | Deploy one locker per tranche, or extend with Map of schedules (§18) |
| Liquidity lock | Lock LP amount until releaseTime; publish contract address for verification |
| Escrow | Set beneficiary to counterparty pk; release after deadline when conditions met off-chain |
| NFT lock | Not covered here — use nft/ skill + custom receive/send pattern for shielded assets |
17) Reading Lock Status (Indexer)
// 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) Extensions
Multi-lock (Map)
Replace single lockActive cell with:
struct LockRecord {
amount: Uint<128>,
unlockTime: Uint<64>,
beneficiary: Bytes<32>,
active: Boolean,
}
export ledger locks: Map<Bytes<32>, LockRecord>;
export ledger nextLockId: Counter;Add lockId parameter to release; increment counter on each lockTokens.
Linear vesting
Deploy multiple locker contracts with staggered unlockTime values, or add a vesting.compact with List<VestTranche> and separate claimTranche(index) circuit using blockTimeGte(tranche.unlockTime).
React-only wallet shell
If the user only needs connect UI first, scaffold with react-wallet-connector/ then add provider wiring from references/midnight-session.md.
20) Troubleshooting
See references/gotchas.md for the full table. Common locker-specific issues:
| Symptom | Fix |
unlockTime overflow | Use Uint<64> for Unix seconds, not Uint<16> |
| Release before deadline | blockTimeGte(unlockTime) must pass — check indexer block time |
| Wrong beneficiary on release | Witness beneficiaryKey must match bytes stored at lock time |
| Deploy hangs on preprod | Use createUnprovenDeployTx + submitTxAsync, not deployContract() |
21) Related Skills
| Next step | Skill |
| Wallet connect only | react-wallet-connector/ |
| Payment vault (no timelock) | example-payment-dapp/ |
| Full CLI + tests reference | example-counter/ |
| NFT locking | nft/ |
| Privacy audit | security/ |
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

