关于 example-locker-dapp
用于构建 Midnight Network 锁仓 dApp 的开发技能,面向需要用 Compact 合约锁定 NIGHT 的开发者。模板实现到期前不可提取、到期后仅受益人可释放,并提供可运行的 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 会将未屏蔽的 NIGHT 保存在 Compact 合约中,直到 unlockTime(Unix 秒,Uint<64>)。任何人(包括部署者)都无法提前提取。截止时间过后,只有受益人(通过 witness 证明)可以调用 release,将代币发送到收款地址。
**可运行模板:**复制 templates/locker-dapp/,获取完整的 Next.js 项目(合约 + UI)。安装 1AM 钱包后,运行 npm install && npm run compact && npm run dev。
此技能会生成:
contract/— Compact 锁仓金库 + TypeScript witnesses + 编译脚本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 部署挂起、GraphQLoffset: 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 上会挂起) - 使用修补后的
queryContractState包装indexerPublicDataProvider(GraphQLoffset: null错误) - 代币金额以 Stars 为单位(1 NIGHT = 1_000_000 Stars);任何地方都使用
BigInt - 受益人的公钥字节在锁仓时存储到链上;
beneficiaryKeywitness 用于证明释放权限
工作流
帮助用户时,请按以下顺序进行:
- 合约 — 编译
locker.compact+ witnesses - 提供程序 —
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
每个合约实例只能有一个活跃锁仓(简单金库)。部署多个合约地址以实现并行锁仓,或扩展为 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。使用 Uint<64> Unix 秒。不要在账本上存储原始 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 §“提供方设置”复制完整的提供方代码块:
ConnectedSession类型createConnectedSession(ZK 路径 →/zk/locker/)coinPublicKeyToBytescreatePatchedPublicDataProvidercreatePrivateStateProvider
只更改:
- 将
ZK_ASSET_PATH/FetchZkConfigProvider的 base 改为/zk/locker - 在
api.getConfiguration()之后调用setNetworkId(config.networkId)
钱包检测(优先 1AM,Lace 作为后备):
const wallet =
(window as any).midnight?.['1am'] ??
Object.values((window as any).midnight ?? {})[0];
优先枚举 Object.values(window.midnight),不要硬编码 Lace 键 — 参见 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 相同:
lib/isomorphic-ws-fix.mjsWebSocket shim- 配置
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 | 接收方地址编码错误 | 向电路传入 { bytes: Uint8Array },而不是 encodeUserAddress(coinPk) |
| UI 中的释放按钮在时间到之前可用 | 仅在 UI 中进行了检查 | 合约仍会拒绝 — 修复 UX,但信任链上的 blockTimeGte |
15) Agent 检查清单
为用户生成此 dApp 时:
- 创建包含
lockTokens+release+blockTimeGte的locker.compact - 编译合约;将 ZK 资产同步到
public/zk/locker/ - 使用修补后的索引器(来自 payment dapp)连接
createConnectedSession - 对锁定/释放使用低级部署 +
submitCallTxAsync - 在私有状态中存储
beneficiarySecretKey;锁定时传入coinPublicKeyBytes - 使用
ledger(contractState.data)解码账本 — 不要直接使用原始ContractState - 金额和 Unix 时间戳使用
BigInt - Next.js 使用
--webpack标志支持 WASM - 说明:1 NIGHT = 1_000_000 Stars
16)用例映射
| 用例 | 此模板的映射方式 |
|---|---|
| 定期存款 | 用户锁定资产,直到 unlockTime,然后将其释放给自己 |
| 代币归属 | 每个批次部署一个锁仓合约,或扩展为使用 Map 管理多个计划(§18) |
| 流动性锁定 | 锁定 LP 数量,直到 releaseTime;发布合约地址以供验证 |
| 托管 | 将受益人设为交易对手的 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 值各不相同的锁仓合约,或添加一个 vesting.compact,其中包含 List<VestTranche>,并使用 blockTimeGte(tranche.unlockTime) 电路单独实现 claimTranche(index)。
仅 React 钱包外壳
如果用户目前只需要连接 UI,请先使用 react-wallet-connector/ 搭建框架,然后从 references/midnight-session.md 添加提供程序接线。
20)故障排除
完整表格请参阅 references/gotchas.md。锁仓合约常见问题:
| 症状 | 修复方法 |
|---|---|
unlockTime 溢出 | Unix 秒使用 Uint<64>,而不是 Uint<16> |
| 截止时间前释放 | 必须通过 blockTimeGte(unlockTime)——检查索引器区块时间 |
| 释放时受益人错误 | Witness beneficiaryKey 必须与锁定时存储的字节相匹配 |
| 在 preprod 上部署卡住 | 使用 createUnprovenDeployTx + submitTxAsync,而不是 deployContract() |
21)相关技能
| 后续步骤 | 技能 |
|---|---|
| 仅连接钱包 | react-wallet-connector/ |
| 支付金库(无时间锁) | example-payment-dapp/ |
| CLI + 测试完整参考 | example-counter/ |
| NFT 锁定 | nft/ |
| 隐私审计 | security/ |