chia_sdk_driver/action_system/
singleton_spends.rs

1use std::fmt::Debug;
2
3use chia_protocol::{Bytes32, Coin};
4use chia_puzzles::{SETTLEMENT_PAYMENT_HASH, SINGLETON_LAUNCHER_HASH};
5use chia_sdk_types::{
6    conditions::{
7        AssertPuzzleAnnouncement, CreateCoin, NewMetadataOutput, TransferNft, UpdateNftMetadata,
8    },
9    Conditions,
10};
11use clvm_traits::clvm_list;
12use clvmr::NodePtr;
13
14use crate::{
15    Asset, Did, DidInfo, DriverError, FungibleSpend, HashedPtr, Launcher, Nft, NftInfo,
16    OptionContract, OutputSet, Spend, SpendContext, SpendKind,
17};
18
19#[derive(Debug, Clone)]
20pub struct SingletonSpends<A>
21where
22    A: SingletonAsset,
23{
24    pub lineage: Vec<SingletonSpend<A>>,
25    pub ephemeral: bool,
26}
27
28impl<A> SingletonSpends<A>
29where
30    A: SingletonAsset,
31{
32    pub fn new(asset: A, ephemeral: bool) -> Self {
33        Self {
34            lineage: vec![SingletonSpend::new(asset)],
35            ephemeral,
36        }
37    }
38
39    pub fn last(&self) -> Result<&SingletonSpend<A>, DriverError> {
40        self.lineage.last().ok_or(DriverError::NoSourceForOutput)
41    }
42
43    pub fn last_mut(&mut self) -> Result<&mut SingletonSpend<A>, DriverError> {
44        self.lineage
45            .last_mut()
46            .ok_or(DriverError::NoSourceForOutput)
47    }
48
49    pub fn last_or_create_settlement(
50        &mut self,
51        ctx: &mut SpendContext,
52    ) -> Result<usize, DriverError> {
53        let last = self
54            .lineage
55            .last_mut()
56            .ok_or(DriverError::NoSourceForOutput)?;
57
58        if !last.kind.missing_singleton_output() {
59            return Err(DriverError::NoSourceForOutput);
60        }
61
62        if last.kind.is_settlement() {
63            return Ok(self.lineage.len() - 1);
64        }
65
66        let Some(child) = A::finalize(
67            ctx,
68            last,
69            SETTLEMENT_PAYMENT_HASH.into(),
70            SETTLEMENT_PAYMENT_HASH.into(),
71        )?
72        else {
73            return Err(DriverError::NoSourceForOutput);
74        };
75
76        self.lineage.push(child);
77
78        Ok(self.lineage.len() - 1)
79    }
80
81    pub fn finalize(
82        &mut self,
83        ctx: &mut SpendContext,
84        intermediate_puzzle_hash: Bytes32,
85        change_puzzle_hash: Bytes32,
86    ) -> Result<Option<A>, DriverError> {
87        let asset = loop {
88            let last = self
89                .lineage
90                .last_mut()
91                .ok_or(DriverError::NoSourceForOutput)?;
92
93            if !last.kind.missing_singleton_output() {
94                break None;
95            }
96
97            let Some(child) = A::finalize(ctx, last, intermediate_puzzle_hash, change_puzzle_hash)?
98            else {
99                break None;
100            };
101
102            if A::needs_additional_spend(&child.child_info) {
103                self.lineage.push(child);
104            } else {
105                break Some(child.asset);
106            }
107        };
108
109        Ok(asset)
110    }
111
112    pub fn intermediate_fungible_xch_spend(
113        &mut self,
114        ctx: &mut SpendContext,
115        intermediate_puzzle_hash: Bytes32,
116    ) -> Result<Option<FungibleSpend<Coin>>, DriverError> {
117        let Some((index, amount)) = self.lineage.iter().enumerate().find_map(|(index, item)| {
118            item.kind
119                .find_amount(intermediate_puzzle_hash, &item.asset.constraints())
120                .map(|amount| (index, amount))
121        }) else {
122            return Ok(None);
123        };
124
125        let source = &mut self.lineage[index];
126
127        let hint = ctx.hint(intermediate_puzzle_hash)?;
128
129        source.kind.create_intermediate_coin(CreateCoin::new(
130            intermediate_puzzle_hash,
131            amount,
132            hint,
133        ));
134
135        let child = FungibleSpend::new(
136            Coin::new(source.asset.coin_id(), intermediate_puzzle_hash, amount),
137            true,
138        );
139
140        Ok(Some(child))
141    }
142
143    pub fn launcher_source(&mut self) -> Result<(usize, u64), DriverError> {
144        let Some((index, amount)) = self.lineage.iter().enumerate().find_map(|(index, item)| {
145            item.kind
146                .find_amount(SINGLETON_LAUNCHER_HASH.into(), &item.asset.constraints())
147                .map(|amount| (index, amount))
148        }) else {
149            return Err(DriverError::NoSourceForOutput);
150        };
151
152        Ok((index, amount))
153    }
154
155    pub fn create_launcher(
156        &mut self,
157        singleton_amount: u64,
158    ) -> Result<(usize, Launcher), DriverError> {
159        let (index, launcher_amount) = self.launcher_source()?;
160
161        let (create_coin, launcher) =
162            Launcher::create_early(self.lineage[index].asset.coin_id(), launcher_amount);
163
164        self.lineage[index]
165            .kind
166            .create_intermediate_coin(create_coin);
167
168        Ok((index, launcher.with_singleton_amount(singleton_amount)))
169    }
170}
171
172#[derive(Debug, Clone)]
173pub struct SingletonSpend<A>
174where
175    A: SingletonAsset,
176{
177    pub asset: A,
178    pub kind: SpendKind,
179    pub child_info: A::ChildInfo,
180    pub payment_assertions: Vec<AssertPuzzleAnnouncement>,
181}
182
183impl<A> SingletonSpend<A>
184where
185    A: SingletonAsset,
186{
187    pub fn new(asset: A) -> Self {
188        let kind = if asset.p2_puzzle_hash() == SETTLEMENT_PAYMENT_HASH.into() {
189            SpendKind::settlement()
190        } else {
191            SpendKind::conditions()
192        };
193        let child_info = A::default_child_info(&asset, &kind);
194
195        Self {
196            asset,
197            kind,
198            child_info,
199            payment_assertions: Vec::new(),
200        }
201    }
202}
203
204pub trait SingletonAsset: Debug + Clone + Asset {
205    type ChildInfo: Debug + Clone;
206
207    fn default_child_info(asset: &Self, spend_kind: &SpendKind) -> Self::ChildInfo;
208    fn needs_additional_spend(child_info: &Self::ChildInfo) -> bool;
209    fn finalize(
210        ctx: &mut SpendContext,
211        singleton: &mut SingletonSpend<Self>,
212        intermediate_puzzle_hash: Bytes32,
213        change_puzzle_hash: Bytes32,
214    ) -> Result<Option<SingletonSpend<Self>>, DriverError>;
215}
216
217impl SingletonAsset for Did<HashedPtr> {
218    type ChildInfo = ChildDidInfo;
219
220    fn default_child_info(asset: &Self, spend_kind: &SpendKind) -> Self::ChildInfo {
221        ChildDidInfo {
222            recovery_list_hash: asset.info.recovery_list_hash,
223            num_verifications_required: asset.info.num_verifications_required,
224            metadata: asset.info.metadata,
225            destination: None,
226            new_spend_kind: spend_kind.empty_copy(),
227            needs_update: false,
228        }
229    }
230
231    fn needs_additional_spend(child_info: &Self::ChildInfo) -> bool {
232        child_info.needs_update
233    }
234
235    fn finalize(
236        ctx: &mut SpendContext,
237        singleton: &mut SingletonSpend<Self>,
238        _conditions_puzzle_hash: Bytes32,
239        change_puzzle_hash: Bytes32,
240    ) -> Result<Option<SingletonSpend<Self>>, DriverError> {
241        let change_hint = ctx.hint(change_puzzle_hash)?;
242
243        let current_info = singleton.asset.info;
244        let child_info = &singleton.child_info;
245
246        // If the DID layer has changed, we need to perform an update spend to ensure wallets can properly sync the coin.
247        let needs_update = current_info.recovery_list_hash != child_info.recovery_list_hash
248            || current_info.num_verifications_required != child_info.num_verifications_required
249            || current_info.metadata != child_info.metadata;
250
251        let final_destination = child_info.destination;
252
253        let destination = if needs_update {
254            let p2_puzzle_hash = current_info.p2_puzzle_hash;
255            let hint = ctx.hint(p2_puzzle_hash)?;
256            SingletonDestination::CreateCoin(CreateCoin::new(
257                p2_puzzle_hash,
258                singleton.asset.coin.amount,
259                hint,
260            ))
261        } else {
262            child_info
263                .destination
264                .unwrap_or(SingletonDestination::CreateCoin(CreateCoin::new(
265                    change_puzzle_hash,
266                    singleton.asset.coin.amount,
267                    change_hint,
268                )))
269        };
270
271        match destination {
272            SingletonDestination::CreateCoin(destination) => {
273                let child_info = DidInfo::new(
274                    current_info.launcher_id,
275                    child_info.recovery_list_hash,
276                    child_info.num_verifications_required,
277                    child_info.metadata,
278                    destination.puzzle_hash,
279                );
280
281                // Create the new DID coin with the updated DID info. The DID puzzle does not automatically wrap the output.
282                let create_coin = CreateCoin::new(
283                    child_info.inner_puzzle_hash().into(),
284                    destination.amount,
285                    destination.memos,
286                );
287                let parent_puzzle_hash = singleton.asset.full_puzzle_hash();
288                singleton.kind.create_coin_with_assertion(
289                    ctx,
290                    parent_puzzle_hash,
291                    &mut singleton.payment_assertions,
292                    create_coin,
293                );
294
295                // Create a new singleton spend with the child and the new spend kind.
296                // This will only be added to the lineage if an additional spend is required.
297                let mut new_spend = SingletonSpend::new(
298                    singleton
299                        .asset
300                        .child_with(child_info, singleton.asset.coin.amount),
301                );
302
303                // Signal that an additional spend is required.
304                new_spend.child_info.needs_update = needs_update;
305
306                if needs_update {
307                    new_spend.child_info.destination = final_destination;
308                }
309
310                Ok(Some(new_spend))
311            }
312            SingletonDestination::Melt => {
313                match &mut singleton.kind {
314                    SpendKind::Conditions(conditions) => {
315                        conditions.add_conditions(Conditions::new().melt_singleton());
316                    }
317                    SpendKind::Settlement(_) => {
318                        return Err(DriverError::CannotEmitConditions);
319                    }
320                }
321
322                Ok(None)
323            }
324        }
325    }
326}
327
328impl SingletonAsset for Nft<HashedPtr> {
329    type ChildInfo = ChildNftInfo;
330
331    fn default_child_info(_asset: &Self, spend_kind: &SpendKind) -> Self::ChildInfo {
332        ChildNftInfo {
333            metadata_update_spends: Vec::new(),
334            transfer_condition: None,
335            destination: None,
336            new_spend_kind: spend_kind.empty_copy(),
337        }
338    }
339
340    fn needs_additional_spend(child_info: &Self::ChildInfo) -> bool {
341        !child_info.metadata_update_spends.is_empty() || child_info.transfer_condition.is_some()
342    }
343
344    fn finalize(
345        ctx: &mut SpendContext,
346        singleton: &mut SingletonSpend<Self>,
347        intermediate_puzzle_hash: Bytes32,
348        change_puzzle_hash: Bytes32,
349    ) -> Result<Option<SingletonSpend<Self>>, DriverError> {
350        if !singleton.kind.is_conditions()
351            && (!singleton.child_info.metadata_update_spends.is_empty()
352                || singleton.child_info.transfer_condition.is_some())
353        {
354            let create_coin = CreateCoin::new(
355                intermediate_puzzle_hash,
356                singleton.asset.coin.amount,
357                ctx.hint(intermediate_puzzle_hash)?,
358            );
359            let parent_puzzle_hash = singleton.asset.full_puzzle_hash();
360            singleton.kind.create_coin_with_assertion(
361                ctx,
362                parent_puzzle_hash,
363                &mut singleton.payment_assertions,
364                create_coin,
365            );
366
367            let new_info = NftInfo {
368                p2_puzzle_hash: intermediate_puzzle_hash,
369                ..singleton.asset.info
370            };
371
372            let mut spend = SingletonSpend::new(
373                singleton
374                    .asset
375                    .child_with(new_info, singleton.asset.coin.amount),
376            );
377
378            spend.child_info = singleton.child_info.clone();
379
380            return Ok(Some(spend));
381        }
382
383        let change_hint = ctx.hint(change_puzzle_hash)?;
384
385        let mut new_child_info = singleton.child_info.clone();
386
387        let metadata_update_spend = new_child_info.metadata_update_spends.pop();
388        let transfer_condition = new_child_info.transfer_condition.take();
389        let needs_additional_spend = Self::needs_additional_spend(&new_child_info);
390
391        let destination = if needs_additional_spend {
392            let p2_puzzle_hash = singleton.asset.info.p2_puzzle_hash;
393            let hint = ctx.hint(p2_puzzle_hash)?;
394            CreateCoin::new(p2_puzzle_hash, singleton.asset.coin.amount, hint)
395        } else {
396            new_child_info.destination.unwrap_or(CreateCoin::new(
397                change_puzzle_hash,
398                singleton.asset.coin.amount,
399                change_hint,
400            ))
401        };
402
403        let mut nft_info = singleton.asset.info;
404        nft_info.p2_puzzle_hash = destination.puzzle_hash;
405
406        // Create the new NFT coin with the updated info.
407        let parent_puzzle_hash = singleton.asset.full_puzzle_hash();
408
409        singleton.kind.create_coin_with_assertion(
410            ctx,
411            parent_puzzle_hash,
412            &mut singleton.payment_assertions,
413            destination,
414        );
415
416        let mut conditions = Conditions::new();
417
418        if let Some(spend) = metadata_update_spend {
419            conditions.push(UpdateNftMetadata::new(spend.puzzle, spend.solution));
420
421            let metadata_updater_solution = ctx.alloc(&clvm_list!(
422                singleton.asset.info.metadata,
423                singleton.asset.info.metadata_updater_puzzle_hash,
424                spend.solution
425            ))?;
426            let ptr = ctx.run(spend.puzzle, metadata_updater_solution)?;
427            let output = ctx.extract::<NewMetadataOutput<HashedPtr, NodePtr>>(ptr)?;
428
429            nft_info.metadata = output.metadata_info.new_metadata;
430            nft_info.metadata_updater_puzzle_hash = output.metadata_info.new_updater_puzzle_hash;
431        }
432
433        if let Some(transfer_condition) = transfer_condition {
434            nft_info.current_owner = transfer_condition.launcher_id;
435            conditions.push(transfer_condition);
436        }
437
438        if !conditions.is_empty() {
439            match &mut singleton.kind {
440                SpendKind::Conditions(spend) => {
441                    spend.add_conditions(conditions);
442                }
443                SpendKind::Settlement(_) => {
444                    return Err(DriverError::CannotEmitConditions);
445                }
446            }
447        }
448
449        // Create a new singleton spend with the child and the new spend kind.
450        let mut spend = SingletonSpend::new(
451            singleton
452                .asset
453                .child_with(nft_info, singleton.asset.coin.amount),
454        );
455
456        spend.child_info = new_child_info;
457
458        Ok(Some(spend))
459    }
460}
461
462impl SingletonAsset for OptionContract {
463    type ChildInfo = ChildOptionInfo;
464
465    fn default_child_info(_asset: &Self, spend_kind: &SpendKind) -> Self::ChildInfo {
466        ChildOptionInfo {
467            destination: None,
468            new_spend_kind: spend_kind.empty_copy(),
469        }
470    }
471
472    fn needs_additional_spend(_child_info: &Self::ChildInfo) -> bool {
473        false
474    }
475
476    fn finalize(
477        ctx: &mut SpendContext,
478        singleton: &mut SingletonSpend<Self>,
479        _conditions_puzzle_hash: Bytes32,
480        change_puzzle_hash: Bytes32,
481    ) -> Result<Option<SingletonSpend<Self>>, DriverError> {
482        let change_hint = ctx.hint(change_puzzle_hash)?;
483
484        let default_destination = SingletonDestination::CreateCoin(CreateCoin::new(
485            change_puzzle_hash,
486            singleton.asset.coin.amount,
487            change_hint,
488        ));
489
490        let destination = singleton
491            .child_info
492            .destination
493            .unwrap_or(default_destination);
494
495        match destination {
496            SingletonDestination::CreateCoin(destination) => {
497                // Create the new option contract coin.
498                let parent_puzzle_hash = singleton.asset.full_puzzle_hash();
499                singleton.kind.create_coin_with_assertion(
500                    ctx,
501                    parent_puzzle_hash,
502                    &mut singleton.payment_assertions,
503                    destination,
504                );
505
506                // Create a new singleton spend with the child and the new spend kind.
507                Ok(Some(SingletonSpend::new(singleton.asset.child(
508                    destination.puzzle_hash,
509                    singleton.asset.coin.amount,
510                ))))
511            }
512            SingletonDestination::Melt => {
513                // We need to emit a message to the underlying coin to exercise the option and melt it.
514                let message = singleton.asset.info.underlying_delegated_puzzle_hash.into();
515                let data = ctx.alloc(&singleton.asset.info.underlying_coin_id)?;
516
517                match &mut singleton.kind {
518                    SpendKind::Conditions(spend) => {
519                        spend.add_conditions(Conditions::new().melt_singleton().send_message(
520                            23,
521                            message,
522                            vec![data],
523                        ));
524                    }
525                    SpendKind::Settlement(_) => {
526                        return Err(DriverError::CannotEmitConditions);
527                    }
528                }
529
530                Ok(None)
531            }
532        }
533    }
534}
535
536#[derive(Debug, Clone, Copy)]
537pub enum SingletonDestination {
538    CreateCoin(CreateCoin<NodePtr>),
539    Melt,
540}
541
542#[derive(Debug, Clone)]
543pub struct ChildDidInfo {
544    pub recovery_list_hash: Option<Bytes32>,
545    pub num_verifications_required: u64,
546    pub metadata: HashedPtr,
547    pub destination: Option<SingletonDestination>,
548    pub new_spend_kind: SpendKind,
549    pub needs_update: bool,
550}
551
552#[derive(Debug, Clone)]
553pub struct ChildNftInfo {
554    pub metadata_update_spends: Vec<Spend>,
555    pub transfer_condition: Option<TransferNft>,
556    pub destination: Option<CreateCoin<NodePtr>>,
557    pub new_spend_kind: SpendKind,
558}
559
560#[derive(Debug, Clone)]
561pub struct ChildOptionInfo {
562    pub destination: Option<SingletonDestination>,
563    pub new_spend_kind: SpendKind,
564}