Skip to main content

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

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);

Spending Standard Coins

Basic Spend

The simplest spend creates a new coin and optionally pays a fee:

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();

Sending with Hints

Hints help wallets discover coins. Add the recipient's puzzle hash as a memo:

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)?;

Multiple Outputs

A single spend can create multiple output coins:

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)?;

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.

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();
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:

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)

For the complete list of conditions, see the Conditions API in docs.rs.

Using SpendWithConditions

For simpler cases, you can use spend_with_conditions to get a Spend object without immediately adding it to the context:

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:

// 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());

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

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:

use std::collections::HashMap;

// Map public keys to secret keys for lookup
let key_pairs: HashMap<PublicKey, &SecretKey> = 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:

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:

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<SpendBundle, DriverError> {
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))
}

API Reference

For the complete StandardLayer API, see docs.rs.