Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Transaction plans

Start with a plan when a contract needs exact payments, change, a fee budget or named sponsor inputs. The plan resolves allocations before compiling children, then freezes the exact ordered transaction into the compiler’s usual template.

The runnable payment example uses ordinary Rust methods under #[sapio::contract]. Its committed action declares one recipient and reserves a fee. Try it from the repository root:

cargo run --locked -p sapio --example payment

The complete source below is included directly from the tested example:

//! Compile a small payment contract without a node, signer, or WASM runtime.
//! Native CTV is a research target; this example does not fund or broadcast it.
use bitcoin::{Address, Amount, Network};
use sapio::contract::{Compilable, CompilationError, Compiled, Context};
use sapio::template::{OutputAmount, Template};
use sapio_base::covenant::LoweringPlan;
use sapio_base::effects::EffectPath;
use std::sync::Arc;

struct Payment {
    destination: Compiled,
}

#[sapio::contract]
impl Payment {
    #[action(committed)]
    fn pay(&self, ctx: Context) -> Result<Template, CompilationError> {
        let mut plan = ctx.template_plan();
        plan.output(
            "recipient",
            OutputAmount::Exact(Amount::from_sat(1_000)),
            &self.destination,
        )?;
        plan.reserve_fees(Amount::from_sat(500));
        plan.require_feerate(bitcoin::FeeRate::from_sat_per_vb(1).unwrap());
        Ok(plan.finish()?)
    }
}

fn compile_payment(funding: u64) -> Result<Compiled, Box<dyn std::error::Error>> {
    let destination = "bcrt1qumrrqgt7e3a7damzm8x97m6sjs20u8hjw2hcjj"
        .parse::<Address<bitcoin::address::NetworkUnchecked>>()?
        .require_network(Network::Regtest)?;
    let contract = Payment {
        destination: Compiled::from_address(destination, bitcoin::Amount::ZERO),
    };
    let compiled = contract.compile(Context::new(
        Network::Regtest,
        Amount::from_sat(funding),
        LoweringPlan::Native,
        EffectPath::try_from("payment")?,
        Arc::new(Default::default()),
        None,
    ))?;
    compiled.validate()?;
    Ok(compiled)
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let compiled = compile_payment(1_500)?;
    serde_json::to_writer_pretty(
        std::io::stdout().lock(),
        &serde_json::json!({
            "network": "regtest",
            "enforcement": "native_ctv_research",
            "funding_satoshis": 1_500,
            "contract": compiled,
        }),
    )?;
    Ok(())
}

#[test]
fn payment_preserves_destination_value_and_fee_reserve() {
    let compiled = compile_payment(1_500).unwrap();
    assert_eq!(compiled.ctv_to_tx.len(), 1);
    let template = compiled.ctv_to_tx.values().next().unwrap();
    assert_eq!(template.tx.input.len(), 1);
    assert_eq!(template.tx.output.len(), 1);
    assert_eq!(template.tx.output[0].value.to_sat(), 1_000);
    let destination = "bcrt1qumrrqgt7e3a7damzm8x97m6sjs20u8hjw2hcjj"
        .parse::<Address<bitcoin::address::NetworkUnchecked>>()
        .unwrap()
        .require_network(Network::Regtest)
        .unwrap();
    assert_eq!(
        template.tx.output[0].script_pubkey,
        destination.script_pubkey()
    );
    assert_eq!(template.required_input_amount.to_sat(), 1_500);
    assert!(compile_payment(1_499).is_err());
}

OutputAmount::Remainder can assign change to one additional output. Output order is declaration order; the resolver never sorts destinations. Auxiliary inputs have names and declared minimum contributions, checked again when funding is supplied. Repeated fee reservations take their maximum, and height/time lock conflicts are explicit errors.

A fee cap is a local preparation rule. It is not automatically enforced by Bitcoin Script. Final fee-rate checks also need actual previous outputs and the completed witness weight. Native signature finalization and chain checks remain separate from transaction construction.

The lower-level builder described next is useful for procedural constructions. Both APIs produce the same Template.