example-zk-loan-application
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseMidnight Network ZK Loan Application
Midnight Network 零知识贷款应用
A privacy-preserving loan dApp where applicants prove eligibility inside a ZK circuit without revealing credit score, income, or tenure on-chain. A trusted attestation provider signs credit data off-chain; the contract verifies the Schnorr signature in-circuit and writes only the loan outcome (status + authorized amount) to the ledger.
What this skill produces:
- —
contract/+schnorr.compact, witnesses, compile scriptszkloan-credit-scorer.compact - — REST server that Schnorr-signs credit profiles
zkloan-credit-scorer-attestation-api/ - — deploy, register providers, request/respond to loans (headless wallet)
zkloan-credit-scorer-cli/ - (optional) — Next.js + 1AM wallet UI (copy patterns from
app/loan/)templates/leaderboard-dapp/ - — deploy,
lib/zkloan.ts,requestLoan, admin calls, ledger decoderespondToLoan - — wallet session + patched indexer provider (copy from
lib/midnight.ts)references/midnight-session.md - — ZK proving assets synced from contract build
public/zk/zkloan/
Shared references (canonical provider + troubleshooting — do not duplicate in prompts):
- —
references/midnight-session.md, indexer patch, deploy/call helperscreateConnectedSession - — preprod deploy hangs, GraphQL
references/gotchas.md, ZK asset pathsoffset: null - — pinned
references/versions.jsonversions@midnight-ntwrk/*
Primary references:
- /
example-leaderboard-dapp/— Next.js + 1AM, low-level deploy/call, indexer readstemplates/leaderboard-dapp/ - — witness private state,
example-locker-dapp/+createUnprovenDeployTxsubmitTxAsync - — headless CLI wallet, monorepo workspaces, vitest contract tests
example-counter/ - —
compact/, witnesses,disclose(),persistentHash,Map,Set, pure circuitsnew type - — never trust
security/for caller identity; witness-derived keysownPublicKey() - — read public loan ledger without wallet
indexer/
Key architecture notes:
- Credit score, income, tenure, and attestation signature stay private (witness only)
- Only loan status + authorized amount + derived user pubkey bytes appear on-chain
- Never use for auth — it is prover-supplied and bypassable; derive identity from
ownPublicKey()witness + PIN viagetUserSecret()/deriveUserPublicKeyderiveAdminPublicKey - Attestation binds to inside
transientHash(userPubKeyBytes)evaluateApplicant - Eligibility tiers: ≥700 + ≥$2k + ≥24mo → $10k; ≥600 + ≥$1.5k → $7k; ≥580 → $3k; else rejected
- may return Proposed when amount exceeds tier max — user calls
requestLoanto accept/declinerespondToLoan - PIN change migrates loans in batches of 5 via +
changePinledger maponGoingPinMigration - Use +
createUnprovenDeployTx— notsubmitTxAsync(hangs on preprod)deployContract() - Node.js ≥ 22 required for (Iterator helpers on
@midnight-ntwrk/wallet-sdk-shielded)Map.values()
这是一个隐私保护型借贷dApp,申请人在ZK电路中证明自己的贷款资格,无需在链上披露信用评分、收入或工作年限。可信的证明提供商在链下对信用数据进行签名;合约在电路内验证Schnorr签名,仅将贷款结果(状态+授权金额)写入账本。
该方案产出的内容:
- —
contract/+schnorr.compact、见证文件、编译脚本zkloan-credit-scorer.compact - — 对信用档案进行Schnorr签名的REST服务器
zkloan-credit-scorer-attestation-api/ - — 部署、注册提供商、请求/响应贷款的无头钱包工具
zkloan-credit-scorer-cli/ - (可选) — Next.js + 1AM钱包UI(从
app/loan/中复制模式)templates/leaderboard-dapp/ - — 部署、
lib/zkloan.ts、requestLoan、管理员调用、账本解码逻辑respondToLoan - — 钱包会话 + 修补后的索引器提供商(从
lib/midnight.ts复制)references/midnight-session.md - — 从合约构建同步而来的ZK证明资产
public/zk/zkloan/
共享参考资料(标准提供商+故障排查——请勿在提示中重复):
- —
references/midnight-session.md、索引器补丁、部署/调用辅助工具createConnectedSession - — 预生产环境部署卡顿、GraphQL
references/gotchas.md、ZK资产路径问题offset: null - — 固定版本的
references/versions.json依赖@midnight-ntwrk/*
核心参考资料:
- /
example-leaderboard-dapp/— Next.js + 1AM、底层部署/调用、索引器读取templates/leaderboard-dapp/ - — 见证私有状态、
example-locker-dapp/+createUnprovenDeployTxsubmitTxAsync - — 无头CLI钱包、单仓项目工作区、vitest合约测试
example-counter/ - —
compact/、见证、disclose()、persistentHash、Map、Set、纯电路new type - — 绝不要信任
security/用于调用者身份验证;使用见证派生密钥ownPublicKey() - — 无需钱包即可读取公开贷款账本
indexer/
关键架构说明:
- 信用评分、收入、工作年限和证明签名始终保持私密(仅作为见证)
- 仅贷款状态+授权金额+派生用户公钥字节会出现在链上
- **绝不要使用**进行身份验证——它由证明者提供,可被绕过;通过
ownPublicKey()见证+PIN,使用getUserSecret()/deriveUserPublicKey派生身份deriveAdminPublicKey - 证明在内部绑定到
evaluateApplicanttransientHash(userPubKeyBytes) - 资格分层:≥700分 + ≥2000美元 + ≥24个月 → 10000美元;≥600分 + ≥1500美元 →7000美元;≥580分 →3000美元;其余情况拒绝
- 当申请金额超过分层上限时,可能返回Proposed状态——用户需调用
requestLoan接受/拒绝respondToLoan - PIN变更通过+
changePin账本映射批量迁移贷款,每次交易最多处理5笔onGoingPinMigration - 使用+
createUnprovenDeployTx——不要使用submitTxAsync(在预生产环境会卡顿)deployContract() - Node.js ≥22是的必要条件(依赖
@midnight-ntwrk/wallet-sdk-shielded的迭代器辅助方法)Map.values()
Workflow
工作流程
When helping the user, follow this sequence:
- Monorepo — root workspaces:
package.json,contract,attestation-apicli - Contract — compile Compact (+
schnorr.compact) + witnesseszkloan-credit-scorer.compact - Attestation API — generate/load provider Jubjub keypair; sign credit profiles; expose REST endpoint
- Deploy — CLI or browser: low-level deploy, persist + contract address in private state
userSecretKey - Register provider — admin calls on-chain
registerProvider(providerId, providerPk) - Request loan — fetch attestation → populate private state →
requestLoan(amount, secretPin) - Respond — if status is , call
ProposedrespondToLoan(loanId, secretPin, accept) - Read state — indexer GraphQL → decode nested map (public)
loans - Frontend (optional) — connect 1AM, deploy/join, loan form, loan table, admin panel
协助用户时,请遵循以下步骤:
- 单仓项目 — 根目录工作区:
package.json、contract、attestation-apicli - 合约 — 编译Compact文件(+
schnorr.compact)+ 见证文件zkloan-credit-scorer.compact - 证明API — 生成/加载提供商Jubjub密钥对;签名信用档案;暴露REST端点
- 部署 — 通过CLI或浏览器:底层部署,在私有状态中持久化+ 合约地址
userSecretKey - 注册提供商 — 管理员在链上调用
registerProvider(providerId, providerPk) - 申请贷款 — 获取证明 → 填充私有状态 →
requestLoan(amount, secretPin) - 响应申请 — 如果状态为,调用
ProposedrespondToLoan(loanId, secretPin, accept) - 读取状态 — 索引器GraphQL → 解码嵌套的映射(公开数据)
loans - 前端 (可选) — 连接1AM钱包、部署/加入合约、贷款表单、贷款列表、管理员面板
1) Monorepo Structure
1) 单仓项目结构
zkloan-credit-scorer/
├── package.json # workspaces + pinned midnight-js 4.0.4 / ledger 8.0.3
├── .nvmrc # 22
├── contract/
│ ├── package.json
│ └── src/
│ ├── schnorr.compact
│ ├── zkloan-credit-scorer.compact
│ ├── witnesses.ts
│ ├── index.ts
│ └── managed/zkloan-credit-scorer/ # compiler output (gitignored)
├── zkloan-credit-scorer-attestation-api/
│ └── src/ # Express/Fastify REST — sign credit profiles
├── zkloan-credit-scorer-cli/
│ └── src/ # deploy, register-provider, request-loan, admin
└── zkloan-dapp/ # optional Next.js frontend
├── lib/midnight.ts # copy from templates/leaderboard-dapp
├── lib/zkloan.ts
├── app/loan/LoanClient.tsx
├── contract/ # symlink or workspace import from ../contract
├── scripts/sync-zk-assets.mjs
└── public/zk/zkloan/Root engines: . Workspaces pin , , .
package.json"node": ">=22.0.0"@midnight-ntwrk/compact-runtime: ^0.16.0@midnight-ntwrk/midnight-js-*: 4.0.4@midnight-ntwrk/ledger-v8: 8.0.3zkloan-credit-scorer/
├── package.json # 工作区 + 固定版本的midnight-js 4.0.4 / ledger 8.0.3
├── .nvmrc # 指定Node.js版本22
├── contract/
│ ├── package.json
│ └── src/
│ ├── schnorr.compact
│ ├── zkloan-credit-scorer.compact
│ ├── witnesses.ts
│ ├── index.ts
│ └── managed/zkloan-credit-scorer/ # 编译器输出(已加入git忽略)
├── zkloan-credit-scorer-attestation-api/
│ └── src/ # Express/Fastify REST服务——签名信用档案
├── zkloan-credit-scorer-cli/
│ └── src/ # 部署、注册提供商、申请贷款、管理员操作
└── zkloan-dapp/ # 可选Next.js前端
├── lib/midnight.ts # 从templates/leaderboard-dapp复制
├── lib/zkloan.ts
├── app/loan/LoanClient.tsx
├── contract/ # 符号链接或从../contract导入工作区依赖
├── scripts/sync-zk-assets.mjs
└── public/zk/zkloan/根目录引擎配置:。工作区固定、、版本。
package.json"node": ">=22.0.0"@midnight-ntwrk/compact-runtime: ^0.16.0@midnight-ntwrk/midnight-js-*: 4.0.4@midnight-ntwrk/ledger-v8: 8.0.32) Compact Contract
2) Compact合约
Schnorr module (contract/src/schnorr.compact
)
contract/src/schnorr.compactSchnorr模块(contract/src/schnorr.compact
)
contract/src/schnorr.compactVerifies Jubjub Schnorr signatures inside the circuit. Temporary polyfill until ships in Compact Standard Library.
jubjubSchnorrVerifycompact
module schnorr {
import CompactStandardLibrary;
export struct SchnorrSignature { announcement: JubjubPoint; response: Field; }
witness getSchnorrReduction(challengeHash: Field): [Field, Uint<248>];
export circuit schnorrVerify<#n>(msg: Vector<n, Field>, signature: SchnorrSignature, pk: JubjubPoint): [] { /* ... */ }
export pure circuit schnorrChallenge(...): Field { /* transientHash challenge */ }
}JubjubPoint equality: compare / — struct is reference equality and always fails for fresh points.
jubjubPointX()jubjubPointY()==在电路内验证Jubjub Schnorr签名。这是临时的兼容实现,直到加入Compact标准库。
jubjubSchnorrVerifycompact
module schnorr {
import CompactStandardLibrary;
export struct SchnorrSignature { announcement: JubjubPoint; response: Field; }
witness getSchnorrReduction(challengeHash: Field): [Field, Uint<248>];
export circuit schnorrVerify<#n>(msg: Vector<n, Field>, signature: SchnorrSignature, pk: JubjubPoint): [] { /* ... */ }
export pure circuit schnorrChallenge(...): Field { /* transientHash challenge */ }
}JubjubPoint相等性检查: 比较 / ——结构体是引用相等性检查,对于新生成的点始终返回false。
jubjubPointX()jubjubPointY()==Main contract (contract/src/zkloan-credit-scorer.compact
)
contract/src/zkloan-credit-scorer.compact主合约(contract/src/zkloan-credit-scorer.compact
)
contract/src/zkloan-credit-scorer.compactcompact
pragma language_version >= 0.22 && <= 0.23;
import CompactStandardLibrary;
import "schnorr" prefix Schnorr_;
export enum LoanStatus { Approved, Rejected, Proposed, NotAccepted }
export struct LoanApplication { authorizedAmount: Uint<16>; status: LoanStatus; }
export new type UserSecretKey = Bytes<32>;
export new type UserPublicKey = Bytes<32>;
export new type AdminPublicKey = Bytes<32>;
export ledger blacklist: Set<UserPublicKey>;
export ledger loans: Map<Bytes<32>, Map<Uint<16>, LoanApplication>>;
export ledger onGoingPinMigration: Map<Bytes<32>, Uint<16>>;
export ledger contractAdmin: AdminPublicKey;
export ledger providers: Map<Uint<16>, JubjubPoint>;
witness getAttestedScoringWitness(): [Applicant, Schnorr_SchnorrSignature, Uint<16>];
witness getUserSecret(): UserSecretKey;
export pure circuit deriveUserPublicKey(sk: UserSecretKey, pin: Uint<16>): UserPublicKey { /* persistentHash domain "zkloan:user:pk:v1" */ }
export pure circuit deriveAdminPublicKey(sk: UserSecretKey): AdminPublicKey { /* "zkloan:admin:pk:v1" */ }
export circuit requestLoan(amountRequested: Uint<16>, secretPin: Uint<16>): [] { /* ... */ }
export circuit respondToLoan(loanId: Uint<16>, secretPin: Uint<16>, accept: Boolean): [] { /* ... */ }
export circuit changePin(oldPin: Uint<16>, newPin: Uint<16>): [] { /* batched migration, 5 per tx */ }
// Admin (all guard with deriveAdminPublicKey(getUserSecret()) == contractAdmin)
export circuit blacklistUser(account: UserPublicKey): [] { /* ... */ }
export circuit registerProvider(providerId: Uint<16>, providerPk: JubjubPoint): [] { /* ... */ }
export circuit rotateAdmin(newAdmin: AdminPublicKey): [] { /* ... */ }Privacy boundary:
| Data | Visibility |
|---|---|
| Credit score, income, tenure | Private (witness) |
| Attestation signature | Private (ZK input) |
| User secret + PIN | Private (witness / circuit input) |
Derived | Public (ledger) |
| Loan status + authorized amount | Public (ledger) |
| Public (ledger) |
Compile:
bash
cd contract
npm run compact # → src/managed/zkloan-credit-scorer/
npm run build
cd ..For browser UI also run (copies keys/zkir → ).
npm run sync:assetspublic/zk/zkloan/compact
pragma language_version >= 0.22 && <= 0.23;
import CompactStandardLibrary;
import "schnorr" prefix Schnorr_;
export enum LoanStatus { Approved, Rejected, Proposed, NotAccepted }
export struct LoanApplication { authorizedAmount: Uint<16>; status: LoanStatus; }
export new type UserSecretKey = Bytes<32>;
export new type UserPublicKey = Bytes<32>;
export new type AdminPublicKey = Bytes<32>;
export ledger blacklist: Set<UserPublicKey>;
export ledger loans: Map<Bytes<32>, Map<Uint<16>, LoanApplication>>;
export ledger onGoingPinMigration: Map<Bytes<32>, Uint<16>>;
export ledger contractAdmin: AdminPublicKey;
export ledger providers: Map<Uint<16>, JubjubPoint>;
witness getAttestedScoringWitness(): [Applicant, Schnorr_SchnorrSignature, Uint<16>];
witness getUserSecret(): UserSecretKey;
export pure circuit deriveUserPublicKey(sk: UserSecretKey, pin: Uint<16>): UserPublicKey { /* persistentHash域 "zkloan:user:pk:v1" */ }
export pure circuit deriveAdminPublicKey(sk: UserSecretKey): AdminPublicKey { /* "zkloan:admin:pk:v1" */ }
export circuit requestLoan(amountRequested: Uint<16>, secretPin: Uint<16>): [] { /* ... */ }
export circuit respondToLoan(loanId: Uint<16>, secretPin: Uint<16>, accept: Boolean): [] { /* ... */ }
export circuit changePin(oldPin: Uint<16>, newPin: Uint<16>): [] { /* 批量迁移,每次交易处理5笔 */ }
// 管理员操作(均需验证deriveAdminPublicKey(getUserSecret()) == contractAdmin)
export circuit blacklistUser(account: UserPublicKey): [] { /* ... */ }
export circuit registerProvider(providerId: Uint<16>, providerPk: JubjubPoint): [] { /* ... */ }
export circuit rotateAdmin(newAdmin: AdminPublicKey): [] { /* ... */ }隐私边界:
| 数据 | 可见性 |
|---|---|
| 信用评分、收入、工作年限 | 私密(仅作为见证) |
| 证明签名 | 私密(ZK输入) |
| 用户密钥 + PIN | 私密(见证/电路输入) |
派生 | 公开(账本) |
| 贷款状态 + 授权金额 | 公开(账本) |
| 公开(账本) |
编译命令:
bash
cd contract
npm run compact # → src/managed/zkloan-credit-scorer/
npm run build
cd ..如果是浏览器UI,还需运行(将密钥/zkir复制到)。
npm run sync:assetspublic/zk/zkloan/3) Witnesses
3) 见证文件
contract/src/witnesses.tstypescript
export type ZKLoanCreditScorerPrivateState = {
creditScore: bigint;
monthlyIncome: bigint;
monthsAsCustomer: bigint;
attestationSignature: SchnorrSignature;
attestationProviderId: bigint;
userSecretKey: Uint8Array; // 32 bytes — authoritative caller identity
};
export const witnesses = {
getAttestedScoringWitness: (ctx) => [ctx.privateState, [profile, sig, providerId]],
getSchnorrReduction: (ctx, challengeHash) => [ctx.privateState, [q, r]], // challenge / 2^248
getUserSecret: (ctx) => [ctx.privateState, ctx.privateState.userSecretKey],
};Before , TypeScript must:
requestLoan- Call attestation API with credit profile + user's derived pubkey hash
- Store returned signature + provider ID in private state alongside credit fields
- Ensure is 32 bytes (persist in
userSecretKeyor CLI keystore)localStorage
contract/src/witnesses.tstypescript
export type ZKLoanCreditScorerPrivateState = {
creditScore: bigint;
monthlyIncome: bigint;
monthsAsCustomer: bigint;
attestationSignature: SchnorrSignature;
attestationProviderId: bigint;
userSecretKey: Uint8Array; // 32字节——权威调用者身份
};
export const witnesses = {
getAttestedScoringWitness: (ctx) => [ctx.privateState, [profile, sig, providerId]],
getSchnorrReduction: (ctx, challengeHash) => [ctx.privateState, [q, r]], // challenge / 2^248
getUserSecret: (ctx) => [ctx.privateState, ctx.privateState.userSecretKey],
};调用前,TypeScript需完成以下操作:
requestLoan- 使用信用档案+用户派生公钥哈希调用证明API
- 将返回的签名+提供商ID与信用字段一起存储在私有状态中
- 确保为32字节(持久化到
userSecretKey或CLI密钥库)localStorage
4) Attestation API
4) 证明API
Trusted off-chain signer. Flow:
mermaid
sequenceDiagram
participant User
participant API as Attestation API
participant Wallet as 1AM / CLI
participant Chain as Midnight Network
User->>API: POST /attest { creditScore, income, tenure, userPubKeyHash }
API->>API: Schnorr-sign msg = [score, income, tenure, userPubKeyHash]
API-->>User: { signature, providerId }
User->>Wallet: requestLoan(amount, pin) + private state with attestation
Wallet->>Chain: ZK tx — evaluateApplicant verifies signature in-circuit
Chain-->>User: Loan Proposed / Approved / Rejected on ledgerImplementation checklist:
- Generate Jubjub provider keypair at startup (or load from env)
- Compute challenge via contract's exported (same hash as in-circuit)
pureCircuits.schnorrChallenge - Sign with provider secret key; return +
{ announcement, response }providerId - Admin must on-chain before any loan succeeds
registerProvider(providerId, providerPk) - Bind attestation to — same hash the circuit uses
transientHash(deriveUserPublicKey(secret, pin))
Example endpoint shape:
typescript
// POST /attest
// body: { creditScore, monthlyIncome, monthsAsCustomer, userPubKeyHash: string }
// response: { providerId, signature: { announcement: { x, y }, response } }Run locally (default port e.g. 3001):
bash
cd zkloan-credit-scorer-attestation-api
npm run dev可信链下签名者。流程:
mermaid
sequenceDiagram
participant User
participant API as Attestation API
participant Wallet as 1AM / CLI
participant Chain as Midnight Network
User->>API: POST /attest { creditScore, income, tenure, userPubKeyHash }
API->>API: Schnorr-sign msg = [score, income, tenure, userPubKeyHash]
API-->>User: { signature, providerId }
User->>Wallet: requestLoan(amount, pin) + 包含证明的私有状态
Wallet->>Chain: ZK交易——evaluateApplicant在电路内验证签名
Chain-->>User: 账本上的贷款状态(Proposed / Approved / Rejected)实现检查清单:
- 启动时生成Jubjub提供商密钥对(或从环境变量加载)
- 通过合约导出的计算挑战值(与电路内哈希一致)
pureCircuits.schnorrChallenge - 使用提供商密钥签名;返回+
{ announcement, response }providerId - 管理员必须先在链上调用,贷款才能成功
registerProvider(providerId, providerPk) - 将证明绑定到——与电路使用的哈希一致
transientHash(deriveUserPublicKey(secret, pin))
示例端点格式:
typescript
// POST /attest
// 请求体: { creditScore, monthlyIncome, monthsAsCustomer, userPubKeyHash: string }
// 响应: { providerId, signature: { announcement: { x, y }, response } }本地运行(默认端口例如3001):
bash
cd zkloan-credit-scorer-attestation-api
npm run dev5) CLI (headless wallet)
5) CLI(无头钱包)
Mirror CLI patterns:
example-counter/| Command | Purpose |
|---|---|
| |
| Admin: on-chain provider pubkey from attestation API |
| Fetch attestation → set private state → |
| Accept or decline |
| Indexer read of public |
| Repeat until |
| Admin governance |
Use + level private state provider. Point at local proof server + preprod indexer from wallet config or .
@midnight-ntwrk/wallet-sdk-facade.env参考的CLI模式:
example-counter/| 命令 | 用途 |
|---|---|
| |
| 管理员操作:将证明API的提供商公钥注册到链上 |
| 获取证明 → 设置私有状态 → 调用 |
| 接受或拒绝 |
| 通过索引器读取派生用户密钥对应的公开 |
| 重复调用直到 |
| 管理员治理操作 |
使用 + level私有状态提供商。通过钱包配置或指向本地证明服务器+预生产环境索引器。
@midnight-ntwrk/wallet-sdk-facade.env6) TypeScript Integration (browser)
6) TypeScript集成(浏览器)
lib/zkloan.tstemplates/leaderboard-dapp/lib/leaderboard.ts| Function | Purpose |
|---|---|
| Persist 32-byte secret in |
| Call exported pure circuit off-chain |
| POST to attestation API |
| Deploy + seed private state with secret |
| Admin circuit call |
| Attest → update private state → |
| Accept/decline proposed amount |
| Indexer poll + decode nested |
typescript
import { createUnprovenDeployTx, submitCallTxAsync, submitTxAsync } from '@midnight-ntwrk/midnight-js-contracts';
import { ContractState } from '@midnight-ntwrk/compact-runtime';
import { CompiledZKLoanContract, Contract, ledger } from '../contract/src/index';
export const ZK_PATH = '/zk/zkloan';
const PRIVATE_STATE_ID = 'zkloanPrivateState';
export async function requestLoan(session, contractAddress, amount, pin, attestation) {
await session.providers.privateStateProvider.set(PRIVATE_STATE_ID, {
...attestation,
userSecretKey: getOrCreateUserSecret(),
});
await submitCallTxAsync(session.providers, {
compiledContract: CompiledZKLoanContract,
contractAddress,
circuitId: 'requestLoan',
args: [BigInt(amount), BigInt(pin)],
privateStateId: PRIVATE_STATE_ID,
});
}ZK asset path: (synced to ).
/zk/zkloanpublic/zk/zkloan/lib/zkloan.tstemplates/leaderboard-dapp/lib/leaderboard.ts| 函数 | 用途 |
|---|---|
| 将32字节密钥持久化到 |
| 链下调用导出的纯电路 |
| 向证明API发送POST请求 |
| 部署合约 + 将密钥存入私有状态 |
| 管理员电路调用 |
| 获取证明 → 更新私有状态 → 调用 |
| 接受/拒绝提议的金额 |
| 索引轮询 + 解码嵌套的 |
typescript
import { createUnprovenDeployTx, submitCallTxAsync, submitTxAsync } from '@midnight-ntwrk/midnight-js-contracts';
import { ContractState } from '@midnight-ntwrk/compact-runtime';
import { CompiledZKLoanContract, Contract, ledger } from '../contract/src/index';
export const ZK_PATH = '/zk/zkloan';
const PRIVATE_STATE_ID = 'zkloanPrivateState';
export async function requestLoan(session, contractAddress, amount, pin, attestation) {
await session.providers.privateStateProvider.set(PRIVATE_STATE_ID, {
...attestation,
userSecretKey: getOrCreateUserSecret(),
});
await submitCallTxAsync(session.providers, {
compiledContract: CompiledZKLoanContract,
contractAddress,
circuitId: 'requestLoan',
args: [BigInt(amount), BigInt(pin)],
privateStateId: PRIVATE_STATE_ID,
});
}ZK资产路径:(已同步到)。
/zk/zkloanpublic/zk/zkloan/7) Browser UI
7) 浏览器UI
Copy structure from :
templates/leaderboard-dapp/app/leaderboard/LeaderboardClient.tsx- Connect — →
detectWallet()→wallet.connect('preprod')createConnectedSession(api, ZK_PATH) - Deploy / Join — deploy new contract or paste existing address; persist in
localStorage - Admin panel (deployer only) — register attestation provider ID + pubkey from API
- Request loan — form: amount, secret PIN, credit fields → call attestation API →
requestLoan - My loans — decode indexer state for user's derived pubkey; show status badges (Approved / Proposed / Rejected)
- Respond — if , buttons to accept max eligible amount or decline
Proposed - Optional — PIN change wizard (call repeatedly until migration complete)
changePin
Indexer reads work without wallet for public loan records — only writes need connection + attestation.
参考的结构:
templates/leaderboard-dapp/app/leaderboard/LeaderboardClient.tsx- 连接钱包 — →
detectWallet()→wallet.connect('preprod')createConnectedSession(api, ZK_PATH) - 部署/加入合约 — 部署新合约或粘贴现有地址;持久化到
localStorage - 管理员面板 (仅部署者可用) — 注册证明API的提供商ID + 公钥
- 申请贷款 — 表单:金额、私密PIN、信用字段 → 调用证明API → 调用
requestLoan - 我的贷款 — 解码索引器中用户派生公钥对应的状态;显示状态徽章(Approved / Proposed / Rejected)
- 响应申请 — 如果状态为,提供按钮接受最高可贷金额或拒绝
Proposed - 可选功能 — PIN变更向导(重复调用直到迁移完成)
changePin
索引器读取公开贷款记录无需钱包——仅写入操作需要连接钱包和证明。
8) End-to-end local run
8) 本地端到端运行
bash
undefinedbash
undefinedTerminal 1 — proof server
终端1 — 证明服务器
docker run -d -p 6300:6300 midnightntwrk/proof-server:8.0.3 --
midnight-proof-server --network preprod
midnight-proof-server --network preprod
docker run -d -p 6300:6300 midnightntwrk/proof-server:8.0.3 --
midnight-proof-server --network preprod
midnight-proof-server --network preprod
Terminal 2 — attestation API
终端2 — 证明API
cd zkloan-credit-scorer-attestation-api && npm run dev
cd zkloan-credit-scorer-attestation-api && npm run dev
Terminal 3 — CLI or Next.js UI
终端3 — CLI或Next.js UI
cd zkloan-credit-scorer-cli # or zkloan-dapp/
npm install
cd ../contract && npm run compact && npm run build && cd ..
npm run sync:assets # UI only
npm run deploy # CLI
npm run register-provider 1
npm run request-loan 5000 1234
Wallet: 1AM extension on **Preprod**, proof server `http://localhost:6300`, tNIGHT + tDUST from faucet.
---cd zkloan-credit-scorer-cli # 或zkloan-dapp/
npm install
cd ../contract && npm run compact && npm run build && cd ..
npm run sync:assets # 仅UI需要
npm run deploy # CLI命令
npm run register-provider 1
npm run request-loan 5000 1234
钱包:Preprod环境的1AM扩展,证明服务器地址`http://localhost:6300`,从水龙头获取tNIGHT + tDUST。
---9) Prerequisites
9) 前置条件
| Component | Version |
|---|---|
| Node.js | ≥ 22 (required for shielded wallet SDK) |
| Compact compiler | 0.22–0.23 ( |
| Compact runtime | 0.16.0 |
| Ledger | 8.0.3 |
| midnight-js | 4.0.4 |
| proof server | 8.0.3 |
| Docker | For local proof server |
bash
node --version # v22+
compact compile --version
compact update
docker run -d -p 6300:6300 midnightntwrk/proof-server:8.0.3 -- \
midnight-proof-server --network preprod| 组件 | 版本 |
|---|---|
| Node.js | ≥22(shielded钱包SDK的必要条件) |
| Compact编译器 | 0.22–0.23(对应 |
| Compact运行时 | 0.16.0 |
| Ledger | 8.0.3 |
| midnight-js | 4.0.4 |
| 证明服务器 | 8.0.3 |
| Docker | 用于本地证明服务器 |
bash
node --version # v22+
compact compile --version
compact update
docker run -d -p 6300:6300 midnightntwrk/proof-server:8.0.3 -- \
midnight-proof-server --network preprod10) Security checklist
10) 安全检查清单
- Do not use for blacklist, admin, or user identity checks
ownPublicKey() - Attestation provider private key lives only on attestation server — never in browser private state
- marks witness values safe to cross into ledger reads; credit fields never get disclosed
disclose() - Blacklist stores derived , not wallet addresses
UserPublicKey - Re-deploy if domain-separator strings change — preimages affect derived keys
persistentHash - Rate-limit attestation API; validate input ranges before signing
- 绝不要使用进行黑名单、管理员或用户身份验证
ownPublicKey() - 证明提供商私钥仅存储在证明服务器上——绝不要存放在浏览器私有状态中
- 标记见证值可安全进入账本读取;信用字段绝不能被disclose
disclose() - 黑名单存储派生的,而非钱包地址
UserPublicKey - 如果域分隔符字符串变更,需重新部署——的前置输入会影响派生密钥
persistentHash - 对证明API进行限流;签名前验证输入范围
11) Production notes
11) 生产环境注意事项
- Deploy attestation API behind HTTPS; rotate provider keys via + re-register provider
rotateAdmin - Frontend: Vercel/Netlify build step runs
npm run compact && npm run sync:assets - Store default contract address in env or
localStorage - For mainnet, update network ID, indexer URLs, proof server via wallet
getConfiguration() - Pin versions from ; cross-check support matrix
references/versions.json
- 将证明API部署在HTTPS后;通过轮换管理员并重新注册提供商密钥
rotateAdmin - 前端:Vercel/Netlify构建步骤需运行
npm run compact && npm run sync:assets - 将默认合约地址存储在环境变量或中
localStorage - 主网环境下,通过钱包更新网络ID、索引器URL、证明服务器地址
getConfiguration() - 固定中的版本;交叉核对支持矩阵
references/versions.json
Quick Commands
快速命令
bash
undefinedbash
undefinedContract only
仅编译合约
cd contract && npm install && npm run compact && npm run build
cd contract && npm install && npm run compact && npm run build
Full monorepo (from root)
完整单仓项目(从根目录)
npm install
npm run validate:registry # if added to workspace scripts
cd contract && npm run test:compile
npm install
npm run validate:registry # 如果已添加到工作区脚本
cd contract && npm run test:compile
Optional browser UI (after adding zkloan-dapp from leaderboard template)
可选浏览器UI(从排行榜模板添加zkloan-dapp后)
cd zkloan-dapp
npm install && npm run compact && npm run sync:assets && npm run dev
Open http://localhost:3000 → Connect 1AM → Deploy → Register provider → Request loan → View outcome on-chain.cd zkloan-dapp
npm install && npm run compact && npm run sync:assets && npm run dev
打开http://localhost:3000 → 连接1AM钱包 → 部署合约 → 注册提供商 → 申请贷款 → 在链上查看结果。