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}