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

Type-level state machines

Rust types can express which actions a contract implementation provides. An optional action factory returns None when a transition is unavailable; this is separate from an action returning an empty set of transactions.

The following sketch uses Opened and Closed as state tags. Both states retain the owner’s independent spending policy, while only the open state exports the committed close transition.

#![allow(unused)]
fn main() {
use std::marker::PhantomData;

struct Opened;
struct Closed;
struct StatefulContract<State> {
    owner: bitcoin::XOnlyPublicKey,
    state: PhantomData<State>,
}

trait Moves: Sized + 'static {
    sapio::decl_then! { close }
}

impl Moves for StatefulContract<Opened> {
    #[sapio::then]
    fn close(self, ctx: Context) {
        let amount = ctx.funds();
        ctx.template()
            .add_output(amount, &StatefulContract::<Closed> {
                owner: self.owner,
                state: PhantomData,
            }, None)?
            .into()
    }
}

impl Moves for StatefulContract<Closed> {}

#[sapio::contract(actions(Self::close))]
impl<State: 'static> StatefulContract<State>
where
    Self: Moves,
{
    #[spend]
    fn owner(&self) -> Clause {
        Clause::Key(self.owner)
    }
}
}

decl_then! supplies an absent default factory. The open implementation replaces it; the closed implementation keeps it absent. The contract macro explicitly exports that optional interface and the independent owner policy.

For availability that depends on a value rather than a Rust type, use a #[condition] method with compile_if(...). For example, an eltoo state can omit its update action after reaching the maximum state number. Required and Nullable still distinguish an action that must generate a transaction from one that may legitimately return none.

Rust enums, traits, generics and const generics can organize more elaborate state machines. They execute while constructing and compiling contracts. An ordinary Rust state check is not automatically enforced by a spending script: the committed transition or fixed policy/evaluator must enforce the corresponding on-chain rule.