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" }, + ], + }, ], };