Skip to main content

dig_options/
create.rs

1//! Create a covered option — lock an XCH underlying and mint the option singleton.
2//!
3//! [`create`] funds two coins from one `funding_coin` spend: the locked-underlying XCH coin
4//! (at the option's 1-of-2 exercise/clawback path) and the option singleton launcher. The
5//! funding coin is spent through the caller's [`Owner`] layer; dig-options never holds the
6//! key that authorizes it. The returned [`CreatedOption`] is what the caller keeps to later
7//! exercise or claw back the option.
8
9use chia_protocol::Coin;
10use chia_puzzle_types::Memos;
11use chia_wallet_sdk::driver::{
12    OptionLauncher, OptionLauncherInfo, OptionType, SpendContext, SpendWithConditions,
13};
14
15use crate::error::{Error, Result};
16use crate::types::{CreatedOption, OptionSpend, OptionTerms, Owner};
17
18/// Build the unsigned coin spends that CREATE an option per `terms`, funded from
19/// `funding_coin` and authorized by `creator`.
20///
21/// Locks `terms.underlying_amount` mojos of XCH as the underlying and mints the option
22/// singleton to `terms.owner_puzzle_hash`, exercisable for `terms.strike_type` until
23/// `terms.expiry_seconds`. `funding_coin` must hold at least `underlying_amount + 1` mojos
24/// (the underlying plus the 1-mojo singleton); any excess is left as an implicit fee.
25///
26/// The returned [`OptionSpend::created`] is `Some`: retain it to exercise or claw back the
27/// option once this spend is confirmed. **Pure: does NOT sign or broadcast** — `creator`
28/// authorizes the funding-coin spend; the caller signs the reported messages
29/// ([`crate::required_signatures`]).
30pub fn create(
31    ctx: &mut SpendContext,
32    creator: &Owner,
33    funding_coin: Coin,
34    terms: &OptionTerms,
35) -> Result<OptionSpend> {
36    if terms.underlying_amount == 0 {
37        return Err(Error::invalid(
38            "option underlying amount must be greater than zero",
39        ));
40    }
41    // v0.1.0 exercise supports only an XCH strike, so minting a non-XCH-strike option would
42    // create an option no holder could ever exercise (an asymmetric loss — the creator claws it
43    // back after expiry). Keep create/exercise support symmetric by rejecting it up front, with
44    // the same message shape as the exercise guard. CAT/NFT strike lands with the follow-up.
45    if !matches!(terms.strike_type, OptionType::Xch { .. }) {
46        return Err(Error::invalid(
47            "CAT/NFT strike exercise not yet supported — see dig-options CAT/NFT follow-up",
48        ));
49    }
50    let needed = terms.underlying_amount.checked_add(1).ok_or_else(|| {
51        Error::invalid("underlying amount overflows the 1-mojo singleton addition")
52    })?;
53    if funding_coin.amount < needed {
54        return Err(Error::invalid(format!(
55            "funding coin amount {} is too small: need {needed} (underlying {} + 1 mojo singleton)",
56            funding_coin.amount, terms.underlying_amount
57        )));
58    }
59
60    // Build the launcher off the funding coin. The launcher's 1-mojo coin becomes the option
61    // singleton; the terms name the creator (clawback path) and owner (holder) puzzle hashes.
62    let launcher = OptionLauncher::new(
63        ctx,
64        funding_coin.coin_id(),
65        OptionLauncherInfo::new(
66            terms.creator_puzzle_hash,
67            terms.owner_puzzle_hash,
68            terms.expiry_seconds,
69            terms.underlying_amount,
70            terms.strike_type,
71        ),
72        1,
73    )?;
74
75    let underlying = launcher.underlying();
76    let p2_option = launcher.p2_puzzle_hash();
77
78    // Lock the underlying XCH at the option's 1-of-2 path AND create the launcher coin, both
79    // funded by the single funding-coin spend.
80    let underlying_coin = Coin::new(funding_coin.coin_id(), p2_option, terms.underlying_amount);
81    let launcher = launcher.with_underlying(underlying_coin.coin_id());
82    let (mint_conditions, option) = launcher.mint(ctx)?;
83
84    let conditions = mint_conditions.create_coin(p2_option, terms.underlying_amount, Memos::None);
85    let inner_spend = creator.spend_with_conditions(ctx, conditions)?;
86    ctx.spend(funding_coin, inner_spend)?;
87
88    Ok(OptionSpend {
89        coin_spends: ctx.take(),
90        created: Some(CreatedOption {
91            option,
92            underlying,
93            underlying_coin,
94        }),
95    })
96}