diff --git a/docs/sdk/actions.md b/docs/sdk/actions.md
new file mode 100644
index 0000000..066d2ac
--- /dev/null
+++ b/docs/sdk/actions.md
@@ -0,0 +1,1112 @@
+---
+slug: /sdk/actions
+title: Action System
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+# Action System
+
+The Action System is a high-level, declarative API for building Chia blockchain transactions. Instead of manually constructing coin spends, puzzles, and solutions, you declare *what* you want to do using actions, and the system generates the appropriate spend bundles.
+
+## Overview
+
+The action system solves several challenges in transaction construction:
+
+- **Complexity reduction** - No need to understand CLVM puzzles, conditions, and solutions directly
+- **Transaction composition** - Easily combine multiple operations in a single transaction
+- **Asset tracking** - Automatic handling of coin IDs, amounts, and asset types
+- **Delta calculation** - Automatic balancing of inputs and outputs across all assets
+
+## Key Types
+
+| Type | Description |
+|------|-------------|
+| `Action` | A declarative operation (send, mint, issue, etc.) |
+| `Spends` | Orchestrates actions and manages coin selection |
+| `Deltas` | Tracks input/output requirements per asset |
+| `Id` | Identifies assets (XCH, existing CAT/NFT, or newly created) |
+| `Outputs` | Results of a completed transaction |
+
+## Asset Identification with Id
+
+The `Id` type identifies assets in the action system:
+
+
+
+
+```rust
+use chia_wallet_sdk::prelude::*;
+
+// Reference native XCH
+let xch = Id::Xch;
+
+// Reference an existing asset by its ID (CAT asset ID, NFT launcher ID, etc.)
+let existing_cat = Id::Existing(asset_id);
+
+// Reference a new asset created in the current transaction
+// The index refers to the action that creates the asset
+let new_asset = Id::New(0); // First action that creates an asset
+```
+
+
+
+
+```typescript
+import { Id } from "chia-wallet-sdk";
+
+// Reference native XCH
+const xch = Id.xch();
+
+// Reference an existing asset by its ID
+const existingCat = Id.existing(assetId);
+
+// Reference a new asset created in the current transaction
+const newAsset = Id.new(0n); // First action that creates an asset
+
+// Check asset type
+if (xch.isXch()) { /* ... */ }
+const assetBytes = existingCat.asExisting(); // Returns Uint8Array or null
+const index = newAsset.asNew(); // Returns bigint or null
+```
+
+
+
+
+```python
+from chia_wallet_sdk import Id
+
+# Reference native XCH
+xch = Id.xch()
+
+# Reference an existing asset by its ID
+existing_cat = Id.existing(asset_id)
+
+# Reference a new asset created in the current transaction
+new_asset = Id.new(0) # First action that creates an asset
+
+# Check asset type
+if xch.is_xch():
+ pass
+asset_bytes = existing_cat.as_existing() # Returns bytes or None
+index = new_asset.as_new() # Returns int or None
+```
+
+
+
+
+## Creating Actions
+
+Actions are created using static factory methods on the `Action` class/enum.
+
+### Send Action
+
+Send assets to a puzzle hash:
+
+
+
+
+```rust
+use chia_wallet_sdk::prelude::*;
+
+// Send XCH
+let send_xch = Action::send(Id::Xch, recipient_puzzle_hash, 1000, Memos::None);
+
+// Send a CAT
+let send_cat = Action::send(Id::Existing(asset_id), recipient_puzzle_hash, 500, memos);
+
+// Send a newly created asset (from action at index 0)
+let send_new = Action::send(Id::New(0), recipient_puzzle_hash, 100, memos);
+
+// Burn assets (send to unspendable address)
+let burn = Action::burn(Id::Xch, 1000, Memos::None);
+```
+
+
+
+
+```typescript
+import { Action, Id } from "chia-wallet-sdk";
+
+// Send XCH
+const sendXch = Action.send(Id.xch(), recipientPuzzleHash, 1000n);
+
+// Send a CAT
+const sendCat = Action.send(Id.existing(assetId), recipientPuzzleHash, 500n);
+
+// Send a newly created asset (from action at index 0)
+const sendNew = Action.send(Id.new(0n), recipientPuzzleHash, 100n);
+
+// With memos for hints
+const sendWithMemo = Action.send(Id.xch(), recipientPuzzleHash, 1000n, memoProgram);
+```
+
+
+
+
+```python
+from chia_wallet_sdk import Action, Id
+
+# Send XCH
+send_xch = Action.send(Id.xch(), recipient_puzzle_hash, 1000)
+
+# Send a CAT
+send_cat = Action.send(Id.existing(asset_id), recipient_puzzle_hash, 500)
+
+# Send a newly created asset (from action at index 0)
+send_new = Action.send(Id.new(0), recipient_puzzle_hash, 100)
+
+# With memos for hints
+send_with_memo = Action.send(Id.xch(), recipient_puzzle_hash, 1000, memo_program)
+```
+
+
+
+
+### Fee Action
+
+Reserve XCH for transaction fees:
+
+
+
+
+```rust
+let fee = Action::fee(1000);
+```
+
+
+
+
+```typescript
+const fee = Action.fee(1000n);
+```
+
+
+
+
+```python
+fee = Action.fee(1000)
+```
+
+
+
+
+### CAT Issuance Actions
+
+Issue new Chia Asset Tokens:
+
+
+
+
+```rust
+// Single issuance CAT (genesis by coin ID - can only mint once)
+let issue = Action::single_issue_cat(hidden_puzzle_hash, 1_000_000);
+
+// Multi-issuance CAT with custom TAIL
+let issue_with_tail = Action::issue_cat(tail_spend, hidden_puzzle_hash, 1_000_000);
+```
+
+
+
+
+```typescript
+// Single issuance CAT (genesis by coin ID - can only mint once)
+const issue = Action.singleIssueCat(null, 1_000_000n);
+
+// Multi-issuance CAT with custom TAIL
+const issueWithTail = Action.issueCat(tailSpend, null, 1_000_000n);
+```
+
+
+
+
+```python
+# Single issuance CAT (genesis by coin ID - can only mint once)
+issue = Action.single_issue_cat(None, 1_000_000)
+
+# Multi-issuance CAT with custom TAIL
+issue_with_tail = Action.issue_cat(tail_spend, None, 1_000_000)
+```
+
+
+
+
+### NFT Actions
+
+Mint and update NFTs:
+
+
+
+
+```rust
+// Mint an NFT
+let mint = Action::mint_nft(
+ metadata, // NFT metadata (HashedPtr)
+ metadata_updater_puzzle, // Puzzle hash for metadata updates
+ royalty_puzzle_hash, // Where royalties are paid
+ royalty_basis_points, // Royalty percentage (300 = 3%)
+ amount, // Amount (usually 1)
+);
+
+// Mint an empty NFT with defaults
+let mint_empty = Action::mint_empty_nft();
+
+// Update an existing NFT's metadata
+let update = Action::update_nft(
+ Id::Existing(launcher_id),
+ metadata_spends, // Spends that update metadata
+ transfer, // Optional transfer info
+);
+```
+
+
+
+
+```typescript
+import { Action, NftMetadata, Constants, Spend } from "chia-wallet-sdk";
+
+// Define metadata
+const metadata = new NftMetadata(
+ 1n, // edition number
+ 1n, // edition total
+ ["https://example.com/image.png"], // data URIs
+ null, // data hash
+ ["https://example.com/metadata.json"], // metadata URIs
+ null, // metadata hash
+ [], // license URIs
+ null // license hash
+);
+
+// Mint an NFT
+const mint = Action.mintNft(
+ clvm,
+ clvm.nftMetadata(metadata),
+ Constants.nftMetadataUpdaterDefaultHash(),
+ royaltyPuzzleHash,
+ 300, // 3% royalty
+ 1n, // amount
+ null // parent ID (optional)
+);
+
+// Update NFT metadata
+const metadataUpdate = new Spend(
+ clvm.nftMetadataUpdaterDefault(),
+ clvm.list([clvm.string("u"), clvm.string("https://example.com/new-uri")])
+);
+const update = Action.updateNft(Id.existing(launcherId), [metadataUpdate]);
+```
+
+
+
+
+```python
+from chia_wallet_sdk import Action, NftMetadata, Constants, Spend, Id
+
+# Define metadata
+metadata = NftMetadata(
+ 1, # edition number
+ 1, # edition total
+ ["https://example.com/image.png"], # data URIs
+ None, # data hash
+ ["https://example.com/metadata.json"], # metadata URIs
+ None, # metadata hash
+ [], # license URIs
+ None # license hash
+)
+
+# Mint an NFT
+mint = Action.mint_nft(
+ clvm,
+ clvm.nft_metadata(metadata),
+ Constants.nft_metadata_updater_default_hash(),
+ royalty_puzzle_hash,
+ 300, # 3% royalty
+ 1, # amount
+ None # parent ID (optional)
+)
+
+# Update NFT metadata
+metadata_update = Spend(
+ clvm.nft_metadata_updater_default(),
+ clvm.list([clvm.string("u"), clvm.string("https://example.com/new-uri")])
+)
+update = Action.update_nft(Id.existing(launcher_id), [metadata_update])
+```
+
+
+
+
+## Using the Spends Orchestrator
+
+The `Spends` struct orchestrates actions and manages the transaction building process.
+
+### Basic Workflow
+
+
+
+
+```rust
+use chia_wallet_sdk::prelude::*;
+
+let ctx = &mut SpendContext::new();
+
+// 1. Create Spends with a change puzzle hash
+let mut spends = Spends::new(change_puzzle_hash);
+
+// 2. Add coins to spend
+spends.add(xch_coin);
+spends.add_cat(cat);
+
+// 3. Apply actions and get deltas
+let deltas = spends.apply(ctx, &[
+ Action::send(Id::Xch, recipient, 500, Memos::None),
+ Action::fee(100),
+])?;
+
+// 4. Finish with signing keys
+let outputs = spends.finish_with_keys(
+ ctx,
+ &deltas,
+ Relation::None,
+ &indexmap! { puzzle_hash => public_key },
+)?;
+
+// 5. Extract coin spends
+let coin_spends = ctx.take();
+```
+
+
+
+
+```typescript
+import { Action, Clvm, Id, Spends, Simulator, standardPuzzleHash, BlsPair } from "chia-wallet-sdk";
+
+const sim = new Simulator();
+const clvm = new Clvm();
+
+// Create a wallet
+const pair = BlsPair.fromSeed(0n);
+const puzzleHash = standardPuzzleHash(pair.pk);
+sim.newCoin(puzzleHash, 1000n);
+
+// 1. Create Spends with a change puzzle hash
+const spends = new Spends(clvm, puzzleHash);
+
+// 2. Add coins to spend
+const coins = sim.unspentCoins(puzzleHash, false);
+for (const coin of coins) {
+ spends.addXch(coin);
+}
+
+// 3. Apply actions and get deltas
+const deltas = spends.apply([
+ Action.send(Id.xch(), recipientPuzzleHash, 500n),
+ Action.fee(100n),
+]);
+
+// 4. Prepare the spends
+const finished = spends.prepare(deltas);
+
+// 5. Insert p2 spends for each pending coin
+for (const spend of finished.pendingSpends()) {
+ finished.insert(
+ spend.coin().coinId(),
+ clvm.standardSpend(pair.pk, clvm.delegatedSpend(spend.conditions()))
+ );
+}
+
+// 6. Execute and get outputs
+const outputs = finished.spend();
+
+// 7. Sign and broadcast
+sim.spendCoins(clvm.coinSpends(), [pair.sk]);
+```
+
+
+
+
+```python
+from chia_wallet_sdk import Action, Clvm, Id, Spends, Simulator, standard_puzzle_hash, BlsPair
+
+sim = Simulator()
+clvm = Clvm()
+
+# Create a wallet
+pair = BlsPair.from_seed(0)
+puzzle_hash = standard_puzzle_hash(pair.pk)
+sim.new_coin(puzzle_hash, 1000)
+
+# 1. Create Spends with a change puzzle hash
+spends = Spends(clvm, puzzle_hash)
+
+# 2. Add coins to spend
+coins = sim.unspent_coins(puzzle_hash, False)
+for coin in coins:
+ spends.add_xch(coin)
+
+# 3. Apply actions and get deltas
+deltas = spends.apply([
+ Action.send(Id.xch(), recipient_puzzle_hash, 500),
+ Action.fee(100),
+])
+
+# 4. Prepare the spends
+finished = spends.prepare(deltas)
+
+# 5. Insert p2 spends for each pending coin
+for spend in finished.pending_spends():
+ finished.insert(
+ spend.coin().coin_id(),
+ clvm.standard_spend(pair.pk, clvm.delegated_spend(spend.conditions()))
+ )
+
+# 6. Execute and get outputs
+outputs = finished.spend()
+
+# 7. Sign and broadcast
+sim.spend_coins(clvm.coin_spends(), [pair.sk])
+```
+
+
+
+
+## Understanding Deltas
+
+Deltas track the input/output requirements for each asset type in a transaction. This enables automatic coin selection and change calculation.
+
+
+
+
+```rust
+// Calculate deltas from actions
+let deltas = Deltas::from_actions(&actions);
+
+// Check requirements for a specific asset
+if let Some(delta) = deltas.get(&Id::Xch) {
+ println!("XCH input needed: {}", delta.input);
+ println!("XCH output created: {}", delta.output);
+}
+
+// Check which assets are needed
+for id in deltas.needed() {
+ println!("Need to provide: {:?}", id);
+}
+```
+
+
+
+
+```typescript
+import { Deltas, Delta, Id } from "chia-wallet-sdk";
+
+// Calculate deltas from actions
+const deltas = Deltas.fromActions(actions);
+
+// Iterate over all assets
+for (const id of deltas.ids()) {
+ const delta = deltas.get(id) ?? new Delta(0n, 0n);
+ console.log(`Asset ${id}: input=${delta.input}, output=${delta.output}`);
+}
+
+// Check if an asset needs to be provided
+if (deltas.isNeeded(Id.xch())) {
+ console.log("Need to provide XCH");
+}
+```
+
+
+
+
+```python
+from chia_wallet_sdk import Deltas, Delta, Id
+
+# Calculate deltas from actions
+deltas = Deltas.from_actions(actions)
+
+# Iterate over all assets
+for id in deltas.ids():
+ delta = deltas.get(id) or Delta(0, 0)
+ print(f"Asset {id}: input={delta.input}, output={delta.output}")
+
+# Check if an asset needs to be provided
+if deltas.is_needed(Id.xch()):
+ print("Need to provide XCH")
+```
+
+
+
+
+## Working with Outputs
+
+After completing a transaction, the `Outputs` struct provides access to the created assets:
+
+
+
+
+```rust
+// Get created XCH coins
+for coin in &outputs.xch {
+ println!("Created XCH coin: {} mojos", coin.amount);
+}
+
+// Get created CATs by asset ID
+for (id, cats) in &outputs.cats {
+ for cat in cats {
+ println!("Created CAT: {} of {:?}", cat.coin.amount, id);
+ }
+}
+
+// Access fee amount
+println!("Fee: {} mojos", outputs.fee);
+```
+
+
+
+
+```typescript
+// Get all CAT asset IDs in the outputs
+const catIds = outputs.cats();
+
+// Get CATs for a specific asset
+const cats = outputs.cat(catIds[0]);
+for (const cat of cats) {
+ console.log(`Created CAT: ${cat.coin.amount}`);
+}
+
+// Get all NFT IDs in the outputs
+const nftIds = outputs.nfts();
+
+// Get a specific NFT
+const nft = outputs.nft(nftIds[0]);
+console.log(`NFT launcher ID: ${Buffer.from(nft.info.launcherId).toString("hex")}`);
+```
+
+
+
+
+```python
+# Get all CAT asset IDs in the outputs
+cat_ids = outputs.cats()
+
+# Get CATs for a specific asset
+cats = outputs.cat(cat_ids[0])
+for cat in cats:
+ print(f"Created CAT: {cat.coin.amount}")
+
+# Get all NFT IDs in the outputs
+nft_ids = outputs.nfts()
+
+# Get a specific NFT
+nft = outputs.nft(nft_ids[0])
+print(f"NFT launcher ID: {nft.info.launcher_id.hex()}")
+```
+
+
+
+
+## Complete Examples
+
+### Send XCH
+
+
+
+
+```rust
+use chia_wallet_sdk::prelude::*;
+
+fn send_xch(
+ coin: Coin,
+ sender_pk: PublicKey,
+ recipient: Bytes32,
+ amount: u64,
+ fee: u64,
+) -> Result, DriverError> {
+ let ctx = &mut SpendContext::new();
+ let sender_ph = StandardLayer::puzzle_hash(sender_pk);
+
+ let mut spends = Spends::new(sender_ph);
+ spends.add(coin);
+
+ let deltas = spends.apply(ctx, &[
+ Action::send(Id::Xch, recipient, amount, Memos::None),
+ Action::fee(fee),
+ ])?;
+
+ let _outputs = spends.finish_with_keys(
+ ctx,
+ &deltas,
+ Relation::None,
+ &indexmap! { sender_ph => sender_pk },
+ )?;
+
+ Ok(ctx.take())
+}
+```
+
+
+
+
+```typescript
+import {
+ Action, BlsPair, Clvm, Id, Simulator, Spends, standardPuzzleHash
+} from "chia-wallet-sdk";
+
+function sendXch(recipientPuzzleHash: Uint8Array, amount: bigint, fee: bigint) {
+ const sim = new Simulator();
+ const clvm = new Clvm();
+
+ // Setup sender wallet
+ const sender = BlsPair.fromSeed(0n);
+ const senderPh = standardPuzzleHash(sender.pk);
+ sim.newCoin(senderPh, 1000n);
+
+ // Create spends
+ const spends = new Spends(clvm, senderPh);
+ for (const coin of sim.unspentCoins(senderPh, false)) {
+ spends.addXch(coin);
+ }
+
+ // Apply actions
+ const deltas = spends.apply([
+ Action.send(Id.xch(), recipientPuzzleHash, amount),
+ Action.fee(fee),
+ ]);
+
+ // Prepare and insert p2 spends
+ const finished = spends.prepare(deltas);
+ for (const spend of finished.pendingSpends()) {
+ finished.insert(
+ spend.coin().coinId(),
+ clvm.standardSpend(sender.pk, clvm.delegatedSpend(spend.conditions()))
+ );
+ }
+
+ // Execute
+ const outputs = finished.spend();
+ sim.spendCoins(clvm.coinSpends(), [sender.sk]);
+
+ return outputs;
+}
+```
+
+
+
+
+```python
+from chia_wallet_sdk import (
+ Action, BlsPair, Clvm, Id, Simulator, Spends, standard_puzzle_hash
+)
+
+def send_xch(recipient_puzzle_hash: bytes, amount: int, fee: int):
+ sim = Simulator()
+ clvm = Clvm()
+
+ # Setup sender wallet
+ sender = BlsPair.from_seed(0)
+ sender_ph = standard_puzzle_hash(sender.pk)
+ sim.new_coin(sender_ph, 1000)
+
+ # Create spends
+ spends = Spends(clvm, sender_ph)
+ for coin in sim.unspent_coins(sender_ph, False):
+ spends.add_xch(coin)
+
+ # Apply actions
+ deltas = spends.apply([
+ Action.send(Id.xch(), recipient_puzzle_hash, amount),
+ Action.fee(fee),
+ ])
+
+ # Prepare and insert p2 spends
+ finished = spends.prepare(deltas)
+ for spend in finished.pending_spends():
+ finished.insert(
+ spend.coin().coin_id(),
+ clvm.standard_spend(sender.pk, clvm.delegated_spend(spend.conditions()))
+ )
+
+ # Execute
+ outputs = finished.spend()
+ sim.spend_coins(clvm.coin_spends(), [sender.sk])
+
+ return outputs
+```
+
+
+
+
+### Issue and Send a CAT
+
+
+
+
+```rust
+use chia_wallet_sdk::prelude::*;
+
+fn issue_and_send_cat(
+ coin: Coin,
+ sender_pk: PublicKey,
+ recipient: Bytes32,
+ issuance_amount: u64,
+ send_amount: u64,
+) -> Result<(Bytes32, Vec), DriverError> {
+ let ctx = &mut SpendContext::new();
+ let sender_ph = StandardLayer::puzzle_hash(sender_pk);
+
+ let mut spends = Spends::new(sender_ph);
+ spends.add(coin);
+
+ // Issue CAT at index 0, then send from it
+ let deltas = spends.apply(ctx, &[
+ Action::single_issue_cat(None, issuance_amount),
+ Action::send(Id::New(0), recipient, send_amount, Memos::None),
+ ])?;
+
+ let outputs = spends.finish_with_keys(
+ ctx,
+ &deltas,
+ Relation::None,
+ &indexmap! { sender_ph => sender_pk },
+ )?;
+
+ // Get the asset ID of the newly created CAT
+ let asset_id = outputs.cats.keys().next()
+ .and_then(|id| if let Id::New(0) = id { Some(*id) } else { None })
+ .expect("CAT should be created");
+
+ Ok((asset_id.into(), ctx.take()))
+}
+```
+
+
+
+
+```typescript
+import {
+ Action, BlsPair, Clvm, Id, Simulator, Spends, standardPuzzleHash
+} from "chia-wallet-sdk";
+
+function issueAndSendCat(
+ recipientPuzzleHash: Uint8Array,
+ issuanceAmount: bigint,
+ sendAmount: bigint
+) {
+ const sim = new Simulator();
+ const clvm = new Clvm();
+
+ // Setup wallet
+ const alice = BlsPair.fromSeed(0n);
+ const alicePh = standardPuzzleHash(alice.pk);
+ sim.newCoin(alicePh, 1n); // Just need 1 mojo for CAT issuance
+
+ // Create spends
+ const spends = new Spends(clvm, alicePh);
+ for (const coin of sim.unspentCoins(alicePh, false)) {
+ spends.addXch(coin);
+ }
+
+ // Issue CAT at index 0, then send from it
+ const deltas = spends.apply([
+ Action.singleIssueCat(null, issuanceAmount),
+ Action.send(Id.new(0n), recipientPuzzleHash, sendAmount),
+ ]);
+
+ // Prepare and insert p2 spends
+ const finished = spends.prepare(deltas);
+ for (const spend of finished.pendingSpends()) {
+ finished.insert(
+ spend.coin().coinId(),
+ clvm.standardSpend(alice.pk, clvm.delegatedSpend(spend.conditions()))
+ );
+ }
+
+ // Execute
+ const outputs = finished.spend();
+ sim.spendCoins(clvm.coinSpends(), [alice.sk]);
+
+ // Get the CAT asset ID
+ const catIds = outputs.cats();
+ const assetId = outputs.cat(catIds[0])[0].info.assetId;
+
+ return { assetId, outputs };
+}
+```
+
+
+
+
+```python
+from chia_wallet_sdk import (
+ Action, BlsPair, Clvm, Id, Simulator, Spends, standard_puzzle_hash
+)
+
+def issue_and_send_cat(
+ recipient_puzzle_hash: bytes,
+ issuance_amount: int,
+ send_amount: int
+):
+ sim = Simulator()
+ clvm = Clvm()
+
+ # Setup wallet
+ alice = BlsPair.from_seed(0)
+ alice_ph = standard_puzzle_hash(alice.pk)
+ sim.new_coin(alice_ph, 1) # Just need 1 mojo for CAT issuance
+
+ # Create spends
+ spends = Spends(clvm, alice_ph)
+ for coin in sim.unspent_coins(alice_ph, False):
+ spends.add_xch(coin)
+
+ # Issue CAT at index 0, then send from it
+ deltas = spends.apply([
+ Action.single_issue_cat(None, issuance_amount),
+ Action.send(Id.new(0), recipient_puzzle_hash, send_amount),
+ ])
+
+ # Prepare and insert p2 spends
+ finished = spends.prepare(deltas)
+ for spend in finished.pending_spends():
+ finished.insert(
+ spend.coin().coin_id(),
+ clvm.standard_spend(alice.pk, clvm.delegated_spend(spend.conditions()))
+ )
+
+ # Execute
+ outputs = finished.spend()
+ sim.spend_coins(clvm.coin_spends(), [alice.sk])
+
+ # Get the CAT asset ID
+ cat_ids = outputs.cats()
+ asset_id = outputs.cat(cat_ids[0])[0].info.asset_id
+
+ return asset_id, outputs
+```
+
+
+
+
+### Mint and Update NFT
+
+
+
+
+```rust
+use chia_wallet_sdk::prelude::*;
+
+fn mint_nft(
+ coin: Coin,
+ minter_pk: PublicKey,
+ metadata: NftMetadata,
+ royalty_puzzle_hash: Bytes32,
+ royalty_basis_points: u16,
+) -> Result<(Bytes32, Vec), DriverError> {
+ let ctx = &mut SpendContext::new();
+ let minter_ph = StandardLayer::puzzle_hash(minter_pk);
+
+ let mut spends = Spends::new(minter_ph);
+ spends.add(coin);
+
+ let deltas = spends.apply(ctx, &[
+ Action::mint_nft(
+ ctx.alloc(&metadata)?.into(),
+ NFT_METADATA_UPDATER_PUZZLE_HASH.into(),
+ royalty_puzzle_hash,
+ royalty_basis_points,
+ 1,
+ ),
+ ])?;
+
+ let outputs = spends.finish_with_keys(
+ ctx,
+ &deltas,
+ Relation::None,
+ &indexmap! { minter_ph => minter_pk },
+ )?;
+
+ let launcher_id = outputs.nfts.keys().next()
+ .and_then(|id| outputs.nfts.get(id))
+ .map(|nft| nft.info.launcher_id)
+ .expect("NFT should be created");
+
+ Ok((launcher_id, ctx.take()))
+}
+```
+
+
+
+
+```typescript
+import {
+ Action, BlsPair, Clvm, Constants, Id, NftMetadata,
+ Simulator, Spend, Spends, standardPuzzleHash
+} from "chia-wallet-sdk";
+
+function mintAndUpdateNft() {
+ const sim = new Simulator();
+ const clvm = new Clvm();
+
+ // Setup wallet
+ const alice = BlsPair.fromSeed(0n);
+ const alicePh = standardPuzzleHash(alice.pk);
+ sim.newCoin(alicePh, 2n);
+
+ // Define metadata
+ const metadata = new NftMetadata(
+ 1n, 1n,
+ ["https://example.com/image.png"],
+ null, [], null, [], null
+ );
+
+ // Create spends
+ const spends = new Spends(clvm, alicePh);
+ for (const coin of sim.unspentCoins(alicePh, false)) {
+ spends.addXch(coin);
+ }
+
+ // Mint NFT and update metadata in one transaction
+ const metadataUpdate = new Spend(
+ clvm.nftMetadataUpdaterDefault(),
+ clvm.list([clvm.string("u"), clvm.string("https://example.com/updated.png")])
+ );
+
+ const deltas = spends.apply([
+ Action.mintNft(
+ clvm,
+ clvm.nftMetadata(metadata),
+ Constants.nftMetadataUpdaterDefaultHash(),
+ alicePh,
+ 300, // 3% royalty
+ 1n,
+ null
+ ),
+ Action.updateNft(Id.new(0n), [metadataUpdate]),
+ ]);
+
+ // Prepare and insert p2 spends
+ const finished = spends.prepare(deltas);
+ for (const spend of finished.pendingSpends()) {
+ finished.insert(
+ spend.coin().coinId(),
+ clvm.standardSpend(alice.pk, clvm.delegatedSpend(spend.conditions()))
+ );
+ }
+
+ // Execute
+ const outputs = finished.spend();
+ sim.spendCoins(clvm.coinSpends(), [alice.sk]);
+
+ const nftId = outputs.nfts()[0];
+ const nft = outputs.nft(nftId);
+
+ return { launcherId: nft.info.launcherId, outputs };
+}
+```
+
+
+
+
+```python
+from chia_wallet_sdk import (
+ Action, BlsPair, Clvm, Constants, Id, NftMetadata,
+ Simulator, Spend, Spends, standard_puzzle_hash
+)
+
+def mint_and_update_nft():
+ sim = Simulator()
+ clvm = Clvm()
+
+ # Setup wallet
+ alice = BlsPair.from_seed(0)
+ alice_ph = standard_puzzle_hash(alice.pk)
+ sim.new_coin(alice_ph, 2)
+
+ # Define metadata
+ metadata = NftMetadata(
+ 1, 1,
+ ["https://example.com/image.png"],
+ None, [], None, [], None
+ )
+
+ # Create spends
+ spends = Spends(clvm, alice_ph)
+ for coin in sim.unspent_coins(alice_ph, False):
+ spends.add_xch(coin)
+
+ # Mint NFT and update metadata in one transaction
+ metadata_update = Spend(
+ clvm.nft_metadata_updater_default(),
+ clvm.list([clvm.string("u"), clvm.string("https://example.com/updated.png")])
+ )
+
+ deltas = spends.apply([
+ Action.mint_nft(
+ clvm,
+ clvm.nft_metadata(metadata),
+ Constants.nft_metadata_updater_default_hash(),
+ alice_ph,
+ 300, # 3% royalty
+ 1,
+ None
+ ),
+ Action.update_nft(Id.new(0), [metadata_update]),
+ ])
+
+ # Prepare and insert p2 spends
+ finished = spends.prepare(deltas)
+ for spend in finished.pending_spends():
+ finished.insert(
+ spend.coin().coin_id(),
+ clvm.standard_spend(alice.pk, clvm.delegated_spend(spend.conditions()))
+ )
+
+ # Execute
+ outputs = finished.spend()
+ sim.spend_coins(clvm.coin_spends(), [alice.sk])
+
+ nft_id = outputs.nfts()[0]
+ nft = outputs.nft(nft_id)
+
+ return nft.info.launcher_id, outputs
+```
+
+
+
+
+## Action Types Reference
+
+| Action | Description | Creates Asset |
+|--------|-------------|---------------|
+| `send` | Transfer assets to a puzzle hash | No |
+| `burn` | Send assets to unspendable address | No |
+| `fee` | Reserve XCH for transaction fees | No |
+| `single_issue_cat` | Issue a single-issuance CAT | Yes (`Id::New`) |
+| `issue_cat` | Issue a multi-issuance CAT with TAIL | Yes (`Id::New`) |
+| `run_tail` | Execute CAT TAIL logic | No |
+| `mint_nft` | Mint a new NFT | Yes (`Id::New`) |
+| `update_nft` | Update NFT metadata or transfer | No |
+| `create_did` | Create a DID | Yes (`Id::New`) |
+| `update_did` | Update DID metadata | No |
+| `settle` | Settle a payment with notarized payments | No |
+| `melt_singleton` | Destroy a singleton | No |
+
+## Best Practices
+
+1. **Use `Id::New(index)` for chained operations** - When one action creates an asset and another action uses it, reference it by its action index
+2. **Check deltas before applying** - Use `Deltas::from_actions()` to verify you have sufficient coins before committing
+3. **Handle change automatically** - The action system calculates and creates change coins for you
+4. **Batch related operations** - Combine multiple actions in a single transaction to reduce fees
+5. **Test with Simulator** - Always validate transactions in the simulator before mainnet
+
+## Comparison with Low-Level API
+
+| Feature | Action System | Low-Level (SpendContext) |
+|---------|--------------|--------------------------|
+| Complexity | Declarative, high-level | Imperative, detailed |
+| Flexibility | Covers common patterns | Full control |
+| Coin management | Automatic | Manual |
+| Change handling | Automatic | Manual |
+| Learning curve | Lower | Higher |
+
+Choose the action system for standard operations and the low-level API when you need precise control over puzzle construction.
diff --git a/docs/sdk/connectivity.md b/docs/sdk/connectivity.md
new file mode 100644
index 0000000..71aeabe
--- /dev/null
+++ b/docs/sdk/connectivity.md
@@ -0,0 +1,139 @@
+---
+slug: /sdk/connectivity
+title: Connectivity
+---
+
+# Connectivity
+
+The Wallet SDK provides client crates for connecting to the Chia network. This page covers the basics of establishing connections and querying blockchain state.
+
+## Overview
+
+The SDK includes two client approaches:
+
+| Crate | Use Case |
+|-------|----------|
+| `chia-sdk-client` | Direct peer-to-peer connections using the Chia protocol |
+| `chia-sdk-coinset` | HTTP/RPC client for querying coin state |
+
+## Peer Connections
+
+The `Peer` type provides direct connections to Chia full nodes using the native protocol:
+
+```rust
+use chia_wallet_sdk::prelude::*;
+
+// Connect to a peer
+let peer = Peer::connect(
+ "node.example.com:8444",
+ network_id,
+ tls_connector,
+).await?;
+
+// Query coin state
+let coin_states = peer.request_coin_state(
+ coin_ids,
+ None, // previous_height
+ genesis_challenge,
+).await?;
+```
+
+### Connection Requirements
+
+Peer connections require:
+
+- Network ID (mainnet, testnet, etc.)
+- TLS configuration
+- Knowledge of the genesis challenge for the network
+
+## Coinset Client
+
+For simpler HTTP-based queries, use `CoinsetClient`:
+
+```rust
+use chia_wallet_sdk::prelude::*;
+
+// Create client for a coinset API endpoint
+let client = CoinsetClient::new(
+ "https://api.example.com",
+ network_id,
+);
+
+// Query coins by puzzle hash
+let coins = client.get_coins_by_puzzle_hash(puzzle_hash).await?;
+
+// Get coin state
+let states = client.get_coin_state(coin_ids).await?;
+```
+
+## Full Node Client
+
+For direct full node RPC access:
+
+```rust
+use chia_wallet_sdk::prelude::*;
+
+let client = FullNodeClient::new(
+ "https://localhost:8555",
+ cert_path,
+ key_path,
+)?;
+
+// Use full node RPC methods
+let blockchain_state = client.get_blockchain_state().await?;
+```
+
+## Broadcasting Transactions
+
+After building a spend bundle, broadcast it to the network:
+
+```rust
+// Build your transaction
+let ctx = &mut SpendContext::new();
+// ... add spends ...
+let coin_spends = ctx.take();
+
+// Sign the spend bundle
+let spend_bundle = SpendBundle::new(coin_spends, aggregated_signature);
+
+// Broadcast via peer
+let response = peer.send_transaction(spend_bundle).await?;
+
+// Or via full node client
+let response = client.push_tx(spend_bundle).await?;
+```
+
+## Network Configuration
+
+Different networks require different configuration:
+
+| Network | Default Port | Genesis Challenge |
+|---------|--------------|-------------------|
+| Mainnet | 8444 | See Chia docs |
+| Testnet | 58444 | See Chia docs |
+
+:::info
+For production applications, consider connecting to multiple peers for redundancy and using the coinset API for efficient queries.
+:::
+
+## TLS Configuration
+
+Peer connections require TLS. The SDK supports both `native-tls` and `rustls` backends via feature flags:
+
+```toml
+# Use native TLS (default)
+chia-wallet-sdk = { version = "0.32", features = ["native-tls"] }
+
+# Or use rustls
+chia-wallet-sdk = { version = "0.32", features = ["rustls"] }
+```
+
+## Beyond This Guide
+
+Detailed network programming with the SDK is beyond the scope of this documentation. For:
+
+- Production connection management
+- Peer discovery
+- Network protocol details
+
+See the [chia-sdk-client rustdocs](https://docs.rs/chia-sdk-client) and [chia-sdk-coinset rustdocs](https://docs.rs/chia-sdk-coinset).
diff --git a/docs/sdk/index.md b/docs/sdk/index.md
new file mode 100644
index 0000000..02cf38f
--- /dev/null
+++ b/docs/sdk/index.md
@@ -0,0 +1,171 @@
+---
+slug: /sdk
+title: Quick Start
+---
+
+# Wallet SDK
+
+The Chia Wallet SDK is a Rust library for building applications that interact with the Chia blockchain. It provides high-level abstractions for creating transactions, managing coins, and working with Chia primitives like CATs, NFTs, and Vaults.
+
+:::info
+This documentation assumes familiarity with Chia blockchain concepts such as coins, puzzles, conditions, and singletons. For background, see the [Chia Documentation](https://docs.chia.net) and [Chialisp Documentation](https://chialisp.com).
+:::
+
+## Installation
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+
+
+
+Add the SDK to your `Cargo.toml`:
+
+```toml
+[dependencies]
+chia-wallet-sdk = "0.32"
+```
+
+For the latest version and detailed API reference, see [docs.rs/chia-wallet-sdk](https://docs.rs/chia-wallet-sdk).
+
+
+
+
+Install via npm:
+
+```bash
+npm install chia-wallet-sdk
+```
+
+The Node.js bindings provide a similar API with JavaScript/TypeScript support. Full TypeScript type definitions are included.
+
+
+
+
+Install via pip:
+
+```bash
+pip install chia-wallet-sdk
+```
+
+The Python bindings provide a Pythonic API with full type stub support for IDE autocompletion.
+
+
+
+
+## Quick Example
+
+Here's a minimal example that creates and spends a standard XCH coin:
+
+
+
+
+```rust
+use chia_wallet_sdk::prelude::*;
+
+// Create a spend context to build the transaction
+let ctx = &mut SpendContext::new();
+
+// Define the conditions for this spend:
+// - Create a new coin with 900 mojos
+// - Reserve 100 mojos as transaction fee
+let conditions = Conditions::new()
+ .create_coin(puzzle_hash, 900, Memos::None)
+ .reserve_fee(100);
+
+// Create the spend using StandardLayer (p2 puzzle)
+StandardLayer::new(public_key).spend(ctx, coin, conditions)?;
+
+// Extract the coin spends for signing and broadcast
+let coin_spends = ctx.take();
+```
+
+
+
+
+```typescript
+import { Clvm, Coin, Simulator } from "chia-wallet-sdk";
+
+// Create a CLVM instance to build the transaction
+const clvm = new Clvm();
+
+// Create conditions:
+// - Create a new coin with 900 mojos
+// - Reserve 100 mojos as transaction fee
+const conditions = [
+ clvm.createCoin(puzzleHash, 900n, null),
+ clvm.reserveFee(100n),
+];
+
+// Create and spend using delegated spend (p2 puzzle)
+clvm.spendStandardCoin(
+ coin,
+ publicKey,
+ clvm.delegatedSpend(conditions)
+);
+
+// Extract the coin spends for signing and broadcast
+const coinSpends = clvm.coinSpends();
+```
+
+
+
+
+```python
+from chia_wallet_sdk import Clvm, Coin, Simulator
+
+# Create a CLVM instance to build the transaction
+clvm = Clvm()
+
+# Create conditions:
+# - Create a new coin with 900 mojos
+# - Reserve 100 mojos as transaction fee
+conditions = [
+ clvm.create_coin(puzzle_hash, 900, None),
+ clvm.reserve_fee(100),
+]
+
+# Create and spend using delegated spend (p2 puzzle)
+clvm.spend_standard_coin(
+ coin,
+ public_key,
+ clvm.delegated_spend(conditions)
+)
+
+# Extract the coin spends for signing and broadcast
+coin_spends = clvm.coin_spends()
+```
+
+
+
+
+This example demonstrates the core pattern you'll use throughout the SDK:
+
+1. **Create a context** - In Rust, use `SpendContext`; in Node.js/Python, use the `Clvm` class
+2. **Build conditions** - Define what the transaction should do (create coins, fees, announcements)
+3. **Spend coins** - Use primitives like `StandardLayer` (Rust) or `spendStandardCoin` (bindings)
+4. **Extract and broadcast** - Take the collected spends, sign them, and submit to the network
+
+## Core Concepts
+
+The SDK is organized around these key abstractions:
+
+| Concept | Rust | Node.js / Python | Description |
+|---------|------|------------------|-------------|
+| **Context** | `SpendContext` | `Clvm` | Transaction builder that manages memory and collects coin spends |
+| **Conditions** | `Conditions` builder | Method calls (`createCoin`, etc.) | Output conditions (create coin, fees, announcements) |
+| **Actions** | `Action`, `Spends` | `Action`, `Spends` | High-level declarative transaction API |
+| **Primitives** | `Cat`, `Nft`, `Vault`, etc. | `spendCats`, `spendNft`, etc. | High-level APIs for Chia constructs |
+| **Simulator** | `Simulator` | `Simulator` | Test transaction validation locally |
+
+## Next Steps
+
+- [SpendContext](/sdk/spend-context) - Understanding the core transaction builder
+- [Action System](/sdk/actions) - High-level declarative transaction API
+- [Standard (XCH)](/sdk/primitives/standard) - Working with basic XCH coins
+- [CAT](/sdk/primitives/cat) - Issuing and spending custom asset tokens
+- [NFT](/sdk/primitives/nft) - Minting and transferring NFTs
+
+## Relationship to chia-rs
+
+The Wallet SDK builds on top of the lower-level [chia-rs](https://github.com/Chia-Network/chia_rs) crates, providing ergonomic APIs for common operations. If you need lower-level control, the underlying types from `chia-protocol`, `clvm-traits`, and `clvmr` are re-exported through the SDK.
diff --git a/docs/sdk/offers.md b/docs/sdk/offers.md
new file mode 100644
index 0000000..2c0cecf
--- /dev/null
+++ b/docs/sdk/offers.md
@@ -0,0 +1,483 @@
+---
+slug: /sdk/offers
+title: Offers
+---
+
+# Offers
+
+Offers enable trustless, atomic swaps of assets on the Chia blockchain. Both parties' transactions execute together or not at all.
+
+## Overview
+
+Chia offers work by:
+
+1. **Maker** creates an offer specifying what they give and what they want
+2. Offer is shared off-chain (file, URL, marketplace)
+3. **Taker** accepts by completing the transaction with their assets
+4. Both sides execute atomically - either both succeed or neither does
+
+## Key Concepts
+
+### Settlement Layer
+
+The settlement layer is a special puzzle (`SETTLEMENT_PAYMENT_HASH`) that locks assets until the offer is completed. Assets sent to this puzzle hash can only be claimed by satisfying the offer's requirements.
+
+```rust
+use chia_wallet_sdk::prelude::*;
+use chia_puzzles::SETTLEMENT_PAYMENT_HASH;
+
+// Assets locked to settlement can only be spent when
+// the corresponding requested payments are satisfied
+let settlement_puzzle_hash: Bytes32 = SETTLEMENT_PAYMENT_HASH.into();
+```
+
+### Offer Structure
+
+An offer contains:
+
+| Component | Description |
+|-----------|-------------|
+| `spend_bundle` | Maker's pre-signed spends |
+| `offered_coins` | Assets being given (locked to settlement) |
+| `requested_payments` | What the maker wants in return |
+| `asset_info` | Metadata about the assets involved |
+
+### Notarized Payments
+
+Requested payments use a nonce (derived from offered coin IDs) to link the maker's and taker's spends:
+
+```rust
+use chia_puzzle_types::offer::{NotarizedPayment, Payment};
+
+// The nonce links this payment request to specific offered coins
+let nonce = Offer::nonce(vec![offered_coin.coin_id()]);
+
+let notarized_payment = NotarizedPayment::new(
+ nonce,
+ vec![Payment::new(recipient_puzzle_hash, amount, memos)],
+);
+```
+
+## XCH for CAT Offer
+
+### Maker: Offering XCH for CAT
+
+```rust
+use chia_wallet_sdk::prelude::*;
+use chia_puzzles::SETTLEMENT_PAYMENT_HASH;
+use chia_puzzle_types::offer::{NotarizedPayment, Payment};
+
+fn create_xch_for_cat_offer(
+ xch_coin: Coin,
+ maker_pk: PublicKey,
+ maker_sk: &SecretKey,
+ cat_asset_id: Bytes32,
+ xch_amount: u64, // XCH to offer
+ cat_amount: u64, // CAT to receive
+ maker_puzzle_hash: Bytes32,
+ agg_sig_data: Bytes32,
+) -> Result {
+ let ctx = &mut SpendContext::new();
+
+ // Step 1: Lock XCH to the settlement puzzle
+ let conditions = Conditions::new()
+ .create_coin(SETTLEMENT_PAYMENT_HASH.into(), xch_amount, Memos::None);
+
+ StandardLayer::new(maker_pk).spend(ctx, xch_coin, conditions)?;
+
+ // Step 2: Define what we want in return (CAT tokens)
+ let nonce = Offer::nonce(vec![xch_coin.coin_id()]);
+ let memos = ctx.hint(maker_puzzle_hash)?;
+
+ let mut requested_payments = RequestedPayments::new();
+ requested_payments.cats.insert(
+ cat_asset_id,
+ vec![NotarizedPayment::new(
+ nonce,
+ vec![Payment::new(maker_puzzle_hash, cat_amount, memos)],
+ )],
+ );
+
+ // Step 3: Sign the maker's spends
+ let coin_spends = ctx.take();
+ let mut allocator = Allocator::new();
+ let required = RequiredSignature::from_coin_spends(
+ &mut allocator,
+ &coin_spends,
+ &AggSigConstants::new(agg_sig_data),
+ )?;
+
+ let mut signature = Signature::default();
+ for req in required {
+ let RequiredSignature::Bls(bls_req) = req else { continue };
+ signature += &sign(maker_sk, bls_req.message());
+ }
+
+ // Step 4: Create the offer
+ let spend_bundle = SpendBundle::new(coin_spends, signature);
+ let offer = Offer::from_input_spend_bundle(
+ &mut ctx,
+ spend_bundle,
+ requested_payments,
+ AssetInfo::new(), // No special asset info needed for simple CAT
+ )?;
+
+ Ok(offer)
+}
+```
+
+### Taker: Accepting with CAT
+
+```rust
+use chia_wallet_sdk::prelude::*;
+
+fn accept_xch_for_cat_offer(
+ offer: Offer,
+ cat: Cat,
+ taker_pk: PublicKey,
+ taker_sk: &SecretKey,
+ taker_puzzle_hash: Bytes32,
+ agg_sig_data: Bytes32,
+) -> Result {
+ let ctx = &mut SpendContext::new();
+
+ // Step 1: Get the offered XCH coins from the offer
+ let offered_xch = &offer.offered_coins().xch;
+
+ // Step 2: Spend the offered XCH to ourselves
+ // (The settlement puzzle allows this once we provide the CAT)
+ for xch_coin in offered_xch {
+ let conditions = Conditions::new()
+ .create_coin(taker_puzzle_hash, xch_coin.amount, ctx.hint(taker_puzzle_hash)?);
+
+ // Spend using SettlementLayer
+ let spend = SettlementLayer.construct_spend(
+ ctx,
+ SettlementPaymentsSolution::new(vec![]),
+ )?;
+ ctx.spend(*xch_coin, spend)?;
+ }
+
+ // Step 3: Send CAT to satisfy the maker's request
+ let requested = offer.requested_payments();
+ let p2 = StandardLayer::new(taker_pk);
+
+ for (asset_id, payments) in &requested.cats {
+ // Create inner spend that outputs to maker's requested destination
+ let mut conditions = Conditions::new();
+ for notarized in payments {
+ for payment in ¬arized.payments {
+ conditions = conditions.create_coin(
+ payment.puzzle_hash,
+ payment.amount,
+ Memos::from(payment.memos.clone()),
+ );
+ }
+ }
+
+ let inner_spend = p2.spend_with_conditions(ctx, conditions)?;
+ let cat_spends = [CatSpend::new(cat, inner_spend)];
+ Cat::spend_all(ctx, &cat_spends)?;
+ }
+
+ // Step 4: Sign taker's spends
+ let coin_spends = ctx.take();
+ let mut allocator = Allocator::new();
+ let required = RequiredSignature::from_coin_spends(
+ &mut allocator,
+ &coin_spends,
+ &AggSigConstants::new(agg_sig_data),
+ )?;
+
+ let mut signature = Signature::default();
+ for req in required {
+ let RequiredSignature::Bls(bls_req) = req else { continue };
+ signature += &sign(taker_sk, bls_req.message());
+ }
+
+ // Step 5: Combine with maker's spend bundle
+ let taker_bundle = SpendBundle::new(coin_spends, signature);
+ let final_bundle = offer.take(taker_bundle);
+
+ Ok(final_bundle)
+}
+```
+
+## NFT Offers
+
+NFTs have built-in methods for offer settlement that handle royalties.
+
+### Maker: Offering NFT
+
+```rust
+use chia_wallet_sdk::prelude::*;
+use chia_puzzle_types::offer::{NotarizedPayment, Payment};
+
+fn create_nft_offer(
+ nft: Nft,
+ maker_pk: PublicKey,
+ maker_sk: &SecretKey,
+ requested_xch: u64,
+ maker_puzzle_hash: Bytes32,
+ agg_sig_data: Bytes32,
+) -> Result {
+ let ctx = &mut SpendContext::new();
+ let p2 = StandardLayer::new(maker_pk);
+
+ // Step 1: Lock NFT to settlement with trade prices (for royalty calculation)
+ let trade_prices = vec![TradePrice {
+ amount: requested_xch,
+ puzzle_hash: SETTLEMENT_PAYMENT_HASH.into(), // XCH
+ }];
+
+ let locked_nft = nft.lock_settlement(
+ ctx,
+ &p2,
+ trade_prices,
+ Conditions::new(),
+ )?;
+
+ // Step 2: Define requested payment (XCH)
+ let nonce = Offer::nonce(vec![nft.coin.coin_id()]);
+ let memos = ctx.hint(maker_puzzle_hash)?;
+
+ let mut requested_payments = RequestedPayments::new();
+ requested_payments.xch.push(NotarizedPayment::new(
+ nonce,
+ vec![Payment::new(maker_puzzle_hash, requested_xch, memos)],
+ ));
+
+ // Step 3: Build asset info for the NFT
+ let mut asset_info = AssetInfo::new();
+ asset_info.insert_nft(
+ nft.info.launcher_id,
+ NftAssetInfo::new(
+ nft.info.metadata,
+ nft.info.metadata_updater_puzzle_hash,
+ nft.info.royalty_puzzle_hash,
+ nft.info.royalty_basis_points,
+ ),
+ )?;
+
+ // Step 4: Sign and create offer
+ let coin_spends = ctx.take();
+ let signature = sign_spends(&coin_spends, maker_sk, agg_sig_data)?;
+
+ let offer = Offer::from_input_spend_bundle(
+ &mut ctx,
+ SpendBundle::new(coin_spends, signature),
+ requested_payments,
+ asset_info,
+ )?;
+
+ Ok(offer)
+}
+```
+
+### Taker: Accepting NFT Offer
+
+```rust
+use chia_wallet_sdk::prelude::*;
+
+fn accept_nft_offer(
+ offer: Offer,
+ xch_coin: Coin,
+ taker_pk: PublicKey,
+ taker_sk: &SecretKey,
+ taker_puzzle_hash: Bytes32,
+ agg_sig_data: Bytes32,
+) -> Result {
+ let ctx = &mut SpendContext::new();
+ let p2 = StandardLayer::new(taker_pk);
+
+ // Step 1: Get the locked NFT from the offer
+ let offered_nfts = &offer.offered_coins().nfts;
+ let (launcher_id, locked_nft) = offered_nfts.iter().next()
+ .ok_or(DriverError::MissingChild)?;
+
+ // Step 2: Calculate royalties
+ let royalty_amounts = offer.requested_royalty_amounts();
+ let royalty_info = offer.requested_royalties();
+
+ // Step 3: Unlock the NFT to ourselves
+ let nonce = Offer::nonce(vec![locked_nft.coin.coin_id()]);
+ let memos = ctx.hint(taker_puzzle_hash)?;
+
+ let notarized_payments = vec![NotarizedPayment::new(
+ nonce,
+ vec![Payment::new(taker_puzzle_hash, 1, memos)],
+ )];
+
+ locked_nft.unlock_settlement(ctx, notarized_payments)?;
+
+ // Step 4: Pay the maker (XCH) plus royalties
+ let requested_xch = offer.requested_payments().amounts().xch;
+ let royalty_xch = royalty_amounts.xch;
+ let total_needed = requested_xch + royalty_xch;
+
+ let mut conditions = Conditions::new();
+
+ // Pay the maker
+ for notarized in &offer.requested_payments().xch {
+ for payment in ¬arized.payments {
+ conditions = conditions.create_coin(
+ payment.puzzle_hash,
+ payment.amount,
+ Memos::from(payment.memos.clone()),
+ );
+ }
+ }
+
+ // Pay royalties
+ for royalty in &royalty_info {
+ let amount = royalty_amounts.xch / royalty_info.len() as u64;
+ if amount > 0 {
+ conditions = conditions.create_coin(
+ royalty.puzzle_hash,
+ amount,
+ Memos::None,
+ );
+ }
+ }
+
+ // Change back to taker
+ let change = xch_coin.amount - total_needed;
+ if change > 0 {
+ conditions = conditions.create_coin(
+ taker_puzzle_hash,
+ change,
+ ctx.hint(taker_puzzle_hash)?,
+ );
+ }
+
+ p2.spend(ctx, xch_coin, conditions)?;
+
+ // Step 5: Combine and sign
+ let coin_spends = ctx.take();
+ let signature = sign_spends(&coin_spends, taker_sk, agg_sig_data)?;
+
+ let final_bundle = offer.take(SpendBundle::new(coin_spends, signature));
+
+ Ok(final_bundle)
+}
+```
+
+## Parsing Existing Offers
+
+To parse an offer received from elsewhere:
+
+```rust
+use chia_wallet_sdk::prelude::*;
+
+fn parse_offer(spend_bundle: &SpendBundle) -> Result {
+ let mut allocator = Allocator::new();
+
+ // Parse the offer from a complete spend bundle
+ let offer = Offer::from_spend_bundle(&mut allocator, spend_bundle)?;
+
+ // Inspect what's being offered
+ let offered = offer.offered_coins();
+ println!("Offered XCH: {} mojos", offered.amounts().xch);
+ for (asset_id, amount) in &offered.amounts().cats {
+ println!("Offered CAT {}: {} mojos", asset_id, amount);
+ }
+ for launcher_id in offered.nfts.keys() {
+ println!("Offered NFT: {}", launcher_id);
+ }
+
+ // Inspect what's requested
+ let requested = offer.requested_payments();
+ println!("Requested XCH: {} mojos", requested.amounts().xch);
+ for (asset_id, amount) in &requested.amounts().cats {
+ println!("Requested CAT {}: {} mojos", asset_id, amount);
+ }
+
+ Ok(offer)
+}
+```
+
+## Royalties
+
+NFT royalties are automatically calculated based on trade prices:
+
+```rust
+// Get royalty info from an offer
+let royalties = offer.requested_royalties(); // For NFTs being offered
+let royalty_amounts = offer.requested_royalty_amounts();
+
+for royalty in &royalties {
+ println!(
+ "NFT {} requires {}% royalty to {}",
+ royalty.launcher_id,
+ royalty.basis_points as f64 / 100.0,
+ royalty.puzzle_hash
+ );
+}
+
+println!("Total royalty XCH: {} mojos", royalty_amounts.xch);
+```
+
+## Offer Compression
+
+For sharing offers efficiently, enable compression:
+
+```toml
+chia-wallet-sdk = { version = "0.32", features = ["offer-compression"] }
+```
+
+```rust
+use chia_wallet_sdk::driver::{compress_offer, decompress_offer};
+
+// Compress for sharing
+let compressed = compress_offer(&offer_bytes)?;
+
+// Decompress when receiving
+let decompressed = decompress_offer(&compressed)?;
+```
+
+## Helper Function
+
+A utility for signing spends used in the examples above:
+
+```rust
+fn sign_spends(
+ coin_spends: &[CoinSpend],
+ secret_key: &SecretKey,
+ agg_sig_data: Bytes32,
+) -> Result {
+ let mut allocator = Allocator::new();
+ let required = RequiredSignature::from_coin_spends(
+ &mut allocator,
+ coin_spends,
+ &AggSigConstants::new(agg_sig_data),
+ )?;
+
+ let mut signature = Signature::default();
+ for req in required {
+ let RequiredSignature::Bls(bls_req) = req else { continue };
+ signature += &sign(secret_key, bls_req.message());
+ }
+
+ Ok(signature)
+}
+```
+
+## Security Considerations
+
+:::warning
+Always verify offer terms before accepting:
+- Confirm the assets being offered match expectations
+- Verify the requested amounts are acceptable
+- Check NFT royalty terms and amounts
+- Ensure you understand the full transaction
+- Validate with the simulator before mainnet
+:::
+
+## API Reference
+
+For offer-related APIs, see:
+
+- [Offer](https://docs.rs/chia-sdk-driver/latest/chia_sdk_driver/struct.Offer.html)
+- [OfferCoins](https://docs.rs/chia-sdk-driver/latest/chia_sdk_driver/struct.OfferCoins.html)
+- [RequestedPayments](https://docs.rs/chia-sdk-driver/latest/chia_sdk_driver/struct.RequestedPayments.html)
+- [SettlementLayer](https://docs.rs/chia-sdk-driver/latest/chia_sdk_driver/struct.SettlementLayer.html)
diff --git a/docs/sdk/patterns.md b/docs/sdk/patterns.md
new file mode 100644
index 0000000..362b846
--- /dev/null
+++ b/docs/sdk/patterns.md
@@ -0,0 +1,295 @@
+---
+slug: /sdk/patterns
+title: Application Patterns
+---
+
+# Application Patterns
+
+This page covers common patterns for structuring applications that use the Wallet SDK.
+
+## SpendContext Lifecycle
+
+### Single Transaction Pattern
+
+For simple operations, create a SpendContext, use it, and discard:
+
+```rust
+fn send_payment(/* params */) -> Result {
+ let ctx = &mut SpendContext::new();
+
+ // Build transaction
+ StandardLayer::new(pk).spend(ctx, coin, conditions)?;
+
+ // Extract, sign, return
+ let spends = ctx.take();
+ sign_and_bundle(spends)
+}
+```
+
+### Reusable Context Pattern
+
+For multiple transactions, reuse the context to benefit from puzzle caching:
+
+```rust
+struct TransactionBuilder {
+ ctx: SpendContext,
+}
+
+impl TransactionBuilder {
+ fn new() -> Self {
+ Self {
+ ctx: SpendContext::new(),
+ }
+ }
+
+ fn build_payment(&mut self, /* params */) -> Result> {
+ // Use self.ctx for building
+ StandardLayer::new(pk).spend(&mut self.ctx, coin, conditions)?;
+
+ // take() empties the context but preserves puzzle cache
+ Ok(self.ctx.take())
+ }
+}
+```
+
+## Batching Transactions
+
+### Multiple Independent Spends
+
+When spending multiple coins, batch them in one transaction and link them with `assert_concurrent_spend`:
+
+```rust
+let ctx = &mut SpendContext::new();
+let coins: Vec<(Coin, PublicKey)> = /* your coins */;
+
+// Collect all coin IDs for concurrent spend assertions
+let coin_ids: Vec = coins.iter().map(|(c, _)| c.coin_id()).collect();
+
+// Spend multiple coins in one transaction
+for (i, (coin, pk)) in coins.iter().enumerate() {
+ let mut conditions = Conditions::new()
+ .create_coin(destination, coin.amount, Memos::None);
+
+ // Link to all other coins in the transaction
+ for (j, other_id) in coin_ids.iter().enumerate() {
+ if i != j {
+ conditions = conditions.assert_concurrent_spend(*other_id);
+ }
+ }
+
+ StandardLayer::new(*pk).spend(ctx, *coin, conditions)?;
+}
+
+let spends = ctx.take();
+```
+
+:::warning
+Always use `assert_concurrent_spend` to link coins in a multi-spend transaction. Without it, an attacker could extract individual spends from your signed bundle.
+:::
+
+### Dependent Spends
+
+When spends depend on each other (e.g., parent-child), ensure proper ordering:
+
+```rust
+let ctx = &mut SpendContext::new();
+
+// First spend creates a coin
+let conditions = Conditions::new()
+ .create_coin(intermediate_ph, 1000, Memos::None);
+StandardLayer::new(pk1).spend(ctx, parent_coin, conditions)?;
+
+// Calculate the created coin
+let child_coin = Coin::new(parent_coin.coin_id(), intermediate_ph, 1000);
+
+// Second spend uses the created coin (ephemeral spend)
+let conditions = Conditions::new()
+ .create_coin(final_destination, 900, Memos::None)
+ .reserve_fee(100);
+StandardLayer::new(pk2).spend(ctx, child_coin, conditions)?;
+
+let spends = ctx.take();
+```
+
+## Coin Management
+
+### Coin Selection
+
+When you have multiple coins, select appropriately:
+
+```rust
+fn select_coins(
+ available: &[Coin],
+ target_amount: u64,
+) -> Vec {
+ let mut selected = Vec::new();
+ let mut total = 0;
+
+ // Simple greedy selection
+ for coin in available {
+ if total >= target_amount {
+ break;
+ }
+ selected.push(*coin);
+ total += coin.amount;
+ }
+
+ selected
+}
+```
+
+### Change Handling
+
+Always account for change when the input exceeds the output:
+
+```rust
+fn build_with_change(
+ ctx: &mut SpendContext,
+ coin: Coin,
+ pk: PublicKey,
+ send_amount: u64,
+ recipient: Bytes32,
+ fee: u64,
+) -> Result<()> {
+ let sender_ph = StandardLayer::puzzle_hash(pk);
+ let change = coin.amount.saturating_sub(send_amount).saturating_sub(fee);
+
+ let mut conditions = Conditions::new()
+ .create_coin(recipient, send_amount, ctx.hint(recipient)?)
+ .reserve_fee(fee);
+
+ if change > 0 {
+ conditions = conditions.create_coin(sender_ph, change, ctx.hint(sender_ph)?);
+ }
+
+ StandardLayer::new(pk).spend(ctx, coin, conditions)?;
+ Ok(())
+}
+```
+
+## Error Handling
+
+### Graceful Error Recovery
+
+```rust
+fn try_build_transaction(/* params */) -> Result> {
+ let ctx = &mut SpendContext::new();
+
+ // Attempt to build
+ match build_complex_spend(ctx, /* params */) {
+ Ok(()) => Ok(ctx.take()),
+ Err(e) => {
+ // Context can be safely dropped
+ // No cleanup needed
+ Err(e)
+ }
+ }
+}
+```
+
+## Signing Patterns
+
+### Collecting Required Signatures
+
+```rust
+fn sign_spends(
+ coin_spends: &[CoinSpend],
+ secret_keys: &[SecretKey],
+ agg_sig_data: &[u8],
+) -> Result {
+ // Calculate what needs to be signed
+ let required = RequiredSignature::from_coin_spends(
+ coin_spends,
+ agg_sig_data,
+ )?;
+
+ // Sign each requirement
+ let mut signatures = Vec::new();
+ for req in required {
+ let sk = find_key_for_pk(&req.public_key, secret_keys)?;
+ signatures.push(sign(&sk, &req.message));
+ }
+
+ // Aggregate signatures
+ Ok(aggregate(&signatures))
+}
+```
+
+### Multi-Party Signing
+
+When multiple parties need to sign:
+
+```rust
+// Party 1 builds and partially signs
+let spends = build_transaction()?;
+let sig1 = sign_my_portion(&spends, &my_keys)?;
+
+// Serialize and send to Party 2
+let partial = PartialTransaction { spends, signatures: vec![sig1] };
+
+// Party 2 adds their signature
+let sig2 = sign_my_portion(&partial.spends, &their_keys)?;
+partial.signatures.push(sig2);
+
+// Combine and broadcast
+let final_sig = aggregate(&partial.signatures);
+let bundle = SpendBundle::new(partial.spends, final_sig);
+```
+
+## State Tracking
+
+### Tracking Coin State
+
+```rust
+struct WalletState {
+ coins: HashMap,
+ pending_spends: HashSet,
+}
+
+impl WalletState {
+ fn mark_spent(&mut self, coin_id: Bytes32) {
+ self.pending_spends.insert(coin_id);
+ }
+
+ fn confirm_spent(&mut self, coin_id: Bytes32) {
+ self.coins.remove(&coin_id);
+ self.pending_spends.remove(&coin_id);
+ }
+
+ fn available_coins(&self) -> impl Iterator- {
+ self.coins.values()
+ .filter(|c| !self.pending_spends.contains(&c.coin_id()))
+ }
+}
+```
+
+### Tracking NFT/CAT State
+
+For singletons and CATs, track the current coin after each spend:
+
+```rust
+struct NftTracker {
+ nft: Nft,
+}
+
+impl NftTracker {
+ fn after_transfer(&mut self, new_nft: Nft) {
+ self.nft = new_nft;
+ }
+
+ fn current_coin(&self) -> &Coin {
+ &self.nft.coin
+ }
+}
+```
+
+## Best Practices
+
+1. **Link multi-coin spends** - Always use `assert_concurrent_spend` when spending multiple coins
+2. **Validate locally first** - Use the simulator before mainnet
+3. **Handle change** - Never lose funds to missing change outputs
+4. **Use hints** - Include memos for wallet discovery
+5. **Batch when possible** - Reduce fees by combining spends
+6. **Track state** - Keep your local view synchronized
+7. **Reuse SpendContext** - Benefit from puzzle caching
+8. **Handle errors gracefully** - SpendContext cleanup is automatic
diff --git a/docs/sdk/primitives/cat.md b/docs/sdk/primitives/cat.md
new file mode 100644
index 0000000..5969074
--- /dev/null
+++ b/docs/sdk/primitives/cat.md
@@ -0,0 +1,646 @@
+---
+slug: /sdk/primitives/cat
+title: CAT
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+# 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
+
+| Type | Description |
+|------|-------------|
+| `Cat` | A CAT coin with its info and optional lineage proof |
+| `CatInfo` | Asset ID and inner puzzle hash |
+| `CatSpend` | Combines a CAT with an inner spend |
+| `LineageProof` | Proof 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):
+
+
+
+
+```rust
+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;
+```
+
+
+
+
+```typescript
+import { Clvm, Coin, CatInfo, Cat, CatSpend, Simulator } from "chia-wallet-sdk";
+
+const clvm = new Clvm();
+const sim = new Simulator();
+const alice = sim.bls(1000n);
+
+// Create a simple TAIL (genesis by coin ID uses nil TAIL for single issuance)
+const tail = clvm.nil();
+const assetId = tail.treeHash();
+
+// Create CAT info with the asset ID and inner puzzle hash
+const catInfo = new CatInfo(assetId, null, alice.puzzleHash);
+
+// Issue the CAT by spending the parent coin
+clvm.spendStandardCoin(
+ alice.coin,
+ alice.pk,
+ clvm.delegatedSpend([clvm.createCoin(catInfo.puzzleHash(), 1000n)])
+);
+
+// Create the eve CAT (first CAT coin)
+const eveCat = new Cat(
+ new Coin(alice.coin.coinId(), catInfo.puzzleHash(), 1000n),
+ null, // No lineage proof for eve
+ catInfo
+);
+```
+
+
+
+
+```python
+from chia_wallet_sdk import Clvm, Coin, CatInfo, Cat, CatSpend, Simulator
+
+clvm = Clvm()
+sim = Simulator()
+alice = sim.bls(1000)
+
+# Create a simple TAIL (genesis by coin ID uses nil TAIL for single issuance)
+tail = clvm.nil()
+asset_id = tail.tree_hash()
+
+# Create CAT info with the asset ID and inner puzzle hash
+cat_info = CatInfo(asset_id, None, alice.puzzle_hash)
+
+# Issue the CAT by spending the parent coin
+clvm.spend_standard_coin(
+ alice.coin,
+ alice.pk,
+ clvm.delegated_spend([clvm.create_coin(cat_info.puzzle_hash(), 1000)])
+)
+
+# Create the eve CAT (first CAT coin)
+eve_cat = Cat(
+ Coin(alice.coin.coin_id(), cat_info.puzzle_hash(), 1000),
+ None, # No lineage proof for eve
+ cat_info
+)
+```
+
+
+
+
+:::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:
+
+
+
+
+```rust
+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();
+```
+
+
+
+
+```typescript
+import { Clvm, CatSpend } from "chia-wallet-sdk";
+
+const clvm = new Clvm();
+
+// Create the inner spend with conditions
+const innerSpend = clvm.standardSpend(
+ publicKey,
+ clvm.delegatedSpend([
+ clvm.createCoin(recipientPuzzleHash, 1000n, clvm.alloc([recipientPuzzleHash])),
+ ])
+);
+
+// Wrap it in a CatSpend and execute
+clvm.spendCats([new CatSpend(cat, innerSpend)]);
+
+const coinSpends = clvm.coinSpends();
+```
+
+
+
+
+```python
+from chia_wallet_sdk import Clvm, CatSpend
+
+clvm = Clvm()
+
+# Create the inner spend with conditions
+inner_spend = clvm.standard_spend(
+ public_key,
+ clvm.delegated_spend([
+ clvm.create_coin(recipient_puzzle_hash, 1000, clvm.alloc([recipient_puzzle_hash])),
+ ])
+)
+
+# Wrap it in a CatSpend and execute
+clvm.spend_cats([CatSpend(cat, inner_spend)])
+
+coin_spends = clvm.coin_spends()
+```
+
+
+
+
+### 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:
+
+
+
+
+```rust
+// 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;
+```
+
+
+
+
+```typescript
+// After spending, compute the new CAT
+const childCat = cat.child(recipientPuzzleHash, 1000n);
+
+// Access the underlying coin
+const childCoin = childCat.coin;
+```
+
+
+
+
+```python
+# After spending, compute the new CAT
+child_cat = cat.child(recipient_puzzle_hash, 1000)
+
+# Access the underlying coin
+child_coin = child_cat.coin
+```
+
+
+
+
+## Multi-Input CAT Spends
+
+When spending multiple CATs of the same asset type together:
+
+
+
+
+```rust
+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)?;
+```
+
+
+
+
+```typescript
+// Build spends for multiple CAT coins
+clvm.spendCats([
+ new CatSpend(cat1, clvm.standardSpend(publicKey, clvm.delegatedSpend([]))),
+ new CatSpend(
+ cat2,
+ clvm.standardSpend(
+ publicKey,
+ clvm.delegatedSpend([
+ clvm.createCoin(recipient, combinedAmount, clvm.alloc([recipient])),
+ ])
+ )
+ ),
+]);
+```
+
+
+
+
+```python
+# Build spends for multiple CAT coins
+clvm.spend_cats([
+ CatSpend(cat1, clvm.standard_spend(public_key, clvm.delegated_spend([]))),
+ CatSpend(
+ cat2,
+ clvm.standard_spend(
+ public_key,
+ clvm.delegated_spend([
+ clvm.create_coin(recipient, combined_amount, clvm.alloc([recipient])),
+ ])
+ )
+ ),
+])
+```
+
+
+
+
+## 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:
+
+
+
+
+```rust
+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();
+```
+
+
+
+
+```typescript
+const clvm = new Clvm();
+
+// Spend the CAT (linked to XCH spend)
+clvm.spendCats([
+ new CatSpend(
+ cat,
+ clvm.standardSpend(
+ publicKey,
+ clvm.delegatedSpend([
+ clvm.createCoin(recipientPuzzleHash, cat.coin.amount, clvm.alloc([recipientPuzzleHash])),
+ clvm.assertConcurrentSpend(xchCoin.coinId()), // Link to XCH spend
+ ])
+ )
+ ),
+]);
+
+// Spend XCH to pay the fee
+const fee = 100_000_000n; // 0.0001 XCH
+const change = xchCoin.amount - fee;
+
+const xchConditions = [
+ clvm.reserveFee(fee),
+ clvm.assertConcurrentSpend(cat.coin.coinId()), // Link to CAT spend
+];
+
+if (change > 0n) {
+ xchConditions.push(
+ clvm.createCoin(myPuzzleHash, change, clvm.alloc([myPuzzleHash]))
+ );
+}
+
+clvm.spendStandardCoin(xchCoin, publicKey, clvm.delegatedSpend(xchConditions));
+
+const coinSpends = clvm.coinSpends();
+```
+
+
+
+
+```python
+clvm = Clvm()
+
+# Spend the CAT (linked to XCH spend)
+clvm.spend_cats([
+ CatSpend(
+ cat,
+ clvm.standard_spend(
+ public_key,
+ clvm.delegated_spend([
+ clvm.create_coin(recipient_puzzle_hash, cat.coin.amount, clvm.alloc([recipient_puzzle_hash])),
+ clvm.assert_concurrent_spend(xch_coin.coin_id()), # Link to XCH spend
+ ])
+ )
+ ),
+])
+
+# Spend XCH to pay the fee
+fee = 100_000_000 # 0.0001 XCH
+change = xch_coin.amount - fee
+
+xch_conditions = [
+ clvm.reserve_fee(fee),
+ clvm.assert_concurrent_spend(cat.coin.coin_id()), # Link to CAT spend
+]
+
+if change > 0:
+ xch_conditions.append(
+ clvm.create_coin(my_puzzle_hash, change, clvm.alloc([my_puzzle_hash]))
+ )
+
+clvm.spend_standard_coin(xch_coin, public_key, clvm.delegated_spend(xch_conditions))
+
+coin_spends = clvm.coin_spends()
+```
+
+
+
+
+:::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:
+
+```rust
+// 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:
+
+
+
+
+```rust
+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), 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()))
+}
+```
+
+
+
+
+```typescript
+import { Clvm, Coin, CatInfo, Cat, CatSpend, Simulator } from "chia-wallet-sdk";
+
+function issueAndSpendCat(recipientPuzzleHash: Uint8Array, amount: bigint) {
+ const clvm = new Clvm();
+ const sim = new Simulator();
+ const alice = sim.bls(amount);
+
+ // Step 1: Create TAIL and issue CAT
+ const tail = clvm.nil();
+ const assetId = tail.treeHash();
+ const catInfo = new CatInfo(assetId, null, alice.puzzleHash);
+
+ // Issue the CAT
+ clvm.spendStandardCoin(
+ alice.coin,
+ alice.pk,
+ clvm.delegatedSpend([clvm.createCoin(catInfo.puzzleHash(), amount)])
+ );
+
+ // Create eve CAT
+ const eveCat = new Cat(
+ new Coin(alice.coin.coinId(), catInfo.puzzleHash(), amount),
+ null,
+ catInfo
+ );
+
+ // Step 2: Spend the CAT with TAIL reveal, then transfer
+ clvm.spendCats([
+ new CatSpend(
+ eveCat,
+ clvm.standardSpend(
+ alice.pk,
+ clvm.delegatedSpend([
+ clvm.createCoin(recipientPuzzleHash, amount, clvm.alloc([recipientPuzzleHash])),
+ clvm.runCatTail(tail, clvm.nil()),
+ ])
+ )
+ ),
+ ]);
+
+ // Sign and validate
+ sim.spendCoins(clvm.coinSpends(), [alice.sk]);
+
+ return assetId;
+}
+```
+
+
+
+
+```python
+from chia_wallet_sdk import Clvm, Coin, CatInfo, Cat, CatSpend, Simulator
+
+def issue_and_spend_cat(recipient_puzzle_hash: bytes, amount: int):
+ clvm = Clvm()
+ sim = Simulator()
+ alice = sim.bls(amount)
+
+ # Step 1: Create TAIL and issue CAT
+ tail = clvm.nil()
+ asset_id = tail.tree_hash()
+ cat_info = CatInfo(asset_id, None, alice.puzzle_hash)
+
+ # Issue the CAT
+ clvm.spend_standard_coin(
+ alice.coin,
+ alice.pk,
+ clvm.delegated_spend([clvm.create_coin(cat_info.puzzle_hash(), amount)])
+ )
+
+ # Create eve CAT
+ eve_cat = Cat(
+ Coin(alice.coin.coin_id(), cat_info.puzzle_hash(), amount),
+ None,
+ cat_info
+ )
+
+ # Step 2: Spend the CAT with TAIL reveal, then transfer
+ clvm.spend_cats([
+ CatSpend(
+ eve_cat,
+ clvm.standard_spend(
+ alice.pk,
+ clvm.delegated_spend([
+ clvm.create_coin(recipient_puzzle_hash, amount, clvm.alloc([recipient_puzzle_hash])),
+ clvm.run_cat_tail(tail, clvm.nil()),
+ ])
+ )
+ ),
+ ])
+
+ # Sign and validate
+ sim.spend_coins(clvm.coin_spends(), [alice.sk])
+
+ return asset_id
+```
+
+
+
+
+## API Reference
+
+For the complete CAT API, see [docs.rs](https://docs.rs/chia-sdk-driver/latest/chia_sdk_driver/struct.Cat.html).
diff --git a/docs/sdk/primitives/nft.md b/docs/sdk/primitives/nft.md
new file mode 100644
index 0000000..e8164a5
--- /dev/null
+++ b/docs/sdk/primitives/nft.md
@@ -0,0 +1,524 @@
+---
+slug: /sdk/primitives/nft
+title: NFT
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+# NFT
+
+NFTs (Non-Fungible Tokens) in Chia are singleton-based assets that can hold metadata, have ownership controls, and support royalties. Each NFT has a unique launcher ID that serves as its permanent identifier.
+
+## Overview
+
+Chia NFTs are built on the singleton pattern, meaning:
+
+- Each NFT has a unique identity that persists across spends
+- Only one instance of an NFT can exist at any time
+- The NFT's launcher ID remains constant even as the coin ID changes
+
+NFTs consist of multiple layers:
+
+| Layer | Purpose |
+|-------|---------|
+| Singleton | Ensures uniqueness and provides the launcher ID |
+| NFT State | Holds metadata URIs and hash |
+| NFT Ownership | Controls transfer and royalty logic |
+| Inner (p2) | Defines who can spend the NFT |
+
+## Key Types
+
+| Type | Description |
+|------|-------------|
+| `Nft` | An NFT with its metadata and inner puzzle info |
+| `NftInfo` | Launcher ID, metadata, owner info, royalties |
+| `NftMetadata` | URIs for data, metadata, and license |
+| `Proof` | Singleton lineage proof for validation |
+
+## Minting NFTs
+
+To mint a new NFT, use the `Nft::mint` function:
+
+
+
+
+```rust
+use chia_wallet_sdk::prelude::*;
+
+let ctx = &mut SpendContext::new();
+let p2 = StandardLayer::new(public_key);
+
+// Define the NFT metadata
+let metadata = NftMetadata {
+ data_uris: vec!["https://example.com/image.png".to_string()],
+ data_hash: Some(image_hash),
+ metadata_uris: vec!["https://example.com/metadata.json".to_string()],
+ metadata_hash: Some(metadata_hash),
+ license_uris: vec![],
+ license_hash: None,
+ edition_number: Some(1),
+ edition_total: Some(100),
+};
+
+// Mint the NFT
+let (mint_conditions, nft) = Nft::mint(
+ ctx,
+ parent_coin_id, // Coin being spent to mint
+ owner_puzzle_hash, // Initial owner
+ Some(royalty_puzzle_hash), // Royalty recipient (or None)
+ royalty_percentage, // Royalty in basis points (e.g., 300 = 3%)
+ metadata,
+ owner_puzzle_hash, // Hint for discovery
+)?;
+
+// Spend the parent coin with mint conditions
+p2.spend(ctx, parent_coin, mint_conditions)?;
+
+// The NFT's permanent identifier
+let launcher_id = nft.info.launcher_id;
+```
+
+
+
+
+```typescript
+import { Clvm, NftMetadata, NftMint, Constants, Simulator } from "chia-wallet-sdk";
+
+const clvm = new Clvm();
+const sim = new Simulator();
+const alice = sim.bls(1n);
+
+// Define the NFT metadata
+const metadata = new NftMetadata(
+ 1n, // edition number
+ 1n, // edition total
+ ["https://example.com/image.png"], // data URIs
+ null, // data hash (optional)
+ ["https://example.com/metadata.json"], // metadata URIs
+ null, // metadata hash (optional)
+ [], // license URIs
+ null // license hash (optional)
+);
+
+// Mint the NFT
+const { nfts, parentConditions } = clvm.mintNfts(alice.coin.coinId(), [
+ new NftMint(
+ clvm.nftMetadata(metadata),
+ Constants.nftMetadataUpdaterDefaultHash(),
+ alice.puzzleHash, // Royalty puzzle hash
+ alice.puzzleHash, // Owner p2 puzzle hash
+ 300 // Royalty in basis points (3%)
+ ),
+]);
+
+// Spend the parent coin with mint conditions
+clvm.spendStandardCoin(
+ alice.coin,
+ alice.pk,
+ clvm.delegatedSpend(parentConditions)
+);
+
+// The NFT's permanent identifier
+const launcherId = nfts[0].info.launcherId;
+```
+
+
+
+
+```python
+from chia_wallet_sdk import Clvm, NftMetadata, NftMint, Constants, Simulator
+
+clvm = Clvm()
+sim = Simulator()
+alice = sim.bls(1)
+
+# Define the NFT metadata
+metadata = NftMetadata(
+ 1, # edition number
+ 1, # edition total
+ ["https://example.com/image.png"], # data URIs
+ None, # data hash (optional)
+ ["https://example.com/metadata.json"], # metadata URIs
+ None, # metadata hash (optional)
+ [], # license URIs
+ None # license hash (optional)
+)
+
+# Mint the NFT
+result = clvm.mint_nfts(alice.coin.coin_id(), [
+ NftMint(
+ clvm.nft_metadata(metadata),
+ Constants.nft_metadata_updater_default_hash(),
+ alice.puzzle_hash, # Royalty puzzle hash
+ alice.puzzle_hash, # Owner p2 puzzle hash
+ 300 # Royalty in basis points (3%)
+ ),
+])
+
+# Spend the parent coin with mint conditions
+clvm.spend_standard_coin(
+ alice.coin,
+ alice.pk,
+ clvm.delegated_spend(result.parent_conditions)
+)
+
+# The NFT's permanent identifier
+launcher_id = result.nfts[0].info.launcher_id
+```
+
+
+
+
+## Transferring NFTs
+
+To transfer an NFT to a new owner:
+
+
+
+
+```rust
+use chia_wallet_sdk::prelude::*;
+
+let ctx = &mut SpendContext::new();
+
+// Transfer to a new owner
+let new_nft = nft.transfer(
+ ctx,
+ &p2, // Current owner's p2 layer
+ new_owner_puzzle_hash, // New owner
+ Conditions::new(), // Additional conditions (fees, etc.)
+)?;
+
+let coin_spends = ctx.take();
+```
+
+
+
+
+```typescript
+// Transfer the NFT to a new owner
+const innerSpend = clvm.standardSpend(
+ alice.pk,
+ clvm.delegatedSpend([
+ clvm.createCoin(newOwnerPuzzleHash, 1n, clvm.alloc([newOwnerPuzzleHash])),
+ ])
+);
+
+const newNft = clvm.spendNft(nft, innerSpend);
+
+const coinSpends = clvm.coinSpends();
+```
+
+
+
+
+```python
+# Transfer the NFT to a new owner
+inner_spend = clvm.standard_spend(
+ alice.pk,
+ clvm.delegated_spend([
+ clvm.create_coin(new_owner_puzzle_hash, 1, clvm.alloc([new_owner_puzzle_hash])),
+ ])
+)
+
+new_nft = clvm.spend_nft(nft, inner_spend)
+
+coin_spends = clvm.coin_spends()
+```
+
+
+
+
+For more control over the transfer, use `transfer_with_metadata`:
+
+```rust
+let new_nft = nft.transfer_with_metadata(
+ ctx,
+ &p2,
+ new_owner_puzzle_hash,
+ updated_metadata, // Can update metadata during transfer
+ Conditions::new(),
+)?;
+```
+
+## Spending NFTs with Custom Conditions
+
+For complex operations beyond simple transfers:
+
+
+
+
+```rust
+let ctx = &mut SpendContext::new();
+
+// Build conditions for the NFT spend
+let conditions = Conditions::new()
+ .create_coin_announcement(b"nft_action")
+ .reserve_fee(fee_amount);
+
+// Spend with custom conditions
+let new_nft = nft.spend(
+ ctx,
+ p2.spend_with_conditions(ctx, conditions)?,
+)?;
+```
+
+
+
+
+```typescript
+// Build conditions for the NFT spend
+const innerSpend = clvm.standardSpend(
+ alice.pk,
+ clvm.delegatedSpend([
+ clvm.createCoinAnnouncement(Buffer.from("nft_action")),
+ clvm.reserveFee(feeAmount),
+ clvm.createCoin(newOwnerPuzzleHash, 1n, clvm.alloc([newOwnerPuzzleHash])),
+ ])
+);
+
+// Spend with custom conditions
+const newNft = clvm.spendNft(nft, innerSpend);
+```
+
+
+
+
+```python
+# Build conditions for the NFT spend
+inner_spend = clvm.standard_spend(
+ alice.pk,
+ clvm.delegated_spend([
+ clvm.create_coin_announcement(b"nft_action"),
+ clvm.reserve_fee(fee_amount),
+ clvm.create_coin(new_owner_puzzle_hash, 1, clvm.alloc([new_owner_puzzle_hash])),
+ ])
+)
+
+# Spend with custom conditions
+new_nft = clvm.spend_nft(nft, inner_spend)
+```
+
+
+
+
+## Royalties
+
+NFTs support royalties that are enforced during trades. The royalty is specified as a percentage in basis points (1/100th of a percent):
+
+| Basis Points | Percentage |
+|--------------|------------|
+| 100 | 1% |
+| 250 | 2.5% |
+| 500 | 5% |
+| 1000 | 10% |
+
+```rust
+// 5% royalty
+let royalty_basis_points = 500;
+
+let (mint_conditions, nft) = Nft::mint(
+ ctx,
+ parent_coin_id,
+ owner_puzzle_hash,
+ Some(royalty_puzzle_hash), // Where royalties are paid
+ royalty_basis_points,
+ metadata,
+ owner_puzzle_hash,
+)?;
+```
+
+:::info
+Royalties are enforced through the offer system. Direct transfers don't automatically pay royalties - they're applied when NFTs are traded via offers.
+:::
+
+## Parsing NFTs
+
+When you need to reconstruct an NFT from blockchain data:
+
+```rust
+// Parse an NFT from a parent coin spend
+let nft = Nft::parse_child(
+ ctx,
+ parent_coin_spend,
+ child_coin,
+)?;
+
+// Or parse directly from puzzle/solution
+let parsed = Nft::::parse(
+ &ctx.allocator(),
+ puzzle_ptr,
+ solution_ptr,
+)?;
+```
+
+## NFT Ownership Layer
+
+The ownership layer tracks:
+
+- Current owner (puzzle hash)
+- Transfer program (for trading/offers)
+- Royalty information
+
+When building custom NFT interactions, be aware that the ownership layer requires proper handling for the NFT to remain valid.
+
+## Complete Example
+
+Here's a full example of minting and transferring an NFT:
+
+
+
+
+```rust
+use chia_wallet_sdk::prelude::*;
+
+fn mint_and_transfer_nft(
+ minter_coin: Coin,
+ minter_public_key: PublicKey,
+ recipient_puzzle_hash: Bytes32,
+ metadata: NftMetadata,
+) -> Result<(Bytes32, Vec), DriverError> {
+ let ctx = &mut SpendContext::new();
+ let p2 = StandardLayer::new(minter_public_key);
+ let minter_puzzle_hash = StandardLayer::puzzle_hash(minter_public_key);
+
+ // Step 1: Mint the NFT
+ let (mint_conditions, nft) = Nft::mint(
+ ctx,
+ minter_coin.coin_id(),
+ minter_puzzle_hash,
+ None, // No royalties for this example
+ 0,
+ metadata,
+ minter_puzzle_hash,
+ )?;
+
+ p2.spend(ctx, minter_coin, mint_conditions)?;
+ let launcher_id = nft.info.launcher_id;
+
+ // Step 2: Transfer to recipient
+ let _new_nft = nft.transfer(
+ ctx,
+ &p2,
+ recipient_puzzle_hash,
+ Conditions::new(),
+ )?;
+
+ Ok((launcher_id, ctx.take()))
+}
+```
+
+
+
+
+```typescript
+import { Clvm, NftMetadata, NftMint, Constants, Simulator } from "chia-wallet-sdk";
+
+function mintAndTransferNft(recipientPuzzleHash: Uint8Array) {
+ const clvm = new Clvm();
+ const sim = new Simulator();
+ const alice = sim.bls(2n);
+
+ // Step 1: Mint the NFT
+ const metadata = new NftMetadata(
+ 1n, 1n,
+ ["https://example.com/image.png"],
+ null, [], null, [], null
+ );
+
+ const { nfts, parentConditions } = clvm.mintNfts(alice.coin.coinId(), [
+ new NftMint(
+ clvm.nftMetadata(metadata),
+ Constants.nftMetadataUpdaterDefaultHash(),
+ alice.puzzleHash,
+ alice.puzzleHash,
+ 0 // No royalties
+ ),
+ ]);
+
+ clvm.spendStandardCoin(
+ alice.coin,
+ alice.pk,
+ clvm.delegatedSpend(parentConditions)
+ );
+
+ const launcherId = nfts[0].info.launcherId;
+
+ // Step 2: Transfer to recipient
+ clvm.spendNft(
+ nfts[0],
+ clvm.standardSpend(
+ alice.pk,
+ clvm.delegatedSpend([
+ clvm.createCoin(recipientPuzzleHash, 1n, clvm.alloc([recipientPuzzleHash])),
+ ])
+ )
+ );
+
+ // Sign and validate
+ sim.spendCoins(clvm.coinSpends(), [alice.sk]);
+
+ return launcherId;
+}
+```
+
+
+
+
+```python
+from chia_wallet_sdk import Clvm, NftMetadata, NftMint, Constants, Simulator
+
+def mint_and_transfer_nft(recipient_puzzle_hash: bytes):
+ clvm = Clvm()
+ sim = Simulator()
+ alice = sim.bls(2)
+
+ # Step 1: Mint the NFT
+ metadata = NftMetadata(
+ 1, 1,
+ ["https://example.com/image.png"],
+ None, [], None, [], None
+ )
+
+ result = clvm.mint_nfts(alice.coin.coin_id(), [
+ NftMint(
+ clvm.nft_metadata(metadata),
+ Constants.nft_metadata_updater_default_hash(),
+ alice.puzzle_hash,
+ alice.puzzle_hash,
+ 0 # No royalties
+ ),
+ ])
+
+ clvm.spend_standard_coin(
+ alice.coin,
+ alice.pk,
+ clvm.delegated_spend(result.parent_conditions)
+ )
+
+ launcher_id = result.nfts[0].info.launcher_id
+
+ # Step 2: Transfer to recipient
+ clvm.spend_nft(
+ result.nfts[0],
+ clvm.standard_spend(
+ alice.pk,
+ clvm.delegated_spend([
+ clvm.create_coin(recipient_puzzle_hash, 1, clvm.alloc([recipient_puzzle_hash])),
+ ])
+ )
+ )
+
+ # Sign and validate
+ sim.spend_coins(clvm.coin_spends(), [alice.sk])
+
+ return launcher_id
+```
+
+
+
+
+## API Reference
+
+For the complete NFT API, see [docs.rs](https://docs.rs/chia-sdk-driver/latest/chia_sdk_driver/struct.Nft.html).
diff --git a/docs/sdk/primitives/other.md b/docs/sdk/primitives/other.md
new file mode 100644
index 0000000..8b06e81
--- /dev/null
+++ b/docs/sdk/primitives/other.md
@@ -0,0 +1,245 @@
+---
+slug: /sdk/primitives/other
+title: Other Primitives
+---
+
+# Other Primitives
+
+The SDK includes several additional primitives beyond the core CAT and NFT types. This page provides brief overviews of these primitives.
+
+## Vault
+
+Vaults provide multi-signature custody for Chia assets. They enable secure storage patterns where multiple parties or conditions must be satisfied to spend funds.
+
+```rust
+use chia_wallet_sdk::prelude::*;
+
+// Vault structure
+let vault = Vault {
+ coin,
+ launcher_id,
+ proof,
+ custody_hash, // Defines spending requirements
+};
+
+// Spend a vault with a MIPS (Multi-Input Puzzle Spend)
+vault.spend(ctx, &mips_spend)?;
+
+// Compute the child vault after a spend
+let child_vault = vault.child(new_custody_hash, new_amount);
+```
+
+For vault internals and specification, see [CHIP-0043 (MIPS)](https://github.com/Chia-Network/chips/blob/main/CHIPs/chip-0043.md).
+
+**Use cases:**
+- Multi-signature wallets requiring M-of-N approval
+- Time-locked custody arrangements
+- Institutional custody solutions
+- Escrow patterns
+
+For the complete API, see [Vault in docs.rs](https://docs.rs/chia-sdk-driver/latest/chia_sdk_driver/struct.Vault.html).
+
+---
+
+## DID (Decentralized Identifiers)
+
+DIDs provide on-chain identity management following the singleton pattern. A DID maintains a persistent identity that can be associated with metadata and used for authentication.
+
+```rust
+use chia_wallet_sdk::prelude::*;
+
+// DID structure
+let did = Did {
+ coin,
+ info: DidInfo {
+ launcher_id,
+ recovery_list_hash,
+ num_verifications_required,
+ metadata,
+ inner_puzzle_hash,
+ },
+ proof,
+};
+```
+
+**Use cases:**
+- Creator verification for NFTs
+- On-chain identity for applications
+- Recovery mechanisms with trusted parties
+
+For the complete API, see [Did in docs.rs](https://docs.rs/chia-sdk-driver/latest/chia_sdk_driver/struct.Did.html).
+
+---
+
+## Option Contracts
+
+Options enable on-chain derivatives trading where one party has the right (but not obligation) to buy or sell an asset at a predetermined price.
+
+```rust
+use chia_wallet_sdk::prelude::*;
+
+// Option contract structure
+let option = OptionContract {
+ coin,
+ info: OptionInfo {
+ launcher_id,
+ underlying,
+ option_type, // Call or Put
+ strike_price,
+ expiry,
+ },
+ proof,
+};
+```
+
+**Use cases:**
+- Hedging price risk
+- Speculation on asset prices
+- Structured financial products
+
+For the complete API, see [OptionContract in docs.rs](https://docs.rs/chia-sdk-driver/latest/chia_sdk_driver/struct.OptionContract.html).
+
+---
+
+## ClawbackV2
+
+ClawbackV2 enables recoverable payments where the sender can reclaim funds within a specified time window if needed.
+
+```rust
+use chia_wallet_sdk::prelude::*;
+
+// Clawback allows sender to recover funds before timeout
+let clawback = ClawbackV2 {
+ coin,
+ sender_puzzle_hash,
+ recipient_puzzle_hash,
+ clawback_timeout, // Blocks until recipient can claim
+};
+```
+
+**Use cases:**
+- Reversible payments for dispute resolution
+- Escrow with sender recovery option
+- Safe transfers to new/unverified addresses
+
+For the complete API, see [ClawbackV2 in docs.rs](https://docs.rs/chia-sdk-driver/latest/chia_sdk_driver/struct.ClawbackV2.html).
+
+---
+
+## StreamedAsset
+
+StreamedAsset implements time-locked or vesting payments where funds are released gradually over time.
+
+```rust
+use chia_wallet_sdk::prelude::*;
+
+// Streamed payments release funds over time
+let streamed = StreamedAsset {
+ coin,
+ start_timestamp,
+ end_timestamp,
+ recipient_puzzle_hash,
+ // Funds unlock linearly between start and end
+};
+```
+
+**Use cases:**
+- Employee vesting schedules
+- Subscription payments
+- Gradual fund release for projects
+
+For the complete API, see [StreamedAsset in docs.rs](https://docs.rs/chia-sdk-driver/latest/chia_sdk_driver/struct.StreamedAsset.html).
+
+---
+
+## Bulletin
+
+Bulletin provides on-chain data storage using the singleton pattern. It allows storing arbitrary data that can be updated over time.
+
+```rust
+use chia_wallet_sdk::prelude::*;
+
+// Bulletin for on-chain data storage
+// Uses BulletinLayer for puzzle construction
+```
+
+**Use cases:**
+- On-chain configuration storage
+- Decentralized content publishing
+- Data availability proofs
+
+For the complete API, see [BulletinLayer in docs.rs](https://docs.rs/chia-sdk-driver/latest/chia_sdk_driver/struct.BulletinLayer.html).
+
+---
+
+## Singleton
+
+The Singleton primitive is the foundation for NFTs, DIDs, Vaults, and other unique assets. It ensures only one instance of an asset exists at any time.
+
+```rust
+use chia_wallet_sdk::prelude::*;
+
+// Generic singleton structure
+let singleton = Singleton {
+ coin,
+ info: SingletonInfo {
+ launcher_id, // Permanent unique identifier
+ inner_puzzle_hash,
+ },
+ proof,
+};
+```
+
+**Key properties:**
+- Unique identity via launcher_id
+- Lineage proofs ensure authenticity
+- State persists across spends
+
+For the complete API, see [Singleton in docs.rs](https://docs.rs/chia-sdk-driver/latest/chia_sdk_driver/struct.Singleton.html).
+
+---
+
+## Launcher
+
+Launchers are used to create new singletons (NFTs, DIDs, etc.). The launcher coin's ID becomes the permanent identifier for the singleton.
+
+```rust
+use chia_wallet_sdk::prelude::*;
+
+// Create a launcher for minting singletons
+let launcher = Launcher::new(parent_coin_id, amount);
+let launcher_id = launcher.coin().coin_id();
+
+// The launcher_id becomes the permanent identifier
+// for the resulting NFT, DID, or other singleton
+```
+
+For the complete API, see [Launcher in docs.rs](https://docs.rs/chia-sdk-driver/latest/chia_sdk_driver/struct.Launcher.html).
+
+---
+
+## Feature-Gated Primitives
+
+Some primitives require feature flags to enable:
+
+### DataStore (chip-0035)
+
+Data layer support for off-chain data with on-chain proofs:
+
+```toml
+chia-wallet-sdk = { version = "0.32", features = ["chip-0035"] }
+```
+
+### Action Layer (action-layer)
+
+High-level transaction builder for complex multi-asset operations:
+
+```toml
+chia-wallet-sdk = { version = "0.32", features = ["action-layer"] }
+```
+
+---
+
+## API Reference
+
+For complete documentation on all primitives, see the [chia-sdk-driver rustdocs](https://docs.rs/chia-sdk-driver).
diff --git a/docs/sdk/primitives/standard.md b/docs/sdk/primitives/standard.md
new file mode 100644
index 0000000..4f68ba3
--- /dev/null
+++ b/docs/sdk/primitives/standard.md
@@ -0,0 +1,727 @@
+---
+slug: /sdk/primitives/standard
+title: Standard (XCH)
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+# Standard (XCH)
+
+The `StandardLayer` is the foundation for XCH ownership in Chia. It implements the "standard transaction" puzzle (`p2_delegated_puzzle_or_hidden_puzzle`), which allows coins to be spent by providing a signature from the owner's public key.
+
+## Overview
+
+Standard coins are the basic unit of XCH ownership. When you hold XCH in a wallet, your coins use the standard puzzle locked to your public key. The puzzle allows spending by:
+
+1. Providing a valid BLS signature from the corresponding secret key
+2. Outputting conditions that define what happens (create coins, fees, etc.)
+
+## Creating a StandardLayer
+
+
+
+
+```rust
+use chia_wallet_sdk::prelude::*;
+
+// Create a layer from a public key
+let p2 = StandardLayer::new(public_key);
+
+// The puzzle hash can be computed from the public key
+let puzzle_hash = StandardLayer::puzzle_hash(public_key);
+```
+
+
+
+
+```typescript
+import { standardPuzzleHash, PublicKey } from "chia-wallet-sdk";
+
+// The puzzle hash can be computed from a synthetic public key
+const puzzleHash = standardPuzzleHash(publicKey);
+
+// In Node.js/Python, spends are created directly via the Clvm class
+// rather than through a separate layer object
+```
+
+
+
+
+```python
+from chia_wallet_sdk import standard_puzzle_hash, PublicKey
+
+# The puzzle hash can be computed from a synthetic public key
+puzzle_hash = standard_puzzle_hash(public_key)
+
+# In Node.js/Python, spends are created directly via the Clvm class
+# rather than through a separate layer object
+```
+
+
+
+
+## Spending Standard Coins
+
+### Basic Spend
+
+The simplest spend creates a new coin and optionally pays a fee:
+
+
+
+
+```rust
+use chia_wallet_sdk::prelude::*;
+
+let ctx = &mut SpendContext::new();
+
+// Build the conditions
+let conditions = Conditions::new()
+ .create_coin(recipient_puzzle_hash, 900, Memos::None)
+ .reserve_fee(100);
+
+// Create the spend
+let p2 = StandardLayer::new(public_key);
+p2.spend(ctx, coin, conditions)?;
+
+let coin_spends = ctx.take();
+```
+
+
+
+
+```typescript
+import { Clvm, Coin } from "chia-wallet-sdk";
+
+const clvm = new Clvm();
+
+// Build the conditions
+const conditions = [
+ clvm.createCoin(recipientPuzzleHash, 900n, null),
+ clvm.reserveFee(100n),
+];
+
+// Create the spend
+clvm.spendStandardCoin(coin, publicKey, clvm.delegatedSpend(conditions));
+
+const coinSpends = clvm.coinSpends();
+```
+
+
+
+
+```python
+from chia_wallet_sdk import Clvm, Coin
+
+clvm = Clvm()
+
+# Build the conditions
+conditions = [
+ clvm.create_coin(recipient_puzzle_hash, 900, None),
+ clvm.reserve_fee(100),
+]
+
+# Create the spend
+clvm.spend_standard_coin(coin, public_key, clvm.delegated_spend(conditions))
+
+coin_spends = clvm.coin_spends()
+```
+
+
+
+
+### Sending with Hints
+
+Hints help wallets discover coins. Add the recipient's puzzle hash as a memo:
+
+
+
+
+```rust
+let ctx = &mut SpendContext::new();
+
+let memos = ctx.hint(recipient_puzzle_hash)?;
+let conditions = Conditions::new()
+ .create_coin(recipient_puzzle_hash, amount, memos);
+
+StandardLayer::new(public_key).spend(ctx, coin, conditions)?;
+```
+
+
+
+
+```typescript
+const clvm = new Clvm();
+
+// Include puzzle hash as memo for coin discovery
+const conditions = [
+ clvm.createCoin(recipientPuzzleHash, amount, clvm.alloc([recipientPuzzleHash])),
+];
+
+clvm.spendStandardCoin(coin, publicKey, clvm.delegatedSpend(conditions));
+```
+
+
+
+
+```python
+clvm = Clvm()
+
+# Include puzzle hash as memo for coin discovery
+conditions = [
+ clvm.create_coin(recipient_puzzle_hash, amount, clvm.alloc([recipient_puzzle_hash])),
+]
+
+clvm.spend_standard_coin(coin, public_key, clvm.delegated_spend(conditions))
+```
+
+
+
+
+### Multiple Outputs
+
+A single spend can create multiple output coins:
+
+
+
+
+```rust
+let conditions = Conditions::new()
+ .create_coin(recipient_a, 500, ctx.hint(recipient_a)?)
+ .create_coin(recipient_b, 400, ctx.hint(recipient_b)?)
+ .reserve_fee(100);
+
+StandardLayer::new(public_key).spend(ctx, coin, conditions)?;
+```
+
+
+
+
+```typescript
+const conditions = [
+ clvm.createCoin(recipientA, 500n, clvm.alloc([recipientA])),
+ clvm.createCoin(recipientB, 400n, clvm.alloc([recipientB])),
+ clvm.reserveFee(100n),
+];
+
+clvm.spendStandardCoin(coin, publicKey, clvm.delegatedSpend(conditions));
+```
+
+
+
+
+```python
+conditions = [
+ clvm.create_coin(recipient_a, 500, clvm.alloc([recipient_a])),
+ clvm.create_coin(recipient_b, 400, clvm.alloc([recipient_b])),
+ clvm.reserve_fee(100),
+]
+
+clvm.spend_standard_coin(coin, public_key, clvm.delegated_spend(conditions))
+```
+
+
+
+
+### Spending Multiple Coins
+
+When spending multiple coins in one transaction, you must link them using `assert_concurrent_spend` to ensure atomicity. This prevents malicious actors from separating your spends and executing them individually.
+
+
+
+
+```rust
+let ctx = &mut SpendContext::new();
+
+// First coin - sends to recipient, asserts second coin is spent together
+let conditions1 = Conditions::new()
+ .create_coin(recipient, 1000, ctx.hint(recipient)?)
+ .assert_concurrent_spend(coin2.coin_id());
+StandardLayer::new(pk1).spend(ctx, coin1, conditions1)?;
+
+// Second coin - pays fee, asserts first coin is spent together
+let conditions2 = Conditions::new()
+ .reserve_fee(100)
+ .assert_concurrent_spend(coin1.coin_id());
+StandardLayer::new(pk2).spend(ctx, coin2, conditions2)?;
+
+let coin_spends = ctx.take();
+```
+
+
+
+
+```typescript
+const clvm = new Clvm();
+
+// First coin - sends to recipient, asserts second coin is spent together
+const conditions1 = [
+ clvm.createCoin(recipient, 1000n, clvm.alloc([recipient])),
+ clvm.assertConcurrentSpend(coin2.coinId()),
+];
+clvm.spendStandardCoin(coin1, pk1, clvm.delegatedSpend(conditions1));
+
+// Second coin - pays fee, asserts first coin is spent together
+const conditions2 = [
+ clvm.reserveFee(100n),
+ clvm.assertConcurrentSpend(coin1.coinId()),
+];
+clvm.spendStandardCoin(coin2, pk2, clvm.delegatedSpend(conditions2));
+
+const coinSpends = clvm.coinSpends();
+```
+
+
+
+
+```python
+clvm = Clvm()
+
+# First coin - sends to recipient, asserts second coin is spent together
+conditions1 = [
+ clvm.create_coin(recipient, 1000, clvm.alloc([recipient])),
+ clvm.assert_concurrent_spend(coin2.coin_id()),
+]
+clvm.spend_standard_coin(coin1, pk1, clvm.delegated_spend(conditions1))
+
+# Second coin - pays fee, asserts first coin is spent together
+conditions2 = [
+ clvm.reserve_fee(100),
+ clvm.assert_concurrent_spend(coin1.coin_id()),
+]
+clvm.spend_standard_coin(coin2, pk2, clvm.delegated_spend(conditions2))
+
+coin_spends = clvm.coin_spends()
+```
+
+
+
+
+:::warning
+Always use `assert_concurrent_spend` when spending multiple coins together. Without it, an attacker could take your signed spend bundle and submit only some of the spends, potentially stealing funds.
+:::
+
+## Building Conditions
+
+The `Conditions` builder provides methods for common operations:
+
+
+
+
+```rust
+Conditions::new()
+ // Create output coins
+ .create_coin(puzzle_hash, amount, memos)
+
+ // Transaction fee (goes to farmers)
+ .reserve_fee(fee_amount)
+
+ // Link multiple spends together (IMPORTANT for security)
+ .assert_concurrent_spend(other_coin_id)
+
+ // Coin announcements for coordinating multi-spend transactions
+ .create_coin_announcement(message)
+ .assert_coin_announcement(announcement_id)
+
+ // Puzzle announcements
+ .create_puzzle_announcement(message)
+ .assert_puzzle_announcement(announcement_id)
+
+ // Time conditions
+ .assert_seconds_absolute(timestamp)
+ .assert_height_absolute(block_height)
+```
+
+
+
+
+```typescript
+// Conditions are built as an array of Program objects
+const conditions = [
+ // Create output coins
+ clvm.createCoin(puzzleHash, amount, memos),
+
+ // Transaction fee (goes to farmers)
+ clvm.reserveFee(feeAmount),
+
+ // Link multiple spends together (IMPORTANT for security)
+ clvm.assertConcurrentSpend(otherCoinId),
+
+ // Coin announcements for coordinating multi-spend transactions
+ clvm.createCoinAnnouncement(message),
+ clvm.assertCoinAnnouncement(announcementId),
+
+ // Puzzle announcements
+ clvm.createPuzzleAnnouncement(message),
+ clvm.assertPuzzleAnnouncement(announcementId),
+
+ // Time conditions
+ clvm.assertSecondsAbsolute(timestamp),
+ clvm.assertHeightAbsolute(blockHeight),
+];
+```
+
+
+
+
+```python
+# Conditions are built as a list of Program objects
+conditions = [
+ # Create output coins
+ clvm.create_coin(puzzle_hash, amount, memos),
+
+ # Transaction fee (goes to farmers)
+ clvm.reserve_fee(fee_amount),
+
+ # Link multiple spends together (IMPORTANT for security)
+ clvm.assert_concurrent_spend(other_coin_id),
+
+ # Coin announcements for coordinating multi-spend transactions
+ clvm.create_coin_announcement(message),
+ clvm.assert_coin_announcement(announcement_id),
+
+ # Puzzle announcements
+ clvm.create_puzzle_announcement(message),
+ clvm.assert_puzzle_announcement(announcement_id),
+
+ # Time conditions
+ clvm.assert_seconds_absolute(timestamp),
+ clvm.assert_height_absolute(block_height),
+]
+```
+
+
+
+
+For the complete list of conditions, see the [Conditions API in docs.rs](https://docs.rs/chia-sdk-types/latest/chia_sdk_types/struct.Conditions.html).
+
+## Using SpendWithConditions
+
+For simpler cases, you can use `spend_with_conditions` to get a `Spend` object without immediately adding it to the context:
+
+```rust
+let p2 = StandardLayer::new(public_key);
+
+// Get the inner spend (useful for wrapping in CAT, etc.)
+let inner_spend = p2.spend_with_conditions(
+ ctx,
+ Conditions::new().create_coin(recipient, amount, memos),
+)?;
+```
+
+This is particularly useful when the standard spend needs to be wrapped by another layer (like CAT or NFT).
+
+## Calculating the New Coin
+
+After spending, you often need to reference the newly created coin:
+
+
+
+
+```rust
+// The parent coin
+let parent_coin = coin;
+
+// Conditions create a new coin
+let conditions = Conditions::new()
+ .create_coin(recipient_puzzle_hash, 900, Memos::None);
+
+StandardLayer::new(public_key).spend(ctx, parent_coin, conditions)?;
+
+// Calculate the new coin's ID
+let new_coin = Coin::new(parent_coin.coin_id(), recipient_puzzle_hash, 900);
+println!("New coin ID: {}", new_coin.coin_id());
+```
+
+
+
+
+```typescript
+import { Coin } from "chia-wallet-sdk";
+
+// The parent coin
+const parentCoin = coin;
+
+// Conditions create a new coin
+const conditions = [clvm.createCoin(recipientPuzzleHash, 900n, null)];
+
+clvm.spendStandardCoin(parentCoin, publicKey, clvm.delegatedSpend(conditions));
+
+// Calculate the new coin's ID
+const newCoin = new Coin(parentCoin.coinId(), recipientPuzzleHash, 900n);
+console.log("New coin ID:", newCoin.coinId());
+```
+
+
+
+
+```python
+from chia_wallet_sdk import Coin
+
+# The parent coin
+parent_coin = coin
+
+# Conditions create a new coin
+conditions = [clvm.create_coin(recipient_puzzle_hash, 900, None)]
+
+clvm.spend_standard_coin(parent_coin, public_key, clvm.delegated_spend(conditions))
+
+# Calculate the new coin's ID
+new_coin = Coin(parent_coin.coin_id(), recipient_puzzle_hash, 900)
+print("New coin ID:", new_coin.coin_id())
+```
+
+
+
+
+## Signing Transactions
+
+After building spends, you need to sign them before broadcasting. The SDK calculates what signatures are required and you provide them.
+
+### Basic Signing
+
+```rust
+use chia_wallet_sdk::prelude::*;
+
+let ctx = &mut SpendContext::new();
+
+// Build your spends
+StandardLayer::new(public_key).spend(ctx, coin, conditions)?;
+let coin_spends = ctx.take();
+
+// Calculate required signatures
+let mut allocator = Allocator::new();
+let required = RequiredSignature::from_coin_spends(
+ &mut allocator,
+ &coin_spends,
+ &AggSigConstants::new(agg_sig_me_additional_data),
+)?;
+
+// Sign each requirement and aggregate
+let mut aggregated_signature = Signature::default();
+for req in required {
+ let RequiredSignature::Bls(bls_req) = req else { continue };
+ aggregated_signature += &sign(&secret_key, bls_req.message());
+}
+
+// Create the spend bundle
+let spend_bundle = SpendBundle::new(coin_spends, aggregated_signature);
+```
+
+### Signing with Multiple Keys
+
+When multiple coins are spent with different keys:
+
+```rust
+use std::collections::HashMap;
+
+// Map public keys to secret keys for lookup
+let key_pairs: HashMap = secret_keys
+ .iter()
+ .map(|sk| (sk.public_key(), sk))
+ .collect();
+
+// Sign each required signature with the correct key
+let mut aggregated_signature = Signature::default();
+for req in required {
+ let RequiredSignature::Bls(bls_req) = req else { continue };
+ let sk = key_pairs.get(&bls_req.public_key)
+ .expect("missing key for signature");
+ aggregated_signature += &sign(sk, bls_req.message());
+}
+```
+
+### Network Constants
+
+The `AggSigConstants` vary by network. Use the appropriate constants:
+
+```rust
+use chia_sdk_types::{MAINNET_CONSTANTS, TESTNET11_CONSTANTS};
+
+// For mainnet
+let constants = AggSigConstants::new(MAINNET_CONSTANTS.agg_sig_me_additional_data);
+
+// For testnet11
+let constants = AggSigConstants::new(TESTNET11_CONSTANTS.agg_sig_me_additional_data);
+```
+
+:::info
+The signature message includes data specific to the coin being spent (coin ID, puzzle hash, etc.) combined with network-specific constants. This prevents signatures from being replayed across networks.
+:::
+
+## Complete Example
+
+Here's a full example of building, signing, and creating a spend bundle for an XCH transfer:
+
+
+
+
+```rust
+use std::collections::HashMap;
+use chia_wallet_sdk::prelude::*;
+
+fn send_xch(
+ source_coin: Coin,
+ source_secret_key: &SecretKey,
+ recipient_puzzle_hash: Bytes32,
+ amount: u64,
+ fee: u64,
+ agg_sig_data: Bytes32, // Network-specific (e.g., MAINNET_CONSTANTS.agg_sig_me_additional_data)
+) -> Result {
+ let ctx = &mut SpendContext::new();
+ let source_public_key = source_secret_key.public_key();
+
+ // Calculate change (if any)
+ let change = source_coin.amount - amount - fee;
+ let source_puzzle_hash = StandardLayer::puzzle_hash(source_public_key);
+
+ // Build conditions
+ let mut conditions = Conditions::new()
+ .create_coin(recipient_puzzle_hash, amount, ctx.hint(recipient_puzzle_hash)?)
+ .reserve_fee(fee);
+
+ // Add change output if needed
+ if change > 0 {
+ conditions = conditions.create_coin(
+ source_puzzle_hash,
+ change,
+ ctx.hint(source_puzzle_hash)?
+ );
+ }
+
+ // Create the spend
+ StandardLayer::new(source_public_key).spend(ctx, source_coin, conditions)?;
+ let coin_spends = ctx.take();
+
+ // Calculate required signatures
+ let mut allocator = Allocator::new();
+ let required = RequiredSignature::from_coin_spends(
+ &mut allocator,
+ &coin_spends,
+ &AggSigConstants::new(agg_sig_data),
+ )?;
+
+ // Sign and aggregate
+ let mut aggregated_signature = Signature::default();
+ for req in required {
+ let RequiredSignature::Bls(bls_req) = req else { continue };
+ aggregated_signature += &sign(source_secret_key, bls_req.message());
+ }
+
+ // Return complete spend bundle
+ Ok(SpendBundle::new(coin_spends, aggregated_signature))
+}
+```
+
+
+
+
+```typescript
+import { Clvm, Coin, Simulator, standardPuzzleHash } from "chia-wallet-sdk";
+
+function sendXch(
+ sourceCoin: Coin,
+ sourcePublicKey: PublicKey,
+ recipientPuzzleHash: Uint8Array,
+ amount: bigint,
+ fee: bigint
+) {
+ const clvm = new Clvm();
+ const sourcePuzzleHash = standardPuzzleHash(sourcePublicKey);
+
+ // Calculate change (if any)
+ const change = sourceCoin.amount - amount - fee;
+
+ // Build conditions
+ const conditions = [
+ clvm.createCoin(recipientPuzzleHash, amount, clvm.alloc([recipientPuzzleHash])),
+ clvm.reserveFee(fee),
+ ];
+
+ // Add change output if needed
+ if (change > 0n) {
+ conditions.push(
+ clvm.createCoin(sourcePuzzleHash, change, clvm.alloc([sourcePuzzleHash]))
+ );
+ }
+
+ // Create the spend
+ clvm.spendStandardCoin(sourceCoin, sourcePublicKey, clvm.delegatedSpend(conditions));
+
+ return clvm.coinSpends();
+}
+
+// Example usage with Simulator (handles signing automatically)
+const sim = new Simulator();
+const alice = sim.bls(1000n); // Creates key pair with 1000 mojo coin
+
+const coinSpends = sendXch(
+ alice.coin,
+ alice.pk,
+ recipientPuzzleHash,
+ 900n,
+ 100n
+);
+
+// Simulator signs and validates the transaction
+sim.spendCoins(coinSpends, [alice.sk]);
+```
+
+
+
+
+```python
+from chia_wallet_sdk import Clvm, Coin, Simulator, standard_puzzle_hash
+
+def send_xch(
+ source_coin: Coin,
+ source_public_key: PublicKey,
+ recipient_puzzle_hash: bytes,
+ amount: int,
+ fee: int
+):
+ clvm = Clvm()
+ source_puzzle_hash = standard_puzzle_hash(source_public_key)
+
+ # Calculate change (if any)
+ change = source_coin.amount - amount - fee
+
+ # Build conditions
+ conditions = [
+ clvm.create_coin(recipient_puzzle_hash, amount, clvm.alloc([recipient_puzzle_hash])),
+ clvm.reserve_fee(fee),
+ ]
+
+ # Add change output if needed
+ if change > 0:
+ conditions.append(
+ clvm.create_coin(source_puzzle_hash, change, clvm.alloc([source_puzzle_hash]))
+ )
+
+ # Create the spend
+ clvm.spend_standard_coin(source_coin, source_public_key, clvm.delegated_spend(conditions))
+
+ return clvm.coin_spends()
+
+# Example usage with Simulator (handles signing automatically)
+sim = Simulator()
+alice = sim.bls(1000) # Creates key pair with 1000 mojo coin
+
+coin_spends = send_xch(
+ alice.coin,
+ alice.pk,
+ recipient_puzzle_hash,
+ 900,
+ 100
+)
+
+# Simulator signs and validates the transaction
+sim.spend_coins(coin_spends, [alice.sk])
+```
+
+
+
+
+## API Reference
+
+For the complete `StandardLayer` API, see [docs.rs](https://docs.rs/chia-sdk-driver/latest/chia_sdk_driver/struct.StandardLayer.html).
diff --git a/docs/sdk/spend-context.md b/docs/sdk/spend-context.md
new file mode 100644
index 0000000..7b490ad
--- /dev/null
+++ b/docs/sdk/spend-context.md
@@ -0,0 +1,373 @@
+---
+slug: /sdk/spend-context
+title: SpendContext
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+# SpendContext
+
+`SpendContext` is the central abstraction for building transactions in the Wallet SDK. It manages CLVM memory allocation, caches puzzle hashes for efficiency, and collects coin spends that will form your transaction.
+
+:::info Language Bindings
+In Node.js and Python, the equivalent functionality is provided by the `Clvm` class. While the API differs slightly, the core concepts remain the same: allocate CLVM values, build spends, and collect coin spends.
+:::
+
+## What SpendContext Does
+
+SpendContext serves three primary purposes:
+
+1. **CLVM Memory Management** - Wraps the CLVM `Allocator` to handle memory for puzzle and solution construction
+2. **Puzzle Caching** - Caches compiled puzzles by their tree hash to avoid redundant serialization
+3. **Spend Collection** - Accumulates `CoinSpend` objects that will be combined into a spend bundle
+
+## Creating a SpendContext
+
+
+
+
+```rust
+use chia_wallet_sdk::prelude::*;
+
+let ctx = &mut SpendContext::new();
+```
+
+The `&mut` reference pattern is used throughout the SDK because most operations need to allocate CLVM nodes or add spends to the context.
+
+
+
+
+```typescript
+import { Clvm } from "chia-wallet-sdk";
+
+const clvm = new Clvm();
+```
+
+The `Clvm` class combines the functionality of `SpendContext` (memory management and spend collection) with direct methods for creating conditions and spending coins.
+
+
+
+
+```python
+from chia_wallet_sdk import Clvm
+
+clvm = Clvm()
+```
+
+The `Clvm` class combines the functionality of `SpendContext` (memory management and spend collection) with direct methods for creating conditions and spending coins.
+
+
+
+
+## Core Operations
+
+### Adding Spends
+
+The primary way to add spends is through the primitive APIs, which handle puzzle construction internally:
+
+
+
+
+```rust
+// StandardLayer handles puzzle construction and calls ctx.spend() internally
+StandardLayer::new(public_key).spend(ctx, coin, conditions)?;
+```
+
+For lower-level control, you can add spends directly:
+
+```rust
+// Add a pre-constructed spend
+ctx.spend(coin, Spend::new(puzzle_ptr, solution_ptr))?;
+
+// Or insert a fully serialized CoinSpend
+ctx.insert(coin_spend);
+```
+
+
+
+
+```typescript
+// spendStandardCoin handles puzzle construction and collects spends internally
+clvm.spendStandardCoin(coin, publicKey, clvm.delegatedSpend(conditions));
+
+// For CATs and NFTs, use the dedicated methods
+clvm.spendCats([catSpend]);
+clvm.spendNft(nft, innerSpend);
+```
+
+
+
+
+```python
+# spend_standard_coin handles puzzle construction and collects spends internally
+clvm.spend_standard_coin(coin, public_key, clvm.delegated_spend(conditions))
+
+# For CATs and NFTs, use the dedicated methods
+clvm.spend_cats([cat_spend])
+clvm.spend_nft(nft, inner_spend)
+```
+
+
+
+
+### Extracting Spends
+
+When you're ready to sign and broadcast, extract the collected spends:
+
+
+
+
+```rust
+let coin_spends: Vec = ctx.take();
+```
+
+
+
+
+```typescript
+const coinSpends = clvm.coinSpends();
+```
+
+
+
+
+```python
+coin_spends = clvm.coin_spends()
+```
+
+
+
+
+The `take()` / `coinSpends()` / `coin_spends()` method removes all spends from the context, allowing you to reuse it for building another transaction.
+
+### Allocating CLVM Values
+
+When working with custom puzzles or conditions, you may need to allocate values directly:
+
+
+
+
+```rust
+// Allocate a value to CLVM
+let node_ptr = ctx.alloc(&my_value)?;
+
+// Extract a value from CLVM
+let value: MyType = ctx.extract(node_ptr)?;
+
+// Compute tree hash of a node
+let hash = ctx.tree_hash(node_ptr);
+```
+
+
+
+
+```typescript
+// Allocate values to CLVM (returns a Program)
+const program = clvm.alloc([puzzleHash, amount]);
+
+// Get tree hash of a program
+const hash = program.treeHash();
+
+// Allocate nil (empty list)
+const nil = clvm.nil();
+```
+
+
+
+
+```python
+# Allocate values to CLVM (returns a Program)
+program = clvm.alloc([puzzle_hash, amount])
+
+# Get tree hash of a program
+hash = program.tree_hash()
+
+# Allocate nil (empty list)
+nil = clvm.nil()
+```
+
+
+
+
+### Serialization
+
+Convert CLVM values to `Program` for use in coin spends:
+
+```rust
+// Standard serialization
+let program = ctx.serialize(&my_value)?;
+
+// With back-references (smaller output for repeated structures)
+let program = ctx.serialize_with_backrefs(&my_value)?;
+```
+
+### Memos and Hints
+
+The SDK provides helpers for creating memos (used for coin hints/discovery):
+
+```rust
+// Create a hint memo from a puzzle hash
+let memos = ctx.hint(puzzle_hash)?;
+
+// Create custom memos from any serializable value
+let memos = ctx.memos(&[puzzle_hash, other_data])?;
+```
+
+### Currying Puzzles
+
+For constructing curried puzzles from known modules:
+
+```rust
+// Curry arguments into a module
+let curried_ptr = ctx.curry(MyModArgs { arg1, arg2 })?;
+```
+
+## Lifecycle Pattern
+
+The typical lifecycle of a SpendContext:
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ 1. Create SpendContext │
+│ let ctx = &mut SpendContext::new(); │
+└─────────────────────────────────────────────────────────────┘
+ │
+ ▼
+┌─────────────────────────────────────────────────────────────┐
+│ 2. Build Spends │
+│ - Use primitive APIs (StandardLayer, Cat, Nft, etc.) │
+│ - Each call adds CoinSpends to the context │
+│ - Puzzles are cached automatically │
+└─────────────────────────────────────────────────────────────┘
+ │
+ ▼
+┌─────────────────────────────────────────────────────────────┐
+│ 3. Extract Spends │
+│ let coin_spends = ctx.take(); │
+└─────────────────────────────────────────────────────────────┘
+ │
+ ▼
+┌─────────────────────────────────────────────────────────────┐
+│ 4. Sign │
+│ - Calculate required signatures │
+│ - Sign with appropriate keys │
+│ - Create SpendBundle │
+└─────────────────────────────────────────────────────────────┘
+ │
+ ▼
+┌─────────────────────────────────────────────────────────────┐
+│ 5. Broadcast │
+│ - Submit SpendBundle to network │
+│ - Or validate with Simulator for testing │
+└─────────────────────────────────────────────────────────────┘
+```
+
+## Mental Model
+
+Think of SpendContext as a **transaction builder** that:
+
+- Acts as a scratchpad for constructing CLVM data structures
+- Remembers puzzles you've used so they don't need to be rebuilt
+- Collects all the individual coin spends that will form your transaction
+
+The mutable reference pattern (`&mut ctx`) reflects that building a transaction is an inherently stateful operation - each spend you add modifies the context.
+
+## Complete Example
+
+
+
+
+```rust
+use chia_wallet_sdk::prelude::*;
+
+fn build_transaction(
+ coin: Coin,
+ public_key: PublicKey,
+ recipient: Bytes32,
+ amount: u64,
+ fee: u64,
+) -> Result, DriverError> {
+ // 1. Create context
+ let ctx = &mut SpendContext::new();
+
+ // 2. Build conditions
+ let memos = ctx.hint(recipient)?;
+ let conditions = Conditions::new()
+ .create_coin(recipient, amount, memos)
+ .reserve_fee(fee);
+
+ // 3. Spend the coin
+ StandardLayer::new(public_key).spend(ctx, coin, conditions)?;
+
+ // 4. Extract and return spends
+ Ok(ctx.take())
+}
+```
+
+
+
+
+```typescript
+import { Clvm, Coin, PublicKey, CoinSpend } from "chia-wallet-sdk";
+
+function buildTransaction(
+ coin: Coin,
+ publicKey: PublicKey,
+ recipient: Uint8Array,
+ amount: bigint,
+ fee: bigint
+): CoinSpend[] {
+ // 1. Create context
+ const clvm = new Clvm();
+
+ // 2. Build conditions with hints
+ const conditions = [
+ clvm.createCoin(recipient, amount, clvm.alloc([recipient])),
+ clvm.reserveFee(fee),
+ ];
+
+ // 3. Spend the coin
+ clvm.spendStandardCoin(coin, publicKey, clvm.delegatedSpend(conditions));
+
+ // 4. Extract and return spends
+ return clvm.coinSpends();
+}
+```
+
+
+
+
+```python
+from chia_wallet_sdk import Clvm, Coin, PublicKey, CoinSpend
+from typing import List
+
+def build_transaction(
+ coin: Coin,
+ public_key: PublicKey,
+ recipient: bytes,
+ amount: int,
+ fee: int
+) -> List[CoinSpend]:
+ # 1. Create context
+ clvm = Clvm()
+
+ # 2. Build conditions with hints
+ conditions = [
+ clvm.create_coin(recipient, amount, clvm.alloc([recipient])),
+ clvm.reserve_fee(fee),
+ ]
+
+ # 3. Spend the coin
+ clvm.spend_standard_coin(coin, public_key, clvm.delegated_spend(conditions))
+
+ # 4. Extract and return spends
+ return clvm.coin_spends()
+```
+
+
+
+
+## API Reference
+
+For the complete API, see [SpendContext in docs.rs](https://docs.rs/chia-sdk-driver/latest/chia_sdk_driver/struct.SpendContext.html).
diff --git a/docs/sdk/testing.md b/docs/sdk/testing.md
new file mode 100644
index 0000000..5057a31
--- /dev/null
+++ b/docs/sdk/testing.md
@@ -0,0 +1,651 @@
+---
+slug: /sdk/testing
+title: Testing
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+# Testing
+
+The SDK includes a built-in simulator for testing transactions without connecting to a real network. This enables fast, deterministic testing of your Chia applications.
+
+## Overview
+
+The `chia-sdk-test` crate provides:
+
+- **Simulator** - An in-memory blockchain for validation
+- **Test utilities** - Helpers for creating keys and coins
+- **Spend validation** - Verify transactions before broadcast
+
+## Setting Up the Simulator
+
+
+
+
+```rust
+use chia_wallet_sdk::prelude::*;
+
+// Create a new simulator instance
+let mut sim = Simulator::new();
+```
+
+
+
+
+```typescript
+import { Simulator } from "chia-wallet-sdk";
+
+// Create a new simulator instance
+const sim = new Simulator();
+```
+
+
+
+
+```python
+from chia_wallet_sdk import Simulator
+
+# Create a new simulator instance
+sim = Simulator()
+```
+
+
+
+
+The simulator starts with an empty state. You'll need to create coins before you can spend them.
+
+## Creating Test Keys and Coins
+
+The simulator provides helpers for creating BLS key pairs with funded coins:
+
+
+
+
+```rust
+// Create a key pair with a coin worth 1000 mojos
+let alice = sim.bls(1_000);
+
+// alice contains:
+// - alice.pk: PublicKey
+// - alice.sk: SecretKey
+// - alice.puzzle_hash: Bytes32
+// - alice.coin: Coin
+
+// Create multiple test identities
+let alice = sim.bls(1_000_000);
+let bob = sim.bls(500_000);
+let charlie = sim.bls(0); // No initial funds
+```
+
+
+
+
+```typescript
+// Create a key pair with a coin worth 1000 mojos
+const alice = sim.bls(1000n);
+
+// alice contains:
+// - alice.pk: PublicKey
+// - alice.sk: SecretKey
+// - alice.puzzleHash: Uint8Array
+// - alice.coin: Coin
+
+// Create multiple test identities
+const alice = sim.bls(1_000_000n);
+const bob = sim.bls(500_000n);
+const charlie = sim.bls(0n); // No initial funds
+```
+
+
+
+
+```python
+# Create a key pair with a coin worth 1000 mojos
+alice = sim.bls(1000)
+
+# alice contains:
+# - alice.pk: PublicKey
+# - alice.sk: SecretKey
+# - alice.puzzle_hash: bytes
+# - alice.coin: Coin
+
+# Create multiple test identities
+alice = sim.bls(1_000_000)
+bob = sim.bls(500_000)
+charlie = sim.bls(0) # No initial funds
+```
+
+
+
+
+## Validating Transactions
+
+After building a transaction, validate it with the simulator:
+
+
+
+
+```rust
+let ctx = &mut SpendContext::new();
+
+// Build your transaction
+let conditions = Conditions::new()
+ .create_coin(bob.puzzle_hash, 900, Memos::None)
+ .reserve_fee(100);
+
+StandardLayer::new(alice.pk).spend(ctx, alice.coin, conditions)?;
+
+// Extract spends and validate
+let coin_spends = ctx.take();
+sim.spend_coins(coin_spends, &[alice.sk])?;
+```
+
+
+
+
+```typescript
+const clvm = new Clvm();
+
+// Build your transaction
+const conditions = [
+ clvm.createCoin(bob.puzzleHash, 900n, null),
+ clvm.reserveFee(100n),
+];
+
+clvm.spendStandardCoin(alice.coin, alice.pk, clvm.delegatedSpend(conditions));
+
+// Extract spends and validate
+const coinSpends = clvm.coinSpends();
+sim.spendCoins(coinSpends, [alice.sk]);
+```
+
+
+
+
+```python
+clvm = Clvm()
+
+# Build your transaction
+conditions = [
+ clvm.create_coin(bob.puzzle_hash, 900, None),
+ clvm.reserve_fee(100),
+]
+
+clvm.spend_standard_coin(alice.coin, alice.pk, clvm.delegated_spend(conditions))
+
+# Extract spends and validate
+coin_spends = clvm.coin_spends()
+sim.spend_coins(coin_spends, [alice.sk])
+```
+
+
+
+
+If the transaction is invalid, `spend_coins` returns an error describing the failure.
+
+## Simulator State
+
+The simulator maintains blockchain state:
+
+```rust
+// Check if a coin exists and is unspent
+let coin_state = sim.coin_state(coin_id);
+
+// The simulator tracks:
+// - Created coins
+// - Spent coins
+// - Current block height
+```
+
+## Testing Patterns
+
+### Basic Transaction Test
+
+
+
+
+```rust
+#[test]
+fn test_simple_transfer() -> Result<()> {
+ let mut sim = Simulator::new();
+ let ctx = &mut SpendContext::new();
+
+ let alice = sim.bls(1_000);
+ let bob_puzzle_hash = sim.bls(0).puzzle_hash;
+
+ let conditions = Conditions::new()
+ .create_coin(bob_puzzle_hash, 900, Memos::None)
+ .reserve_fee(100);
+
+ StandardLayer::new(alice.pk).spend(ctx, alice.coin, conditions)?;
+ sim.spend_coins(ctx.take(), &[alice.sk])?;
+
+ Ok(())
+}
+```
+
+
+
+
+```typescript
+import test from "ava";
+import { Clvm, Simulator } from "chia-wallet-sdk";
+
+test("simple transfer", (t) => {
+ const sim = new Simulator();
+ const clvm = new Clvm();
+
+ const alice = sim.bls(1000n);
+ const bobPuzzleHash = sim.bls(0n).puzzleHash;
+
+ const conditions = [
+ clvm.createCoin(bobPuzzleHash, 900n, null),
+ clvm.reserveFee(100n),
+ ];
+
+ clvm.spendStandardCoin(alice.coin, alice.pk, clvm.delegatedSpend(conditions));
+ sim.spendCoins(clvm.coinSpends(), [alice.sk]);
+
+ t.pass();
+});
+```
+
+
+
+
+```python
+import pytest
+from chia_wallet_sdk import Clvm, Simulator
+
+def test_simple_transfer():
+ sim = Simulator()
+ clvm = Clvm()
+
+ alice = sim.bls(1000)
+ bob_puzzle_hash = sim.bls(0).puzzle_hash
+
+ conditions = [
+ clvm.create_coin(bob_puzzle_hash, 900, None),
+ clvm.reserve_fee(100),
+ ]
+
+ clvm.spend_standard_coin(alice.coin, alice.pk, clvm.delegated_spend(conditions))
+ sim.spend_coins(clvm.coin_spends(), [alice.sk])
+```
+
+
+
+
+### Testing CAT Operations
+
+
+
+
+```rust
+#[test]
+fn test_cat_issuance() -> Result<()> {
+ let mut sim = Simulator::new();
+ let ctx = &mut SpendContext::new();
+
+ let alice = sim.bls(1_000);
+ let p2 = StandardLayer::new(alice.pk);
+ let memos = ctx.hint(alice.puzzle_hash)?;
+
+ // Issue CAT
+ let conditions = Conditions::new()
+ .create_coin(alice.puzzle_hash, 1_000, memos);
+
+ let (issue_cat, cats) = Cat::issue_with_coin(
+ ctx,
+ alice.coin.coin_id(),
+ 1_000,
+ conditions,
+ )?;
+
+ p2.spend(ctx, alice.coin, issue_cat)?;
+ sim.spend_coins(ctx.take(), &[alice.sk])?;
+
+ // Verify CAT was created
+ assert_eq!(cats.len(), 1);
+ assert_eq!(cats[0].coin.amount, 1_000);
+
+ Ok(())
+}
+```
+
+
+
+
+```typescript
+import test from "ava";
+import { Cat, CatInfo, CatSpend, Clvm, Coin, Simulator } from "chia-wallet-sdk";
+
+test("issues and spends a cat", (t) => {
+ const sim = new Simulator();
+ const clvm = new Clvm();
+
+ const alice = sim.bls(1n);
+
+ const tail = clvm.nil();
+ const assetId = tail.treeHash();
+ const catInfo = new CatInfo(assetId, null, alice.puzzleHash);
+
+ // Issue a CAT
+ clvm.spendStandardCoin(
+ alice.coin,
+ alice.pk,
+ clvm.delegatedSpend([clvm.createCoin(catInfo.puzzleHash(), 1n)])
+ );
+
+ const eve = new Cat(
+ new Coin(alice.coin.coinId(), catInfo.puzzleHash(), 1n),
+ null,
+ catInfo
+ );
+
+ clvm.spendCats([
+ new CatSpend(
+ eve,
+ clvm.standardSpend(
+ alice.pk,
+ clvm.delegatedSpend([
+ clvm.createCoin(alice.puzzleHash, 1n, clvm.alloc([alice.puzzleHash])),
+ clvm.runCatTail(tail, clvm.nil()),
+ ])
+ )
+ ),
+ ]);
+
+ sim.spendCoins(clvm.coinSpends(), [alice.sk]);
+
+ t.pass();
+});
+```
+
+
+
+
+```python
+from chia_wallet_sdk import Cat, CatInfo, CatSpend, Clvm, Coin, Simulator
+
+def test_issues_and_spends_a_cat():
+ sim = Simulator()
+ clvm = Clvm()
+
+ alice = sim.bls(1)
+
+ tail = clvm.nil()
+ asset_id = tail.tree_hash()
+ cat_info = CatInfo(asset_id, None, alice.puzzle_hash)
+
+ # Issue a CAT
+ clvm.spend_standard_coin(
+ alice.coin,
+ alice.pk,
+ clvm.delegated_spend([clvm.create_coin(cat_info.puzzle_hash(), 1)])
+ )
+
+ eve = Cat(
+ Coin(alice.coin.coin_id(), cat_info.puzzle_hash(), 1),
+ None,
+ cat_info
+ )
+
+ clvm.spend_cats([
+ CatSpend(
+ eve,
+ clvm.standard_spend(
+ alice.pk,
+ clvm.delegated_spend([
+ clvm.create_coin(alice.puzzle_hash, 1, clvm.alloc([alice.puzzle_hash])),
+ clvm.run_cat_tail(tail, clvm.nil()),
+ ])
+ )
+ ),
+ ])
+
+ sim.spend_coins(clvm.coin_spends(), [alice.sk])
+```
+
+
+
+
+### Testing Invalid Transactions
+
+
+
+
+```rust
+#[test]
+fn test_insufficient_funds_fails() {
+ let mut sim = Simulator::new();
+ let ctx = &mut SpendContext::new();
+
+ let alice = sim.bls(1_000);
+
+ // Try to create more than we have
+ let conditions = Conditions::new()
+ .create_coin(alice.puzzle_hash, 2_000, Memos::None);
+
+ StandardLayer::new(alice.pk)
+ .spend(ctx, alice.coin, conditions)
+ .unwrap();
+
+ // This should fail
+ let result = sim.spend_coins(ctx.take(), &[alice.sk]);
+ assert!(result.is_err());
+}
+```
+
+
+
+
+```typescript
+test("insufficient funds fails", (t) => {
+ const sim = new Simulator();
+ const clvm = new Clvm();
+
+ const alice = sim.bls(1000n);
+
+ // Try to create more than we have
+ const conditions = [clvm.createCoin(alice.puzzleHash, 2000n, null)];
+
+ clvm.spendStandardCoin(alice.coin, alice.pk, clvm.delegatedSpend(conditions));
+
+ // This should throw an error
+ t.throws(() => {
+ sim.spendCoins(clvm.coinSpends(), [alice.sk]);
+ });
+});
+```
+
+
+
+
+```python
+def test_insufficient_funds_fails():
+ sim = Simulator()
+ clvm = Clvm()
+
+ alice = sim.bls(1000)
+
+ # Try to create more than we have
+ conditions = [clvm.create_coin(alice.puzzle_hash, 2000, None)]
+
+ clvm.spend_standard_coin(alice.coin, alice.pk, clvm.delegated_spend(conditions))
+
+ # This should raise an exception
+ with pytest.raises(Exception):
+ sim.spend_coins(clvm.coin_spends(), [alice.sk])
+```
+
+
+
+
+### Testing Multi-Spend Transactions
+
+
+
+
+```rust
+#[test]
+fn test_multi_spend() -> Result<()> {
+ let mut sim = Simulator::new();
+ let ctx = &mut SpendContext::new();
+
+ let alice = sim.bls(1_000);
+ let bob = sim.bls(500);
+ let charlie_ph = sim.bls(0).puzzle_hash;
+
+ // Alice sends 900
+ StandardLayer::new(alice.pk).spend(
+ ctx,
+ alice.coin,
+ Conditions::new().create_coin(charlie_ph, 900, Memos::None),
+ )?;
+
+ // Bob pays the fee
+ StandardLayer::new(bob.pk).spend(
+ ctx,
+ bob.coin,
+ Conditions::new()
+ .create_coin(bob.puzzle_hash, 400, Memos::None)
+ .reserve_fee(100),
+ )?;
+
+ sim.spend_coins(ctx.take(), &[alice.sk, bob.sk])?;
+
+ Ok(())
+}
+```
+
+
+
+
+```typescript
+test("multi spend", (t) => {
+ const sim = new Simulator();
+ const clvm = new Clvm();
+
+ const alice = sim.bls(1000n);
+ const bob = sim.bls(500n);
+ const charliePh = sim.bls(0n).puzzleHash;
+
+ // Alice sends 900
+ clvm.spendStandardCoin(
+ alice.coin,
+ alice.pk,
+ clvm.delegatedSpend([clvm.createCoin(charliePh, 900n, null)])
+ );
+
+ // Bob pays the fee
+ clvm.spendStandardCoin(
+ bob.coin,
+ bob.pk,
+ clvm.delegatedSpend([
+ clvm.createCoin(bob.puzzleHash, 400n, null),
+ clvm.reserveFee(100n),
+ ])
+ );
+
+ sim.spendCoins(clvm.coinSpends(), [alice.sk, bob.sk]);
+
+ t.pass();
+});
+```
+
+
+
+
+```python
+def test_multi_spend():
+ sim = Simulator()
+ clvm = Clvm()
+
+ alice = sim.bls(1000)
+ bob = sim.bls(500)
+ charlie_ph = sim.bls(0).puzzle_hash
+
+ # Alice sends 900
+ clvm.spend_standard_coin(
+ alice.coin,
+ alice.pk,
+ clvm.delegated_spend([clvm.create_coin(charlie_ph, 900, None)])
+ )
+
+ # Bob pays the fee
+ clvm.spend_standard_coin(
+ bob.coin,
+ bob.pk,
+ clvm.delegated_spend([
+ clvm.create_coin(bob.puzzle_hash, 400, None),
+ clvm.reserve_fee(100),
+ ])
+ )
+
+ sim.spend_coins(clvm.coin_spends(), [alice.sk, bob.sk])
+```
+
+
+
+
+## Organizing Test Code
+
+### Test Module Structure
+
+```rust
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use chia_wallet_sdk::prelude::*;
+
+ #[test]
+ fn test_feature_a() -> Result<()> {
+ // ...
+ }
+
+ #[test]
+ fn test_feature_b() -> Result<()> {
+ // ...
+ }
+}
+```
+
+### Reusable Test Helpers
+
+```rust
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn setup_test() -> (Simulator, SpendContext, BlsPairWithCoin) {
+ let sim = Simulator::new();
+ let ctx = SpendContext::new();
+ let alice = sim.bls(1_000_000);
+ (sim, ctx, alice)
+ }
+
+ #[test]
+ fn test_with_helper() -> Result<()> {
+ let (mut sim, ctx, alice) = setup_test();
+ let ctx = &mut ctx;
+ // ...
+ }
+}
+```
+
+## Limitations
+
+The simulator is designed for transaction validation, not full blockchain simulation:
+
+- No mempool simulation
+- No block timing
+- No network latency
+- Simplified fee handling
+
+For integration testing against real network behavior, use testnet.
+
+## API Reference
+
+For the complete testing API, see [chia-sdk-test on docs.rs](https://docs.rs/chia-sdk-test).
diff --git a/docs/sdk/troubleshooting.md b/docs/sdk/troubleshooting.md
new file mode 100644
index 0000000..74cf850
--- /dev/null
+++ b/docs/sdk/troubleshooting.md
@@ -0,0 +1,377 @@
+---
+slug: /sdk/troubleshooting
+title: Troubleshooting
+---
+
+# Troubleshooting
+
+This page covers common issues when working with the Wallet SDK and how to resolve them.
+
+## My spend failed to validate
+
+### Symptoms
+
+- Simulator returns an error
+- Transaction rejected by full node
+- CLVM execution fails
+
+### Common Causes
+
+**Incorrect puzzle hash**
+
+The puzzle hash used to create a coin must match the puzzle used to spend it:
+
+```rust
+// When creating a coin
+let puzzle_hash = StandardLayer::puzzle_hash(public_key);
+conditions.create_coin(puzzle_hash, amount, memos);
+
+// When spending, use the same key
+StandardLayer::new(public_key).spend(ctx, coin, conditions)?;
+```
+
+**Mismatched amounts**
+
+Output amounts must not exceed input amounts:
+
+```rust
+// Input: 1000 mojos
+let coin_amount = 1000;
+
+// Outputs must sum to <= 1000
+let send_amount = 900;
+let fee = 100;
+// Total: 900 + 100 = 1000 ✓
+```
+
+**Missing lineage proof**
+
+CATs and singletons require valid lineage proofs:
+
+```rust
+// Ensure CAT has lineage proof set
+let cat = Cat {
+ coin,
+ info,
+ lineage_proof: Some(lineage_proof), // Required for spending
+};
+```
+
+---
+
+## Signature invalid
+
+### Symptoms
+
+- `AggSig validation failed`
+- Transaction rejected with signature error
+- Spend bundle won't aggregate
+
+### Common Causes
+
+**Wrong key used for signing**
+
+Ensure you sign with the key that matches the puzzle:
+
+```rust
+// The public key in the puzzle
+let p2 = StandardLayer::new(alice.pk);
+p2.spend(ctx, coin, conditions)?;
+
+// Must sign with the corresponding secret key
+sim.spend_coins(spends, &[alice.sk])?; // Not bob.sk!
+```
+
+**Missing signatures**
+
+Multi-input transactions may require multiple signatures:
+
+```rust
+// If spending coins from different keys
+StandardLayer::new(alice.pk).spend(ctx, coin1, conditions1)?;
+StandardLayer::new(bob.pk).spend(ctx, coin2, conditions2)?;
+
+// Both keys must sign
+sim.spend_coins(spends, &[alice.sk, bob.sk])?;
+```
+
+**Incorrect AGG_SIG_ME data**
+
+When signing manually, ensure you use the correct AGG_SIG_ME additional data:
+
+```rust
+// AGG_SIG_ME includes coin_id + genesis_challenge
+// Make sure you're using the right network's genesis challenge
+```
+
+---
+
+## Coin not found
+
+### Symptoms
+
+- `Coin not in database`
+- `Unknown coin`
+- Spend references non-existent coin
+
+### Common Causes
+
+**Coin already spent**
+
+A coin can only be spent once. Check if it's already been used:
+
+```rust
+// Each coin has a unique ID
+let coin_id = coin.coin_id();
+
+// If this coin was spent in a previous transaction,
+// you cannot spend it again
+```
+
+**Incorrect coin construction**
+
+When computing child coins, ensure the values match:
+
+```rust
+// The child coin is determined by:
+let child = Coin::new(
+ parent_coin.coin_id(), // Parent's coin ID
+ puzzle_hash, // Must match create_coin puzzle hash
+ amount, // Must match create_coin amount
+);
+```
+
+**Transaction not confirmed**
+
+If depending on a recent transaction, ensure it's confirmed:
+
+```rust
+// In simulation, spends are instant
+// On mainnet, wait for block confirmation before using outputs
+```
+
+---
+
+## Announcement assertion failed
+
+### Symptoms
+
+- `ASSERT_COIN_ANNOUNCEMENT_FAIL`
+- `ASSERT_PUZZLE_ANNOUNCEMENT_FAIL`
+- Announcements don't match
+
+### Common Causes
+
+**Incorrect announcement ID calculation**
+
+Coin announcements include the coin ID:
+
+```rust
+// Coin announcement ID = sha256(coin_id + message)
+// Puzzle announcement ID = sha256(puzzle_hash + message)
+
+// When asserting, the ID must match exactly
+conditions
+ .create_coin_announcement(message)
+ .assert_coin_announcement(expected_announcement_id);
+```
+
+**Missing announcement creation**
+
+Every assertion needs a corresponding creation:
+
+```rust
+// Spend 1: Create the announcement
+let conditions1 = Conditions::new()
+ .create_coin_announcement(b"hello");
+
+// Spend 2: Assert the announcement
+let announcement_id = /* calculate from coin_id + message */;
+let conditions2 = Conditions::new()
+ .assert_coin_announcement(announcement_id);
+```
+
+**Different message bytes**
+
+Ensure message bytes match exactly:
+
+```rust
+// These are different!
+b"hello" // [104, 101, 108, 108, 111]
+"hello" // String, needs .as_bytes()
+```
+
+---
+
+## Insufficient fee
+
+### Symptoms
+
+- Transaction sits in mempool
+- `Fee too low`
+- Transaction eventually dropped
+
+### Common Causes
+
+**No fee specified**
+
+Always include a fee for mainnet transactions:
+
+```rust
+let conditions = Conditions::new()
+ .create_coin(recipient, amount, memos)
+ .reserve_fee(fee); // Don't forget this
+```
+
+**Fee calculation**
+
+Fees are in mojos. During high demand, fees may need to be higher:
+
+```rust
+// Minimum fee depends on network conditions
+// Check current fee estimates from a full node
+let fee = 100_000_000; // 0.0001 XCH = 100M mojos
+```
+
+**Fee in wrong spend**
+
+If batching multiple spends, fee can be in any one of them:
+
+```rust
+// This is fine - fee in second spend
+StandardLayer::new(pk1).spend(ctx, coin1, Conditions::new()
+ .create_coin(dest, amount1, memos))?;
+
+StandardLayer::new(pk2).spend(ctx, coin2, Conditions::new()
+ .create_coin(dest, amount2, memos)
+ .reserve_fee(fee))?; // Fee here covers both
+```
+
+---
+
+## CAT amount mismatch
+
+### Symptoms
+
+- CAT spend fails validation
+- `CAT amount mismatch`
+- Lineage verification fails
+
+### Common Causes
+
+**Input/output imbalance**
+
+CAT amounts must balance (no creation or destruction):
+
+```rust
+// If spending 1000 CAT, must output 1000 CAT
+let cat_spends = [CatSpend::new(
+ cat, // 1000 CAT input
+ inner_spend_with_conditions, // Must create exactly 1000 CAT output
+)];
+```
+
+:::info
+CATs cannot pay transaction fees directly. Fees must be paid with XCH in a separate spend within the same transaction.
+:::
+
+---
+
+## Spends can be separated / Partial bundle attack
+
+### Symptoms
+
+- Multi-coin transaction behaves unexpectedly
+- Funds lost when only some spends execute
+- Attacker extracts individual spends from your bundle
+
+### Common Causes
+
+**Missing `assert_concurrent_spend`**
+
+When spending multiple coins together, you must link them:
+
+```rust
+// WRONG: Spends can be separated
+let conditions1 = Conditions::new()
+ .create_coin(recipient, 1000, memos);
+StandardLayer::new(pk1).spend(ctx, coin1, conditions1)?;
+
+let conditions2 = Conditions::new()
+ .reserve_fee(100);
+StandardLayer::new(pk2).spend(ctx, coin2, conditions2)?;
+
+// RIGHT: Spends are linked and atomic
+let conditions1 = Conditions::new()
+ .create_coin(recipient, 1000, memos)
+ .assert_concurrent_spend(coin2.coin_id()); // Link to coin2
+StandardLayer::new(pk1).spend(ctx, coin1, conditions1)?;
+
+let conditions2 = Conditions::new()
+ .reserve_fee(100)
+ .assert_concurrent_spend(coin1.coin_id()); // Link to coin1
+StandardLayer::new(pk2).spend(ctx, coin2, conditions2)?;
+```
+
+:::warning
+Without `assert_concurrent_spend`, an attacker can take your signed spend bundle, remove some spends, and submit only the ones beneficial to them. Always link all spends in a multi-coin transaction.
+:::
+
+---
+
+## General Debugging Tips
+
+### Use the Simulator
+
+Always test with the simulator first:
+
+```rust
+let mut sim = Simulator::new();
+// Setup state...
+let result = sim.spend_coins(spends, &keys);
+
+if let Err(e) = result {
+ println!("Spend failed: {:?}", e);
+}
+```
+
+### Check Puzzle Hashes
+
+Verify puzzle hashes match:
+
+```rust
+println!("Expected: {}", StandardLayer::puzzle_hash(pk));
+println!("Actual: {}", coin.puzzle_hash);
+```
+
+### Verify Amounts
+
+Ensure amounts balance:
+
+```rust
+let total_input: u64 = coins.iter().map(|c| c.amount).sum();
+let total_output: u64 = /* sum of create_coin amounts + fee */;
+
+assert_eq!(total_input, total_output, "Amount mismatch");
+```
+
+### Inspect Conditions
+
+Print conditions before spending:
+
+```rust
+let conditions = Conditions::new()
+ .create_coin(ph, amount, memos)
+ .reserve_fee(fee);
+
+println!("Conditions: {:?}", conditions);
+```
+
+## Getting Help
+
+If you're stuck:
+
+1. Check the [SDK rustdocs](https://docs.rs/chia-wallet-sdk) for API details
+2. Review the [examples](https://github.com/Chia-Network/chia-wallet-sdk/tree/main/examples)
+3. Search existing issues on [GitHub](https://github.com/Chia-Network/chia-wallet-sdk/issues)
+4. Ask in the Chia developer community
diff --git a/sidebars.ts b/sidebars.ts
index 4feca5a..bac2595 100644
--- a/sidebars.ts
+++ b/sidebars.ts
@@ -11,6 +11,30 @@ const sidebars: SidebarsConfig = {
label: "RPC",
items: [{ type: "doc", id: "rpc-setup" }, ...apiSidebarModule.slice(1)],
},
+ {
+ type: "category",
+ label: "Wallet SDK",
+ items: [
+ { type: "doc", id: "sdk/index", label: "Quick Start" },
+ { type: "doc", id: "sdk/spend-context", label: "SpendContext" },
+ { type: "doc", id: "sdk/actions", label: "Action System" },
+ { type: "doc", id: "sdk/connectivity", label: "Connectivity" },
+ {
+ type: "category",
+ label: "Primitives",
+ items: [
+ { type: "doc", id: "sdk/primitives/standard", label: "Standard (XCH)" },
+ { type: "doc", id: "sdk/primitives/cat", label: "CAT" },
+ { type: "doc", id: "sdk/primitives/nft", label: "NFT" },
+ { type: "doc", id: "sdk/primitives/other", label: "Other Primitives" },
+ ],
+ },
+ { type: "doc", id: "sdk/offers", label: "Offers" },
+ { type: "doc", id: "sdk/testing", label: "Testing" },
+ { type: "doc", id: "sdk/patterns", label: "Application Patterns" },
+ { type: "doc", id: "sdk/troubleshooting", label: "Troubleshooting" },
+ ],
+ },
],
};