chik_sdk_driver/offers/
offer_builder.rs

1use chik_protocol::{Bytes32, Coin, CoinSpend, SpendBundle};
2use chik_puzzle_types::{
3    offer::{NotarizedPayment, Payment, SettlementPaymentsSolution},
4    Memos,
5};
6use chik_sdk_types::{announcement_id, conditions::AssertPuzzleAnnouncement};
7use indexmap::IndexMap;
8use klvm_traits::ToKlvm;
9use klvm_utils::{tree_hash, ToTreeHash, TreeHash};
10use klvmr::{Allocator, NodePtr};
11
12use crate::{DriverError, Offer, ParsedOffer, Puzzle, SpendContext};
13
14#[derive(Debug, Clone)]
15pub struct OfferBuilder<T> {
16    data: T,
17}
18
19#[derive(Debug, Clone)]
20pub struct Make {
21    nonce: Bytes32,
22    requested_payments: IndexMap<Bytes32, (Puzzle, Vec<NotarizedPayment>)>,
23    announcements: Vec<AssertPuzzleAnnouncement>,
24}
25
26#[derive(Debug, Clone)]
27pub struct Partial {
28    requested_payments: IndexMap<Bytes32, (Puzzle, Vec<NotarizedPayment>)>,
29}
30
31#[derive(Debug, Clone)]
32pub struct Take {
33    parsed_offer: ParsedOffer,
34}
35
36impl OfferBuilder<Make> {
37    pub fn new(nonce: Bytes32) -> Self {
38        Self {
39            data: Make {
40                nonce,
41                requested_payments: IndexMap::new(),
42                announcements: Vec::new(),
43            },
44        }
45    }
46
47    /// Adds a list of requested payments for a given puzzle.
48    /// It will use the nonce to create a new [`NotarizedPayment`] and add it to the requested payments.
49    pub fn request<P>(
50        self,
51        ctx: &mut SpendContext,
52        puzzle: &P,
53        payments: Vec<Payment>,
54    ) -> Result<Self, DriverError>
55    where
56        P: ToKlvm<Allocator>,
57    {
58        let nonce = self.data.nonce;
59        self.request_with_nonce(ctx, puzzle, nonce, payments)
60    }
61
62    /// Adds a list of payments in a [`NotarizedPayment`] for a given puzzle and nonce.
63    pub fn request_with_nonce<P>(
64        mut self,
65        ctx: &mut SpendContext,
66        puzzle: &P,
67        nonce: Bytes32,
68        payments: Vec<Payment>,
69    ) -> Result<Self, DriverError>
70    where
71        P: ToKlvm<Allocator>,
72    {
73        let puzzle_ptr = ctx.alloc(puzzle)?;
74        let puzzle_hash = ctx.tree_hash(puzzle_ptr).into();
75        let puzzle = Puzzle::parse(ctx, puzzle_ptr);
76
77        let notarized_payment = NotarizedPayment::new(nonce, payments);
78        let assertion = payment_assertion(
79            puzzle_hash,
80            &tree_hash_notarized_payment(ctx, &notarized_payment),
81        );
82
83        self.data
84            .requested_payments
85            .entry(puzzle_hash)
86            .or_insert_with(|| (puzzle, Vec::new()))
87            .1
88            .push(notarized_payment);
89
90        self.data.announcements.push(assertion);
91
92        Ok(self)
93    }
94
95    /// This will create a new [`OfferBuilder`] with the requested payments frozen.
96    /// It returns a list of announcements that can be asserted by the maker side.
97    pub fn finish(self) -> (Vec<AssertPuzzleAnnouncement>, OfferBuilder<Partial>) {
98        let partial = OfferBuilder {
99            data: Partial {
100                requested_payments: self.data.requested_payments,
101            },
102        };
103        (self.data.announcements, partial)
104    }
105}
106
107impl OfferBuilder<Partial> {
108    pub fn bundle(
109        self,
110        ctx: &mut SpendContext,
111        partial_spend_bundle: SpendBundle,
112    ) -> Result<Offer, DriverError> {
113        let mut spend_bundle = partial_spend_bundle;
114
115        for (puzzle_hash, (puzzle, notarized_payments)) in self.data.requested_payments {
116            let puzzle_reveal = ctx.serialize(&puzzle.ptr())?;
117            let solution = ctx.serialize(&SettlementPaymentsSolution { notarized_payments })?;
118
119            spend_bundle.coin_spends.push(CoinSpend {
120                coin: Coin::new(Bytes32::default(), puzzle_hash, 0),
121                puzzle_reveal,
122                solution,
123            });
124        }
125
126        Ok(spend_bundle.into())
127    }
128
129    /// This will use the partial spend bundle to create a new [`OfferBuilder`] for taking.
130    pub fn take(self, partial_spend_bundle: SpendBundle) -> OfferBuilder<Take> {
131        OfferBuilder {
132            data: Take {
133                parsed_offer: ParsedOffer {
134                    coin_spends: partial_spend_bundle.coin_spends,
135                    aggregated_signature: partial_spend_bundle.aggregated_signature,
136                    requested_payments: self.data.requested_payments,
137                },
138            },
139        }
140    }
141}
142
143impl OfferBuilder<Take> {
144    pub fn from_parsed_offer(parsed_offer: ParsedOffer) -> Self {
145        Self {
146            data: Take { parsed_offer },
147        }
148    }
149
150    pub fn fulfill(&mut self) -> Option<(Puzzle, Vec<NotarizedPayment>)> {
151        Some(
152            self.data
153                .parsed_offer
154                .requested_payments
155                .shift_remove_index(0)?
156                .1,
157        )
158    }
159
160    /// Must be called after [`Self::fulfill`] has been exhausted.
161    /// Creates a new [`SpendBundle`] with the completed offer.
162    pub fn bundle(self, other_spend_bundle: SpendBundle) -> SpendBundle {
163        assert_eq!(self.data.parsed_offer.requested_payments.len(), 0);
164
165        SpendBundle::aggregate(&[
166            SpendBundle::new(
167                self.data.parsed_offer.coin_spends,
168                self.data.parsed_offer.aggregated_signature,
169            ),
170            other_spend_bundle,
171        ])
172    }
173}
174
175pub fn payment_assertion(
176    puzzle_hash: Bytes32,
177    notarized_payment: &NotarizedPayment<TreeHash>,
178) -> AssertPuzzleAnnouncement {
179    AssertPuzzleAnnouncement::new(announcement_id(puzzle_hash, notarized_payment.tree_hash()))
180}
181
182pub fn tree_hash_notarized_payment(
183    allocator: &Allocator,
184    notarized_payment: &NotarizedPayment<NodePtr>,
185) -> NotarizedPayment<TreeHash> {
186    NotarizedPayment {
187        nonce: notarized_payment.nonce,
188        payments: notarized_payment
189            .payments
190            .iter()
191            .map(|payment| Payment {
192                puzzle_hash: payment.puzzle_hash,
193                amount: payment.amount,
194                memos: match payment.memos {
195                    Memos::Some(memos) => Memos::Some(tree_hash(allocator, memos)),
196                    Memos::None => Memos::None,
197                },
198            })
199            .collect(),
200    }
201}