chia-sdk-driver 0.33.0

Driver code for interacting with standard puzzles on the Chia blockchain.
Documentation
use chia_protocol::{Bytes32, Coin};
use chia_puzzle_types::Memos;
use chia_sdk_types::conditions::CreateCoin;
use clvm_utils::ToTreeHash;

use crate::{
    Asset, Deltas, DriverError, Id, OptionType, Output, SingletonSpends, SpendAction, SpendContext,
    SpendKind, Spends,
};

#[derive(Debug, Clone, Copy)]
pub struct MintOptionAction {
    pub creator_puzzle_hash: Bytes32,
    pub seconds: u64,
    pub underlying_id: Id,
    pub underlying_amount: u64,
    pub strike_type: OptionType,
    pub amount: u64,
}

impl MintOptionAction {
    pub fn new(
        creator_puzzle_hash: Bytes32,
        seconds: u64,
        underlying_id: Id,
        underlying_amount: u64,
        strike_type: OptionType,
        amount: u64,
    ) -> Self {
        Self {
            creator_puzzle_hash,
            seconds,
            underlying_id,
            underlying_amount,
            strike_type,
            amount,
        }
    }

    fn lock_underlying(
        &self,
        ctx: &mut SpendContext,
        spends: &mut Spends,
        p2_puzzle_hash: Bytes32,
    ) -> Result<Bytes32, DriverError> {
        let output = Output::new(p2_puzzle_hash, self.underlying_amount);
        let create_coin = CreateCoin::new(p2_puzzle_hash, self.underlying_amount, Memos::None);

        if matches!(self.underlying_id, Id::Xch) {
            let source = spends.xch.output_source(ctx, &output)?;
            let parent = &mut spends.xch.items[source];
            let parent_puzzle_hash = parent.asset.full_puzzle_hash();

            parent.kind.create_coin_with_assertion(
                ctx,
                parent_puzzle_hash,
                &mut spends.xch.payment_assertions,
                create_coin,
            );

            let coin = Coin::new(
                parent.asset.coin_id(),
                p2_puzzle_hash,
                self.underlying_amount,
            );

            spends.outputs.xch.push(coin);

            return Ok(coin.coin_id());
        } else if let Some(cat) = spends.cats.get_mut(&self.underlying_id) {
            let source = cat.output_source(ctx, &output)?;
            let parent = &mut cat.items[source];
            let parent_puzzle_hash = parent.asset.full_puzzle_hash();

            parent.kind.create_coin_with_assertion(
                ctx,
                parent_puzzle_hash,
                &mut cat.payment_assertions,
                create_coin,
            );

            let cat = parent.asset.child(p2_puzzle_hash, self.underlying_amount);

            spends
                .outputs
                .cats
                .entry(self.underlying_id)
                .or_default()
                .push(cat);

            return Ok(cat.coin_id());
        } else if let Some(nft) = spends.nfts.get_mut(&self.underlying_id) {
            let source = nft.last_mut()?;
            source.child_info.destination = Some(create_coin);

            let Some(nft) = nft.finalize(
                ctx,
                spends.intermediate_puzzle_hash,
                spends.change_puzzle_hash,
            )?
            else {
                return Err(DriverError::NoSourceForOutput);
            };

            spends.outputs.nfts.insert(self.underlying_id, nft);

            return Ok(nft.coin_id());
        }

        Err(DriverError::InvalidAssetId)
    }
}

impl SpendAction for MintOptionAction {
    fn calculate_delta(&self, deltas: &mut Deltas, index: usize) {
        deltas.update(Id::Xch).output += self.amount;
        deltas.update(Id::New(index)).input += self.amount;
        deltas.update(self.underlying_id).output += self.underlying_amount;
        deltas.set_needed(self.underlying_id);
        deltas.set_needed(Id::Xch);
    }

    fn spend(
        &self,
        ctx: &mut SpendContext,
        spends: &mut Spends,
        index: usize,
    ) -> Result<(), DriverError> {
        let (source, launcher) = spends.xch.create_option_launcher(
            ctx,
            self.amount,
            self.creator_puzzle_hash,
            self.seconds,
            self.underlying_amount,
            self.strike_type,
        )?;

        let underlying_p2_puzzle_hash = launcher.underlying().tree_hash().into();
        let underlying_coin_id = self.lock_underlying(ctx, spends, underlying_p2_puzzle_hash)?;

        let source = &mut spends.xch.items[source];

        let (parent_conditions, eve_option) = launcher
            .with_underlying(underlying_coin_id)
            .mint_eve(ctx, source.asset.p2_puzzle_hash())?;

        match &mut source.kind {
            SpendKind::Conditions(spend) => {
                spend.add_conditions(parent_conditions);
            }
            SpendKind::Settlement(_) => {
                return Err(DriverError::CannotEmitConditions);
            }
        }

        spends
            .options
            .insert(Id::New(index), SingletonSpends::new(eve_option, true));

        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use anyhow::Result;
    use chia_sdk_test::Simulator;
    use indexmap::indexmap;
    use rstest::rstest;

    use crate::{Action, Relation};

    use super::*;

    #[rstest]
    #[case::normal(None)]
    #[case::revocable(Some(Bytes32::default()))]
    fn test_action_mint_option(#[case] hidden_puzzle_hash: Option<Bytes32>) -> Result<()> {
        let mut sim = Simulator::new();
        let mut ctx = SpendContext::new();

        let alice = sim.bls(6);

        let mut spends = Spends::new(alice.puzzle_hash);
        spends.add(alice.coin);

        let deltas = spends.apply(
            &mut ctx,
            &[
                Action::single_issue_cat(hidden_puzzle_hash, 5),
                Action::mint_option(
                    alice.puzzle_hash,
                    100,
                    Id::New(0),
                    5,
                    OptionType::Xch { amount: 5 },
                    1,
                ),
            ],
        )?;

        let outputs = spends.finish_with_keys(
            &mut ctx,
            &deltas,
            Relation::None,
            &indexmap! { alice.puzzle_hash => alice.pk },
        )?;

        sim.spend_coins(ctx.take(), &[alice.sk])?;

        let option = outputs.options[&Id::New(1)];
        assert_ne!(sim.coin_state(option.coin.coin_id()), None);
        assert_eq!(option.info.p2_puzzle_hash, alice.puzzle_hash);
        assert_eq!(option.coin.amount, 1);

        Ok(())
    }
}