Skip to main content

dig_options/
exercise.rs

1//! Exercise an option — pay the strike and unlock the underlying to the holder.
2//!
3//! [`exercise`] builds the complete exercise in one bundle: it spends the option singleton
4//! through its exercise path, unlocks the locked XCH underlying to the holder, and — for an
5//! XCH strike — pays the strike into the settlement puzzle and settles it to the creator's
6//! requested payment. The holder authorizes both the singleton spend and the strike-funding
7//! spend through the caller's [`Owner`] layer. The exercise is valid only strictly before
8//! `expiry_seconds` (enforced by the consensus).
9
10use chia_protocol::Coin;
11use chia_puzzle_types::offer::{NotarizedPayment, Payment, SettlementPaymentsSolution};
12use chia_puzzle_types::Memos;
13use chia_puzzles::SETTLEMENT_PAYMENT_HASH;
14use chia_wallet_sdk::driver::{
15    Layer, OptionType, SettlementLayer, SingletonInfo, SpendContext, SpendWithConditions,
16};
17use chia_wallet_sdk::types::Conditions;
18
19use crate::error::{Error, Result};
20use crate::types::{CreatedOption, OptionSpend, Owner};
21
22/// The strike payment funding a [`exercise`]: the caller-supplied XCH coin the holder spends
23/// to pay the strike into the settlement puzzle.
24///
25/// It must hold at least the strike amount (`created.underlying.strike_type.amount()`); any
26/// excess is left as an implicit fee.
27#[derive(Clone, Copy, Debug)]
28pub struct StrikePayment {
29    /// The XCH coin the holder spends to fund the strike payment.
30    pub funding_coin: Coin,
31}
32
33/// Build the unsigned coin spends that EXERCISE `created` by its `holder`, paying `strike`.
34///
35/// Spends the option singleton through its exercise path, unlocks the locked XCH underlying to
36/// the holder, and pays the XCH strike into the settlement puzzle — settled to the creator's
37/// requested payment — all in one bundle. Rejects a `strike.funding_coin` smaller than the
38/// strike amount.
39///
40/// **v0.1.0 scope:** only an XCH strike can be exercised. A CAT/NFT strike returns
41/// [`Error::InvalidInput`] rather than emitting an incorrect spend — building the CAT/NFT
42/// settlement leg is a documented follow-up.
43///
44/// **Pure: does NOT sign or broadcast.** Returns [`OptionSpend`] with `created: None`.
45///
46/// **CRITICAL: The returned `coin_spends` include the settlement leg that claims the unlocked
47/// underlying to the holder.** That leg is REQUIRED but only builder-enforced: consensus forces
48/// the strike payment to the creator, but does NOT force the underlying claim. Callers MUST
49/// broadcast the full returned bundle intact — dropping or reordering the underlying-claim spend
50/// strands the underlying at a publicly-claimable settlement coin that anyone can take. Never
51/// submit a subset of these spends.
52pub fn exercise(
53    ctx: &mut SpendContext,
54    holder: &Owner,
55    created: &CreatedOption,
56    strike: &StrikePayment,
57) -> Result<OptionSpend> {
58    let OptionType::Xch {
59        amount: strike_amount,
60    } = created.underlying.strike_type
61    else {
62        return Err(Error::invalid(
63            "CAT/NFT strike exercise not yet supported — see dig-options CAT/NFT follow-up",
64        ));
65    };
66
67    if strike.funding_coin.amount < strike_amount {
68        return Err(Error::invalid(format!(
69            "strike funding coin amount {} is too small: need {strike_amount} for the XCH strike",
70            strike.funding_coin.amount
71        )));
72    }
73
74    // Spend the option singleton through its exercise path (the holder authorizes it).
75    // `OptionContract` is `Copy`, so this copies rather than moves `created.option`.
76    created.option.exercise(ctx, holder, Conditions::new())?;
77
78    // Unlock the locked underlying. The underlying's exercise-path delegated puzzle emits
79    // `CreateCoin(SETTLEMENT_PAYMENT_HASH, underlying_amount)` — the unlocked XCH lands on a BARE
80    // settlement/offer coin (spendable by anyone with a `SettlementPaymentsSolution`, no key). The
81    // `inner_puzzle_hash` argument here is ONLY the singleton co-spend proof (a `SingletonMember`
82    // check), NOT a payout destination. We MUST claim that settlement coin to the holder in this
83    // same bundle — otherwise the underlying is stranded and any mempool watcher steals it while
84    // the holder has paid the strike and received nothing.
85    created.underlying.exercise_coin_spend(
86        ctx,
87        created.underlying_coin,
88        created.option.info.inner_puzzle_hash().into(),
89        created.option.coin.amount,
90    )?;
91
92    // Claim the unlocked-underlying settlement coin to the holder (the option's current p2 owner),
93    // paying the full underlying amount, in the same bundle. Mirrors the strike leg below.
94    let underlying_settlement_coin = Coin::new(
95        created.underlying_coin.coin_id(),
96        SETTLEMENT_PAYMENT_HASH.into(),
97        created.underlying.amount,
98    );
99    let holder_payment = NotarizedPayment::new(
100        created.option.info.launcher_id,
101        vec![Payment::new(
102            created.option.info.p2_puzzle_hash,
103            created.underlying.amount,
104            Memos::None,
105        )],
106    );
107    let underlying_claim = SettlementLayer.construct_coin_spend(
108        ctx,
109        underlying_settlement_coin,
110        SettlementPaymentsSolution::new(vec![holder_payment]),
111    )?;
112    ctx.insert(underlying_claim);
113
114    // Pay the XCH strike into the settlement puzzle, then settle it to the creator's
115    // requested payment. The holder authorizes the strike-funding spend.
116    let strike_inner = holder.spend_with_conditions(
117        ctx,
118        Conditions::new().create_coin(SETTLEMENT_PAYMENT_HASH.into(), strike_amount, Memos::None),
119    )?;
120    ctx.spend(strike.funding_coin, strike_inner)?;
121
122    let settlement_coin = Coin::new(
123        strike.funding_coin.coin_id(),
124        SETTLEMENT_PAYMENT_HASH.into(),
125        strike_amount,
126    );
127    let payment = created.underlying.requested_payment(&mut **ctx)?;
128    let coin_spend = SettlementLayer.construct_coin_spend(
129        ctx,
130        settlement_coin,
131        SettlementPaymentsSolution::new(vec![payment]),
132    )?;
133    ctx.insert(coin_spend);
134
135    Ok(OptionSpend {
136        coin_spends: ctx.take(),
137        created: None,
138    })
139}