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(
306 Clone, Eq, PartialEq, Default, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen,
307 )]
308 pub struct Bid<Balance, AccountId> {
309 pub amount: Balance,
311 pub who: AccountId,
313 }
314
315 #[derive(
317 Clone, Eq, PartialEq, Default, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen,
318 )]
319 pub struct ReceiptRecord<AccountId, BlockNumber, Balance> {
320 pub proportion: Perquintill,
322 pub owner: Option<(AccountId, Balance)>,
326 pub expiry: BlockNumber,
328 }
329
330 pub type ReceiptIndex = u32;
332
333 #[derive(
342 Clone, Eq, PartialEq, Default, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen,
343 )]
344 pub struct SummaryRecord<BlockNumber, Balance> {
345 pub proportion_owed: Perquintill,
347 pub index: ReceiptIndex,
349 pub thawed: Perquintill,
351 pub last_period: BlockNumber,
353 pub receipts_on_hold: Balance,
356 }
357
358 pub struct OnEmptyQueueTotals<T>(core::marker::PhantomData<T>);
359 impl<T: Config> Get<QueueTotalsTypeOf<T>> for OnEmptyQueueTotals<T> {
360 fn get() -> QueueTotalsTypeOf<T> {
361 BoundedVec::truncate_from(vec![
362 (0, Zero::zero());
363 <T as Config>::QueueCount::get() as usize
364 ])
365 }
366 }
367
368 #[pallet::storage]
374 pub type QueueTotals<T: Config> =
375 StorageValue<_, QueueTotalsTypeOf<T>, ValueQuery, OnEmptyQueueTotals<T>>;
376
377 #[pallet::storage]
379 pub type Queues<T: Config> =
380 StorageMap<_, Blake2_128Concat, u32, BoundedVec<BidOf<T>, T::MaxQueueLen>, ValueQuery>;
381
382 #[pallet::storage]
384 pub type Summary<T> = StorageValue<_, SummaryRecordOf<T>, ValueQuery>;
385
386 #[pallet::storage]
388 pub type Receipts<T> =
389 StorageMap<_, Blake2_128Concat, ReceiptIndex, ReceiptRecordOf<T>, OptionQuery>;
390
391 #[pallet::event]
392 #[pallet::generate_deposit(pub(super) fn deposit_event)]
393 pub enum Event<T: Config> {
394 BidPlaced { who: T::AccountId, amount: BalanceOf<T>, duration: u32 },
396 BidRetracted { who: T::AccountId, amount: BalanceOf<T>, duration: u32 },
398 BidDropped { who: T::AccountId, amount: BalanceOf<T>, duration: u32 },
400 Issued {
402 index: ReceiptIndex,
404 expiry: BlockNumberFor<T>,
406 who: T::AccountId,
408 proportion: Perquintill,
410 amount: BalanceOf<T>,
412 },
413 Thawed {
415 index: ReceiptIndex,
417 who: T::AccountId,
419 proportion: Perquintill,
421 amount: BalanceOf<T>,
423 dropped: bool,
425 },
426 Funded { deficit: BalanceOf<T> },
428 Transferred { from: T::AccountId, to: T::AccountId, index: ReceiptIndex },
430 }
431
432 #[pallet::error]
433 pub enum Error<T> {
434 DurationTooSmall,
436 DurationTooBig,
438 AmountTooSmall,
440 BidTooLow,
443 UnknownReceipt,
445 NotOwner,
447 NotExpired,
449 UnknownBid,
451 PortionTooBig,
453 Unfunded,
455 AlreadyFunded,
457 Throttled,
459 MakesDust,
461 AlreadyCommunal,
463 AlreadyPrivate,
465 }
466
467 #[pallet::composite_enum]
469 pub enum HoldReason {
470 #[codec(index = 0)]
472 NftReceipt,
473 }
474
475 pub(crate) struct WeightCounter {
476 pub(crate) used: Weight,
477 pub(crate) limit: Weight,
478 }
479 impl WeightCounter {
480 #[allow(dead_code)]
481 pub(crate) fn unlimited() -> Self {
482 WeightCounter { used: Weight::zero(), limit: Weight::max_value() }
483 }
484 fn check_accrue(&mut self, w: Weight) -> bool {
485 let test = self.used.saturating_add(w);
486 if test.any_gt(self.limit) {
487 false
488 } else {
489 self.used = test;
490 true
491 }
492 }
493 fn can_accrue(&mut self, w: Weight) -> bool {
494 self.used.saturating_add(w).all_lte(self.limit)
495 }
496 }
497
498 #[pallet::hooks]
499 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
500 fn on_initialize(n: BlockNumberFor<T>) -> Weight {
501 let mut weight_counter =
502 WeightCounter { used: Weight::zero(), limit: T::MaxIntakeWeight::get() };
503 if T::IntakePeriod::get().is_zero() || (n % T::IntakePeriod::get()).is_zero() {
504 if weight_counter.check_accrue(T::WeightInfo::process_queues()) {
505 Self::process_queues(
506 T::Target::get(),
507 T::QueueCount::get(),
508 u32::max_value(),
509 &mut weight_counter,
510 )
511 }
512 }
513 weight_counter.used
514 }
515
516 fn integrity_test() {
517 assert!(!T::IntakePeriod::get().is_zero());
518 assert!(!T::MaxQueueLen::get().is_zero());
519 }
520 }
521
522 #[pallet::call]
523 impl<T: Config> Pallet<T> {
524 #[pallet::call_index(0)]
536 #[pallet::weight(T::WeightInfo::place_bid_max())]
537 pub fn place_bid(
538 origin: OriginFor<T>,
539 #[pallet::compact] amount: BalanceOf<T>,
540 duration: u32,
541 ) -> DispatchResult {
542 let who = ensure_signed(origin)?;
543
544 ensure!(amount >= T::MinBid::get(), Error::<T>::AmountTooSmall);
545 let queue_count = T::QueueCount::get() as usize;
546 let queue_index = duration.checked_sub(1).ok_or(Error::<T>::DurationTooSmall)? as usize;
547 ensure!(queue_index < queue_count, Error::<T>::DurationTooBig);
548
549 let net = Queues::<T>::try_mutate(
550 duration,
551 |q| -> Result<(u32, BalanceOf<T>), DispatchError> {
552 let queue_full = q.len() == T::MaxQueueLen::get() as usize;
553 ensure!(!queue_full || q[0].amount < amount, Error::<T>::BidTooLow);
554 T::Currency::hold(&HoldReason::NftReceipt.into(), &who, amount)?;
555
556 let mut bid = Bid { amount, who: who.clone() };
558 let net = if queue_full {
559 core::mem::swap(&mut q[0], &mut bid);
560 let _ = T::Currency::release(
561 &HoldReason::NftReceipt.into(),
562 &bid.who,
563 bid.amount,
564 BestEffort,
565 );
566 Self::deposit_event(Event::<T>::BidDropped {
567 who: bid.who,
568 amount: bid.amount,
569 duration,
570 });
571 (0, amount - bid.amount)
572 } else {
573 q.try_insert(0, bid).expect("verified queue was not full above. qed.");
574 (1, amount)
575 };
576
577 let sorted_item_count = q.len().saturating_sub(T::FifoQueueLen::get() as usize);
578 if sorted_item_count > 1 {
579 q[0..sorted_item_count].sort_by_key(|x| x.amount);
580 }
581
582 Ok(net)
583 },
584 )?;
585 QueueTotals::<T>::mutate(|qs| {
586 qs.bounded_resize(queue_count, (0, Zero::zero()));
587 qs[queue_index].0 += net.0;
588 qs[queue_index].1.saturating_accrue(net.1);
589 });
590 Self::deposit_event(Event::BidPlaced { who, amount, duration });
591
592 Ok(())
593 }
594
595 #[pallet::call_index(1)]
603 #[pallet::weight(T::WeightInfo::retract_bid(T::MaxQueueLen::get()))]
604 pub fn retract_bid(
605 origin: OriginFor<T>,
606 #[pallet::compact] amount: BalanceOf<T>,
607 duration: u32,
608 ) -> DispatchResult {
609 let who = ensure_signed(origin)?;
610
611 let queue_count = T::QueueCount::get() as usize;
612 let queue_index = duration.checked_sub(1).ok_or(Error::<T>::DurationTooSmall)? as usize;
613 ensure!(queue_index < queue_count, Error::<T>::DurationTooBig);
614
615 let bid = Bid { amount, who };
616
617 let mut queue = Queues::<T>::get(duration);
618 let pos = queue.iter().position(|i| i == &bid).ok_or(Error::<T>::UnknownBid)?;
619 queue.remove(pos);
620 let new_len = queue.len() as u32;
621
622 T::Currency::release(&HoldReason::NftReceipt.into(), &bid.who, bid.amount, BestEffort)?;
623
624 Queues::<T>::insert(duration, queue);
625 QueueTotals::<T>::mutate(|qs| {
626 qs.bounded_resize(queue_count, (0, Zero::zero()));
627 qs[queue_index].0 = new_len;
628 qs[queue_index].1.saturating_reduce(bid.amount);
629 });
630
631 Self::deposit_event(Event::BidRetracted { who: bid.who, amount: bid.amount, duration });
632
633 Ok(())
634 }
635
636 #[pallet::call_index(2)]
640 #[pallet::weight(T::WeightInfo::fund_deficit())]
641 pub fn fund_deficit(origin: OriginFor<T>) -> DispatchResult {
642 T::FundOrigin::ensure_origin(origin)?;
643 let summary: SummaryRecordOf<T> = Summary::<T>::get();
644 let our_account = Self::account_id();
645 let issuance = Self::issuance_with(&our_account, &summary);
646 let deficit = issuance.required.saturating_sub(issuance.holdings);
647 ensure!(!deficit.is_zero(), Error::<T>::AlreadyFunded);
648 T::Deficit::on_unbalanced(T::Currency::deposit(&our_account, deficit, Exact)?);
649 Self::deposit_event(Event::<T>::Funded { deficit });
650 Ok(())
651 }
652
653 #[pallet::call_index(3)]
662 #[pallet::weight(T::WeightInfo::thaw_private())]
663 pub fn thaw_private(
664 origin: OriginFor<T>,
665 #[pallet::compact] index: ReceiptIndex,
666 maybe_proportion: Option<Perquintill>,
667 ) -> DispatchResult {
668 let who = ensure_signed(origin)?;
669
670 let mut receipt: ReceiptRecordOf<T> =
672 Receipts::<T>::get(index).ok_or(Error::<T>::UnknownReceipt)?;
673 let (owner, mut on_hold) = receipt.owner.ok_or(Error::<T>::AlreadyCommunal)?;
675 ensure!(owner == who, Error::<T>::NotOwner);
676
677 let now = frame_system::Pallet::<T>::block_number();
678 ensure!(now >= receipt.expiry, Error::<T>::NotExpired);
679
680 let mut summary: SummaryRecordOf<T> = Summary::<T>::get();
681
682 let proportion = if let Some(proportion) = maybe_proportion {
683 ensure!(proportion <= receipt.proportion, Error::<T>::PortionTooBig);
684 let remaining = receipt.proportion.saturating_sub(proportion);
685 ensure!(
686 remaining.is_zero() || remaining >= T::MinReceipt::get(),
687 Error::<T>::MakesDust
688 );
689 proportion
690 } else {
691 receipt.proportion
692 };
693
694 let (throttle, throttle_period) = T::ThawThrottle::get();
695 if now.saturating_sub(summary.last_period) >= throttle_period {
696 summary.thawed = Zero::zero();
697 summary.last_period = now;
698 }
699 summary.thawed.saturating_accrue(proportion);
700 ensure!(summary.thawed <= throttle, Error::<T>::Throttled);
701
702 let our_account = Self::account_id();
704 let effective_issuance = Self::issuance_with(&our_account, &summary).effective;
705 let amount = proportion * effective_issuance;
707
708 receipt.proportion.saturating_reduce(proportion);
709 summary.proportion_owed.saturating_reduce(proportion);
710
711 let dropped = receipt.proportion.is_zero();
712
713 if amount > on_hold {
714 T::Currency::release(&HoldReason::NftReceipt.into(), &who, on_hold, Exact)?;
715 let deficit = amount - on_hold;
716 summary.receipts_on_hold.saturating_reduce(on_hold);
718 on_hold = Zero::zero();
719 T::Currency::transfer(&our_account, &who, deficit, Expendable)
720 .map_err(|_| Error::<T>::Unfunded)?;
721 } else {
722 on_hold.saturating_reduce(amount);
723 summary.receipts_on_hold.saturating_reduce(amount);
724 if dropped && !on_hold.is_zero() {
725 T::Currency::transfer_on_hold(
729 &HoldReason::NftReceipt.into(),
730 &who,
731 &our_account,
732 on_hold,
733 Exact,
734 Free,
735 Polite,
736 )
737 .map(|_| ())
738 .or_else(|e| {
741 if e == TokenError::CannotCreate.into() {
742 Ok(())
743 } else {
744 Err(e)
745 }
746 })?;
747 summary.receipts_on_hold.saturating_reduce(on_hold);
748 }
749 T::Currency::release(&HoldReason::NftReceipt.into(), &who, amount, Exact)?;
750 }
751
752 if dropped {
753 Receipts::<T>::remove(index);
754 } else {
755 receipt.owner = Some((owner, on_hold));
756 Receipts::<T>::insert(index, &receipt);
757 }
758 Summary::<T>::put(&summary);
759
760 Self::deposit_event(Event::Thawed { index, who, amount, proportion, dropped });
761
762 Ok(())
763 }
764
765 #[pallet::call_index(4)]
772 #[pallet::weight(T::WeightInfo::thaw_communal())]
773 pub fn thaw_communal(
774 origin: OriginFor<T>,
775 #[pallet::compact] index: ReceiptIndex,
776 ) -> DispatchResult {
777 let who = ensure_signed(origin)?;
778
779 let receipt: ReceiptRecordOf<T> =
781 Receipts::<T>::get(index).ok_or(Error::<T>::UnknownReceipt)?;
782 ensure!(receipt.owner.is_none(), Error::<T>::NotOwner);
784 let now = frame_system::Pallet::<T>::block_number();
785 ensure!(now >= receipt.expiry, Error::<T>::NotExpired);
786
787 let mut summary: SummaryRecordOf<T> = Summary::<T>::get();
788
789 let (throttle, throttle_period) = T::ThawThrottle::get();
790 if now.saturating_sub(summary.last_period) >= throttle_period {
791 summary.thawed = Zero::zero();
792 summary.last_period = now;
793 }
794 summary.thawed.saturating_accrue(receipt.proportion);
795 ensure!(summary.thawed <= throttle, Error::<T>::Throttled);
796
797 let cp_amount = T::CounterpartAmount::convert(receipt.proportion);
798 T::Counterpart::burn_from(&who, cp_amount, Expendable, Exact, Polite)?;
799
800 let our_account = Self::account_id();
802 let effective_issuance = Self::issuance_with(&our_account, &summary).effective;
803 let amount = receipt.proportion * effective_issuance;
804
805 summary.proportion_owed.saturating_reduce(receipt.proportion);
806
807 T::Currency::transfer(&our_account, &who, amount, Expendable)
809 .map_err(|_| Error::<T>::Unfunded)?;
810
811 Receipts::<T>::remove(index);
812 Summary::<T>::put(&summary);
813
814 let e =
815 Event::Thawed { index, who, amount, proportion: receipt.proportion, dropped: true };
816 Self::deposit_event(e);
817
818 Ok(())
819 }
820
821 #[pallet::call_index(5)]
823 #[pallet::weight(T::WeightInfo::communify())]
824 pub fn communify(
825 origin: OriginFor<T>,
826 #[pallet::compact] index: ReceiptIndex,
827 ) -> DispatchResult {
828 let who = ensure_signed(origin)?;
829
830 let mut receipt: ReceiptRecordOf<T> =
832 Receipts::<T>::get(index).ok_or(Error::<T>::UnknownReceipt)?;
833
834 let (owner, on_hold) = receipt.owner.take().ok_or(Error::<T>::AlreadyCommunal)?;
836
837 ensure!(owner == who, Error::<T>::NotOwner);
839
840 let reason = HoldReason::NftReceipt.into();
842 let us = Self::account_id();
843 T::Currency::transfer_on_hold(&reason, &who, &us, on_hold, Exact, Free, Polite)
844 .map_err(|_| Error::<T>::Unfunded)?;
845
846 let mut summary: SummaryRecordOf<T> = Summary::<T>::get();
848 summary.receipts_on_hold.saturating_reduce(on_hold);
849 Summary::<T>::put(&summary);
850 Receipts::<T>::insert(index, &receipt);
851
852 let fung_eq = T::CounterpartAmount::convert(receipt.proportion);
854 let _ = T::Counterpart::mint_into(&who, fung_eq).defensive();
855
856 Ok(())
857 }
858
859 #[pallet::call_index(6)]
861 #[pallet::weight(T::WeightInfo::privatize())]
862 pub fn privatize(
863 origin: OriginFor<T>,
864 #[pallet::compact] index: ReceiptIndex,
865 ) -> DispatchResult {
866 let who = ensure_signed(origin)?;
867
868 let mut receipt: ReceiptRecordOf<T> =
870 Receipts::<T>::get(index).ok_or(Error::<T>::UnknownReceipt)?;
871
872 ensure!(receipt.owner.is_none(), Error::<T>::AlreadyPrivate);
874
875 let mut summary: SummaryRecordOf<T> = Summary::<T>::get();
877 let our_account = Self::account_id();
878 let effective_issuance = Self::issuance_with(&our_account, &summary).effective;
879 let max_amount = receipt.proportion * effective_issuance;
880 let amount = max_amount.min(T::Currency::balance(&our_account));
882
883 T::Counterpart::burn_from(
885 &who,
886 T::CounterpartAmount::convert(receipt.proportion),
887 Expendable,
888 Exact,
889 Polite,
890 )?;
891
892 let reason = HoldReason::NftReceipt.into();
894 let us = Self::account_id();
895 T::Currency::transfer_and_hold(&reason, &us, &who, amount, Exact, Expendable, Polite)?;
896
897 summary.receipts_on_hold.saturating_accrue(amount);
899
900 receipt.owner = Some((who, amount));
901
902 Summary::<T>::put(&summary);
903 Receipts::<T>::insert(index, &receipt);
904
905 Ok(())
906 }
907 }
908
909 #[derive(Debug)]
911 pub struct IssuanceInfo<Balance> {
912 pub holdings: Balance,
915 pub other: Balance,
917 pub effective: Balance,
920 pub required: Balance,
923 }
924
925 impl<T: Config> NftInspect<T::AccountId> for Pallet<T> {
926 type ItemId = ReceiptIndex;
927
928 fn owner(item: &ReceiptIndex) -> Option<T::AccountId> {
929 Receipts::<T>::get(item).and_then(|r| r.owner).map(|(who, _)| who)
930 }
931
932 fn attribute(item: &Self::ItemId, key: &[u8]) -> Option<Vec<u8>> {
933 let item = Receipts::<T>::get(item)?;
934 match key {
935 b"proportion" => Some(item.proportion.encode()),
936 b"expiry" => Some(item.expiry.encode()),
937 b"owner" => item.owner.as_ref().map(|x| x.0.encode()),
938 b"on_hold" => item.owner.as_ref().map(|x| x.1.encode()),
939 _ => None,
940 }
941 }
942 }
943
944 impl<T: Config> NftTransfer<T::AccountId> for Pallet<T> {
945 fn transfer(index: &ReceiptIndex, dest: &T::AccountId) -> DispatchResult {
946 let mut item = Receipts::<T>::get(index).ok_or(TokenError::UnknownAsset)?;
947 let (owner, on_hold) = item.owner.take().ok_or(Error::<T>::AlreadyCommunal)?;
948
949 let reason = HoldReason::NftReceipt.into();
950 T::Currency::transfer_on_hold(&reason, &owner, dest, on_hold, Exact, OnHold, Polite)?;
951
952 item.owner = Some((dest.clone(), on_hold));
953 Receipts::<T>::insert(&index, &item);
954 Pallet::<T>::deposit_event(Event::<T>::Transferred {
955 from: owner,
956 to: dest.clone(),
957 index: *index,
958 });
959 Ok(())
960 }
961 }
962
963 impl<T: Config> Pallet<T> {
964 pub fn account_id() -> T::AccountId {
969 T::PalletId::get().into_account_truncating()
970 }
971
972 pub fn issuance() -> IssuanceInfo<BalanceOf<T>> {
974 Self::issuance_with(&Self::account_id(), &Summary::<T>::get())
975 }
976
977 pub fn issuance_with(
986 our_account: &T::AccountId,
987 summary: &SummaryRecordOf<T>,
988 ) -> IssuanceInfo<BalanceOf<T>> {
989 let total_issuance =
990 T::Currency::active_issuance().saturating_sub(T::IgnoredIssuance::get());
991 let holdings =
992 T::Currency::balance(our_account).saturating_add(summary.receipts_on_hold);
993 let other = total_issuance.saturating_sub(holdings);
994 let effective =
995 summary.proportion_owed.left_from_one().saturating_reciprocal_mul(other);
996 let required = summary.proportion_owed * effective;
997 IssuanceInfo { holdings, other, effective, required }
998 }
999
1000 pub(crate) fn process_queues(
1006 target: Perquintill,
1007 max_queues: u32,
1008 max_bids: u32,
1009 weight: &mut WeightCounter,
1010 ) {
1011 let mut summary: SummaryRecordOf<T> = Summary::<T>::get();
1012 if summary.proportion_owed >= target {
1013 return
1014 }
1015
1016 let now = frame_system::Pallet::<T>::block_number();
1017 let our_account = Self::account_id();
1018 let issuance: IssuanceInfoOf<T> = Self::issuance_with(&our_account, &summary);
1019 let mut remaining = target.saturating_sub(summary.proportion_owed) * issuance.effective;
1020
1021 let mut queues_hit = 0;
1022 let mut bids_hit = 0;
1023 let mut totals = QueueTotals::<T>::get();
1024 let queue_count = T::QueueCount::get();
1025 totals.bounded_resize(queue_count as usize, (0, Zero::zero()));
1026 for duration in (1..=queue_count).rev() {
1027 if totals[duration as usize - 1].0.is_zero() {
1028 continue
1029 }
1030 if remaining.is_zero() || queues_hit >= max_queues
1031 || !weight.check_accrue(T::WeightInfo::process_queue())
1032 || !weight.can_accrue(T::WeightInfo::process_bid())
1034 {
1035 break
1036 }
1037
1038 let b = Self::process_queue(
1039 duration,
1040 now,
1041 &our_account,
1042 &issuance,
1043 max_bids.saturating_sub(bids_hit),
1044 &mut remaining,
1045 &mut totals[duration as usize - 1],
1046 &mut summary,
1047 weight,
1048 );
1049
1050 bids_hit.saturating_accrue(b);
1051 queues_hit.saturating_inc();
1052 }
1053 QueueTotals::<T>::put(&totals);
1054 Summary::<T>::put(&summary);
1055 }
1056
1057 pub(crate) fn process_queue(
1058 duration: u32,
1059 now: BlockNumberFor<T>,
1060 our_account: &T::AccountId,
1061 issuance: &IssuanceInfo<BalanceOf<T>>,
1062 max_bids: u32,
1063 remaining: &mut BalanceOf<T>,
1064 queue_total: &mut (u32, BalanceOf<T>),
1065 summary: &mut SummaryRecordOf<T>,
1066 weight: &mut WeightCounter,
1067 ) -> u32 {
1068 let mut queue: BoundedVec<BidOf<T>, _> = Queues::<T>::get(&duration);
1069 let expiry = now.saturating_add(T::BasePeriod::get().saturating_mul(duration.into()));
1070 let mut count = 0;
1071
1072 while count < max_bids &&
1073 !queue.is_empty() &&
1074 !remaining.is_zero() &&
1075 weight.check_accrue(T::WeightInfo::process_bid())
1076 {
1077 let bid = match queue.pop() {
1078 Some(b) => b,
1079 None => break,
1080 };
1081 if let Some(bid) = Self::process_bid(
1082 bid,
1083 expiry,
1084 our_account,
1085 issuance,
1086 remaining,
1087 &mut queue_total.1,
1088 summary,
1089 ) {
1090 queue.try_push(bid).expect("just popped, so there must be space. qed");
1091 }
1094 count.saturating_inc();
1095 }
1096 queue_total.0 = queue.len() as u32;
1097 Queues::<T>::insert(&duration, &queue);
1098 count
1099 }
1100
1101 pub(crate) fn process_bid(
1102 mut bid: BidOf<T>,
1103 expiry: BlockNumberFor<T>,
1104 _our_account: &T::AccountId,
1105 issuance: &IssuanceInfo<BalanceOf<T>>,
1106 remaining: &mut BalanceOf<T>,
1107 queue_amount: &mut BalanceOf<T>,
1108 summary: &mut SummaryRecordOf<T>,
1109 ) -> Option<BidOf<T>> {
1110 let result = if *remaining < bid.amount {
1111 let overflow = bid.amount - *remaining;
1112 bid.amount = *remaining;
1113 Some(Bid { amount: overflow, who: bid.who.clone() })
1114 } else {
1115 None
1116 };
1117 let amount = bid.amount;
1118 summary.receipts_on_hold.saturating_accrue(amount);
1119
1120 remaining.saturating_reduce(amount);
1122 queue_amount.defensive_saturating_reduce(amount);
1125
1126 let n = amount;
1128 let d = issuance.effective;
1129 let proportion = Perquintill::from_rational_with_rounding(n, d, Rounding::Down)
1130 .defensive_unwrap_or_default();
1131 let who = bid.who;
1132 let index = summary.index;
1133 summary.proportion_owed.defensive_saturating_accrue(proportion);
1134 summary.index += 1;
1135
1136 let e = Event::Issued { index, expiry, who: who.clone(), amount, proportion };
1137 Self::deposit_event(e);
1138 let receipt = ReceiptRecord { proportion, owner: Some((who, amount)), expiry };
1139 Receipts::<T>::insert(index, receipt);
1140
1141 result
1142 }
1143 }
1144}