chia_sdk_driver/offers/
offer.rs

1use std::collections::HashSet;
2
3use chia_protocol::{Bytes32, Coin, CoinSpend, SpendBundle};
4use chia_puzzle_types::offer::SettlementPaymentsSolution;
5use chia_puzzles::SETTLEMENT_PAYMENT_HASH;
6use chia_sdk_types::{Condition, puzzles::SettlementPayment, run_puzzle};
7use clvm_traits::{FromClvm, ToClvm};
8use clvm_utils::ToTreeHash;
9use clvmr::Allocator;
10use indexmap::IndexSet;
11
12use crate::{
13    Arbitrage, AssetInfo, CatInfo, DriverError, Layer, NftInfo, OfferAmounts, OfferCoins,
14    OptionInfo, Puzzle, RequestedPayments, RoyaltyInfo, SingletonInfo, SpendContext,
15    calculate_royalty_amounts, calculate_trade_price_amounts,
16};
17
18#[derive(Debug, Clone)]
19pub struct Offer {
20    spend_bundle: SpendBundle,
21    offered_coins: OfferCoins,
22    requested_payments: RequestedPayments,
23    asset_info: AssetInfo,
24}
25
26impl Offer {
27    pub fn new(
28        spend_bundle: SpendBundle,
29        offered_coins: OfferCoins,
30        requested_payments: RequestedPayments,
31        asset_info: AssetInfo,
32    ) -> Self {
33        Self {
34            spend_bundle,
35            offered_coins,
36            requested_payments,
37            asset_info,
38        }
39    }
40
41    pub fn cancellable_coin_spends(&self) -> Result<Vec<CoinSpend>, DriverError> {
42        let mut allocator = Allocator::new();
43        let mut created_coin_ids = HashSet::new();
44
45        for coin_spend in &self.spend_bundle.coin_spends {
46            let puzzle = coin_spend.puzzle_reveal.to_clvm(&mut allocator)?;
47            let solution = coin_spend.solution.to_clvm(&mut allocator)?;
48
49            let output = run_puzzle(&mut allocator, puzzle, solution)?;
50            let conditions = Vec::<Condition>::from_clvm(&allocator, output)?;
51
52            for condition in conditions {
53                if let Some(create_coin) = condition.into_create_coin() {
54                    created_coin_ids.insert(
55                        Coin::new(
56                            coin_spend.coin.coin_id(),
57                            create_coin.puzzle_hash,
58                            create_coin.amount,
59                        )
60                        .coin_id(),
61                    );
62                }
63            }
64        }
65
66        Ok(self
67            .spend_bundle
68            .coin_spends
69            .iter()
70            .filter_map(|cs| {
71                if created_coin_ids.contains(&cs.coin.coin_id()) {
72                    None
73                } else {
74                    Some(cs.clone())
75                }
76            })
77            .collect())
78    }
79
80    pub fn spend_bundle(&self) -> &SpendBundle {
81        &self.spend_bundle
82    }
83
84    pub fn offered_coins(&self) -> &OfferCoins {
85        &self.offered_coins
86    }
87
88    pub fn requested_payments(&self) -> &RequestedPayments {
89        &self.requested_payments
90    }
91
92    pub fn asset_info(&self) -> &AssetInfo {
93        &self.asset_info
94    }
95
96    /// Returns the royalty info for requested NFTs, since those are the royalties
97    /// that need to be paid by the offered side.
98    pub fn offered_royalties(&self) -> Vec<RoyaltyInfo> {
99        self.requested_payments
100            .nfts
101            .keys()
102            .filter_map(|&launcher_id| {
103                self.asset_info.nft(launcher_id).map(|nft| {
104                    RoyaltyInfo::new(
105                        launcher_id,
106                        nft.royalty_puzzle_hash,
107                        nft.royalty_basis_points,
108                    )
109                })
110            })
111            .filter(|royalty| royalty.basis_points > 0)
112            .collect()
113    }
114
115    /// Returns the royalty info for offered NFTs, since those are the royalties
116    /// that need to be paid by the requested side.
117    pub fn requested_royalties(&self) -> Vec<RoyaltyInfo> {
118        self.offered_coins
119            .nfts
120            .values()
121            .map(|nft| {
122                RoyaltyInfo::new(
123                    nft.info.launcher_id,
124                    nft.info.royalty_puzzle_hash,
125                    nft.info.royalty_basis_points,
126                )
127            })
128            .filter(|royalty| royalty.basis_points > 0)
129            .collect()
130    }
131
132    pub fn offered_royalty_amounts(&self) -> OfferAmounts {
133        let offered_amounts = self.offered_coins.amounts();
134        let royalties = self.offered_royalties();
135        let trade_prices = calculate_trade_price_amounts(&offered_amounts, royalties.len());
136        calculate_royalty_amounts(&trade_prices, &royalties)
137    }
138
139    pub fn requested_royalty_amounts(&self) -> OfferAmounts {
140        let requested_amounts = self.requested_payments.amounts();
141        let royalties = self.requested_royalties();
142        let trade_prices = calculate_trade_price_amounts(&requested_amounts, royalties.len());
143        calculate_royalty_amounts(&trade_prices, &royalties)
144    }
145
146    pub fn arbitrage(&self) -> Arbitrage {
147        let offered = self.offered_coins.amounts();
148        let requested = self.requested_payments.amounts();
149
150        let mut arbitrage = Arbitrage::new();
151
152        if requested.xch > offered.xch {
153            arbitrage.offered.xch = requested.xch - offered.xch;
154        } else {
155            arbitrage.requested.xch = offered.xch - requested.xch;
156        }
157
158        for &asset_id in offered
159            .cats
160            .keys()
161            .chain(requested.cats.keys())
162            .collect::<IndexSet<_>>()
163        {
164            let &offered_amount = offered.cats.get(&asset_id).unwrap_or(&0);
165            let &requested_amount = requested.cats.get(&asset_id).unwrap_or(&0);
166
167            if requested_amount > offered_amount {
168                let diff = requested_amount - offered_amount;
169                arbitrage.offered.cats.insert(asset_id, diff);
170            } else {
171                let diff = offered_amount - requested_amount;
172                arbitrage.requested.cats.insert(asset_id, diff);
173            }
174        }
175
176        for &launcher_id in self
177            .offered_coins
178            .nfts
179            .keys()
180            .chain(self.requested_payments.nfts.keys())
181            .collect::<IndexSet<_>>()
182        {
183            let is_offered = self.offered_coins.nfts.contains_key(&launcher_id);
184            let is_requested = self.requested_payments.nfts.contains_key(&launcher_id);
185
186            if is_offered && !is_requested {
187                arbitrage.requested.nfts.push(launcher_id);
188            } else if !is_offered && is_requested {
189                arbitrage.offered.nfts.push(launcher_id);
190            }
191        }
192
193        for &launcher_id in self
194            .offered_coins
195            .options
196            .keys()
197            .chain(self.requested_payments.options.keys())
198            .collect::<IndexSet<_>>()
199        {
200            let is_offered = self.offered_coins.options.contains_key(&launcher_id);
201            let is_requested = self.requested_payments.options.contains_key(&launcher_id);
202
203            if is_offered && !is_requested {
204                arbitrage.requested.options.push(launcher_id);
205            } else if !is_offered && is_requested {
206                arbitrage.offered.options.push(launcher_id);
207            }
208        }
209
210        arbitrage
211    }
212
213    pub fn nonce(mut coin_ids: Vec<Bytes32>) -> Bytes32 {
214        coin_ids.sort();
215        coin_ids.tree_hash().into()
216    }
217
218    pub fn from_input_spend_bundle(
219        allocator: &mut Allocator,
220        spend_bundle: SpendBundle,
221        requested_payments: RequestedPayments,
222        requested_asset_info: AssetInfo,
223    ) -> Result<Self, DriverError> {
224        let mut offered_coins = OfferCoins::new();
225        let mut asset_info = requested_asset_info;
226
227        let spent_coin_ids: HashSet<Bytes32> = spend_bundle
228            .coin_spends
229            .iter()
230            .map(|cs| cs.coin.coin_id())
231            .collect();
232
233        for coin_spend in &spend_bundle.coin_spends {
234            let puzzle = coin_spend.puzzle_reveal.to_clvm(allocator)?;
235            let puzzle = Puzzle::parse(allocator, puzzle);
236            let solution = coin_spend.solution.to_clvm(allocator)?;
237
238            offered_coins.parse(
239                allocator,
240                &mut asset_info,
241                &spent_coin_ids,
242                coin_spend.coin,
243                puzzle,
244                solution,
245            )?;
246        }
247
248        Ok(Self::new(
249            spend_bundle,
250            offered_coins,
251            requested_payments,
252            asset_info,
253        ))
254    }
255
256    pub fn from_spend_bundle(
257        allocator: &mut Allocator,
258        spend_bundle: &SpendBundle,
259    ) -> Result<Self, DriverError> {
260        let mut input_spend_bundle =
261            SpendBundle::new(Vec::new(), spend_bundle.aggregated_signature.clone());
262        let mut offered_coins = OfferCoins::new();
263        let mut requested_payments = RequestedPayments::new();
264        let mut asset_info = AssetInfo::new();
265
266        let spent_coin_ids: HashSet<Bytes32> = spend_bundle
267            .coin_spends
268            .iter()
269            .filter_map(|cs| {
270                if cs.coin.parent_coin_info == Bytes32::default() {
271                    None
272                } else {
273                    Some(cs.coin.coin_id())
274                }
275            })
276            .collect();
277
278        for coin_spend in &spend_bundle.coin_spends {
279            let puzzle = coin_spend.puzzle_reveal.to_clvm(allocator)?;
280            let puzzle = Puzzle::parse(allocator, puzzle);
281            let solution = coin_spend.solution.to_clvm(allocator)?;
282
283            if coin_spend.coin.parent_coin_info == Bytes32::default() {
284                requested_payments.parse(allocator, &mut asset_info, puzzle, solution)?;
285            } else {
286                input_spend_bundle.coin_spends.push(coin_spend.clone());
287
288                offered_coins.parse(
289                    allocator,
290                    &mut asset_info,
291                    &spent_coin_ids,
292                    coin_spend.coin,
293                    puzzle,
294                    solution,
295                )?;
296            }
297        }
298
299        Ok(Self::new(
300            input_spend_bundle,
301            offered_coins,
302            requested_payments,
303            asset_info,
304        ))
305    }
306
307    pub fn to_spend_bundle(mut self, ctx: &mut SpendContext) -> Result<SpendBundle, DriverError> {
308        let settlement = ctx.alloc_mod::<SettlementPayment>()?;
309
310        if !self.requested_payments.xch.is_empty() {
311            let solution = SettlementPaymentsSolution::new(self.requested_payments.xch);
312
313            self.spend_bundle.coin_spends.push(CoinSpend::new(
314                Coin::new(Bytes32::default(), SETTLEMENT_PAYMENT_HASH.into(), 0),
315                ctx.serialize(&settlement)?,
316                ctx.serialize(&solution)?,
317            ));
318        }
319
320        for (asset_id, notarized_payments) in self.requested_payments.cats {
321            let cat_info = CatInfo::new(
322                asset_id,
323                self.asset_info
324                    .cat(asset_id)
325                    .and_then(|info| info.hidden_puzzle_hash),
326                SETTLEMENT_PAYMENT_HASH.into(),
327            );
328
329            let puzzle = cat_info.construct_puzzle(ctx, settlement)?;
330            let solution = SettlementPaymentsSolution::new(notarized_payments);
331
332            self.spend_bundle.coin_spends.push(CoinSpend::new(
333                Coin::new(Bytes32::default(), cat_info.puzzle_hash().into(), 0),
334                ctx.serialize(&puzzle)?,
335                ctx.serialize(&solution)?,
336            ));
337        }
338
339        for (launcher_id, notarized_payments) in self.requested_payments.nfts {
340            let info = self
341                .asset_info
342                .nft(launcher_id)
343                .ok_or(DriverError::MissingAssetInfo)?;
344
345            let nft_info = NftInfo::new(
346                launcher_id,
347                info.metadata,
348                info.metadata_updater_puzzle_hash,
349                None,
350                info.royalty_puzzle_hash,
351                info.royalty_basis_points,
352                SETTLEMENT_PAYMENT_HASH.into(),
353            );
354
355            let puzzle = nft_info.into_layers(settlement).construct_puzzle(ctx)?;
356            let solution = SettlementPaymentsSolution::new(notarized_payments);
357
358            self.spend_bundle.coin_spends.push(CoinSpend::new(
359                Coin::new(Bytes32::default(), nft_info.puzzle_hash().into(), 0),
360                ctx.serialize(&puzzle)?,
361                ctx.serialize(&solution)?,
362            ));
363        }
364
365        for (launcher_id, notarized_payments) in self.requested_payments.options {
366            let info = self
367                .asset_info
368                .option(launcher_id)
369                .ok_or(DriverError::MissingAssetInfo)?;
370
371            let option_info = OptionInfo::new(
372                launcher_id,
373                info.underlying_coin_id,
374                info.underlying_delegated_puzzle_hash,
375                SETTLEMENT_PAYMENT_HASH.into(),
376            );
377
378            let puzzle = option_info.into_layers(settlement).construct_puzzle(ctx)?;
379            let solution = SettlementPaymentsSolution::new(notarized_payments);
380
381            self.spend_bundle.coin_spends.push(CoinSpend::new(
382                Coin::new(Bytes32::default(), option_info.puzzle_hash().into(), 0),
383                ctx.serialize(&puzzle)?,
384                ctx.serialize(&solution)?,
385            ));
386        }
387
388        Ok(self.spend_bundle)
389    }
390
391    pub fn extend(&mut self, other: Self) -> Result<(), DriverError> {
392        self.spend_bundle
393            .coin_spends
394            .extend(other.spend_bundle.coin_spends);
395        self.spend_bundle.aggregated_signature += &other.spend_bundle.aggregated_signature;
396        self.offered_coins.extend(other.offered_coins)?;
397        self.requested_payments.extend(other.requested_payments)?;
398        self.asset_info.extend(other.asset_info)?;
399
400        Ok(())
401    }
402
403    pub fn take(self, spend_bundle: SpendBundle) -> SpendBundle {
404        SpendBundle::new(
405            [self.spend_bundle.coin_spends, spend_bundle.coin_spends].concat(),
406            self.spend_bundle.aggregated_signature + &spend_bundle.aggregated_signature,
407        )
408    }
409}
410
411#[cfg(test)]
412mod tests {
413    use std::slice;
414
415    use chia_puzzle_types::{
416        Memos,
417        offer::{NotarizedPayment, Payment},
418    };
419    use chia_sdk_test::{Simulator, sign_transaction};
420    use indexmap::indexmap;
421
422    use crate::{Action, Id, NftAssetInfo, Relation, SpendContext, Spends};
423
424    use super::*;
425
426    #[test]
427    fn test_offer_nft_for_nft() -> anyhow::Result<()> {
428        let mut sim = Simulator::new();
429        let mut ctx = SpendContext::new();
430
431        let alice = sim.bls(2);
432        let bob = sim.bls(0);
433
434        let alice_hint = ctx.hint(alice.puzzle_hash)?;
435        let bob_hint = ctx.hint(bob.puzzle_hash)?;
436
437        // Mint NFTs
438        let mut spends = Spends::new(alice.puzzle_hash);
439        spends.add(alice.coin);
440
441        let deltas = spends.apply(
442            &mut ctx,
443            &[
444                Action::mint_empty_royalty_nft(alice.puzzle_hash, 300),
445                Action::mint_empty_royalty_nft(bob.puzzle_hash, 300),
446                Action::send(Id::New(1), bob.puzzle_hash, 1, bob_hint),
447            ],
448        )?;
449
450        let outputs = spends.finish_with_keys(
451            &mut ctx,
452            &deltas,
453            Relation::AssertConcurrent,
454            &indexmap! { alice.puzzle_hash => alice.pk },
455        )?;
456
457        let alice_nft = outputs.nfts[&Id::New(0)];
458        let bob_nft = outputs.nfts[&Id::New(1)];
459
460        sim.spend_coins(ctx.take(), slice::from_ref(&alice.sk))?;
461
462        // Make offer
463        let mut requested_payments = RequestedPayments::new();
464        let mut requested_asset_info = AssetInfo::new();
465
466        requested_payments.nfts.insert(
467            bob_nft.info.launcher_id,
468            vec![NotarizedPayment::new(
469                Offer::nonce(vec![alice_nft.coin.coin_id()]),
470                vec![Payment::new(alice.puzzle_hash, 1, alice_hint)],
471            )],
472        );
473        requested_asset_info.insert_nft(
474            bob_nft.info.launcher_id,
475            NftAssetInfo::new(
476                bob_nft.info.metadata,
477                bob_nft.info.metadata_updater_puzzle_hash,
478                bob_nft.info.royalty_puzzle_hash,
479                bob_nft.info.royalty_basis_points,
480            ),
481        )?;
482
483        let mut spends = Spends::new(alice.puzzle_hash);
484        spends.add(alice_nft);
485
486        let deltas = spends.apply(
487            &mut ctx,
488            &[Action::send(
489                Id::Existing(alice_nft.info.launcher_id),
490                SETTLEMENT_PAYMENT_HASH.into(),
491                1,
492                Memos::None,
493            )],
494        )?;
495
496        spends.conditions.required = spends
497            .conditions
498            .required
499            .extend(requested_payments.assertions(&mut ctx, &requested_asset_info)?);
500
501        spends.finish_with_keys(
502            &mut ctx,
503            &deltas,
504            Relation::AssertConcurrent,
505            &indexmap! { alice.puzzle_hash => alice.pk },
506        )?;
507
508        let coin_spends = ctx.take();
509        let signature = sign_transaction(&coin_spends, &[alice.sk])?;
510
511        let offer = Offer::from_input_spend_bundle(
512            &mut ctx,
513            SpendBundle::new(coin_spends, signature),
514            requested_payments,
515            requested_asset_info,
516        )?;
517
518        // Take offer
519        let mut spends = Spends::new(bob.puzzle_hash);
520        spends.add(offer.offered_coins().clone());
521        spends.add(bob_nft);
522
523        let deltas = spends.apply(&mut ctx, &offer.requested_payments().actions())?;
524
525        let outputs = spends.finish_with_keys(
526            &mut ctx,
527            &deltas,
528            Relation::AssertConcurrent,
529            &indexmap! { bob.puzzle_hash => bob.pk },
530        )?;
531
532        let coin_spends = ctx.take();
533        let signature = sign_transaction(&coin_spends, &[bob.sk])?;
534
535        let spend_bundle = offer.take(SpendBundle::new(coin_spends, signature));
536
537        sim.new_transaction(spend_bundle)?;
538
539        let final_bob_nft = outputs.nfts[&Id::Existing(alice_nft.info.launcher_id)];
540        let final_alice_nft = outputs.nfts[&Id::Existing(bob_nft.info.launcher_id)];
541
542        assert_eq!(final_bob_nft.info.p2_puzzle_hash, bob.puzzle_hash);
543        assert_eq!(final_alice_nft.info.p2_puzzle_hash, alice.puzzle_hash);
544
545        Ok(())
546    }
547}