example-zk-loan-application

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Midnight 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
    +
    zkloan-credit-scorer.compact
    , witnesses, compile scripts
  • zkloan-credit-scorer-attestation-api/
    — REST server that Schnorr-signs credit profiles
  • zkloan-credit-scorer-cli/
    — deploy, register providers, request/respond to loans (headless wallet)
  • app/loan/
    (optional) — Next.js + 1AM wallet UI (copy patterns from
    templates/leaderboard-dapp/
    )
  • lib/zkloan.ts
    — deploy,
    requestLoan
    ,
    respondToLoan
    , admin calls, ledger decode
  • lib/midnight.ts
    — wallet session + patched indexer provider (copy from
    references/midnight-session.md
    )
  • public/zk/zkloan/
    — 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 helpers
  • references/gotchas.md
    — preprod deploy hangs, GraphQL
    offset: null
    , ZK asset paths
  • references/versions.json
    — pinned
    @midnight-ntwrk/*
    versions
Primary references:
  • example-leaderboard-dapp/
    /
    templates/leaderboard-dapp/
    — Next.js + 1AM, low-level deploy/call, indexer reads
  • example-locker-dapp/
    — witness private state,
    createUnprovenDeployTx
    +
    submitTxAsync
  • example-counter/
    — headless CLI wallet, monorepo workspaces, vitest contract tests
  • compact/
    disclose()
    , witnesses,
    persistentHash
    ,
    Map
    ,
    Set
    ,
    new type
    , pure circuits
  • security/
    — never trust
    ownPublicKey()
    for caller identity; witness-derived keys
  • indexer/
    — read public loan ledger without wallet
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
    ownPublicKey()
    for auth — it is prover-supplied and bypassable; derive identity from
    getUserSecret()
    witness + PIN via
    deriveUserPublicKey
    /
    deriveAdminPublicKey
  • Attestation binds to
    transientHash(userPubKeyBytes)
    inside
    evaluateApplicant
  • Eligibility tiers: ≥700 + ≥$2k + ≥24mo → $10k; ≥600 + ≥$1.5k → $7k; ≥580 → $3k; else rejected
  • requestLoan
    may return Proposed when amount exceeds tier max — user calls
    respondToLoan
    to accept/decline
  • PIN change migrates loans in batches of 5 via
    changePin
    +
    onGoingPinMigration
    ledger map
  • Use
    createUnprovenDeployTx
    +
    submitTxAsync
    — not
    deployContract()
    (hangs on preprod)
  • Node.js ≥ 22 required for
    @midnight-ntwrk/wallet-sdk-shielded
    (Iterator helpers on
    Map.values()
    )

这是一个隐私保护型借贷dApp,申请人在ZK电路中证明自己的贷款资格,无需在链上披露信用评分、收入或工作年限。可信的证明提供商在链下对信用数据进行签名;合约在电路内验证Schnorr签名,仅将贷款结果(状态+授权金额)写入账本。
该方案产出的内容:
  • contract/
    schnorr.compact
    +
    zkloan-credit-scorer.compact
    、见证文件、编译脚本
  • zkloan-credit-scorer-attestation-api/
    — 对信用档案进行Schnorr签名的REST服务器
  • zkloan-credit-scorer-cli/
    — 部署、注册提供商、请求/响应贷款的无头钱包工具
  • app/loan/
    (可选) — Next.js + 1AM钱包UI(从
    templates/leaderboard-dapp/
    中复制模式)
  • lib/zkloan.ts
    — 部署、
    requestLoan
    respondToLoan
    、管理员调用、账本解码逻辑
  • lib/midnight.ts
    — 钱包会话 + 修补后的索引器提供商(
    references/midnight-session.md
    复制)
  • public/zk/zkloan/
    — 从合约构建同步而来的ZK证明资产
共享参考资料(标准提供商+故障排查——请勿在提示中重复):
  • references/midnight-session.md
    createConnectedSession
    、索引器补丁、部署/调用辅助工具
  • references/gotchas.md
    — 预生产环境部署卡顿、GraphQL
    offset: null
    、ZK资产路径问题
  • references/versions.json
    — 固定版本的
    @midnight-ntwrk/*
    依赖
核心参考资料:
  • example-leaderboard-dapp/
    /
    templates/leaderboard-dapp/
    — Next.js + 1AM、底层部署/调用、索引器读取
  • example-locker-dapp/
    — 见证私有状态、
    createUnprovenDeployTx
    +
    submitTxAsync
  • example-counter/
    — 无头CLI钱包、单仓项目工作区、vitest合约测试
  • compact/
    disclose()
    、见证、
    persistentHash
    Map
    Set
    new type
    、纯电路
  • security/
    — 绝不要信任
    ownPublicKey()
    用于调用者身份验证;使用见证派生密钥
  • indexer/
    — 无需钱包即可读取公开贷款账本
关键架构说明:
  • 信用评分、收入、工作年限和证明签名始终保持私密(仅作为见证)
  • 仅贷款状态+授权金额+派生用户公钥字节会出现在链上
  • **绝不要使用
    ownPublicKey()
    **进行身份验证——它由证明者提供,可被绕过;通过
    getUserSecret()
    见证+PIN,使用
    deriveUserPublicKey
    /
    deriveAdminPublicKey
    派生身份
  • 证明在
    evaluateApplicant
    内部绑定到
    transientHash(userPubKeyBytes)
  • 资格分层:≥700分 + ≥2000美元 + ≥24个月 → 10000美元;≥600分 + ≥1500美元 →7000美元;≥580分 →3000美元;其余情况拒绝
  • 当申请金额超过分层上限时,
    requestLoan
    可能返回Proposed状态——用户需调用
    respondToLoan
    接受/拒绝
  • PIN变更通过
    changePin
    +
    onGoingPinMigration
    账本映射批量迁移贷款,每次交易最多处理5笔
  • 使用
    createUnprovenDeployTx
    +
    submitTxAsync
    ——不要使用
    deployContract()
    (在预生产环境会卡顿)
  • Node.js ≥22
    @midnight-ntwrk/wallet-sdk-shielded
    的必要条件(依赖
    Map.values()
    的迭代器辅助方法)

Workflow

工作流程

When helping the user, follow this sequence:
  1. Monorepo — root
    package.json
    workspaces:
    contract
    ,
    attestation-api
    ,
    cli
  2. Contract — compile Compact (
    schnorr.compact
    +
    zkloan-credit-scorer.compact
    ) + witnesses
  3. Attestation API — generate/load provider Jubjub keypair; sign credit profiles; expose REST endpoint
  4. Deploy — CLI or browser: low-level deploy, persist
    userSecretKey
    + contract address in private state
  5. Register provider — admin calls
    registerProvider(providerId, providerPk)
    on-chain
  6. Request loan — fetch attestation → populate private state →
    requestLoan(amount, secretPin)
  7. Respond — if status is
    Proposed
    , call
    respondToLoan(loanId, secretPin, accept)
  8. Read state — indexer GraphQL → decode
    loans
    nested map (public)
  9. Frontend (optional) — connect 1AM, deploy/join, loan form, loan table, admin panel

协助用户时,请遵循以下步骤:
  1. 单仓项目 — 根目录
    package.json
    工作区:
    contract
    attestation-api
    cli
  2. 合约 — 编译Compact文件(
    schnorr.compact
    +
    zkloan-credit-scorer.compact
    )+ 见证文件
  3. 证明API — 生成/加载提供商Jubjub密钥对;签名信用档案;暴露REST端点
  4. 部署 — 通过CLI或浏览器:底层部署,在私有状态中持久化
    userSecretKey
    + 合约地址
  5. 注册提供商 — 管理员在链上调用
    registerProvider(providerId, providerPk)
  6. 申请贷款 — 获取证明 → 填充私有状态 →
    requestLoan(amount, secretPin)
  7. 响应申请 — 如果状态为
    Proposed
    ,调用
    respondToLoan(loanId, secretPin, accept)
  8. 读取状态 — 索引器GraphQL → 解码嵌套的
    loans
    映射(公开数据)
  9. 前端 (可选) — 连接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
package.json
engines:
"node": ">=22.0.0"
. Workspaces pin
@midnight-ntwrk/compact-runtime: ^0.16.0
,
@midnight-ntwrk/midnight-js-*: 4.0.4
,
@midnight-ntwrk/ledger-v8: 8.0.3
.

zkloan-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.3
版本。

2) Compact Contract

2) Compact合约

Schnorr module (
contract/src/schnorr.compact
)

Schnorr模块(
contract/src/schnorr.compact

Verifies Jubjub Schnorr signatures inside the circuit. Temporary polyfill until
jubjubSchnorrVerify
ships in Compact Standard Library.
compact
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
jubjubPointX()
/
jubjubPointY()
— struct
==
is reference equality and always fails for fresh points.
在电路内验证Jubjub Schnorr签名。这是临时的兼容实现,直到
jubjubSchnorrVerify
加入Compact标准库。
compact
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相等性检查: 比较
jubjubPointX()
/
jubjubPointY()
——结构体
==
是引用相等性检查,对于新生成的点始终返回false。

Main contract (
contract/src/zkloan-credit-scorer.compact
)

主合约(
contract/src/zkloan-credit-scorer.compact

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 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:
DataVisibility
Credit score, income, tenurePrivate (witness)
Attestation signaturePrivate (ZK input)
User secret + PINPrivate (witness / circuit input)
Derived
UserPublicKey
(loan map key)
Public (ledger)
Loan status + authorized amountPublic (ledger)
contractAdmin
,
providers
,
blacklist
Public (ledger)
Compile:
bash
cd contract
npm run compact    # → src/managed/zkloan-credit-scorer/
npm run build
cd ..
For browser UI also run
npm run sync:assets
(copies keys/zkir →
public/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私密(见证/电路输入)
派生
UserPublicKey
(贷款映射键)
公开(账本)
贷款状态 + 授权金额公开(账本)
contractAdmin
providers
blacklist
公开(账本)
编译命令:
bash
cd contract
npm run compact    # → src/managed/zkloan-credit-scorer/
npm run build
cd ..
如果是浏览器UI,还需运行
npm run sync:assets
(将密钥/zkir复制到
public/zk/zkloan/
)。

3) Witnesses

3) 见证文件

contract/src/witnesses.ts
:
typescript
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
requestLoan
, TypeScript must:
  1. Call attestation API with credit profile + user's derived pubkey hash
  2. Store returned signature + provider ID in private state alongside credit fields
  3. Ensure
    userSecretKey
    is 32 bytes (persist in
    localStorage
    or CLI keystore)

contract/src/witnesses.ts
:
typescript
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],
};
调用
requestLoan
前,TypeScript需完成以下操作:
  1. 使用信用档案+用户派生公钥哈希调用证明API
  2. 将返回的签名+提供商ID与信用字段一起存储在私有状态中
  3. 确保
    userSecretKey
    为32字节(持久化到
    localStorage
    或CLI密钥库)

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 ledger
Implementation checklist:
  • Generate Jubjub provider keypair at startup (or load from env)
  • Compute challenge via contract's exported
    pureCircuits.schnorrChallenge
    (same hash as in-circuit)
  • Sign with provider secret key; return
    { announcement, response }
    +
    providerId
  • Admin must
    registerProvider(providerId, providerPk)
    on-chain before any loan succeeds
  • Bind attestation to
    transientHash(deriveUserPublicKey(secret, pin))
    — same hash the circuit uses
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 dev

5) CLI (headless wallet)

5) CLI(无头钱包)

Mirror
example-counter/
CLI patterns:
CommandPurpose
deploy
createUnprovenDeployTx
→ stores deployer as admin via constructor
register-provider <id>
Admin: on-chain provider pubkey from attestation API
request-loan <amount> <pin>
Fetch attestation → set private state →
requestLoan
respond-loan <id> <pin> accept|decline
Accept or decline
Proposed
loan
list-loans
Indexer read of public
loans
map for derived user key
change-pin <old> <new>
Repeat until
onGoingPinMigration
cleared
blacklist-user <userPk>
Admin governance
Use
@midnight-ntwrk/wallet-sdk-facade
+ level private state provider. Point at local proof server + preprod indexer from wallet config or
.env
.

参考
example-counter/
的CLI模式:
命令用途
deploy
createUnprovenDeployTx
→ 通过构造函数将部署者设为管理员
register-provider <id>
管理员操作:将证明API的提供商公钥注册到链上
request-loan <amount> <pin>
获取证明 → 设置私有状态 → 调用
requestLoan
respond-loan <id> <pin> accept|decline
接受或拒绝
Proposed
状态的贷款
list-loans
通过索引器读取派生用户密钥对应的公开
loans
映射
change-pin <old> <new>
重复调用直到
onGoingPinMigration
被清空
blacklist-user <userPk>
管理员治理操作
使用
@midnight-ntwrk/wallet-sdk-facade
+ level私有状态提供商。通过钱包配置或
.env
指向本地证明服务器+预生产环境索引器。

6) TypeScript Integration (browser)

6) TypeScript集成(浏览器)

lib/zkloan.ts
— same low-level pattern as
templates/leaderboard-dapp/lib/leaderboard.ts
:
FunctionPurpose
getOrCreateUserSecret()
Persist 32-byte secret in
localStorage
deriveUserPublicKey(session, pin)
Call exported pure circuit off-chain
fetchAttestation(apiUrl, profile, userPubKeyHash)
POST to attestation API
deployZkLoan(session)
Deploy + seed private state with secret
registerProvider(session, addr, id, pk)
Admin circuit call
requestLoan(session, addr, amount, pin, profile)
Attest → update private state →
requestLoan
respondToLoan(session, addr, loanId, pin, accept)
Accept/decline proposed amount
fetchLoanState(queryUrl, addr, userPkBytes)
Indexer poll + decode nested
loans
map
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:
/zk/zkloan
(synced to
public/zk/zkloan/
).

lib/zkloan.ts
——与
templates/leaderboard-dapp/lib/leaderboard.ts
使用相同的底层模式:
函数用途
getOrCreateUserSecret()
将32字节密钥持久化到
localStorage
deriveUserPublicKey(session, pin)
链下调用导出的纯电路
fetchAttestation(apiUrl, profile, userPubKeyHash)
向证明API发送POST请求
deployZkLoan(session)
部署合约 + 将密钥存入私有状态
registerProvider(session, addr, id, pk)
管理员电路调用
requestLoan(session, addr, amount, pin, profile)
获取证明 → 更新私有状态 → 调用
requestLoan
respondToLoan(session, addr, loanId, pin, accept)
接受/拒绝提议的金额
fetchLoanState(queryUrl, addr, userPkBytes)
索引轮询 + 解码嵌套的
loans
映射
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/zkloan
(已同步到
public/zk/zkloan/
)。

7) Browser UI

7) 浏览器UI

Copy structure from
templates/leaderboard-dapp/app/leaderboard/LeaderboardClient.tsx
:
  1. Connect
    detectWallet()
    wallet.connect('preprod')
    createConnectedSession(api, ZK_PATH)
  2. Deploy / Join — deploy new contract or paste existing address; persist in
    localStorage
  3. Admin panel (deployer only) — register attestation provider ID + pubkey from API
  4. Request loan — form: amount, secret PIN, credit fields → call attestation API →
    requestLoan
  5. My loans — decode indexer state for user's derived pubkey; show status badges (Approved / Proposed / Rejected)
  6. Respond — if
    Proposed
    , buttons to accept max eligible amount or decline
  7. Optional — PIN change wizard (call
    changePin
    repeatedly until migration complete)
Indexer reads work without wallet for public loan records — only writes need connection + attestation.

参考
templates/leaderboard-dapp/app/leaderboard/LeaderboardClient.tsx
的结构:
  1. 连接钱包
    detectWallet()
    wallet.connect('preprod')
    createConnectedSession(api, ZK_PATH)
  2. 部署/加入合约 — 部署新合约或粘贴现有地址;持久化到
    localStorage
  3. 管理员面板 (仅部署者可用) — 注册证明API的提供商ID + 公钥
  4. 申请贷款 — 表单:金额、私密PIN、信用字段 → 调用证明API → 调用
    requestLoan
  5. 我的贷款 — 解码索引器中用户派生公钥对应的状态;显示状态徽章(Approved / Proposed / Rejected)
  6. 响应申请 — 如果状态为
    Proposed
    ,提供按钮接受最高可贷金额或拒绝
  7. 可选功能 — PIN变更向导(重复调用
    changePin
    直到迁移完成)
索引器读取公开贷款记录无需钱包——仅写入操作需要连接钱包和证明。

8) End-to-end local run

8) 本地端到端运行

bash
undefined
bash
undefined

Terminal 1 — proof server

终端1 — 证明服务器

docker run -d -p 6300:6300 midnightntwrk/proof-server:8.0.3 --
midnight-proof-server --network preprod
docker run -d -p 6300:6300 midnightntwrk/proof-server:8.0.3 --
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) 前置条件

ComponentVersion
Node.js≥ 22 (required for shielded wallet SDK)
Compact compiler0.22–0.23 (
pragma language_version
)
Compact runtime0.16.0
Ledger8.0.3
midnight-js4.0.4
proof server8.0.3
DockerFor 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(对应
pragma language_version
Compact运行时0.16.0
Ledger8.0.3
midnight-js4.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 preprod

10) Security checklist

10) 安全检查清单

  • Do not use
    ownPublicKey()
    for blacklist, admin, or user identity checks
  • Attestation provider private key lives only on attestation server — never in browser private state
  • disclose()
    marks witness values safe to cross into ledger reads; credit fields never get disclosed
  • Blacklist stores derived
    UserPublicKey
    , not wallet addresses
  • Re-deploy if domain-separator strings change —
    persistentHash
    preimages affect derived keys
  • 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
    rotateAdmin
    + re-register provider
  • 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
    references/versions.json
    ; cross-check support matrix

  • 将证明API部署在HTTPS后;通过
    rotateAdmin
    轮换管理员并重新注册提供商密钥
  • 前端:Vercel/Netlify构建步骤需运行
    npm run compact && npm run sync:assets
  • 将默认合约地址存储在环境变量或
    localStorage
  • 主网环境下,通过钱包
    getConfiguration()
    更新网络ID、索引器URL、证明服务器地址
  • 固定
    references/versions.json
    中的版本;交叉核对支持矩阵

Quick Commands

快速命令

bash
undefined
bash
undefined

Contract 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钱包 → 部署合约 → 注册提供商 → 申请贷款 → 在链上查看结果。