clarity-patterns

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Clarity Patterns Skill

Clarity模式Skill

Canonical pattern library for Clarity smart contract development on Stacks. All patterns and templates are bundled in this skill — no external dependencies.
This is a doc-only skill. Agents read this file and the colocated reference files directly. The CLI interface documents the planned implementation.
bun run clarity-patterns/clarity-patterns.ts <subcommand> [options]
这是用于在Stacks上进行Clarity智能合约开发的权威模式库。所有模式和模板都集成在本Skill中——无外部依赖。
这是一个纯文档型Skill。Agent直接读取本文件和同目录下的参考文件。CLI接口记录了规划中的实现方案。
bun run clarity-patterns/clarity-patterns.ts <subcommand> [options]

Subcommands

子命令

  • list [--category <category>]
    — List available patterns and templates (categories:
    code
    ,
    registry
    ,
    templates
    ,
    testing
    )
  • get --name <pattern-name>
    — Return a specific pattern with code and notes
  • template --name <template-name>
    — Return a complete contract template with source, tests, and checklist

  • list [--category <category>]
    — 列出可用的模式和模板(分类:
    code
    registry
    templates
    testing
  • get --name <pattern-name>
    — 返回带代码和注释的指定模式
  • template --name <template-name>
    — 返回包含源码、测试和检查清单的完整合约模板

Code Patterns

代码模式

Public Function Template

公共函数模板

Standard structure for public functions with guards and error handling.
clarity
(define-public (transfer (amount uint) (to principal))
  (begin
    (asserts! (is-eq tx-sender owner) ERR_UNAUTHORIZED)
    (try! (ft-transfer? TOKEN amount tx-sender to))
    (ok true)))
  • Use
    try!
    for subcalls to propagate errors
  • Use
    asserts!
    for guards before state changes
  • Add post-conditions on tx for asset safety
带有守卫和错误处理的公共函数标准结构。
clarity
(define-public (transfer (amount uint) (to principal))
  (begin
    (asserts! (is-eq tx-sender owner) ERR_UNAUTHORIZED)
    (try! (ft-transfer? TOKEN amount tx-sender to))
    (ok true)))
  • 使用
    try!
    处理子调用以传播错误
  • 在状态变更前使用
    asserts!
    设置守卫
  • 为资产安全添加交易后置条件

Standardized Events

标准化事件

Emit structured events for off-chain indexing.
clarity
(print {
  notification: "contract-event",
  payload: {
    amount: amount,
    sender: tx-sender,
    recipient: to
  }
})
发出结构化事件供链下索引使用。
clarity
(print {
  notification: "contract-event",
  payload: {
    amount: amount,
    sender: tx-sender,
    recipient: to
  }
})

Error Handling with Match

基于Match的错误处理

Handle external call failures gracefully.
clarity
(match (contract-call? .other fn args)
  success (ok success)
  error (err ERR_EXTERNAL_CALL_FAILED))
优雅处理外部调用失败。
clarity
(match (contract-call? .other fn args)
  success (ok success)
  error (err ERR_EXTERNAL_CALL_FAILED))

Bit Flags for Status/Permissions

用于状态/权限的位标志

Pack multiple booleans into a single uint.
clarity
(define-constant STATUS_ACTIVE (pow u2 u0))   ;; 1
(define-constant STATUS_PAID (pow u2 u1))     ;; 2
(define-constant STATUS_VERIFIED (pow u2 u2)) ;; 4

;; Pack multiple flags: (+ STATUS_ACTIVE STATUS_PAID) → u3
;; Check flag: (> (bit-and status STATUS_ACTIVE) u0)
;; Set flag: (var-set status (bit-or (var-get status) NEW_FLAG))
;; Clear flag: (var-set status (bit-and (var-get status) (bit-not FLAG)))
将多个布尔值打包到单个uint中。
clarity
(define-constant STATUS_ACTIVE (pow u2 u0))   ;; 1
(define-constant STATUS_PAID (pow u2 u1))     ;; 2
(define-constant STATUS_VERIFIED (pow u2 u2)) ;; 4

;; 打包多个标志:(+ STATUS_ACTIVE STATUS_PAID) → u3
;; 检查标志:(> (bit-and status STATUS_ACTIVE) u0)
;; 设置标志:(var-set status (bit-or (var-get status) NEW_FLAG))
;; 清除标志:(var-set status (bit-and (var-get status) (bit-not FLAG)))

Multi-Send Pattern

多发送模式

Send to multiple recipients in one transaction using fold.
clarity
(define-private (send-maybe
    (recipient {to: principal, ustx: uint})
    (prior (response bool uint)))
  (match prior
    ok-result (let (
      (to (get to recipient))
      (ustx (get ustx recipient)))
      (try! (stx-transfer? ustx tx-sender to))
      (ok true))
    err-result (err err-result)))

(define-public (send-many (recipients (list 200 {to: principal, ustx: uint})))
  (fold send-maybe recipients (ok true)))
使用fold在一笔交易中向多个接收方发送资产。
clarity
(define-private (send-maybe
    (recipient {to: principal, ustx: uint})
    (prior (response bool uint)))
  (match prior
    ok-result (let (
      (to (get to recipient))
      (ustx (get ustx recipient)))
      (try! (stx-transfer? ustx tx-sender to))
      (ok true))
    err-result (err err-result)))

(define-public (send-many (recipients (list 200 {to: principal, ustx: uint})))
  (fold send-maybe recipients (ok true)))

Parent-Child Maps (Hierarchical Data)

父子映射(分层数据)

Store hierarchical data with pagination support.
clarity
(define-map Parents uint {name: (string-ascii 32), lastChildId: uint})
(define-map Children {parentId: uint, id: uint} uint)

(define-read-only (get-child (parentId uint) (childId uint))
  (map-get? Children {parentId: parentId, id: childId}))

(define-private (is-some? (x (optional uint)))
  (is-some x))

(define-read-only (get-children (parentId uint) (shift uint))
  (filter is-some?
    (list
      (get-child parentId (+ shift u1))
      (get-child parentId (+ shift u2))
      (get-child parentId (+ shift u3))
      ;; ... up to page size
    )))
存储带有分页支持的分层数据。
clarity
(define-map Parents uint {name: (string-ascii 32), lastChildId: uint})
(define-map Children {parentId: uint, id: uint} uint)

(define-read-only (get-child (parentId uint) (childId uint))
  (map-get? Children {parentId: parentId, id: childId}))

(define-private (is-some? (x (optional uint)))
  (is-some x))

(define-read-only (get-children (parentId uint) (shift uint))
  (filter is-some?
    (list
      (get-child parentId (+ shift u1))
      (get-child parentId (+ shift u2))
      (get-child parentId (+ shift u3))
      ;; ... 直至分页大小
    )))

Whitelisting (Assets/Contracts)

白名单(资产/合约)

Control which contracts/assets can interact.
clarity
(define-map Allowed {contract: principal, type: uint} bool)

;; Check in function
(asserts! (default-to false (map-get? Allowed {contract: contract, type: type}))
          ERR_NOT_ALLOWED)

;; Batch update
(define-public (set-allowed-list (items (list 100 {token: principal, enabled: bool})))
  (ok (map set-iter items (ok true))))
控制哪些合约/资产可以交互。
clarity
(define-map Allowed {contract: principal, type: uint} bool)

;; 在函数中检查
(asserts! (default-to false (map-get? Allowed {contract: contract, type: type}))
          ERR_NOT_ALLOWED)

;; 批量更新
(define-public (set-allowed-list (items (list 100 {token: principal, enabled: bool})))
  (ok (map set-iter items (ok true))))

Trait Whitelisting

Trait白名单

Only allow calls from trusted trait implementations.
clarity
(define-map TrustedTraits principal bool)

;; In functions accepting traits
(asserts! (default-to false (map-get? TrustedTraits (contract-of t)))
          ERR_UNTRUSTED)
仅允许来自可信Trait实现的调用。
clarity
(define-map TrustedTraits principal bool)

;; 在接受Trait的函数中
(asserts! (default-to false (map-get? TrustedTraits (contract-of t)))
          ERR_UNTRUSTED)

Delayed Activation

延迟激活

Activate functionality after a Bitcoin block delay.
clarity
(define-constant DELAY u21000) ;; ~146 days in BTC blocks
(define-data-var activation-block uint u0)

;; Set on deploy or init
(var-set activation-block (+ burn-block-height DELAY))

(define-read-only (is-active?)
  (>= burn-block-height (var-get activation-block)))
Example: usabtc-token
在比特币区块延迟后激活功能。
clarity
(define-constant DELAY u21000) ;; ~146天(按BTC区块计算)
(define-data-var activation-block uint u0)

;; 在部署或初始化时设置
(var-set activation-block (+ burn-block-height DELAY))

(define-read-only (is-active?)
  (>= burn-block-height (var-get activation-block)))
示例:usabtc-token

Rate Limiting

速率限制

Prevent rapid repeated actions.
clarity
(define-data-var last-action-block uint u0)

(define-public (rate-limited-action)
  (begin
    (asserts! (> burn-block-height (var-get last-action-block)) ERR_RATE_LIMIT)
    (var-set last-action-block burn-block-height)
    ;; ... action
    (ok true)))
防止快速重复操作。
clarity
(define-data-var last-action-block uint u0)

(define-public (rate-limited-action)
  (begin
    (asserts! (> burn-block-height (var-get last-action-block)) ERR_RATE_LIMIT)
    (var-set last-action-block burn-block-height)
    ;; ... 执行操作
    (ok true)))

DAO Proposals with Historic Balances

带历史余额的DAO提案

Use
at-block
for snapshot voting.
clarity
(define-map Proposals uint {
  votesFor: uint,
  votesAgainst: uint,
  status: uint,
  liquidTokens: uint,
  blockHash: (buff 32)
})

;; Get voting power at proposal creation
(define-read-only (get-vote-power (proposal-id uint) (voter principal))
  (let ((proposal (unwrap! (map-get? Proposals proposal-id) u0)))
    (at-block (get blockHash proposal)
      (contract-call? .token get-balance voter))))

;; Quorum check: (>= (/ (* total-votes u100) liquid-supply) QUORUM_PERCENT)
使用
at-block
进行快照投票。
clarity
(define-map Proposals uint {
  votesFor: uint,
  votesAgainst: uint,
  status: uint,
  liquidTokens: uint,
  blockHash: (buff 32)
})

;; 获取提案创建时的投票权
(define-read-only (get-vote-power (proposal-id uint) (voter principal))
  (let ((proposal (unwrap! (map-get? Proposals proposal-id) u0)))
    (at-block (get blockHash proposal)
      (contract-call? .token get-balance voter))))

;; 法定人数检查:(>= (/ (* total-votes u100) liquid-supply) QUORUM_PERCENT)

Fixed-Point Arithmetic

定点算术

Handle decimal values with scale factor.
clarity
(define-constant SCALE (pow u10 u8)) ;; 8 decimal places

;; Multiply then divide to preserve precision
(define-read-only (calculate-share (amount uint) (percentage uint))
  (/ (* amount percentage) SCALE))

;; Convert to/from scaled values
(define-read-only (to-scaled (amount uint))
  (* amount SCALE))

(define-read-only (from-scaled (amount uint))
  (/ amount SCALE))
使用比例因子处理十进制值。
clarity
(define-constant SCALE (pow u10 u8)) ;; 8位小数

;; 先乘后除以保留精度
(define-read-only (calculate-share (amount uint) (percentage uint))
  (/ (* amount percentage) SCALE))

;; 在缩放值与原值间转换
(define-read-only (to-scaled (amount uint))
  (* amount SCALE))

(define-read-only (from-scaled (amount uint))
  (/ amount SCALE))

Treasury Pattern with as-contract

带as-contract的金库模式

Use
as-contract
for contract-controlled funds.
clarity
(define-public (withdraw (amount uint) (recipient principal))
  (begin
    (asserts! (is-authorized tx-sender) ERR_UNAUTHORIZED)
    (as-contract (stx-transfer? amount (as-contract tx-sender) recipient))))
Warning:
as-contract
changes both
tx-sender
and
contract-caller
to the contract principal.
使用
as-contract
管理合约控制的资金。
clarity
(define-public (withdraw (amount uint) (recipient principal))
  (begin
    (asserts! (is-authorized tx-sender) ERR_UNAUTHORIZED)
    (as-contract (stx-transfer? amount (as-contract tx-sender) recipient))))
警告:
as-contract
会将
tx-sender
contract-caller
都改为合约主体。

tx-sender vs contract-caller Decision Framework

tx-sender与contract-caller决策框架

Call Pathcontract-callertx-sender
user -> targetuseruser
user -> proxy -> targetproxyuser
user -> proxy (as-contract) -> targetproxyproxy
  • tx-sender: Use for auth checks, identity attribution, self-action guards. Preserves human identity through normal proxies. Preferred for composability.
  • contract-caller: Use when you need the IMMEDIATE caller identity specifically.
  • Security note: Using
    contract-caller
    for self-action guards (e.g., "owner can't give themselves feedback") is bypassable — owner routes through any proxy and
    contract-caller
    shows the proxy, not the owner.
    tx-sender
    catches this because it preserves the human origin.
调用路径contract-callertx-sender
用户 -> 目标合约用户用户
用户 -> 代理合约 -> 目标合约代理合约用户
用户 -> 代理合约(as-contract) -> 目标合约代理合约代理合约
  • tx-sender:用于权限检查、身份归属、自操作守卫。在普通代理调用中保留用户身份。是组合性开发的首选。
  • contract-caller:仅当你需要获取直接调用者身份时使用。
  • 安全注意:使用
    contract-caller
    实现自操作守卫(例如“所有者不能给自己反馈”)存在绕过风险——所有者可通过任意代理路由调用,此时
    contract-caller
    显示的是代理合约而非所有者。
    tx-sender
    可捕获此情况,因为它保留了初始用户身份。

Clarity 4: Asset Restrictions

Clarity 4:资产限制

Restrict what assets a contract call can move.
clarity
(as-contract
  (with-stx u1000000)                              ;; Allow 1 STX
  (with-ft .token TOKEN u500)                      ;; Allow 500 fungible tokens
  (with-nft .nft-contract NFT (list u1 u2 u3))    ;; Allow specific NFT IDs
  ;; ... body
)

;; DANGER: Avoid unless necessary
(with-all-assets-unsafe)
限制合约调用可转移的资产。
clarity
(as-contract
  (with-stx u1000000)                              ;; 允许1个STX
  (with-ft .token TOKEN u500)                      ;; 允许500个 fungible token
  (with-nft .nft-contract NFT (list u1 u2 u3))    ;; 允许特定NFT ID
  ;; ... 函数体
)

;; 危险:非必要请勿使用
(with-all-assets-unsafe)

Multi-Party Coordination

多方协作

Coordinate actions requiring multiple signatures.
clarity
;; Proposal state
(define-map Intents uint {
  participants: (list 20 principal),
  accepts: uint,     ;; Bitmask of who accepted
  status: uint,      ;; 0=pending, 1=ready, 2=executed, 3=cancelled
  expiry: uint,
  payload: (buff 256)
})

;; Accept via signature verification
(define-public (accept (intent-id uint) (signature (buff 65)))
  (let (
    (intent (unwrap! (map-get? Intents intent-id) ERR_NOT_FOUND))
    (msg-hash (sha256 (concat (int-to-ascii intent-id) (get payload intent))))
    (signer (try! (secp256k1-recover? msg-hash signature))))
    ;; Verify signer is participant, update accepts bitmask
    (ok true)))
Reference: ERC-8001 pattern for decidable multi-party coordination.

协调需要多签名的操作。
clarity
;; 提案状态
(define-map Intents uint {
  participants: (list 20 principal),
  accepts: uint,     ;; 已接受者的位掩码
  status: uint,      ;; 0=待处理,1=就绪,2=已执行,3=已取消
  expiry: uint,
  payload: (buff 256)
})

;; 通过签名验证接受提案
(define-public (accept (intent-id uint) (signature (buff 65)))
  (let (
    (intent (unwrap! (map-get? Intents intent-id) ERR_NOT_FOUND))
    (msg-hash (sha256 (concat (int-to-ascii intent-id) (get payload intent))))
    (signer (try! (secp256k1-recover? msg-hash signature))))
    ;; 验证签名者为参与者,更新接受位掩码
    (ok true)))
参考:用于可判定多方协作的ERC-8001模式。

Registry Patterns

注册器模式

Block Snapshot Pattern

区块快照模式

Capture comprehensive chain state at transaction time. This is the "receipt" that makes a transaction worth the fee.
clarity
;; Full snapshot — comprehensive (use for high-value records)
(define-private (capture-snapshot)
  {
    stacksBlock: stacks-block-height,
    burnBlock: burn-block-height,
    tenure: tenure-height,
    blockTime: stacks-block-time,
    chainId: chain-id,
    txSender: tx-sender,
    contractCaller: contract-caller,
    txSponsor: tx-sponsor?,
    stacksBlockHash: (get-stacks-block-info? id-header-hash (- stacks-block-height u1)),
    burnBlockHash: (get-burn-block-info? header-hash (- burn-block-height u1))
  }
)

;; Standard snapshot — balanced cost
;; {stacksBlock, burnBlock, blockTime, txSender}

;; Minimal snapshot — cheapest
;; {stacksBlock, burnBlock}
Previous block hashes are captured because the current block's hash isn't finalized until after the transaction. The previous block's hash is immutable and independently verifiable.
在交易发生时捕获完整的链上状态。这是让交易值得支付手续费的“收据”。
clarity
;; 完整快照——全面记录(用于高价值记录)
(define-private (capture-snapshot)
  {
    stacksBlock: stacks-block-height,
    burnBlock: burn-block-height,
    tenure: tenure-height,
    blockTime: stacks-block-time,
    chainId: chain-id,
    txSender: tx-sender,
    contractCaller: contract-caller,
    txSponsor: tx-sponsor?,
    stacksBlockHash: (get-stacks-block-info? id-header-hash (- stacks-block-height u1)),
    burnBlockHash: (get-burn-block-info? header-hash (- burn-block-height u1))
  }
)

;; 标准快照——平衡成本
;; {stacksBlock, burnBlock, blockTime, txSender}

;; 最小快照——成本最低
;; {stacksBlock, burnBlock}
当前区块的哈希要到交易完成后才会最终确定,因此捕获的是前一区块的哈希,该哈希是不可变且可独立验证的。

Principal-Keyed Registry

主体键注册器

Track state per address (heartbeats, profiles, balances). One entry per address, overwrites on subsequent calls.
clarity
(define-map Registry
  principal
  {
    stacksBlock: uint,
    burnBlock: uint,
    count: uint
  }
)

(map-get? Registry address)
(map-set Registry tx-sender {...})
按地址跟踪状态(心跳、档案、余额)。每个地址对应一条记录,后续调用会覆盖原有记录。
clarity
(define-map Registry
  principal
  {
    stacksBlock: uint,
    burnBlock: uint,
    count: uint
  }
)

(map-get? Registry address)
(map-set Registry tx-sender {...})

Hash-Keyed Registry

哈希键注册器

Track unique data (attestations, commitments). One entry per unique hash, first-write-wins.
clarity
(define-map Registry
  (buff 32)
  {
    attestor: principal,
    stacksBlock: uint
  }
)

;; First attestor wins
(asserts! (is-none (map-get? Registry hash)) ERR_ALREADY_EXISTS)
(map-set Registry hash {...})
跟踪唯一数据(证明、承诺)。每个唯一哈希对应一条记录,先写入者胜出。
clarity
(define-map Registry
  (buff 32)
  {
    attestor: principal,
    stacksBlock: uint
  }
)

;; 先写入者胜出
(asserts! (is-none (map-get? Registry hash)) ERR_ALREADY_EXISTS)
(map-set Registry hash {...})

Composite-Keyed Registry

复合键注册器

Multi-dimensional tracking (votes per proposal, actions per agent).
clarity
(define-map Registry
  {entity: principal, action: uint}
  {stacksBlock: uint}
)

(map-get? Registry {entity: address, action: action-id})
多维跟踪(每个提案的投票数、每个Agent的操作数)。
clarity
(define-map Registry
  {entity: principal, action: uint}
  {stacksBlock: uint}
)

(map-get? Registry {entity: address, action: action-id})

Secondary Index Pattern

二级索引模式

Enable enumeration of entries by address when primary key isn't the address.
clarity
;; Primary: hash -> data
(define-map Attestations (buff 32) {...})

;; Secondary: address + index -> hash
(define-map AttestorIndex
  {attestor: principal, index: uint}
  (buff 32)
)

;; Counter for next index
(define-map AttestorCount principal uint)

;; On insert:
(let ((idx (default-to u0 (map-get? AttestorCount attestor))))
  (map-set AttestorIndex {attestor: attestor, index: idx} hash)
  (map-set AttestorCount attestor (+ idx u1)))

;; Enumerate:
(define-read-only (get-attestor-hash-at (attestor principal) (index uint))
  (map-get? AttestorIndex {attestor: attestor, index: index}))
当主键不是地址时,支持按地址枚举记录。
clarity
;; 主键:哈希 -> 数据
(define-map Attestations (buff 32) {...})

;; 二级索引:地址 + 索引 -> 哈希
(define-map AttestorIndex
  {attestor: principal, index: uint}
  (buff 32)
)

;; 下一个索引的计数器
(define-map AttestorCount principal uint)

;; 插入时:
(let ((idx (default-to u0 (map-get? AttestorCount attestor))))
  (map-set AttestorIndex {attestor: attestor, index: idx} hash)
  (map-set AttestorCount attestor (+ idx u1)))

;; 枚举:
(define-read-only (get-attestor-hash-at (attestor principal) (index uint))
  (map-get? AttestorIndex {attestor: attestor, index: index}))

Global Stats Pattern

全局统计模式

Track aggregate metrics without iterating.
clarity
(define-data-var totalEntries uint u0)
(define-data-var uniqueAddresses uint u0)

;; On new entry:
(var-set totalEntries (+ (var-get totalEntries) u1))
(if isNewAddress
  (var-set uniqueAddresses (+ (var-get uniqueAddresses) u1))
  true)

;; Read stats:
(define-read-only (get-stats)
  {
    totalEntries: (var-get totalEntries),
    uniqueAddresses: (var-get uniqueAddresses)
  }
)
无需迭代即可跟踪聚合指标。
clarity
(define-data-var totalEntries uint u0)
(define-data-var uniqueAddresses uint u0)

;; 新增记录时:
(var-set totalEntries (+ (var-get totalEntries) u1))
(if isNewAddress
  (var-set uniqueAddresses (+ (var-get uniqueAddresses) u1))
  true)

;; 读取统计数据:
(define-read-only (get-stats)
  {
    totalEntries: (var-get totalEntries),
    uniqueAddresses: (var-get uniqueAddresses)
  }
)

Write Semantics

写入语义

First-Write-Wins (Attestations):
clarity
(define-public (attest (key (buff 32)))
  (begin
    (asserts! (is-none (map-get? Registry key)) ERR_ALREADY_EXISTS)
    (map-set Registry key {...})
    (ok true)))
Last-Write-Wins (Heartbeats):
clarity
(define-public (check-in)
  (begin
    (map-set Registry tx-sender {...})
    (ok true)))
Append-Only (History):
clarity
(define-map History
  {address: principal, index: uint}
  {...snapshot...}
)

(define-public (record)
  (let ((idx (default-to u0 (map-get? HistoryCount tx-sender))))
    (map-set History {address: tx-sender, index: idx} {...})
    (map-set HistoryCount tx-sender (+ idx u1))
    (ok idx)))
先写入者胜出(证明类):
clarity
(define-public (attest (key (buff 32)))
  (begin
    (asserts! (is-none (map-get? Registry key)) ERR_ALREADY_EXISTS)
    (map-set Registry key {...})
    (ok true)))
后写入者胜出(心跳类):
clarity
(define-public (check-in)
  (begin
    (map-set Registry tx-sender {...})
    (ok true)))
仅追加(历史记录类):
clarity
(define-map History
  {address: principal, index: uint}
  {...snapshot...}
)

(define-public (record)
  (let ((idx (default-to u0 (map-get? HistoryCount tx-sender))))
    (map-set History {address: tx-sender, index: idx} {...})
    (map-set HistoryCount tx-sender (+ idx u1))
    (ok idx)))

Access Control Patterns

访问控制模式

Open (anyone can write):
clarity
(define-public (register)
  (ok (map-set Registry tx-sender {...})))
Self-Only (registered users update own entries):
clarity
(define-public (update (data (buff 64)))
  (begin
    (asserts! (is-some (map-get? Registry tx-sender)) ERR_NOT_REGISTERED)
    (map-set Registry tx-sender {...})
    (ok true)))
Admin-Gated:
clarity
(define-data-var admin principal CONTRACT_OWNER)

(define-public (register-address (address principal))
  (begin
    (asserts! (is-eq tx-sender (var-get admin)) ERR_UNAUTHORIZED)
    (map-set Registry address {...})
    (ok true)))

开放(任何人可写入):
clarity
(define-public (register)
  (ok (map-set Registry tx-sender {...})))
仅自身(已注册用户可更新自己的记录):
clarity
(define-public (update (data (buff 64)))
  (begin
    (asserts! (is-some (map-get? Registry tx-sender)) ERR_NOT_REGISTERED)
    (map-set Registry tx-sender {...})
    (ok true)))
管理员 gated
clarity
(define-data-var admin principal CONTRACT_OWNER)

(define-public (register-address (address principal))
  (begin
    (asserts! (is-eq tx-sender (var-get admin)) ERR_UNAUTHORIZED)
    (map-set Registry address {...})
    (ok true)))

Testing Reference

测试参考

Testing Pyramid

测试金字塔

Stxer (Historical Simulation)      — Mainnet fork, pre-deployment validation
RV (Property-Based Fuzzing)        — Invariants, edge cases, battle-grade
Vitest + Clarinet SDK              — Integration tests, TypeScript
Clarunit                           — Unit tests in Clarity itself
Stxer(历史模拟)      — 主网分叉,部署前验证
RV(基于属性的模糊测试)        — 不变量、边缘情况、生产级测试
Vitest + Clarinet SDK              — 集成测试、TypeScript
Clarunit                           — 纯Clarity编写的单元测试

When to Use Each Tool

各工具适用场景

ToolUse WhenSkip When
Clarinet SDKStandard testing, CI/CD, type-safe-
ClarunitTesting Clarity logic in Clarity, simple assertionsComplex multi-account flows
RVTreasuries, DAOs, high-value contracts, finding edge casesSimple contracts, time pressure
StxerPre-mainnet validation, governance simulationsEarly development, testnet-only
工具适用场景不适用场景
Clarinet SDK标准测试、CI/CD、类型安全-
Clarunit用Clarity测试Clarity逻辑、简单断言复杂多账户流程
RV金库、DAO、高价值合约、寻找边缘情况简单合约、时间紧张的开发
Stxer主网部署前验证、治理模拟早期开发、仅测试网部署

Vitest Config

Vitest配置

javascript
import { defineConfig } from "vitest/config";
import { vitestSetupFilePath, getClarinetVitestsArgv } from "@hirosystems/clarinet-sdk/vitest";

export default defineConfig({
  test: {
    environment: "clarinet",
    singleThread: true,
    setupFiles: [vitestSetupFilePath],
    environmentOptions: {
      clarinet: getClarinetVitestsArgv(),
    },
  },
});
javascript
import { defineConfig } from "vitest/config";
import { vitestSetupFilePath, getClarinetVitestsArgv } from "@hirosystems/clarinet-sdk/vitest";

export default defineConfig({
  test: {
    environment: "clarinet",
    singleThread: true,
    setupFiles: [vitestSetupFilePath],
    environmentOptions: {
      clarinet: getClarinetVitestsArgv(),
    },
  },
});

Test Structure (Arrange-Act-Assert)

测试结构(Arrange-Act-Assert)

typescript
import { Cl } from "@stacks/transactions";
import { describe, expect, it } from "vitest";

describe("my-contract", function () {
  it("transfers tokens correctly", function () {
    // ARRANGE
    const deployer = simnet.deployer;
    const wallet1 = simnet.getAccounts().get("wallet_1")!;
    const amount = 100;

    // ACT
    const result = simnet.callPublicFn(
      "my-contract",
      "transfer",
      [Cl.uint(amount), Cl.principal(wallet1)],
      deployer
    );

    // ASSERT
    expect(result.result).toBeOk(Cl.bool(true));
  });
});
typescript
import { Cl } from "@stacks/transactions";
import { describe, expect, it } from "vitest";

describe("my-contract", function () {
  it("transfers tokens correctly", function () {
    // 准备(ARRANGE)
    const deployer = simnet.deployer;
    const wallet1 = simnet.getAccounts().get("wallet_1")!;
    const amount = 100;

    // 执行(ACT)
    const result = simnet.callPublicFn(
      "my-contract",
      "transfer",
      [Cl.uint(amount), Cl.principal(wallet1)],
      deployer
    );

    // 断言(ASSERT)
    expect(result.result).toBeOk(Cl.bool(true));
  });
});

Key Gotchas

关键注意事项

  • NO
    beforeAll
    /
    beforeEach
    — simnet resets each test file session
  • Single thread required
    singleThread: true
    for simnet isolation
  • Functions over arrow functions for test helpers
  • Use
    cvToValue()
    and
    cvToJSON()
    for Clarity-to-JS conversion
  • 禁止使用
    beforeAll
    /
    beforeEach
    — simnet会在每个测试文件会话开始时重置
  • 必须单线程运行
    singleThread: true
    以保证simnet隔离
  • 测试辅助函数使用普通函数而非箭头函数
  • 使用
    cvToValue()
    cvToJSON()
    进行Clarity到JS的转换

Clarity Value Constructors

Clarity值构造器

typescript
import { Cl, cvToValue, cvToJSON } from "@stacks/transactions";

Cl.uint(100)                           // uint
Cl.int(-50)                            // int
Cl.bool(true)                          // bool
Cl.principal("SP123...")               // principal
Cl.contractPrincipal("SP123", "name")  // contract principal
Cl.stringAscii("hello")               // (string-ascii N)
Cl.stringUtf8("hello")                // (string-utf8 N)
Cl.bufferFromHex("deadbeef")           // (buff N)
Cl.tuple({ amount: Cl.uint(100) })     // tuple
Cl.list([Cl.uint(1), Cl.uint(2)])      // list
Cl.some(Cl.uint(100))                  // (some value)
Cl.none()                              // none
typescript
import { Cl, cvToValue, cvToJSON } from "@stacks/transactions";

Cl.uint(100)                           // uint类型
Cl.int(-50)                            // int类型
Cl.bool(true)                          // bool类型
Cl.principal("SP123...")               // 主体
Cl.contractPrincipal("SP123", "name")  // 合约主体
Cl.stringAscii("hello")               // (string-ascii N)
Cl.stringUtf8("hello")                // (string-utf8 N)
Cl.bufferFromHex("deadbeef")           // (buff N)
Cl.tuple({ amount: Cl.uint(100) })     // 元组
Cl.list([Cl.uint(1), Cl.uint(2)])      // 列表
Cl.some(Cl.uint(100))                  // (some value)
Cl.none()                              // none

Custom Matchers

自定义匹配器

typescript
expect(result.result).toBeOk(Cl.uint(100));
expect(result.result).toBeErr(Cl.uint(1));
expect(result.result).toBeBool(true);
expect(result.result).toBeUint(100);
expect(result.result).toBePrincipal("SP123...");
typescript
expect(result.result).toBeOk(Cl.uint(100));
expect(result.result).toBeErr(Cl.uint(1));
expect(result.result).toBeBool(true);
expect(result.result).toBeUint(100);
expect(result.result).toBePrincipal("SP123...");

RV (Rendezvous) Fuzz Tests

RV(Rendezvous)模糊测试

clarity
;; Property: loan amount always increases correctly
(define-public (test-borrow (amount uint))
  (if (is-eq amount u0)
    (ok false)  ;; Discard invalid input
    (let ((initial (get-loan tx-sender)))
      (try! (borrow amount))
      (asserts! (is-eq (get-loan tx-sender) (+ initial amount))
        (err u999))
      (ok true))))

;; Invariant: total supply never exceeds cap
(define-read-only (invariant-supply-capped)
  (<= (var-get total-supply) MAX_SUPPLY))
Run:
npx rv . my-contract test
(properties),
npx rv . my-contract invariant
(invariants)
clarity
;; 属性:贷款金额始终正确增加
(define-public (test-borrow (amount uint))
  (if (is-eq amount u0)
    (ok false)  ;; 丢弃无效输入
    (let ((initial (get-loan tx-sender)))
      (try! (borrow amount))
      (asserts! (is-eq (get-loan tx-sender) (+ initial amount))
        (err u999))
      (ok true))))

;; 不变量:总供应量绝不超过上限
(define-read-only (invariant-supply-capped)
  (<= (var-get total-supply) MAX_SUPPLY))
运行:
npx rv . my-contract test
(属性测试)、
npx rv . my-contract invariant
(不变量测试)

Clarunit Tests

Clarunit测试

clarity
;; @name Multiplication works correctly
(define-public (test-multiply)
  (begin
    (asserts! (is-eq u8 (contract-call? .math multiply u2 u4))
      (err "2 * 4 should equal 8"))
    (ok true)))
File:
tests/my-contract_test.clar
, functions start with
test-
.
clarity
;; @name 乘法功能正常
(define-public (test-multiply)
  (begin
    (asserts! (is-eq u8 (contract-call? .math multiply u2 u4))
      (err "2 * 4 应等于 8"))
    (ok true)))
文件:
tests/my-contract_test.clar
,测试函数以
test-
开头。

Project Structure (Full Stack)

项目结构(全栈)

my-project/
├── Clarinet.toml
├── vitest.config.js
├── package.json
├── contracts/
│   ├── my-contract.clar
│   └── my-contract.tests.clar      # RV tests
├── tests/
│   ├── my-contract.test.ts         # Vitest
│   ├── my-contract_test.clar       # Clarunit
│   └── clarunit.test.ts            # Clarunit runner
└── simulations/
    └── my-contract-stxer.ts        # Stxer
my-project/
├── Clarinet.toml
├── vitest.config.js
├── package.json
├── contracts/
│   ├── my-contract.clar
│   └── my-contract.tests.clar      # RV测试
├── tests/
│   ├── my-contract.test.ts         # Vitest测试
│   ├── my-contract_test.clar       # Clarunit测试
│   └── clarunit.test.ts            # Clarunit运行器
└── simulations/
    └── my-contract-stxer.ts        # Stxer模拟

package.json Scripts

package.json脚本

json
{
  "scripts": {
    "test": "vitest run",
    "test:watch": "vitest",
    "test:rv": "npx rv . my-contract test",
    "test:rv:invariant": "npx rv . my-contract invariant",
    "test:stxer": "npx tsx simulations/my-contract-stxer.ts"
  }
}

json
{
  "scripts": {
    "test": "vitest run",
    "test:watch": "vitest",
    "test:rv": "npx rv . my-contract test",
    "test:rv:invariant": "npx rv . my-contract invariant",
    "test:stxer": "npx tsx simulations/my-contract-stxer.ts"
  }
}

Contract Templates

合约模板

Full contract templates with source and tests are in colocated files:
TemplateFileDescription
heartbeat-registry
templates/heartbeat-registry.mdAgent heartbeat with full chain context, address enumeration, liveness checks
proof-of-existence
templates/proof-of-existence.mdDocument timestamping with SIP-018 signatures, first-write-wins, attestor index
registry-minimal
templates/registry-minimal.mdMinimal registry combining snapshot + stats + events

包含源码和测试的完整合约模板位于同目录文件中:
模板文件描述
heartbeat-registry
templates/heartbeat-registry.md带有完整链上上下文、地址枚举、活跃度检查的Agent心跳注册器
proof-of-existence
templates/proof-of-existence.md带有SIP-018签名、先写入者胜出规则、证明者索引的文档时间戳服务
registry-minimal
templates/registry-minimal.md集成快照、统计和事件的最小化注册器

Execution Cost Limits

执行成本限制

CategoryBlock LimitRead-Only Limit
Runtime5,000,000,0001,000,000,000
Read count15,00030
Read bytes100,000,000100,000
Write count15,0000
Write bytes15,000,0000
分类区块限制只读限制
运行时5,000,000,0001,000,000,000
读取次数15,00030
读取字节数100,000,000100,000
写入次数15,0000
写入字节数15,000,0000

Cost Optimization Tips

成本优化技巧

  1. Inline single-use values — avoid unnecessary
    let
    bindings
  2. Constants over data-vars — constants are cheaper to read
  3. Bulk operations — single call with list beats multiple calls
  4. Separate params vs tuples — flat params are cheaper for function calls
  5. Off-chain computation — move non-essential logic to UI/indexer

  1. 内联单次使用的值 — 避免不必要的
    let
    绑定
  2. 使用常量而非数据变量 — 常量读取成本更低
  3. 批量操作 — 单次带列表参数的调用优于多次调用
  4. 使用扁平参数而非元组 — 函数调用时扁平参数成本更低
  5. 链下计算 — 将非核心逻辑移至UI或索引器

Quick Reference

快速参考

  • Use
    stacks-block-height
    not
    block-height
    (deprecated)
  • Use
    tx-sender
    for token operations,
    contract-caller
    only when immediate caller identity is needed
  • Use
    try!
    for error propagation,
    asserts!
    for guards before state changes
  • All public functions must return
    (response ok err)
  • Error codes should be unique constants
  • Events:
    {notification: "event-name", payload: {...}}
    format
  • Check costs with
    ::get_costs
    in clarinet console
  • 使用
    stacks-block-height
    而非
    block-height
    (已废弃)
  • 代币操作使用
    tx-sender
    ,仅当需要直接调用者身份时使用
    contract-caller
  • 使用
    try!
    传播错误,状态变更前使用
    asserts!
    设置守卫
  • 所有公共函数必须返回
    (response ok err)
  • 错误码应使用唯一常量
  • 事件采用
    {notification: "event-name", payload: {...}}
    格式
  • 在clarinet控制台中使用
    ::get_costs
    检查成本

References

参考资料