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 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 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 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 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 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}