Skip to main content

CAT (Chia Asset Tokens)

CATs (Chia Asset Tokens) are fungible tokens on the Chia blockchain. Each CAT has a unique asset ID derived from its TAIL (Token and Asset Issuance Limiter) program, which controls how the token can be minted.

Overview

CATs work by wrapping an inner puzzle (typically a standard p2 puzzle) with the CAT layer. The CAT layer:

  • Enforces that the total CAT amount is preserved across spends (no creation/destruction)
  • Identifies the token by its asset ID
  • Requires lineage proofs to verify the CAT's history

Key Types

TypeDescription
CatA CAT coin with its info and optional lineage proof
CatInfoAsset ID and inner puzzle hash
CatSpendCombines a CAT with an inner spend
LineageProofProof of the CAT's parent for validation

Issuing CATs

The simplest way to issue a new CAT is using the single-issuance TAIL (genesis by coin ID):

use chia_wallet_sdk::prelude::*;

let ctx = &mut SpendContext::new();

// The p2 layer for ownership
let p2 = StandardLayer::new(public_key);

// Create hint memos for coin discovery
let memos = ctx.hint(puzzle_hash)?;

// Conditions for the newly created CAT coins
let conditions = Conditions::new()
.create_coin(puzzle_hash, 1_000, memos);

// Issue the CAT - this returns:
// - issue_cat: Conditions to add to the parent spend
// - cats: Vector of created Cat objects
let (issue_cat, cats) = Cat::issue_with_coin(
ctx,
parent_coin_id, // The coin ID that will be spent to issue
1_000, // Total amount to issue
conditions, // Output conditions
)?;

// Spend the parent coin with the issuance conditions
p2.spend(ctx, parent_coin, issue_cat)?;

// The asset ID is derived from the parent coin ID
let asset_id = cats[0].info.asset_id;
info

The asset ID for single-issuance CATs is deterministically derived from the parent coin ID. This means you can calculate the asset ID before issuing.

Spending CATs

CAT spends require creating a CatSpend that combines the CAT with its inner puzzle spend:

use chia_wallet_sdk::prelude::*;

let ctx = &mut SpendContext::new();

// Setup
let p2 = StandardLayer::new(public_key);
let memos = ctx.hint(recipient_puzzle_hash)?;

// Create the inner spend (what the standard layer would do)
let inner_spend = p2.spend_with_conditions(
ctx,
Conditions::new().create_coin(recipient_puzzle_hash, 1_000, memos),
)?;

// Wrap it in a CatSpend
let cat_spends = [CatSpend::new(cat, inner_spend)];

// Execute all CAT spends
Cat::spend_all(ctx, &cat_spends)?;

let coin_spends = ctx.take();

Why spend_all?

CAT spends must be validated together because the CAT layer verifies that the total amount in equals the total amount out. The Cat::spend_all function handles:

  • Linking CAT spends via announcements
  • Ensuring amount conservation
  • Validating lineage proofs

Computing Child Coins

After spending a CAT, you can compute the child CAT:

// After spending, compute the new CAT
let child_cat = cat.child(recipient_puzzle_hash, 1_000);

// Access the underlying coin
let child_coin = child_cat.coin;

Multi-Input CAT Spends

When spending multiple CATs of the same asset type together:

let ctx = &mut SpendContext::new();
let p2 = StandardLayer::new(public_key);

// Build spends for multiple CAT coins
let cat_spends = [
CatSpend::new(
cat1,
p2.spend_with_conditions(ctx, Conditions::new())?,
),
CatSpend::new(
cat2,
p2.spend_with_conditions(
ctx,
Conditions::new()
.create_coin(recipient, combined_amount, ctx.hint(recipient)?),
)?,
),
];

Cat::spend_all(ctx, &cat_spends)?;

Paying Fees

CATs cannot pay transaction fees directly. To pay fees when spending CATs, you must include an XCH spend in the same transaction and use assert_concurrent_spend to link them together:

use chia_wallet_sdk::prelude::*;

let ctx = &mut SpendContext::new();
let p2 = StandardLayer::new(public_key);

// Spend the CAT
let memos = ctx.hint(recipient_puzzle_hash)?;
let inner_spend = p2.spend_with_conditions(
ctx,
Conditions::new()
.create_coin(recipient_puzzle_hash, cat.coin.amount, memos)
.assert_concurrent_spend(xch_coin.coin_id()), // Link to XCH spend
)?;

Cat::spend_all(ctx, &[CatSpend::new(cat, inner_spend)])?;

// Spend XCH to pay the fee
let fee = 100_000_000; // 0.0001 XCH
let change = xch_coin.amount - fee;

let mut xch_conditions = Conditions::new()
.reserve_fee(fee)
.assert_concurrent_spend(cat.coin.coin_id()); // Link to CAT spend

if change > 0 {
xch_conditions = xch_conditions.create_coin(
my_puzzle_hash,
change,
ctx.hint(my_puzzle_hash)?,
);
}

p2.spend(ctx, xch_coin, xch_conditions)?;

let coin_spends = ctx.take();
warning

Always use assert_concurrent_spend to link CAT and XCH spends together. Without this, an attacker could extract and submit only the XCH spend (with the fee) while discarding your CAT spend.

Lineage Proofs

CATs require lineage proofs to verify their authenticity. When parsing CATs from the blockchain, you'll need to track the parent information:

// A CAT with its lineage proof
let cat = Cat {
coin,
info: CatInfo {
asset_id,
inner_puzzle_hash,
},
lineage_proof: Some(LineageProof {
parent_parent_coin_info: parent_coin.parent_coin_info,
parent_inner_puzzle_hash: parent_inner_puzzle_hash,
parent_amount: parent_coin.amount,
}),
};

When issuing new CATs, the lineage proof is handled automatically by the SDK.

Complete Example

Here's a full example of issuing a CAT and then spending it:

use chia_wallet_sdk::prelude::*;

fn issue_and_spend_cat(
issuer_coin: Coin,
issuer_public_key: PublicKey,
recipient_puzzle_hash: Bytes32,
amount: u64,
) -> Result<(Bytes32, Vec<CoinSpend>), DriverError> {
let ctx = &mut SpendContext::new();
let p2 = StandardLayer::new(issuer_public_key);
let issuer_puzzle_hash = StandardLayer::puzzle_hash(issuer_public_key);

// Step 1: Issue the CAT
let memos = ctx.hint(issuer_puzzle_hash)?;
let issue_conditions = Conditions::new()
.create_coin(issuer_puzzle_hash, amount, memos);

let (issue_cat, cats) = Cat::issue_with_coin(
ctx,
issuer_coin.coin_id(),
amount,
issue_conditions,
)?;

p2.spend(ctx, issuer_coin, issue_cat)?;

let asset_id = cats[0].info.asset_id;
let cat = cats[0];

// Step 2: Immediately spend the CAT to the recipient
let transfer_memos = ctx.hint(recipient_puzzle_hash)?;
let inner_spend = p2.spend_with_conditions(
ctx,
Conditions::new().create_coin(recipient_puzzle_hash, amount, transfer_memos),
)?;

Cat::spend_all(ctx, &[CatSpend::new(cat, inner_spend)])?;

Ok((asset_id, ctx.take()))
}

API Reference

For the complete CAT API, see docs.rs.