1#![cfg_attr(not(feature = "std"), no_std)]
78
79extern crate alloc;
80
81pub mod weights;
82
83mod benchmarking;
84#[cfg(test)]
85mod mock;
86#[cfg(test)]
87mod tests;
88
89pub use pallet::*;
90pub use weights::WeightInfo;
91
92use alloc::{vec, vec::Vec};
93use frame::prelude::*;
94use fungible::{
95 Balanced as FunBalanced, Inspect as FunInspect, Mutate as FunMutate,
96 MutateHold as FunMutateHold,
97};
98use nonfungible::{Inspect as NftInspect, Transfer as NftTransfer};
99use tokens::{Balance, Restriction::*};
100use Fortitude::*;
101use Precision::*;
102use Preservation::*;
103
104pub struct WithMaximumOf<A: TypedGet>(core::marker::PhantomData<A>);
105impl<A: TypedGet> Convert<Perquintill, A::Type> for WithMaximumOf<A>
106where
107 A::Type: Clone + Unsigned + From<u64>,
108 u64: TryFrom<A::Type>,
109{
110 fn convert(a: Perquintill) -> A::Type {
111 a * A::get()
112 }
113}
114impl<A: TypedGet> ConvertBack<Perquintill, A::Type> for WithMaximumOf<A>
115where
116 A::Type: RationalArg + From<u64>,
117 u64: TryFrom<A::Type>,
118 u128: TryFrom<A::Type>,
119{
120 fn convert_back(a: A::Type) -> Perquintill {
121 Perquintill::from_rational(a, A::get())
122 }
123}
124
125pub struct NoCounterpart<T>(core::marker::PhantomData<T>);
126impl<T> FunInspect<T> for NoCounterpart<T> {
127 type Balance = u32;
128 fn total_issuance() -> u32 {
129 0
130 }
131 fn minimum_balance() -> u32 {
132 0
133 }
134 fn balance(_: &T) -> u32 {
135 0
136 }
137 fn total_balance(_: &T) -> u32 {
138 0
139 }
140 fn reducible_balance(_: &T, _: Preservation, _: Fortitude) -> u32 {
141 0
142 }
143 fn can_deposit(_: &T, _: u32, _: Provenance) -> DepositConsequence {
144 DepositConsequence::Success
145 }
146 fn can_withdraw(_: &T, _: u32) -> WithdrawConsequence<u32> {
147 WithdrawConsequence::Success
148 }
149}
150impl<T> fungible::Unbalanced<T> for NoCounterpart<T> {
151 fn handle_dust(_: fungible::Dust<T, Self>) {}
152 fn write_balance(_: &T, _: Self::Balance) -> Result<Option<Self::Balance>, DispatchError> {
153 Ok(None)
154 }
155 fn set_total_issuance(_: Self::Balance) {}
156}
157impl<T: Eq> FunMutate<T> for NoCounterpart<T> {}
158impl<T> Convert<Perquintill, u32> for NoCounterpart<T> {
159 fn convert(_: Perquintill) -> u32 {
160 0
161 }
162}
163
164pub trait BenchmarkSetup {
166 fn create_counterpart_asset();
170}
171
172impl BenchmarkSetup for () {
173 fn create_counterpart_asset() {}
174}
175
176#[frame::pallet]
177pub mod pallet {
178 use super::*;
179
180 type BalanceOf<T> =
181 <<T as Config>::Currency as FunInspect<<T as frame_system::Config>::AccountId>>::Balance;
182 type DebtOf<T> =
183 fungible::Debt<<T as frame_system::Config>::AccountId, <T as Config>::Currency>;
184 type ReceiptRecordOf<T> =
185 ReceiptRecord<<T as frame_system::Config>::AccountId, BlockNumberFor<T>, BalanceOf<T>>;
186 type IssuanceInfoOf<T> = IssuanceInfo<BalanceOf<T>>;
187 type SummaryRecordOf<T> = SummaryRecord<BlockNumberFor<T>, BalanceOf<T>>;
188 type BidOf<T> = Bid<BalanceOf<T>, <T as frame_system::Config>::AccountId>;
189 type QueueTotalsTypeOf<T> = BoundedVec<(u32, BalanceOf<T>), <T as Config>::QueueCount>;
190
191 #[pallet::config]
192 pub trait Config: frame_system::Config {
193 type WeightInfo: WeightInfo;
195
196 #[allow(deprecated)]
198 type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
199
200 #[pallet::constant]
202 type PalletId: Get<PalletId>;
203
204 type Currency: FunInspect<Self::AccountId, Balance = Self::CurrencyBalance>
206 + FunMutate<Self::AccountId>
207 + FunBalanced<Self::AccountId>
208 + FunMutateHold<Self::AccountId, Reason = Self::RuntimeHoldReason>;
209
210 type RuntimeHoldReason: From<HoldReason>;
212
213 type CurrencyBalance: Balance + From<u64>;
216
217 type FundOrigin: EnsureOrigin<Self::RuntimeOrigin>;
219
220 type IgnoredIssuance: Get<BalanceOf<Self>>;
223
224 type Counterpart: FunMutate<Self::AccountId>;
226
227 type CounterpartAmount: ConvertBack<
232 Perquintill,
233 <Self::Counterpart as FunInspect<Self::AccountId>>::Balance,
234 >;
235
236 type Deficit: OnUnbalanced<DebtOf<Self>>;
239
240 type Target: Get<Perquintill>;
242
243 #[pallet::constant]
246 type QueueCount: Get<u32>;
247
248 #[pallet::constant]
252 type MaxQueueLen: Get<u32>;
253
254 #[pallet::constant]
258 type FifoQueueLen: Get<u32>;
259
260 #[pallet::constant]
263 type BasePeriod: Get<BlockNumberFor<Self>>;
264
265 #[pallet::constant]
272 type MinBid: Get<BalanceOf<Self>>;
273
274 #[pallet::constant]
277 type MinReceipt: Get<Perquintill>;
278
279 #[pallet::constant]
284 type IntakePeriod: Get<BlockNumberFor<Self>>;
285
286 #[pallet::constant]
290 type MaxIntakeWeight: Get<Weight>;
291
292 #[pallet::constant]
294 type ThawThrottle: Get<(Perquintill, BlockNumberFor<Self>)>;
295
296 #[cfg(feature = "runtime-benchmarks")]
298 type BenchmarkSetup: crate::BenchmarkSetup;
299 }
300
301 #[pallet::pallet]
302 pub struct Pallet<T>(_);
303
304 #[derive(Clone, Eq, PartialEq, Default, Encode, Decode, Debug, TypeInfo, MaxEncodedLen)]
306 pub struct Bid<Balance, AccountId> {
307 pub amount: Balance,
309 pub who: AccountId,
311 }
312
313 #[derive(Clone, Eq, PartialEq, Default, Encode, Decode, Debug, TypeInfo, MaxEncodedLen)]
315 pub struct ReceiptRecord<AccountId, BlockNumber, Balance> {
316 pub proportion: Perquintill,
318 pub owner: Option<(AccountId, Balance)>,
322 pub expiry: BlockNumber,
324 }
325
326 pub type ReceiptIndex = u32;
328
329 #[derive(Clone, Eq, PartialEq, Default, Encode, Decode, Debug, TypeInfo, MaxEncodedLen)]
338 pub struct SummaryRecord<BlockNumber, Balance> {
339 pub proportion_owed: Perquintill,
341 pub index: ReceiptIndex,
343 pub thawed: Perquintill,
345 pub last_period: BlockNumber,
347 pub receipts_on_hold: Balance,
350 }
351
352 pub struct OnEmptyQueueTotals<T>(core::marker::PhantomData<T>);
353 impl<T: Config> Get<QueueTotalsTypeOf<T>> for OnEmptyQueueTotals<T> {
354 fn get() -> QueueTotalsTypeOf<T> {
355 BoundedVec::truncate_from(vec![
356 (0, Zero::zero());
357 <T as Config>::QueueCount::get() as usize
358 ])
359 }
360 }
361
362 #[pallet::storage]
368 pub type QueueTotals<T: Config> =
369 StorageValue<_, QueueTotalsTypeOf<T>, ValueQuery, OnEmptyQueueTotals<T>>;
370
371 #[pallet::storage]
373 pub type Queues<T: Config> =
374 StorageMap<_, Blake2_128Concat, u32, BoundedVec<BidOf<T>, T::MaxQueueLen>, ValueQuery>;
375
376 #[pallet::storage]
378 pub type Summary<T> = StorageValue<_, SummaryRecordOf<T>, ValueQuery>;
379
380 #[pallet::storage]
382 pub type Receipts<T> =
383 StorageMap<_, Blake2_128Concat, ReceiptIndex, ReceiptRecordOf<T>, OptionQuery>;
384
385 #[pallet::event]
386 #[pallet::generate_deposit(pub(super) fn deposit_event)]
387 pub enum Event<T: Config> {
388 BidPlaced { who: T::AccountId, amount: BalanceOf<T>, duration: u32 },
390 BidRetracted { who: T::AccountId, amount: BalanceOf<T>, duration: u32 },
392 BidDropped { who: T::AccountId, amount: BalanceOf<T>, duration: u32 },
394 Issued {
396 index: ReceiptIndex,
398 expiry: BlockNumberFor<T>,
400 who: T::AccountId,
402 proportion: Perquintill,
404 amount: BalanceOf<T>,
406 },
407 Thawed {
409 index: ReceiptIndex,
411 who: T::AccountId,
413 proportion: Perquintill,
415 amount: BalanceOf<T>,
417 dropped: bool,
419 },
420 Funded { deficit: BalanceOf<T> },
422 Transferred { from: T::AccountId, to: T::AccountId, index: ReceiptIndex },
424 }
425
426 #[pallet::error]
427 pub enum Error<T> {
428 DurationTooSmall,
430 DurationTooBig,
432 AmountTooSmall,
434 BidTooLow,
437 UnknownReceipt,
439 NotOwner,
441 NotExpired,
443 UnknownBid,
445 PortionTooBig,
447 Unfunded,
449 AlreadyFunded,
451 Throttled,
453 MakesDust,
455 AlreadyCommunal,
457 AlreadyPrivate,
459 }
460
461 #[pallet::composite_enum]
463 pub enum HoldReason {
464 #[codec(index = 0)]
466 NftReceipt,
467 }
468
469 pub(crate) struct WeightCounter {
470 pub(crate) used: Weight,
471 pub(crate) limit: Weight,
472 }
473 impl WeightCounter {
474 #[allow(dead_code)]
475 pub(crate) fn unlimited() -> Self {
476 WeightCounter { used: Weight::zero(), limit: Weight::max_value() }
477 }
478 fn check_accrue(&mut self, w: Weight) -> bool {
479 let test = self.used.saturating_add(w);
480 if test.any_gt(self.limit) {
481 false
482 } else {
483 self.used = test;
484 true
485 }
486 }
487 fn can_accrue(&mut self, w: Weight) -> bool {
488 self.used.saturating_add(w).all_lte(self.limit)
489 }
490 }
491
492 #[pallet::hooks]
493 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
494 fn on_initialize(n: BlockNumberFor<T>) -> Weight {
495 let mut weight_counter =
496 WeightCounter { used: Weight::zero(), limit: T::MaxIntakeWeight::get() };
497 if T::IntakePeriod::get().is_zero() || (n % T::IntakePeriod::get()).is_zero() {
498 if weight_counter.check_accrue(T::WeightInfo::process_queues()) {
499 Self::process_queues(
500 T::Target::get(),
501 T::QueueCount::get(),
502 u32::max_value(),
503 &mut weight_counter,
504 )
505 }
506 }
507 weight_counter.used
508 }
509
510 fn integrity_test() {
511 assert!(!T::IntakePeriod::get().is_zero());
512 assert!(!T::MaxQueueLen::get().is_zero());
513 }
514 }
515
516 #[pallet::call]
517 impl<T: Config> Pallet<T> {
518 #[pallet::call_index(0)]
530 #[pallet::weight(T::WeightInfo::place_bid_max())]
531 pub fn place_bid(
532 origin: OriginFor<T>,
533 #[pallet::compact] amount: BalanceOf<T>,
534 duration: u32,
535 ) -> DispatchResult {
536 let who = ensure_signed(origin)?;
537
538 ensure!(amount >= T::MinBid::get(), Error::<T>::AmountTooSmall);
539 let queue_count = T::QueueCount::get() as usize;
540 let queue_index = duration.checked_sub(1).ok_or(Error::<T>::DurationTooSmall)? as usize;
541 ensure!(queue_index < queue_count, Error::<T>::DurationTooBig);
542
543 let net = Queues::<T>::try_mutate(
544 duration,
545 |q| -> Result<(u32, BalanceOf<T>), DispatchError> {
546 let queue_full = q.len() == T::MaxQueueLen::get() as usize;
547 ensure!(!queue_full || q[0].amount < amount, Error::<T>::BidTooLow);
548 T::Currency::hold(&HoldReason::NftReceipt.into(), &who, amount)?;
549
550 let mut bid = Bid { amount, who: who.clone() };
552 let net = if queue_full {
553 core::mem::swap(&mut q[0], &mut bid);
554 let _ = T::Currency::release(
555 &HoldReason::NftReceipt.into(),
556 &bid.who,
557 bid.amount,
558 BestEffort,
559 );
560 Self::deposit_event(Event::<T>::BidDropped {
561 who: bid.who,
562 amount: bid.amount,
563 duration,
564 });
565 (0, amount - bid.amount)
566 } else {
567 q.try_insert(0, bid).expect("verified queue was not full above. qed.");
568 (1, amount)
569 };
570
571 let sorted_item_count = q.len().saturating_sub(T::FifoQueueLen::get() as usize);
572 if sorted_item_count > 1 {
573 q[0..sorted_item_count].sort_by_key(|x| x.amount);
574 }
575
576 Ok(net)
577 },
578 )?;
579 QueueTotals::<T>::mutate(|qs| {
580 qs.bounded_resize(queue_count, (0, Zero::zero()));
581 qs[queue_index].0 += net.0;
582 qs[queue_index].1.saturating_accrue(net.1);
583 });
584 Self::deposit_event(Event::BidPlaced { who, amount, duration });
585
586 Ok(())
587 }
588
589 #[pallet::call_index(1)]
597 #[pallet::weight(T::WeightInfo::retract_bid(T::MaxQueueLen::get()))]
598 pub fn retract_bid(
599 origin: OriginFor<T>,
600 #[pallet::compact] amount: BalanceOf<T>,
601 duration: u32,
602 ) -> DispatchResult {
603 let who = ensure_signed(origin)?;
604
605 let queue_count = T::QueueCount::get() as usize;
606 let queue_index = duration.checked_sub(1).ok_or(Error::<T>::DurationTooSmall)? as usize;
607 ensure!(queue_index < queue_count, Error::<T>::DurationTooBig);
608
609 let bid = Bid { amount, who };
610
611 let mut queue = Queues::<T>::get(duration);
612 let pos = queue.iter().position(|i| i == &bid).ok_or(Error::<T>::UnknownBid)?;
613 queue.remove(pos);
614 let new_len = queue.len() as u32;
615
616 T::Currency::release(&HoldReason::NftReceipt.into(), &bid.who, bid.amount, BestEffort)?;
617
618 Queues::<T>::insert(duration, queue);
619 QueueTotals::<T>::mutate(|qs| {
620 qs.bounded_resize(queue_count, (0, Zero::zero()));
621 qs[queue_index].0 = new_len;
622 qs[queue_index].1.saturating_reduce(bid.amount);
623 });
624
625 Self::deposit_event(Event::BidRetracted { who: bid.who, amount: bid.amount, duration });
626
627 Ok(())
628 }
629
630 #[pallet::call_index(2)]
634 #[pallet::weight(T::WeightInfo::fund_deficit())]
635 pub fn fund_deficit(origin: OriginFor<T>) -> DispatchResult {
636 T::FundOrigin::ensure_origin(origin)?;
637 let summary: SummaryRecordOf<T> = Summary::<T>::get();
638 let our_account = Self::account_id();
639 let issuance = Self::issuance_with(&our_account, &summary);
640 let deficit = issuance.required.saturating_sub(issuance.holdings);
641 ensure!(!deficit.is_zero(), Error::<T>::AlreadyFunded);
642 T::Deficit::on_unbalanced(T::Currency::deposit(&our_account, deficit, Exact)?);
643 Self::deposit_event(Event::<T>::Funded { deficit });
644 Ok(())
645 }
646
647 #[pallet::call_index(3)]
656 #[pallet::weight(T::WeightInfo::thaw_private())]
657 pub fn thaw_private(
658 origin: OriginFor<T>,
659 #[pallet::compact] index: ReceiptIndex,
660 maybe_proportion: Option<Perquintill>,
661 ) -> DispatchResult {
662 let who = ensure_signed(origin)?;
663
664 let mut receipt: ReceiptRecordOf<T> =
666 Receipts::<T>::get(index).ok_or(Error::<T>::UnknownReceipt)?;
667 let (owner, mut on_hold) = receipt.owner.ok_or(Error::<T>::AlreadyCommunal)?;
669 ensure!(owner == who, Error::<T>::NotOwner);
670
671 let now = frame_system::Pallet::<T>::block_number();
672 ensure!(now >= receipt.expiry, Error::<T>::NotExpired);
673
674 let mut summary: SummaryRecordOf<T> = Summary::<T>::get();
675
676 let proportion = if let Some(proportion) = maybe_proportion {
677 ensure!(proportion <= receipt.proportion, Error::<T>::PortionTooBig);
678 let remaining = receipt.proportion.saturating_sub(proportion);
679 ensure!(
680 remaining.is_zero() || remaining >= T::MinReceipt::get(),
681 Error::<T>::MakesDust
682 );
683 proportion
684 } else {
685 receipt.proportion
686 };
687
688 let (throttle, throttle_period) = T::ThawThrottle::get();
689 if now.saturating_sub(summary.last_period) >= throttle_period {
690 summary.thawed = Zero::zero();
691 summary.last_period = now;
692 }
693 summary.thawed.saturating_accrue(proportion);
694 ensure!(summary.thawed <= throttle, Error::<T>::Throttled);
695
696 let our_account = Self::account_id();
698 let effective_issuance = Self::issuance_with(&our_account, &summary).effective;
699 let amount = proportion * effective_issuance;
701
702 receipt.proportion.saturating_reduce(proportion);
703 summary.proportion_owed.saturating_reduce(proportion);
704
705 let dropped = receipt.proportion.is_zero();
706
707 if amount > on_hold {
708 T::Currency::release(&HoldReason::NftReceipt.into(), &who, on_hold, Exact)?;
709 let deficit = amount - on_hold;
710 summary.receipts_on_hold.saturating_reduce(on_hold);
712 on_hold = Zero::zero();
713 T::Currency::transfer(&our_account, &who, deficit, Expendable)
714 .map_err(|_| Error::<T>::Unfunded)?;
715 } else {
716 on_hold.saturating_reduce(amount);
717 summary.receipts_on_hold.saturating_reduce(amount);
718 if dropped && !on_hold.is_zero() {
719 T::Currency::transfer_on_hold(
723 &HoldReason::NftReceipt.into(),
724 &who,
725 &our_account,
726 on_hold,
727 Exact,
728 Free,
729 Polite,
730 )
731 .map(|_| ())
732 .or_else(|e| {
735 if e == TokenError::CannotCreate.into() {
736 Ok(())
737 } else {
738 Err(e)
739 }
740 })?;
741 summary.receipts_on_hold.saturating_reduce(on_hold);
742 }
743 T::Currency::release(&HoldReason::NftReceipt.into(), &who, amount, Exact)?;
744 }
745
746 if dropped {
747 Receipts::<T>::remove(index);
748 } else {
749 receipt.owner = Some((owner, on_hold));
750 Receipts::<T>::insert(index, &receipt);
751 }
752 Summary::<T>::put(&summary);
753
754 Self::deposit_event(Event::Thawed { index, who, amount, proportion, dropped });
755
756 Ok(())
757 }
758
759 #[pallet::call_index(4)]
766 #[pallet::weight(T::WeightInfo::thaw_communal())]
767 pub fn thaw_communal(
768 origin: OriginFor<T>,
769 #[pallet::compact] index: ReceiptIndex,
770 ) -> DispatchResult {
771 let who = ensure_signed(origin)?;
772
773 let receipt: ReceiptRecordOf<T> =
775 Receipts::<T>::get(index).ok_or(Error::<T>::UnknownReceipt)?;
776 ensure!(receipt.owner.is_none(), Error::<T>::NotOwner);
778 let now = frame_system::Pallet::<T>::block_number();
779 ensure!(now >= receipt.expiry, Error::<T>::NotExpired);
780
781 let mut summary: SummaryRecordOf<T> = Summary::<T>::get();
782
783 let (throttle, throttle_period) = T::ThawThrottle::get();
784 if now.saturating_sub(summary.last_period) >= throttle_period {
785 summary.thawed = Zero::zero();
786 summary.last_period = now;
787 }
788 summary.thawed.saturating_accrue(receipt.proportion);
789 ensure!(summary.thawed <= throttle, Error::<T>::Throttled);
790
791 let cp_amount = T::CounterpartAmount::convert(receipt.proportion);
792 T::Counterpart::burn_from(&who, cp_amount, Expendable, Exact, Polite)?;
793
794 let our_account = Self::account_id();
796 let effective_issuance = Self::issuance_with(&our_account, &summary).effective;
797 let amount = receipt.proportion * effective_issuance;
798
799 summary.proportion_owed.saturating_reduce(receipt.proportion);
800
801 T::Currency::transfer(&our_account, &who, amount, Expendable)
803 .map_err(|_| Error::<T>::Unfunded)?;
804
805 Receipts::<T>::remove(index);
806 Summary::<T>::put(&summary);
807
808 let e =
809 Event::Thawed { index, who, amount, proportion: receipt.proportion, dropped: true };
810 Self::deposit_event(e);
811
812 Ok(())
813 }
814
815 #[pallet::call_index(5)]
817 #[pallet::weight(T::WeightInfo::communify())]
818 pub fn communify(
819 origin: OriginFor<T>,
820 #[pallet::compact] index: ReceiptIndex,
821 ) -> DispatchResult {
822 let who = ensure_signed(origin)?;
823
824 let mut receipt: ReceiptRecordOf<T> =
826 Receipts::<T>::get(index).ok_or(Error::<T>::UnknownReceipt)?;
827
828 let (owner, on_hold) = receipt.owner.take().ok_or(Error::<T>::AlreadyCommunal)?;
830
831 ensure!(owner == who, Error::<T>::NotOwner);
833
834 let reason = HoldReason::NftReceipt.into();
836 let us = Self::account_id();
837 T::Currency::transfer_on_hold(&reason, &who, &us, on_hold, Exact, Free, Polite)
838 .map_err(|_| Error::<T>::Unfunded)?;
839
840 let mut summary: SummaryRecordOf<T> = Summary::<T>::get();
842 summary.receipts_on_hold.saturating_reduce(on_hold);
843 Summary::<T>::put(&summary);
844 Receipts::<T>::insert(index, &receipt);
845
846 let fung_eq = T::CounterpartAmount::convert(receipt.proportion);
848 let _ = T::Counterpart::mint_into(&who, fung_eq).defensive();
849
850 Ok(())
851 }
852
853 #[pallet::call_index(6)]
855 #[pallet::weight(T::WeightInfo::privatize())]
856 pub fn privatize(
857 origin: OriginFor<T>,
858 #[pallet::compact] index: ReceiptIndex,
859 ) -> DispatchResult {
860 let who = ensure_signed(origin)?;
861
862 let mut receipt: ReceiptRecordOf<T> =
864 Receipts::<T>::get(index).ok_or(Error::<T>::UnknownReceipt)?;
865
866 ensure!(receipt.owner.is_none(), Error::<T>::AlreadyPrivate);
868
869 let mut summary: SummaryRecordOf<T> = Summary::<T>::get();
871 let our_account = Self::account_id();
872 let effective_issuance = Self::issuance_with(&our_account, &summary).effective;
873 let max_amount = receipt.proportion * effective_issuance;
874 let amount = max_amount.min(T::Currency::balance(&our_account));
876
877 T::Counterpart::burn_from(
879 &who,
880 T::CounterpartAmount::convert(receipt.proportion),
881 Expendable,
882 Exact,
883 Polite,
884 )?;
885
886 let reason = HoldReason::NftReceipt.into();
888 let us = Self::account_id();
889 T::Currency::transfer_and_hold(&reason, &us, &who, amount, Exact, Expendable, Polite)?;
890
891 summary.receipts_on_hold.saturating_accrue(amount);
893
894 receipt.owner = Some((who, amount));
895
896 Summary::<T>::put(&summary);
897 Receipts::<T>::insert(index, &receipt);
898
899 Ok(())
900 }
901 }
902
903 #[derive(Debug)]
905 pub struct IssuanceInfo<Balance> {
906 pub holdings: Balance,
909 pub other: Balance,
911 pub effective: Balance,
914 pub required: Balance,
917 }
918
919 impl<T: Config> NftInspect<T::AccountId> for Pallet<T> {
920 type ItemId = ReceiptIndex;
921
922 fn owner(item: &ReceiptIndex) -> Option<T::AccountId> {
923 Receipts::<T>::get(item).and_then(|r| r.owner).map(|(who, _)| who)
924 }
925
926 fn attribute(item: &Self::ItemId, key: &[u8]) -> Option<Vec<u8>> {
927 let item = Receipts::<T>::get(item)?;
928 match key {
929 b"proportion" => Some(item.proportion.encode()),
930 b"expiry" => Some(item.expiry.encode()),
931 b"owner" => item.owner.as_ref().map(|x| x.0.encode()),
932 b"on_hold" => item.owner.as_ref().map(|x| x.1.encode()),
933 _ => None,
934 }
935 }
936 }
937
938 impl<T: Config> NftTransfer<T::AccountId> for Pallet<T> {
939 fn transfer(index: &ReceiptIndex, dest: &T::AccountId) -> DispatchResult {
940 let mut item = Receipts::<T>::get(index).ok_or(TokenError::UnknownAsset)?;
941 let (owner, on_hold) = item.owner.take().ok_or(Error::<T>::AlreadyCommunal)?;
942
943 let reason = HoldReason::NftReceipt.into();
944 T::Currency::transfer_on_hold(&reason, &owner, dest, on_hold, Exact, OnHold, Polite)?;
945
946 item.owner = Some((dest.clone(), on_hold));
947 Receipts::<T>::insert(&index, &item);
948 Pallet::<T>::deposit_event(Event::<T>::Transferred {
949 from: owner,
950 to: dest.clone(),
951 index: *index,
952 });
953 Ok(())
954 }
955 }
956
957 impl<T: Config> Pallet<T> {
958 pub fn account_id() -> T::AccountId {
963 T::PalletId::get().into_account_truncating()
964 }
965
966 pub fn issuance() -> IssuanceInfo<BalanceOf<T>> {
968 Self::issuance_with(&Self::account_id(), &Summary::<T>::get())
969 }
970
971 pub fn issuance_with(
980 our_account: &T::AccountId,
981 summary: &SummaryRecordOf<T>,
982 ) -> IssuanceInfo<BalanceOf<T>> {
983 let total_issuance =
984 T::Currency::active_issuance().saturating_sub(T::IgnoredIssuance::get());
985 let holdings =
986 T::Currency::balance(our_account).saturating_add(summary.receipts_on_hold);
987 let other = total_issuance.saturating_sub(holdings);
988 let effective =
989 summary.proportion_owed.left_from_one().saturating_reciprocal_mul(other);
990 let required = summary.proportion_owed * effective;
991 IssuanceInfo { holdings, other, effective, required }
992 }
993
994 pub(crate) fn process_queues(
1000 target: Perquintill,
1001 max_queues: u32,
1002 max_bids: u32,
1003 weight: &mut WeightCounter,
1004 ) {
1005 let mut summary: SummaryRecordOf<T> = Summary::<T>::get();
1006 if summary.proportion_owed >= target {
1007 return
1008 }
1009
1010 let now = frame_system::Pallet::<T>::block_number();
1011 let our_account = Self::account_id();
1012 let issuance: IssuanceInfoOf<T> = Self::issuance_with(&our_account, &summary);
1013 let mut remaining = target.saturating_sub(summary.proportion_owed) * issuance.effective;
1014
1015 let mut queues_hit = 0;
1016 let mut bids_hit = 0;
1017 let mut totals = QueueTotals::<T>::get();
1018 let queue_count = T::QueueCount::get();
1019 totals.bounded_resize(queue_count as usize, (0, Zero::zero()));
1020 for duration in (1..=queue_count).rev() {
1021 if totals[duration as usize - 1].0.is_zero() {
1022 continue
1023 }
1024 if remaining.is_zero() || queues_hit >= max_queues
1025 || !weight.check_accrue(T::WeightInfo::process_queue())
1026 || !weight.can_accrue(T::WeightInfo::process_bid())
1028 {
1029 break
1030 }
1031
1032 let b = Self::process_queue(
1033 duration,
1034 now,
1035 &our_account,
1036 &issuance,
1037 max_bids.saturating_sub(bids_hit),
1038 &mut remaining,
1039 &mut totals[duration as usize - 1],
1040 &mut summary,
1041 weight,
1042 );
1043
1044 bids_hit.saturating_accrue(b);
1045 queues_hit.saturating_inc();
1046 }
1047 QueueTotals::<T>::put(&totals);
1048 Summary::<T>::put(&summary);
1049 }
1050
1051 pub(crate) fn process_queue(
1052 duration: u32,
1053 now: BlockNumberFor<T>,
1054 our_account: &T::AccountId,
1055 issuance: &IssuanceInfo<BalanceOf<T>>,
1056 max_bids: u32,
1057 remaining: &mut BalanceOf<T>,
1058 queue_total: &mut (u32, BalanceOf<T>),
1059 summary: &mut SummaryRecordOf<T>,
1060 weight: &mut WeightCounter,
1061 ) -> u32 {
1062 let mut queue: BoundedVec<BidOf<T>, _> = Queues::<T>::get(&duration);
1063 let expiry = now.saturating_add(T::BasePeriod::get().saturating_mul(duration.into()));
1064 let mut count = 0;
1065
1066 while count < max_bids &&
1067 !queue.is_empty() &&
1068 !remaining.is_zero() &&
1069 weight.check_accrue(T::WeightInfo::process_bid())
1070 {
1071 let bid = match queue.pop() {
1072 Some(b) => b,
1073 None => break,
1074 };
1075 if let Some(bid) = Self::process_bid(
1076 bid,
1077 expiry,
1078 our_account,
1079 issuance,
1080 remaining,
1081 &mut queue_total.1,
1082 summary,
1083 ) {
1084 queue.try_push(bid).expect("just popped, so there must be space. qed");
1085 }
1088 count.saturating_inc();
1089 }
1090 queue_total.0 = queue.len() as u32;
1091 Queues::<T>::insert(&duration, &queue);
1092 count
1093 }
1094
1095 pub(crate) fn process_bid(
1096 mut bid: BidOf<T>,
1097 expiry: BlockNumberFor<T>,
1098 _our_account: &T::AccountId,
1099 issuance: &IssuanceInfo<BalanceOf<T>>,
1100 remaining: &mut BalanceOf<T>,
1101 queue_amount: &mut BalanceOf<T>,
1102 summary: &mut SummaryRecordOf<T>,
1103 ) -> Option<BidOf<T>> {
1104 let result = if *remaining < bid.amount {
1105 let overflow = bid.amount - *remaining;
1106 bid.amount = *remaining;
1107 Some(Bid { amount: overflow, who: bid.who.clone() })
1108 } else {
1109 None
1110 };
1111 let amount = bid.amount;
1112 summary.receipts_on_hold.saturating_accrue(amount);
1113
1114 remaining.saturating_reduce(amount);
1116 queue_amount.defensive_saturating_reduce(amount);
1119
1120 let n = amount;
1122 let d = issuance.effective;
1123 let proportion = Perquintill::from_rational_with_rounding(n, d, Rounding::Down)
1124 .defensive_unwrap_or_default();
1125 let who = bid.who;
1126 let index = summary.index;
1127 summary.proportion_owed.defensive_saturating_accrue(proportion);
1128 summary.index += 1;
1129
1130 let e = Event::Issued { index, expiry, who: who.clone(), amount, proportion };
1131 Self::deposit_event(e);
1132 let receipt = ReceiptRecord { proportion, owner: Some((who, amount)), expiry };
1133 Receipts::<T>::insert(index, receipt);
1134
1135 result
1136 }
1137 }
1138}