clarity-patterns について
clarity-patterns は、Stacks 上の Clarity スマートコントラクト開発向けのパターンとテンプレート集です。開発者やエージェントが参照でき、コード、レジストリ、テスト資料を外部依存なしで利用できます。
主な機能
- 01Clarity コントラクトのコードパターン
- 02レジストリとストレージのパターン
- 03完全なコントラクトテンプレートとチェックリスト
- 04テストツール選択のガイド
活用シーン
- 01アクセス制御付きの関数を構築
- 02オンチェーンのレジストリと索引を設計
- 03コントラクトのテスト方法を選択
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ aibtcdev/ skills --skill clarity-patternsインストール後、Claude Code で/clarity-patternsを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
AGENT.mdtemplates/heartbeat-registry.mdproof-of-existence.mdregistry-minimal.md詳細
Clarity パターンスキル
Stacks 上での Clarity スマートコントラクト開発向けの標準パターンライブラリです。すべてのパターンとテンプレートはこのスキルに含まれており、外部依存関係はありません。
これはドキュメントのみのスキルです。エージェントはこのファイルと同じ場所にある参照ファイルを直接読み取ります。CLI インターフェースには、予定されている実装が記載されています。
bun run clarity-patterns/clarity-patterns.ts <subcommand> [options]
サブコマンド
list [--category <category>]— 利用可能なパターンとテンプレートを一覧表示します(カテゴリ:code、registry、templates、testing)get --name <pattern-name>— コードと注記を含む特定のパターンを返しますtemplate --name <template-name>— ソース、テスト、チェックリストを含む完全なコントラクトテンプレートを返します
コードパターン
公開関数テンプレート
ガードとエラー処理を含む公開関数の標準的な構造です。
(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!を使用します - アセットの安全性を確保するため、tx に事後条件を追加します
標準化されたイベント
オフチェーンでのインデックス作成に備えて、構造化されたイベントを発行します。
(print {
notification: "contract-event",
payload: {
amount: amount,
sender: tx-sender,
recipient: to
}
})
notification:イベントタイプを示す文字列識別子payload:camelCase のキーを含むタプル- 例:usabtc-token、ccd002-treasury-v3
match によるエラー処理
外部呼び出しの失敗を適切に処理します。
(match (contract-call? .other fn args)
success (ok success)
error (err ERR_EXTERNAL_CALL_FAILED))
ステータス/権限用のビットフラグ
複数のブール値を単一の uint にまとめます。
(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)))
例:aibtc-action-proposal-voting
複数宛先への送信パターン
fold を使用して、1 回のトランザクションで複数の受取人に送信します。
(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)))
親子マップ(階層データ)
ページネーションに対応した階層データを保存します。
(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
)))
ホワイトリスト登録(アセット/コントラクト)
連携できるコントラクト/アセットを制御します。
(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))))
例:ccd002-treasury-v3、aibtc-agent-account
トレイトのホワイトリスト登録
信頼できるトレイト実装からの呼び出しのみを許可します。
(define-map TrustedTraits principal bool)
;; In functions accepting traits
(asserts! (default-to false (map-get? TrustedTraits (contract-of t)))
ERR_UNTRUSTED)
有効化の遅延
Bitcoin ブロックの遅延後に機能を有効化します。
(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)))
レート制限
短時間に繰り返される操作を防ぎます。
(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)))
スナップショット投票を用いた DAO 提案(Stacks 3.4 以降)
注記:
at-blockを使用するコントラクトは、Stacks 3.4 の有効化後(約 2026-04-02、BTC ブロック 943,333)に失敗します。at-block組み込み関数は Stacks 3.4(SIP-042)で削除されました。at-blockを使用していた aibtcdev-daos の DAO コントラクトは、有効化前に移行する必要があります。
複合キーのマップを使用して、提案作成時のトークン残高スナップショットを保存します。これにより at-block を回避し、読み取り時の filter/リスト走査をなくし、O(1) の検索が可能になります。
;; Stacks 3.4+: at-block removed. Store snapshot at proposal creation time.
;; Using a composite-key map for O(1) lookup — no filter/list scan needed.
(define-map ProposalSnapshots {proposalId: uint, voter: principal} uint)
(define-map Proposals uint {
votesFor: uint,
votesAgainst: uint,
status: uint,
liquidTokens: uint,
snapshotBlock: uint
})
;; At proposal creation: capture token balances for eligible voters
;; (caller supplies voter list; balances read from current block)
(define-public (create-proposal (voters (list 1000 principal)))
(let (
(proposal-id (+ (var-get last-proposal-id) u1))
(snapshot-block stacks-block-height))
;; fold is used (not map) because Clarity has no partial application —
;; map requires a bare function identifier, not a call expression.
;; proposal-id is threaded as the accumulator so store-snapshot can use it.
;; fold threads proposal-id as accumulator; store-snapshot fires map-set as side effect
(fold store-snapshot voters proposal-id)
(map-set Proposals proposal-id {
votesFor: u0, votesAgainst: u0,
status: u0, liquidTokens: u0,
snapshotBlock: snapshot-block})
(var-set last-proposal-id proposal-id)
(ok proposal-id)))
(define-private (store-snapshot (voter principal) (acc uint))
(begin
(map-set ProposalSnapshots
{proposalId: acc, voter: voter}
(unwrap! (contract-call? .token get-balance voter) u0))
acc))
;; O(1) lookup — no list scan, no filter
(define-read-only (get-vote-power (proposal-id uint) (voter principal))
(default-to u0
(map-get? ProposalSnapshots {proposalId: proposal-id, voter: voter})))
;; Quorum check: (>= (/ (* total-votes u100) liquid-supply) QUORUM_PERCENT)
主なポイント:
- クロージャを使った
(filter ...)は使用しません — Clarity には部分適用がありません。複合キーマップによって、filter パターンを完全に置き換えられます。 default-to u0は正しく機能します —map-get?は(optional uint)を返すため、default-to u0はパニックを起こさずに投票していないユーザーを処理できます(フィルター済みリストに対してunwrap-panicを使う場合とは異なります)。- 読み取り時の O(n) のリスト走査ではなく、O(1) の検索を行います。
例:aibtcdev-daos — at-block を使用する DAO コントラクトは、Stacks 3.4 の有効化前に移行が必要です。
固定小数点演算
スケール係数を用いて小数値を処理します。
(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))
as-contract を使用したトレジャリーパターン
コントラクトが管理する資金には as-contract を使用します。
(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 と contract-caller の判断基準
| 呼び出し経路 | contract-caller | tx-sender |
|---|---|---|
| ユーザー -> 対象 | ユーザー | ユーザー |
| ユーザー -> プロキシ -> 対象 | プロキシ | ユーザー |
| ユーザー -> プロキシ(as-contract)-> 対象 | プロキシ | プロキシ |
- tx-sender:認証チェック、ID の帰属、自身への操作を防ぐガードに使用します。通常のプロキシ経由でも人間の ID を保持します。コンポーザビリティを優先する場合に適しています。
- contract-caller:直近の呼び出し元の ID が特に必要な場合に使用します。
- セキュリティ上の注意:自身への操作を防ぐガード(例:「オーナーは自分自身にフィードバックを与えられない」)に
contract-callerを使用すると、回避可能です。オーナーが任意のプロキシ経由で操作すると、contract-callerはオーナーではなくプロキシを示すためです。tx-senderは人間の起点を保持するため、この問題を防げます。
例:ccd002-treasury-v3、aibtc-agent-account
Clarity 4:アセットの制限
コントラクト呼び出しで移動できるアセットを制限します。
(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)
マルチパーティ調整
複数の署名を必要とするアクションを調整します。
;; 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)))
参考:決定可能なマルチパーティ調整の ERC-8001 パターン。
レジストリパターン
ブロックスナップショットパターン
トランザクション時点の包括的なチェーン状態を記録します。これが、トランザクションに手数料を支払う価値をもたらす「領収書」です。
;; 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}
現在のブロックのハッシュはトランザクション後に確定するため、前のブロックのハッシュを記録します。前のブロックのハッシュは不変であり、個別に検証できます。
プリンシパルキー型レジストリ
アドレスごとの状態(ハートビート、プロフィール、残高)を追跡します。アドレスごとにエントリを 1 つ持ち、後続の呼び出しで上書きします。
(define-map Registry
principal
{
stacksBlock: uint,
burnBlock: uint,
count: uint
}
)
(map-get? Registry address)
(map-set Registry tx-sender {...})
ハッシュキー型レジストリ
一意なデータ(証明、コミットメント)を追跡します。一意なハッシュごとにエントリを 1 つ持ち、最初の書き込みを優先します。
(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 {...})
複合キー型レジストリ
多次元の追跡(提案ごとの投票、エージェントごとのアクション)を行います。
(define-map Registry
{entity: principal, action: uint}
{stacksBlock: uint}
)
(map-get? Registry {entity: address, action: action-id})
セカンダリーインデックスパターン
主キーがアドレスではない場合に、アドレスごとにエントリを列挙できるようにします。
;; 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}))
グローバル統計パターン
反復処理を行わずに集計指標を追跡します。
(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)
}
)
書き込みセマンティクス
最初の書き込みを優先(証明):
(define-public (attest (key (buff 32)))
(begin
(asserts! (is-none (map-get? Registry key)) ERR_ALREADY_EXISTS)
(map-set Registry key {...})
(ok true)))
最後の書き込みを優先(ハートビート):
(define-public (check-in)
(begin
(map-set Registry tx-sender {...})
(ok true)))
追記のみ(履歴):
(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)))
アクセス制御パターン
オープン(誰でも書き込み可能):
(define-public (register)
(ok (map-set Registry tx-sender {...})))
本人のみ(登録ユーザーが自身のエントリを更新):
(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)))
管理者による制御:
(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)))
テストリファレンス
テストピラミッド
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
各ツールの使いどころ
| ツール | 使用する場面 | 使用しない場面 |
|---|---|---|
| Clarinet SDK | 標準的なテスト、CI/CD、型安全性が必要な場合 | - |
| Clarunit | Clarity 内で Clarity ロジックをテストする場合、シンプルなアサーション | 複雑なマルチアカウントフロー |
| RV | トレジャリー、DAO、高価値のコントラクト、エッジケースの発見 | シンプルなコントラクト、時間が限られている場合 |
| Stxer | メインネット前の検証、ガバナンスのシミュレーション | 開発初期、テストネットのみの場合 |
Vitest の設定
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(),
},
},
});
テスト構成(準備・実行・検証)
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));
});
});
主な注意点
beforeAll/beforeEachは使用しないでください — simnet はテストファイルのセッションごとにリセットされます- 単一スレッドが必要です — simnet を分離するには
singleThread: trueを指定します - テストヘルパーにはアロー関数より関数を使用します
- Clarity から JavaScript への変換には
cvToValue()とcvToJSON()を使用します
Clarity 値のコンストラクター
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
カスタムマッチャー
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)のファズテスト
;; 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))
実行:npx rv . my-contract test(プロパティ)、npx rv . my-contract invariant(不変条件)
Clarunit テスト
;; @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)))
ファイル:tests/my-contract_test.clar。関数名は test- で始めます。
プロジェクト構成(フルスタック)
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
package.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"
}
}
コントラクトテンプレート
ソースコードとテストを含む完全なコントラクトテンプレートは、同じディレクトリにあるファイルに記載されています。
| テンプレート | ファイル | 説明 |
|---|---|---|
heartbeat-registry | templates/heartbeat-registry.md | 完全なチェーンコンテキスト、アドレス列挙、稼働状況チェックを含むエージェントのハートビート |
proof-of-existence | templates/proof-of-existence.md | SIP-018 署名、先行書き込み優先、証明者インデックスを備えたドキュメントのタイムスタンプ付与 |
registry-minimal | templates/registry-minimal.md | スナップショット、統計、イベントを組み合わせた最小限のレジストリ |
実行コストの上限
| カテゴリ | ブロック上限 | 読み取り専用上限 |
|---|---|---|
| ランタイム | 5,000,000,000 | 1,000,000,000 |
| 読み取り回数 | 15,000 | 30 |
| 読み取りバイト数 | 100,000,000 | 100,000 |
| 書き込み回数 | 15,000 | 0 |
| 書き込みバイト数 | 15,000,000 | 0 |
コスト最適化のヒント
- 単一用途の値はインライン化する — 不要な
letバインディングを避けます - データ変数より定数を使う — 定数は読み取りコストが低くなります
- 一括操作を行う — 複数回の呼び出しより、リストを使った単一の呼び出しが効率的です
- パラメーターとタプルを分ける — 関数呼び出しでは、フラットなパラメーターのほうが低コストです
- オフチェーンで計算する — 必須ではないロジックを UI/インデクサーに移します
クイックリファレンス
- 非推奨の
block-heightではなく、stacks-block-heightを使います - トークン操作には
tx-senderを使い、呼び出し元の識別が必要な場合に限ってcontract-callerを使います - エラーの伝播には
try!を使い、状態変更前のガードにはasserts!を使います - すべてのパブリック関数は
(response ok err)を返す必要があります - エラーコードには一意の定数を使います
- イベントは
{notification: "event-name", payload: {...}}形式にします - clarinet コンソールで
::get_costsを使ってコストを確認します
参考資料
- friedger/clarity-ccip-026 — 4 つすべてのテストツールが統合されています
- kenny-stacks/stacks-starter — テスト環境の構築に関する参考資料
- aibtcdev/aibtcdev-daos — DAO の設計パターン
- citycoins/protocol — トークンとトレジャリーの設計パターン
- clarigen — コントラクトから TypeScript 型を生成します
- secondlayer — 代替の型ジェネレーター