1#![doc = docify::embed!("src/tests.rs", fund_bounty_works)]
66#![doc = docify::embed!("src/tests.rs", award_bounty_works)]
68#![cfg_attr(not(feature = "std"), no_std)]
74
75mod benchmarking;
76mod mock;
77mod tests;
78pub mod weights;
79#[cfg(feature = "runtime-benchmarks")]
80pub use benchmarking::ArgumentsFactory;
81pub use pallet::*;
82pub use weights::WeightInfo;
83
84extern crate alloc;
85use alloc::{boxed::Box, collections::btree_map::BTreeMap};
86use frame_support::{
87 dispatch::{DispatchResult, DispatchResultWithPostInfo},
88 dispatch_context::with_context,
89 pallet_prelude::*,
90 traits::{
91 tokens::{
92 Balance, ConversionFromAssetBalance, ConversionToAssetBalance, PayWithSource,
93 PaymentStatus,
94 },
95 Consideration, EnsureOrigin, Get, QueryPreimage, StorePreimage,
96 },
97 PalletId,
98};
99use frame_system::pallet_prelude::{
100 ensure_signed, BlockNumberFor as SystemBlockNumberFor, OriginFor,
101};
102use scale_info::TypeInfo;
103use sp_runtime::{
104 traits::{
105 AccountIdConversion, BadOrigin, CheckedAdd, Convert, Saturating, StaticLookup, TryConvert,
106 Zero,
107 },
108 Debug, Permill,
109};
110
111pub type BeneficiaryLookupOf<T, I> = <<T as Config<I>>::BeneficiaryLookup as StaticLookup>::Source;
113pub type BountyIndex = u32;
115pub type AccountIdLookupOf<T> = <<T as frame_system::Config>::Lookup as StaticLookup>::Source;
117pub type PaymentIdOf<T, I = ()> = <<T as crate::Config<I>>::Paymaster as PayWithSource>::Id;
119pub type BountyOf<T, I> = Bounty<
121 <T as frame_system::Config>::AccountId,
122 <T as Config<I>>::Balance,
123 <T as Config<I>>::AssetKind,
124 <T as frame_system::Config>::Hash,
125 PaymentIdOf<T, I>,
126 <T as Config<I>>::Beneficiary,
127>;
128pub type ChildBountyOf<T, I> = ChildBounty<
130 <T as frame_system::Config>::AccountId,
131 <T as Config<I>>::Balance,
132 <T as frame_system::Config>::Hash,
133 PaymentIdOf<T, I>,
134 <T as Config<I>>::Beneficiary,
135>;
136
137#[derive(Encode, Decode, Clone, PartialEq, Eq, Debug, TypeInfo, MaxEncodedLen)]
139pub struct Bounty<AccountId, Balance, AssetKind, Hash, PaymentId, Beneficiary> {
140 pub asset_kind: AssetKind,
142 pub value: Balance,
147 pub metadata: Hash,
152 pub status: BountyStatus<AccountId, PaymentId, Beneficiary>,
154}
155
156#[derive(Encode, Decode, Clone, PartialEq, Eq, Debug, TypeInfo, MaxEncodedLen)]
158pub struct ChildBounty<AccountId, Balance, Hash, PaymentId, Beneficiary> {
159 pub parent_bounty: BountyIndex,
161 pub value: Balance,
165 pub metadata: Hash,
170 pub status: BountyStatus<AccountId, PaymentId, Beneficiary>,
172}
173
174#[derive(Encode, Decode, Clone, PartialEq, Eq, Debug, TypeInfo, MaxEncodedLen)]
176pub enum BountyStatus<AccountId, PaymentId, Beneficiary> {
177 FundingAttempted {
184 curator: AccountId,
186 payment_status: PaymentState<PaymentId>,
189 },
190 Funded {
192 curator: AccountId,
194 },
195 CuratorUnassigned,
199 Active {
204 curator: AccountId,
206 },
207 RefundAttempted {
212 curator: Option<AccountId>,
216 payment_status: PaymentState<PaymentId>,
219 },
220 PayoutAttempted {
226 curator: AccountId,
228 beneficiary: Beneficiary,
230 payment_status: PaymentState<PaymentId>,
232 },
233}
234
235#[derive(Encode, Decode, Clone, PartialEq, Eq, MaxEncodedLen, Debug, TypeInfo)]
241pub enum PaymentState<Id> {
242 Pending,
244 Attempted { id: Id },
246 Failed,
248 Succeeded,
250}
251impl<Id: Clone> PaymentState<Id> {
252 pub fn is_pending_or_failed(&self) -> bool {
254 matches!(self, PaymentState::Pending | PaymentState::Failed)
255 }
256
257 pub fn get_attempt_id(&self) -> Option<Id> {
260 match self {
261 PaymentState::Attempted { id } => Some(id.clone()),
262 _ => None,
263 }
264 }
265}
266
267#[frame_support::pallet]
268pub mod pallet {
269 use super::*;
270
271 const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
272
273 #[pallet::pallet]
274 #[pallet::storage_version(STORAGE_VERSION)]
275 pub struct Pallet<T, I = ()>(_);
276
277 #[pallet::config]
278 pub trait Config<I: 'static = ()>: frame_system::Config {
279 type Balance: Balance;
281
282 type RejectOrigin: EnsureOrigin<Self::RuntimeOrigin>;
284
285 type SpendOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = Self::Balance>;
288
289 type AssetKind: Parameter + MaxEncodedLen;
292
293 type Beneficiary: Parameter + MaxEncodedLen;
295
296 type BeneficiaryLookup: StaticLookup<Target = Self::Beneficiary>;
298
299 #[pallet::constant]
301 type BountyValueMinimum: Get<Self::Balance>;
302
303 #[pallet::constant]
305 type ChildBountyValueMinimum: Get<Self::Balance>;
306
307 #[pallet::constant]
309 type MaxActiveChildBountyCount: Get<u32>;
310
311 type WeightInfo: WeightInfo;
313
314 type FundingSource: TryConvert<
318 Self::AssetKind,
319 <<Self as pallet::Config<I>>::Paymaster as PayWithSource>::Source,
320 >;
321
322 type BountySource: TryConvert<
326 (BountyIndex, Self::AssetKind),
327 <<Self as pallet::Config<I>>::Paymaster as PayWithSource>::Source,
328 >;
329
330 type ChildBountySource: TryConvert<
336 (BountyIndex, BountyIndex, Self::AssetKind),
337 <<Self as pallet::Config<I>>::Paymaster as PayWithSource>::Source,
338 >;
339
340 type Paymaster: PayWithSource<
343 Balance = Self::Balance,
344 Source = Self::Beneficiary,
345 Beneficiary = Self::Beneficiary,
346 AssetKind = Self::AssetKind,
347 >;
348
349 type BalanceConverter: ConversionFromAssetBalance<Self::Balance, Self::AssetKind, Self::Balance>
356 + ConversionToAssetBalance<Self::Balance, Self::AssetKind, Self::Balance>;
357
358 type Preimages: QueryPreimage<H = Self::Hashing> + StorePreimage;
360
361 type Consideration: Consideration<Self::AccountId, Self::Balance>;
371
372 #[cfg(feature = "runtime-benchmarks")]
374 type BenchmarkHelper: benchmarking::ArgumentsFactory<
375 Self::AssetKind,
376 Self::Beneficiary,
377 Self::Balance,
378 >;
379 }
380
381 #[pallet::error]
382 pub enum Error<T, I = ()> {
383 InvalidIndex,
385 ReasonTooBig,
387 InvalidValue,
389 FailedToConvertBalance,
392 UnexpectedStatus,
394 RequireCurator,
396 InsufficientPermission,
399 FundingError,
401 RefundError,
403 PayoutError,
405 FundingInconclusive,
407 RefundInconclusive,
409 PayoutInconclusive,
411 FailedToConvertSource,
414 HasActiveChildBounty,
416 TooManyChildBounties,
418 InsufficientBountyValue,
420 PreimageNotExist,
422 }
423
424 #[pallet::event]
425 #[pallet::generate_deposit(pub(super) fn deposit_event)]
426 pub enum Event<T: Config<I>, I: 'static = ()> {
427 BountyCreated { index: BountyIndex },
429 ChildBountyCreated { index: BountyIndex, child_index: BountyIndex },
431 BountyBecameActive {
433 index: BountyIndex,
434 child_index: Option<BountyIndex>,
435 curator: T::AccountId,
436 },
437 BountyAwarded {
439 index: BountyIndex,
440 child_index: Option<BountyIndex>,
441 beneficiary: T::Beneficiary,
442 },
443 BountyPayoutProcessed {
445 index: BountyIndex,
446 child_index: Option<BountyIndex>,
447 asset_kind: T::AssetKind,
448 value: T::Balance,
449 beneficiary: T::Beneficiary,
450 },
451 BountyFundingProcessed { index: BountyIndex, child_index: Option<BountyIndex> },
453 BountyRefundProcessed { index: BountyIndex, child_index: Option<BountyIndex> },
455 BountyCanceled { index: BountyIndex, child_index: Option<BountyIndex> },
457 CuratorUnassigned { index: BountyIndex, child_index: Option<BountyIndex> },
459 CuratorProposed {
461 index: BountyIndex,
462 child_index: Option<BountyIndex>,
463 curator: T::AccountId,
464 },
465 PaymentFailed {
467 index: BountyIndex,
468 child_index: Option<BountyIndex>,
469 payment_id: PaymentIdOf<T, I>,
470 },
471 Paid { index: BountyIndex, child_index: Option<BountyIndex>, payment_id: PaymentIdOf<T, I> },
473 BountyValueIncreased { index: BountyIndex, old_value: T::Balance, new_value: T::Balance },
475 }
476
477 #[pallet::composite_enum]
479 pub enum HoldReason<I: 'static = ()> {
480 #[codec(index = 0)]
482 CuratorDeposit,
483 }
484
485 #[pallet::storage]
487 pub type BountyCount<T: Config<I>, I: 'static = ()> = StorageValue<_, u32, ValueQuery>;
488
489 #[pallet::storage]
491 pub type Bounties<T: Config<I>, I: 'static = ()> =
492 StorageMap<_, Twox64Concat, BountyIndex, BountyOf<T, I>>;
493
494 #[pallet::storage]
498 pub type ChildBounties<T: Config<I>, I: 'static = ()> = StorageDoubleMap<
499 _,
500 Twox64Concat,
501 BountyIndex,
502 Twox64Concat,
503 BountyIndex,
504 ChildBountyOf<T, I>,
505 >;
506
507 #[pallet::storage]
511 pub type ChildBountiesPerParent<T: Config<I>, I: 'static = ()> =
512 StorageMap<_, Twox64Concat, BountyIndex, u32, ValueQuery>;
513
514 #[pallet::storage]
518 pub type TotalChildBountiesPerParent<T: Config<I>, I: 'static = ()> =
519 StorageMap<_, Twox64Concat, BountyIndex, u32, ValueQuery>;
520
521 #[pallet::storage]
526 pub type ChildBountiesValuePerParent<T: Config<I>, I: 'static = ()> =
527 StorageMap<_, Twox64Concat, BountyIndex, T::Balance, ValueQuery>;
528
529 #[pallet::storage]
541 pub type CuratorDeposit<T: Config<I>, I: 'static = ()> = StorageDoubleMap<
542 _,
543 Twox64Concat,
544 BountyIndex,
545 Twox64Concat,
546 Option<BountyIndex>,
547 T::Consideration,
548 >;
549
550 #[derive(Default)]
552 pub struct SpendContext<Balance> {
553 pub spend_in_context: BTreeMap<Balance, Balance>,
554 }
555
556 #[pallet::call]
557 impl<T: Config<I>, I: 'static> Pallet<T, I> {
558 #[pallet::call_index(0)]
585 #[pallet::weight(<T as Config<I>>::WeightInfo::fund_bounty())]
586 pub fn fund_bounty(
587 origin: OriginFor<T>,
588 asset_kind: Box<T::AssetKind>,
589 #[pallet::compact] value: T::Balance,
590 curator: AccountIdLookupOf<T>,
591 metadata: T::Hash,
592 ) -> DispatchResult {
593 let max_amount = T::SpendOrigin::ensure_origin(origin)?;
594 let curator = T::Lookup::lookup(curator)?;
595 ensure!(T::Preimages::len(&metadata).is_some(), Error::<T, I>::PreimageNotExist);
596
597 let native_amount = T::BalanceConverter::from_asset_balance(value, *asset_kind.clone())
598 .map_err(|_| Error::<T, I>::FailedToConvertBalance)?;
599 ensure!(native_amount >= T::BountyValueMinimum::get(), Error::<T, I>::InvalidValue);
600 ensure!(native_amount <= max_amount, Error::<T, I>::InsufficientPermission);
601
602 with_context::<SpendContext<T::Balance>, _>(|v| {
603 let context = v.or_default();
604 let funding = context.spend_in_context.entry(max_amount).or_default();
605
606 if funding.checked_add(&native_amount).map(|s| s > max_amount).unwrap_or(true) {
607 Err(Error::<T, I>::InsufficientPermission)
608 } else {
609 *funding = funding.saturating_add(native_amount);
610 Ok(())
611 }
612 })
613 .unwrap_or(Ok(()))?;
614
615 let index = BountyCount::<T, I>::get();
616 let payment_status =
617 Self::do_process_funding_payment(index, None, *asset_kind.clone(), value, None)?;
618
619 let bounty = BountyOf::<T, I> {
620 asset_kind: *asset_kind,
621 value,
622 metadata,
623 status: BountyStatus::FundingAttempted { curator, payment_status },
624 };
625 Bounties::<T, I>::insert(index, &bounty);
626 T::Preimages::request(&metadata);
627 BountyCount::<T, I>::put(index + 1);
628
629 Self::deposit_event(Event::<T, I>::BountyCreated { index });
630
631 Ok(())
632 }
633
634 #[pallet::call_index(1)]
659 #[pallet::weight(<T as Config<I>>::WeightInfo::fund_child_bounty())]
660 pub fn fund_child_bounty(
661 origin: OriginFor<T>,
662 #[pallet::compact] parent_bounty_id: BountyIndex,
663 #[pallet::compact] value: T::Balance,
664 metadata: T::Hash,
665 curator: Option<AccountIdLookupOf<T>>,
666 ) -> DispatchResult {
667 let signer = ensure_signed(origin)?;
668 ensure!(T::Preimages::len(&metadata).is_some(), Error::<T, I>::PreimageNotExist);
669
670 let (asset_kind, parent_value, _, _, parent_curator) =
671 Self::get_bounty_details(parent_bounty_id, None)
672 .map_err(|_| Error::<T, I>::InvalidIndex)?;
673 let native_amount = T::BalanceConverter::from_asset_balance(value, asset_kind.clone())
674 .map_err(|_| Error::<T, I>::FailedToConvertBalance)?;
675
676 ensure!(
677 native_amount >= T::ChildBountyValueMinimum::get(),
678 Error::<T, I>::InvalidValue
679 );
680 ensure!(
681 ChildBountiesPerParent::<T, I>::get(parent_bounty_id) <
682 T::MaxActiveChildBountyCount::get(),
683 Error::<T, I>::TooManyChildBounties,
684 );
685
686 let parent_curator = parent_curator.ok_or(Error::<T, I>::UnexpectedStatus)?;
688 let final_curator = match curator {
689 Some(curator) => T::Lookup::lookup(curator)?,
690 None => parent_curator.clone(),
691 };
692 ensure!(signer == parent_curator, Error::<T, I>::RequireCurator);
693
694 let child_bounties_value = ChildBountiesValuePerParent::<T, I>::get(parent_bounty_id);
696 let remaining_parent_value = parent_value.saturating_sub(child_bounties_value);
697 ensure!(remaining_parent_value >= value, Error::<T, I>::InsufficientBountyValue);
698
699 let child_bounty_id = TotalChildBountiesPerParent::<T, I>::get(parent_bounty_id);
701
702 let payment_status = Self::do_process_funding_payment(
704 parent_bounty_id,
705 Some(child_bounty_id),
706 asset_kind,
707 value,
708 None,
709 )?;
710
711 let child_bounty = ChildBounty {
712 parent_bounty: parent_bounty_id,
713 value,
714 metadata,
715 status: BountyStatus::FundingAttempted {
716 curator: final_curator,
717 payment_status: payment_status.clone(),
718 },
719 };
720 ChildBounties::<T, I>::insert(parent_bounty_id, child_bounty_id, child_bounty);
721 T::Preimages::request(&metadata);
722
723 ChildBountiesValuePerParent::<T, I>::mutate(parent_bounty_id, |children_value| {
727 *children_value = children_value.saturating_add(value)
728 });
729
730 ChildBountiesPerParent::<T, I>::mutate(parent_bounty_id, |count| {
732 count.saturating_inc()
733 });
734 TotalChildBountiesPerParent::<T, I>::insert(
735 parent_bounty_id,
736 child_bounty_id.saturating_add(1),
737 );
738
739 Self::deposit_event(Event::<T, I>::ChildBountyCreated {
740 index: parent_bounty_id,
741 child_index: child_bounty_id,
742 });
743
744 Ok(())
745 }
746
747 #[pallet::call_index(2)]
769 #[pallet::weight(match child_bounty_id {
770 None => <T as Config<I>>::WeightInfo::propose_curator_parent_bounty(),
771 Some(_) => <T as Config<I>>::WeightInfo::propose_curator_child_bounty(),
772 })]
773 pub fn propose_curator(
774 origin: OriginFor<T>,
775 #[pallet::compact] parent_bounty_id: BountyIndex,
776 child_bounty_id: Option<BountyIndex>,
777 curator: AccountIdLookupOf<T>,
778 ) -> DispatchResult {
779 let maybe_sender = ensure_signed(origin.clone())
780 .map(Some)
781 .or_else(|_| T::SpendOrigin::ensure_origin(origin.clone()).map(|_| None))?;
782 let curator = T::Lookup::lookup(curator)?;
783
784 let (asset_kind, value, _, status, parent_curator) =
785 Self::get_bounty_details(parent_bounty_id, child_bounty_id)?;
786 ensure!(status == BountyStatus::CuratorUnassigned, Error::<T, I>::UnexpectedStatus);
787
788 match child_bounty_id {
789 None => {
791 ensure!(maybe_sender.is_none(), BadOrigin);
792 let max_amount = T::SpendOrigin::ensure_origin(origin)?;
793 let native_amount = T::BalanceConverter::from_asset_balance(value, asset_kind)
794 .map_err(|_| Error::<T, I>::FailedToConvertBalance)?;
795 ensure!(native_amount <= max_amount, Error::<T, I>::InsufficientPermission);
796 },
797 Some(_) => {
799 let parent_curator = parent_curator.ok_or(Error::<T, I>::UnexpectedStatus)?;
800 let sender = maybe_sender.ok_or(BadOrigin)?;
801 ensure!(sender == parent_curator, BadOrigin);
802 },
803 };
804
805 let new_status = BountyStatus::Funded { curator: curator.clone() };
806 Self::update_bounty_status(parent_bounty_id, child_bounty_id, new_status)?;
807
808 Self::deposit_event(Event::<T, I>::CuratorProposed {
809 index: parent_bounty_id,
810 child_index: child_bounty_id,
811 curator,
812 });
813
814 Ok(())
815 }
816
817 #[pallet::call_index(3)]
837 #[pallet::weight(<T as Config<I>>::WeightInfo::accept_curator())]
838 pub fn accept_curator(
839 origin: OriginFor<T>,
840 #[pallet::compact] parent_bounty_id: BountyIndex,
841 child_bounty_id: Option<BountyIndex>,
842 ) -> DispatchResult {
843 let signer = ensure_signed(origin)?;
844
845 let (asset_kind, value, _, status, _) =
846 Self::get_bounty_details(parent_bounty_id, child_bounty_id)?;
847
848 let BountyStatus::Funded { ref curator } = status else {
849 return Err(Error::<T, I>::UnexpectedStatus.into());
850 };
851 ensure!(signer == *curator, Error::<T, I>::RequireCurator);
852
853 let native_amount = T::BalanceConverter::from_asset_balance(value, asset_kind)
854 .map_err(|_| Error::<T, I>::FailedToConvertBalance)?;
855 let curator_deposit = T::Consideration::new(&curator, native_amount)?;
856 CuratorDeposit::<T, I>::insert(parent_bounty_id, child_bounty_id, curator_deposit);
857
858 let new_status = BountyStatus::Active { curator: curator.clone() };
859 Self::update_bounty_status(parent_bounty_id, child_bounty_id, new_status)?;
860
861 Self::deposit_event(Event::<T, I>::BountyBecameActive {
862 index: parent_bounty_id,
863 child_index: child_bounty_id,
864 curator: signer,
865 });
866
867 Ok(())
868 }
869
870 #[pallet::call_index(4)]
896 #[pallet::weight(<T as Config<I>>::WeightInfo::unassign_curator())]
897 pub fn unassign_curator(
898 origin: OriginFor<T>,
899 #[pallet::compact] parent_bounty_id: BountyIndex,
900 child_bounty_id: Option<BountyIndex>,
901 ) -> DispatchResult {
902 let maybe_sender = ensure_signed(origin.clone())
903 .map(Some)
904 .or_else(|_| T::RejectOrigin::ensure_origin(origin).map(|_| None))?;
905
906 let (_, _, _, status, parent_curator) =
907 Self::get_bounty_details(parent_bounty_id, child_bounty_id)?;
908
909 match status {
910 BountyStatus::Funded { ref curator } => {
911 ensure!(
915 maybe_sender.map_or(true, |sender| {
916 sender == *curator ||
917 parent_curator
918 .map_or(false, |parent_curator| sender == parent_curator)
919 }),
920 BadOrigin
921 );
922 },
923 BountyStatus::Active { ref curator, .. } => {
924 match maybe_sender {
926 None => {
928 if let Some(curator_deposit) =
929 CuratorDeposit::<T, I>::take(parent_bounty_id, child_bounty_id)
930 {
931 T::Consideration::burn(curator_deposit, curator);
932 }
933 },
935 Some(sender) if sender == *curator => {
936 if let Some(curator_deposit) =
937 CuratorDeposit::<T, I>::get(parent_bounty_id, child_bounty_id)
938 {
939 T::Consideration::drop(curator_deposit, curator)?;
942 CuratorDeposit::<T, I>::remove(parent_bounty_id, child_bounty_id);
943 }
944 },
946 Some(sender) => {
947 let parent_curator = parent_curator.ok_or(BadOrigin)?;
948 ensure!(
949 sender == parent_curator && *curator != parent_curator,
950 BadOrigin
951 );
952 if let Some(curator_deposit) =
955 CuratorDeposit::<T, I>::take(parent_bounty_id, child_bounty_id)
956 {
957 T::Consideration::burn(curator_deposit, curator);
958 }
959 },
960 }
961 },
962 _ => return Err(Error::<T, I>::UnexpectedStatus.into()),
963 };
964
965 let new_status = BountyStatus::CuratorUnassigned;
966 Self::update_bounty_status(parent_bounty_id, child_bounty_id, new_status)?;
967
968 Self::deposit_event(Event::<T, I>::CuratorUnassigned {
969 index: parent_bounty_id,
970 child_index: child_bounty_id,
971 });
972
973 Ok(())
974 }
975
976 #[pallet::call_index(5)]
1001 #[pallet::weight(<T as Config<I>>::WeightInfo::award_bounty())]
1002 pub fn award_bounty(
1003 origin: OriginFor<T>,
1004 #[pallet::compact] parent_bounty_id: BountyIndex,
1005 child_bounty_id: Option<BountyIndex>,
1006 beneficiary: BeneficiaryLookupOf<T, I>,
1007 ) -> DispatchResult {
1008 let signer = ensure_signed(origin)?;
1009 let beneficiary = T::BeneficiaryLookup::lookup(beneficiary)?;
1010
1011 let (asset_kind, value, _, status, _) =
1012 Self::get_bounty_details(parent_bounty_id, child_bounty_id)?;
1013
1014 if child_bounty_id.is_none() {
1015 ensure!(
1016 ChildBountiesPerParent::<T, I>::get(parent_bounty_id) == 0,
1017 Error::<T, I>::HasActiveChildBounty
1018 );
1019 }
1020
1021 let BountyStatus::Active { ref curator } = status else {
1022 return Err(Error::<T, I>::UnexpectedStatus.into());
1023 };
1024 ensure!(signer == *curator, Error::<T, I>::RequireCurator);
1025
1026 let beneficiary_payment_status = Self::do_process_payout_payment(
1027 parent_bounty_id,
1028 child_bounty_id,
1029 asset_kind,
1030 value,
1031 beneficiary.clone(),
1032 None,
1033 )?;
1034
1035 let new_status = BountyStatus::PayoutAttempted {
1036 curator: curator.clone(),
1037 beneficiary: beneficiary.clone(),
1038 payment_status: beneficiary_payment_status.clone(),
1039 };
1040 Self::update_bounty_status(parent_bounty_id, child_bounty_id, new_status)?;
1041
1042 Self::deposit_event(Event::<T, I>::BountyAwarded {
1043 index: parent_bounty_id,
1044 child_index: child_bounty_id,
1045 beneficiary,
1046 });
1047
1048 Ok(())
1049 }
1050
1051 #[pallet::call_index(6)]
1075 #[pallet::weight(match child_bounty_id {
1076 None => <T as Config<I>>::WeightInfo::close_parent_bounty(),
1077 Some(_) => <T as Config<I>>::WeightInfo::close_child_bounty(),
1078 })]
1079 pub fn close_bounty(
1080 origin: OriginFor<T>,
1081 #[pallet::compact] parent_bounty_id: BountyIndex,
1082 child_bounty_id: Option<BountyIndex>,
1083 ) -> DispatchResult {
1084 let maybe_sender = ensure_signed(origin.clone())
1085 .map(Some)
1086 .or_else(|_| T::RejectOrigin::ensure_origin(origin).map(|_| None))?;
1087
1088 let (asset_kind, value, _, status, parent_curator) =
1089 Self::get_bounty_details(parent_bounty_id, child_bounty_id)?;
1090
1091 let maybe_curator = match status {
1092 BountyStatus::Funded { curator } | BountyStatus::Active { curator, .. } => {
1093 Some(curator)
1094 },
1095 BountyStatus::CuratorUnassigned => None,
1096 _ => return Err(Error::<T, I>::UnexpectedStatus.into()),
1097 };
1098
1099 match child_bounty_id {
1100 None => {
1101 ensure!(
1103 ChildBountiesPerParent::<T, I>::get(parent_bounty_id) == 0,
1104 Error::<T, I>::HasActiveChildBounty
1105 );
1106 if let Some(sender) = maybe_sender.as_ref() {
1108 let is_curator =
1109 maybe_curator.as_ref().map_or(false, |curator| curator == sender);
1110 ensure!(is_curator, BadOrigin);
1111 }
1112 },
1113 Some(_) => {
1114 if let Some(sender) = maybe_sender.as_ref() {
1116 let is_curator =
1117 maybe_curator.as_ref().map_or(false, |curator| curator == sender);
1118 let is_parent_curator = parent_curator
1119 .as_ref()
1120 .map_or(false, |parent_curator| parent_curator == sender);
1121 ensure!(is_curator || is_parent_curator, BadOrigin);
1122 }
1123 },
1124 };
1125
1126 let payment_status = Self::do_process_refund_payment(
1127 parent_bounty_id,
1128 child_bounty_id,
1129 asset_kind,
1130 value,
1131 None,
1132 )?;
1133 let new_status = BountyStatus::RefundAttempted {
1134 payment_status: payment_status.clone(),
1135 curator: maybe_curator.clone(),
1136 };
1137 Self::update_bounty_status(parent_bounty_id, child_bounty_id, new_status)?;
1138
1139 Self::deposit_event(Event::<T, I>::BountyCanceled {
1140 index: parent_bounty_id,
1141 child_index: child_bounty_id,
1142 });
1143
1144 Ok(())
1145 }
1146
1147 #[pallet::call_index(7)]
1173 #[pallet::weight(<T as Config<I>>::WeightInfo::check_status_funding().max(
1174 <T as Config<I>>::WeightInfo::check_status_refund(),
1175 ).max(<T as Config<I>>::WeightInfo::check_status_payout()))]
1176 pub fn check_status(
1177 origin: OriginFor<T>,
1178 #[pallet::compact] parent_bounty_id: BountyIndex,
1179 child_bounty_id: Option<BountyIndex>,
1180 ) -> DispatchResultWithPostInfo {
1181 use BountyStatus::*;
1182
1183 ensure_signed(origin)?;
1184 let (asset_kind, value, metadata, status, parent_curator) =
1185 Self::get_bounty_details(parent_bounty_id, child_bounty_id)?;
1186
1187 let (new_status, weight) = match status {
1188 FundingAttempted { ref payment_status, curator } => {
1189 let new_payment_status = Self::do_check_funding_payment_status(
1190 parent_bounty_id,
1191 child_bounty_id,
1192 payment_status.clone(),
1193 )?;
1194
1195 let new_status = match new_payment_status {
1196 PaymentState::Succeeded => match (child_bounty_id, parent_curator) {
1197 (Some(_), Some(parent_curator)) if curator == parent_curator => {
1198 BountyStatus::Active { curator }
1199 },
1200 _ => BountyStatus::Funded { curator },
1201 },
1202 PaymentState::Pending |
1203 PaymentState::Failed |
1204 PaymentState::Attempted { .. } => BountyStatus::FundingAttempted {
1205 payment_status: new_payment_status,
1206 curator,
1207 },
1208 };
1209
1210 let weight = <T as Config<I>>::WeightInfo::check_status_funding();
1211
1212 (new_status, weight)
1213 },
1214 RefundAttempted { ref payment_status, ref curator } => {
1215 let new_payment_status = Self::do_check_refund_payment_status(
1216 parent_bounty_id,
1217 child_bounty_id,
1218 payment_status.clone(),
1219 )?;
1220
1221 let new_status = match new_payment_status {
1222 PaymentState::Succeeded => {
1223 if let Some(curator) = curator {
1224 if let Some(curator_deposit) =
1228 CuratorDeposit::<T, I>::take(parent_bounty_id, child_bounty_id)
1229 {
1230 T::Consideration::drop(curator_deposit, curator)?;
1231 }
1232 }
1233 if let Some(_) = child_bounty_id {
1234 ChildBountiesValuePerParent::<T, I>::mutate(
1236 parent_bounty_id,
1237 |total_value| *total_value = total_value.saturating_sub(value),
1238 );
1239 }
1240 Self::remove_bounty(parent_bounty_id, child_bounty_id, metadata);
1242 return Ok(Pays::No.into());
1243 },
1244 PaymentState::Pending |
1245 PaymentState::Failed |
1246 PaymentState::Attempted { .. } => BountyStatus::RefundAttempted {
1247 payment_status: new_payment_status,
1248 curator: curator.clone(),
1249 },
1250 };
1251
1252 let weight = <T as Config<I>>::WeightInfo::check_status_refund();
1253
1254 (new_status, weight)
1255 },
1256 PayoutAttempted { ref curator, ref beneficiary, ref payment_status } => {
1257 let new_payment_status = Self::do_check_payout_payment_status(
1258 parent_bounty_id,
1259 child_bounty_id,
1260 asset_kind,
1261 value,
1262 beneficiary.clone(),
1263 payment_status.clone(),
1264 )?;
1265
1266 let new_status = match new_payment_status {
1267 PaymentState::Succeeded => {
1268 if let Some(curator_deposit) =
1269 CuratorDeposit::<T, I>::take(parent_bounty_id, child_bounty_id)
1270 {
1271 T::Consideration::drop(curator_deposit, curator)?;
1275 }
1276 Self::remove_bounty(parent_bounty_id, child_bounty_id, metadata);
1278 return Ok(Pays::No.into());
1279 },
1280 PaymentState::Pending |
1281 PaymentState::Failed |
1282 PaymentState::Attempted { .. } => BountyStatus::PayoutAttempted {
1283 curator: curator.clone(),
1284 beneficiary: beneficiary.clone(),
1285 payment_status: new_payment_status.clone(),
1286 },
1287 };
1288
1289 let weight = <T as Config<I>>::WeightInfo::check_status_payout();
1290
1291 (new_status, weight)
1292 },
1293 _ => return Err(Error::<T, I>::UnexpectedStatus.into()),
1294 };
1295
1296 Self::update_bounty_status(parent_bounty_id, child_bounty_id, new_status)?;
1297
1298 Ok(Some(weight).into())
1299 }
1300
1301 #[pallet::call_index(8)]
1324 #[pallet::weight(<T as Config<I>>::WeightInfo::retry_payment_funding().max(
1325 <T as Config<I>>::WeightInfo::retry_payment_refund(),
1326 ).max(<T as Config<I>>::WeightInfo::retry_payment_payout()))]
1327 pub fn retry_payment(
1328 origin: OriginFor<T>,
1329 #[pallet::compact] parent_bounty_id: BountyIndex,
1330 child_bounty_id: Option<BountyIndex>,
1331 ) -> DispatchResultWithPostInfo {
1332 use BountyStatus::*;
1333
1334 ensure_signed(origin)?;
1335 let (asset_kind, value, _, status, _) =
1336 Self::get_bounty_details(parent_bounty_id, child_bounty_id)?;
1337
1338 let (new_status, weight) = match status {
1339 FundingAttempted { ref payment_status, ref curator } => {
1340 let new_payment_status = Self::do_process_funding_payment(
1341 parent_bounty_id,
1342 child_bounty_id,
1343 asset_kind,
1344 value,
1345 Some(payment_status.clone()),
1346 )?;
1347
1348 (
1349 FundingAttempted {
1350 payment_status: new_payment_status,
1351 curator: curator.clone(),
1352 },
1353 <T as Config<I>>::WeightInfo::retry_payment_funding(),
1354 )
1355 },
1356 RefundAttempted { ref curator, ref payment_status } => {
1357 let new_payment_status = Self::do_process_refund_payment(
1358 parent_bounty_id,
1359 child_bounty_id,
1360 asset_kind,
1361 value,
1362 Some(payment_status.clone()),
1363 )?;
1364 (
1365 RefundAttempted {
1366 curator: curator.clone(),
1367 payment_status: new_payment_status,
1368 },
1369 <T as Config<I>>::WeightInfo::retry_payment_refund(),
1370 )
1371 },
1372 PayoutAttempted { ref curator, ref beneficiary, ref payment_status } => {
1373 let new_payment_status = Self::do_process_payout_payment(
1374 parent_bounty_id,
1375 child_bounty_id,
1376 asset_kind,
1377 value,
1378 beneficiary.clone(),
1379 Some(payment_status.clone()),
1380 )?;
1381 (
1382 PayoutAttempted {
1383 curator: curator.clone(),
1384 beneficiary: beneficiary.clone(),
1385 payment_status: new_payment_status,
1386 },
1387 <T as Config<I>>::WeightInfo::retry_payment_payout(),
1388 )
1389 },
1390 _ => return Err(Error::<T, I>::UnexpectedStatus.into()),
1391 };
1392
1393 Self::update_bounty_status(parent_bounty_id, child_bounty_id, new_status)?;
1394
1395 Ok(Some(weight).into())
1396 }
1397
1398 #[pallet::call_index(9)]
1429 #[pallet::weight(<T as Config<I>>::WeightInfo::increase_value())]
1430 pub fn increase_value(
1431 origin: OriginFor<T>,
1432 #[pallet::compact] parent_bounty_id: BountyIndex,
1433 #[pallet::compact] amount: T::Balance,
1434 ) -> DispatchResult {
1435 let signer = ensure_signed(origin)?;
1436 ensure!(!amount.is_zero(), Error::<T, I>::InvalidValue);
1437
1438 let (old_value, new_value) = Bounties::<T, I>::try_mutate(
1439 parent_bounty_id,
1440 |maybe_bounty| -> Result<(T::Balance, T::Balance), DispatchError> {
1441 let bounty = maybe_bounty.as_mut().ok_or(Error::<T, I>::InvalidIndex)?;
1442
1443 let curator = match &bounty.status {
1446 BountyStatus::Active { curator } => curator.clone(),
1447 _ => return Err(Error::<T, I>::UnexpectedStatus.into()),
1448 };
1449 ensure!(signer == curator, Error::<T, I>::RequireCurator);
1450
1451 let old_value = bounty.value;
1454 let new_value =
1455 old_value.checked_add(&amount).ok_or(Error::<T, I>::InvalidValue)?;
1456
1457 let native_amount = T::BalanceConverter::from_asset_balance(
1460 new_value,
1461 bounty.asset_kind.clone(),
1462 )
1463 .map_err(|_| Error::<T, I>::FailedToConvertBalance)?;
1464 let deposit =
1465 CuratorDeposit::<T, I>::take(parent_bounty_id, None::<BountyIndex>)
1466 .ok_or(Error::<T, I>::UnexpectedStatus)?;
1467 let deposit = deposit.update(&curator, native_amount)?;
1468 CuratorDeposit::<T, I>::insert(parent_bounty_id, None::<BountyIndex>, deposit);
1469
1470 bounty.value = new_value;
1471 Ok((old_value, new_value))
1472 },
1473 )?;
1474
1475 Self::deposit_event(Event::<T, I>::BountyValueIncreased {
1476 index: parent_bounty_id,
1477 old_value,
1478 new_value,
1479 });
1480
1481 Ok(())
1482 }
1483 }
1484
1485 #[pallet::hooks]
1486 impl<T: Config<I>, I: 'static> Hooks<SystemBlockNumberFor<T>> for Pallet<T, I> {
1487 #[cfg(feature = "try-runtime")]
1488 fn try_state(_n: SystemBlockNumberFor<T>) -> Result<(), sp_runtime::TryRuntimeError> {
1489 Self::do_try_state()
1490 }
1491 }
1492}
1493
1494#[cfg(any(feature = "try-runtime", test))]
1495impl<T: Config<I>, I: 'static> Pallet<T, I> {
1496 pub fn do_try_state() -> Result<(), sp_runtime::TryRuntimeError> {
1500 Self::try_state_bounties_count()?;
1501
1502 for parent_bounty_id in Bounties::<T, I>::iter_keys() {
1503 Self::try_state_child_bounties_count(parent_bounty_id)?;
1504 }
1505
1506 Ok(())
1507 }
1508
1509 fn try_state_bounties_count() -> Result<(), sp_runtime::TryRuntimeError> {
1514 let bounties_length = Bounties::<T, I>::iter().count() as u32;
1515
1516 ensure!(
1517 <BountyCount<T, I>>::get() >= bounties_length,
1518 "`BountyCount` must be grater or equals the number of `Bounties` in storage"
1519 );
1520
1521 Ok(())
1522 }
1523
1524 fn try_state_child_bounties_count(
1529 parent_bounty_id: BountyIndex,
1530 ) -> Result<(), sp_runtime::TryRuntimeError> {
1531 let child_bounties_length =
1532 ChildBounties::<T, I>::iter_prefix(parent_bounty_id).count() as u32;
1533
1534 ensure!(
1535 <ChildBountiesPerParent<T, I>>::get(parent_bounty_id) >= child_bounties_length,
1536 "`ChildBountiesPerParent` must be grater or equals the number of `ChildBounties` in storage"
1537 );
1538
1539 Ok(())
1540 }
1541}
1542
1543impl<T: Config<I>, I: 'static> Pallet<T, I> {
1544 pub fn funding_source_account(
1546 asset_kind: T::AssetKind,
1547 ) -> Result<T::Beneficiary, DispatchError> {
1548 T::FundingSource::try_convert(asset_kind)
1549 .map_err(|_| Error::<T, I>::FailedToConvertSource.into())
1550 }
1551
1552 pub fn bounty_account(
1554 bounty_id: BountyIndex,
1555 asset_kind: T::AssetKind,
1556 ) -> Result<T::Beneficiary, DispatchError> {
1557 T::BountySource::try_convert((bounty_id, asset_kind))
1558 .map_err(|_| Error::<T, I>::FailedToConvertSource.into())
1559 }
1560
1561 pub fn child_bounty_account(
1563 parent_bounty_id: BountyIndex,
1564 child_bounty_id: BountyIndex,
1565 asset_kind: T::AssetKind,
1566 ) -> Result<T::Beneficiary, DispatchError> {
1567 T::ChildBountySource::try_convert((parent_bounty_id, child_bounty_id, asset_kind))
1568 .map_err(|_| Error::<T, I>::FailedToConvertSource.into())
1569 }
1570
1571 pub fn get_bounty_details(
1576 parent_bounty_id: BountyIndex,
1577 child_bounty_id: Option<BountyIndex>,
1578 ) -> Result<
1579 (
1580 T::AssetKind,
1581 T::Balance,
1582 T::Hash,
1583 BountyStatus<T::AccountId, PaymentIdOf<T, I>, T::Beneficiary>,
1584 Option<T::AccountId>,
1585 ),
1586 DispatchError,
1587 > {
1588 let parent_bounty =
1589 Bounties::<T, I>::get(parent_bounty_id).ok_or(Error::<T, I>::InvalidIndex)?;
1590
1591 let parent_curator = if let BountyStatus::Active { curator } = &parent_bounty.status {
1593 Some(curator.clone())
1594 } else {
1595 None
1596 };
1597
1598 match child_bounty_id {
1599 None => Ok((
1600 parent_bounty.asset_kind,
1601 parent_bounty.value,
1602 parent_bounty.metadata,
1603 parent_bounty.status,
1604 parent_curator,
1605 )),
1606 Some(child_bounty_id) => {
1607 let child_bounty = ChildBounties::<T, I>::get(parent_bounty_id, child_bounty_id)
1608 .ok_or(Error::<T, I>::InvalidIndex)?;
1609 Ok((
1610 parent_bounty.asset_kind,
1611 child_bounty.value,
1612 child_bounty.metadata,
1613 child_bounty.status,
1614 parent_curator,
1615 ))
1616 },
1617 }
1618 }
1619
1620 pub fn update_bounty_status(
1622 parent_bounty_id: BountyIndex,
1623 child_bounty_id: Option<BountyIndex>,
1624 new_status: BountyStatus<T::AccountId, PaymentIdOf<T, I>, T::Beneficiary>,
1625 ) -> Result<(), DispatchError> {
1626 match child_bounty_id {
1627 None => {
1628 let mut bounty =
1629 Bounties::<T, I>::get(parent_bounty_id).ok_or(Error::<T, I>::InvalidIndex)?;
1630 bounty.status = new_status;
1631 Bounties::<T, I>::insert(parent_bounty_id, bounty);
1632 },
1633 Some(child_bounty_id) => {
1634 let mut bounty = ChildBounties::<T, I>::get(parent_bounty_id, child_bounty_id)
1635 .ok_or(Error::<T, I>::InvalidIndex)?;
1636 bounty.status = new_status;
1637 ChildBounties::<T, I>::insert(parent_bounty_id, child_bounty_id, bounty);
1638 },
1639 }
1640
1641 Ok(())
1642 }
1643
1644 fn calculate_payout(
1646 parent_bounty_id: BountyIndex,
1647 child_bounty_id: Option<BountyIndex>,
1648 value: T::Balance,
1649 ) -> T::Balance {
1650 match child_bounty_id {
1651 None => {
1652 let children_value = ChildBountiesValuePerParent::<T, I>::get(parent_bounty_id);
1655 debug_assert!(children_value <= value);
1656 let payout = value.saturating_sub(children_value);
1657 payout
1658 },
1659 Some(_) => value,
1660 }
1661 }
1662
1663 fn remove_bounty(
1665 parent_bounty_id: BountyIndex,
1666 child_bounty_id: Option<BountyIndex>,
1667 metadata: T::Hash,
1668 ) {
1669 match child_bounty_id {
1670 None => {
1671 Bounties::<T, I>::remove(parent_bounty_id);
1672 ChildBountiesPerParent::<T, I>::remove(parent_bounty_id);
1673 TotalChildBountiesPerParent::<T, I>::remove(parent_bounty_id);
1674 ChildBountiesValuePerParent::<T, I>::remove(parent_bounty_id);
1675 },
1676 Some(child_bounty_id) => {
1677 ChildBounties::<T, I>::remove(parent_bounty_id, child_bounty_id);
1678 ChildBountiesPerParent::<T, I>::mutate(parent_bounty_id, |count| {
1679 count.saturating_dec()
1680 });
1681 },
1682 }
1683
1684 T::Preimages::unrequest(&metadata);
1685 }
1686
1687 fn do_process_funding_payment(
1689 parent_bounty_id: BountyIndex,
1690 child_bounty_id: Option<BountyIndex>,
1691 asset_kind: T::AssetKind,
1692 value: T::Balance,
1693 maybe_payment_status: Option<PaymentState<PaymentIdOf<T, I>>>,
1694 ) -> Result<PaymentState<PaymentIdOf<T, I>>, DispatchError> {
1695 if let Some(payment_status) = maybe_payment_status {
1696 ensure!(payment_status.is_pending_or_failed(), Error::<T, I>::UnexpectedStatus);
1697 }
1698
1699 let (source, beneficiary) = match child_bounty_id {
1700 None => (
1701 Self::funding_source_account(asset_kind.clone())?,
1702 Self::bounty_account(parent_bounty_id, asset_kind.clone())?,
1703 ),
1704 Some(child_bounty_id) => (
1705 Self::bounty_account(parent_bounty_id, asset_kind.clone())?,
1706 Self::child_bounty_account(parent_bounty_id, child_bounty_id, asset_kind.clone())?,
1707 ),
1708 };
1709
1710 let id = <T as Config<I>>::Paymaster::pay(&source, &beneficiary, asset_kind, value)
1711 .map_err(|_| Error::<T, I>::FundingError)?;
1712
1713 Self::deposit_event(Event::<T, I>::Paid {
1714 index: parent_bounty_id,
1715 child_index: child_bounty_id,
1716 payment_id: id,
1717 });
1718
1719 Ok(PaymentState::Attempted { id })
1720 }
1721
1722 fn do_check_funding_payment_status(
1725 parent_bounty_id: BountyIndex,
1726 child_bounty_id: Option<BountyIndex>,
1727 payment_status: PaymentState<PaymentIdOf<T, I>>,
1728 ) -> Result<PaymentState<PaymentIdOf<T, I>>, DispatchError> {
1729 let payment_id = payment_status.get_attempt_id().ok_or(Error::<T, I>::UnexpectedStatus)?;
1730
1731 match <T as Config<I>>::Paymaster::check_payment(payment_id) {
1732 PaymentStatus::Success => {
1733 Self::deposit_event(Event::<T, I>::BountyFundingProcessed {
1734 index: parent_bounty_id,
1735 child_index: child_bounty_id,
1736 });
1737 Ok(PaymentState::Succeeded)
1738 },
1739 PaymentStatus::InProgress | PaymentStatus::Unknown => {
1740 return Err(Error::<T, I>::FundingInconclusive.into())
1741 },
1742 PaymentStatus::Failure => {
1743 Self::deposit_event(Event::<T, I>::PaymentFailed {
1744 index: parent_bounty_id,
1745 child_index: child_bounty_id,
1746 payment_id,
1747 });
1748 return Ok(PaymentState::Failed);
1749 },
1750 }
1751 }
1752
1753 fn do_process_refund_payment(
1756 parent_bounty_id: BountyIndex,
1757 child_bounty_id: Option<BountyIndex>,
1758 asset_kind: T::AssetKind,
1759 value: T::Balance,
1760 payment_status: Option<PaymentState<PaymentIdOf<T, I>>>,
1761 ) -> Result<PaymentState<PaymentIdOf<T, I>>, DispatchError> {
1762 if let Some(payment_status) = payment_status {
1763 ensure!(payment_status.is_pending_or_failed(), Error::<T, I>::UnexpectedStatus);
1764 }
1765
1766 let (source, beneficiary) = match child_bounty_id {
1767 None => (
1768 Self::bounty_account(parent_bounty_id, asset_kind.clone())?,
1769 Self::funding_source_account(asset_kind.clone())?,
1770 ),
1771 Some(child_bounty_id) => (
1772 Self::child_bounty_account(parent_bounty_id, child_bounty_id, asset_kind.clone())?,
1773 Self::bounty_account(parent_bounty_id, asset_kind.clone())?,
1774 ),
1775 };
1776
1777 let id = <T as Config<I>>::Paymaster::pay(&source, &beneficiary, asset_kind, value)
1778 .map_err(|_| Error::<T, I>::RefundError)?;
1779
1780 Self::deposit_event(Event::<T, I>::Paid {
1781 index: parent_bounty_id,
1782 child_index: child_bounty_id,
1783 payment_id: id,
1784 });
1785
1786 Ok(PaymentState::Attempted { id })
1787 }
1788
1789 fn do_check_refund_payment_status(
1792 parent_bounty_id: BountyIndex,
1793 child_bounty_id: Option<BountyIndex>,
1794 payment_status: PaymentState<PaymentIdOf<T, I>>,
1795 ) -> Result<PaymentState<PaymentIdOf<T, I>>, DispatchError> {
1796 let payment_id = payment_status.get_attempt_id().ok_or(Error::<T, I>::UnexpectedStatus)?;
1797
1798 match <T as pallet::Config<I>>::Paymaster::check_payment(payment_id) {
1799 PaymentStatus::Success => {
1800 Self::deposit_event(Event::<T, I>::BountyRefundProcessed {
1801 index: parent_bounty_id,
1802 child_index: child_bounty_id,
1803 });
1804 Ok(PaymentState::Succeeded)
1805 },
1806 PaymentStatus::InProgress | PaymentStatus::Unknown =>
1807 {
1809 Err(Error::<T, I>::RefundInconclusive.into())
1810 },
1811 PaymentStatus::Failure => {
1812 Self::deposit_event(Event::<T, I>::PaymentFailed {
1814 index: parent_bounty_id,
1815 child_index: child_bounty_id,
1816 payment_id,
1817 });
1818 Ok(PaymentState::Failed)
1819 },
1820 }
1821 }
1822
1823 fn do_process_payout_payment(
1825 parent_bounty_id: BountyIndex,
1826 child_bounty_id: Option<BountyIndex>,
1827 asset_kind: T::AssetKind,
1828 value: T::Balance,
1829 beneficiary: T::Beneficiary,
1830 payment_status: Option<PaymentState<PaymentIdOf<T, I>>>,
1831 ) -> Result<PaymentState<PaymentIdOf<T, I>>, DispatchError> {
1832 if let Some(payment_status) = payment_status {
1833 ensure!(payment_status.is_pending_or_failed(), Error::<T, I>::UnexpectedStatus);
1834 }
1835
1836 let payout = Self::calculate_payout(parent_bounty_id, child_bounty_id, value);
1837
1838 let source = match child_bounty_id {
1839 None => Self::bounty_account(parent_bounty_id, asset_kind.clone())?,
1840 Some(child_bounty_id) => {
1841 Self::child_bounty_account(parent_bounty_id, child_bounty_id, asset_kind.clone())?
1842 },
1843 };
1844
1845 let id = <T as Config<I>>::Paymaster::pay(&source, &beneficiary, asset_kind, payout)
1846 .map_err(|_| Error::<T, I>::PayoutError)?;
1847
1848 Self::deposit_event(Event::<T, I>::Paid {
1849 index: parent_bounty_id,
1850 child_index: child_bounty_id,
1851 payment_id: id,
1852 });
1853
1854 Ok(PaymentState::Attempted { id })
1855 }
1856
1857 fn do_check_payout_payment_status(
1860 parent_bounty_id: BountyIndex,
1861 child_bounty_id: Option<BountyIndex>,
1862 asset_kind: T::AssetKind,
1863 value: T::Balance,
1864 beneficiary: T::Beneficiary,
1865 payment_status: PaymentState<PaymentIdOf<T, I>>,
1866 ) -> Result<PaymentState<PaymentIdOf<T, I>>, DispatchError> {
1867 let payment_id = payment_status.get_attempt_id().ok_or(Error::<T, I>::UnexpectedStatus)?;
1868
1869 match <T as pallet::Config<I>>::Paymaster::check_payment(payment_id) {
1870 PaymentStatus::Success => {
1871 let payout = Self::calculate_payout(parent_bounty_id, child_bounty_id, value);
1872
1873 Self::deposit_event(Event::<T, I>::BountyPayoutProcessed {
1874 index: parent_bounty_id,
1875 child_index: child_bounty_id,
1876 asset_kind: asset_kind.clone(),
1877 value: payout,
1878 beneficiary,
1879 });
1880
1881 Ok(PaymentState::Succeeded)
1882 },
1883 PaymentStatus::InProgress | PaymentStatus::Unknown =>
1884 {
1886 Err(Error::<T, I>::PayoutInconclusive.into())
1887 },
1888 PaymentStatus::Failure => {
1889 Self::deposit_event(Event::<T, I>::PaymentFailed {
1891 index: parent_bounty_id,
1892 child_index: child_bounty_id,
1893 payment_id,
1894 });
1895 Ok(PaymentState::Failed)
1896 },
1897 }
1898 }
1899}
1900
1901pub struct CuratorDepositAmount<Mult, Min, Max, Balance>(PhantomData<(Mult, Min, Max, Balance)>);
1906impl<Mult, Min, Max, Balance> Convert<Balance, Balance>
1907 for CuratorDepositAmount<Mult, Min, Max, Balance>
1908where
1909 Balance: frame_support::traits::tokens::Balance,
1910 Min: Get<Option<Balance>>,
1911 Max: Get<Option<Balance>>,
1912 Mult: Get<Permill>,
1913{
1914 fn convert(value: Balance) -> Balance {
1915 let mut deposit = Mult::get().mul_floor(value);
1916
1917 if let Some(min) = Min::get() {
1918 if deposit < min {
1919 deposit = min;
1920 }
1921 }
1922
1923 if let Some(max) = Max::get() {
1924 if deposit > max {
1925 deposit = max;
1926 }
1927 }
1928
1929 deposit
1930 }
1931}
1932
1933pub struct PalletIdAsFundingSource<Id, T, C, I = ()>(PhantomData<(Id, T, C, I)>);
1944impl<Id, T, C, I> TryConvert<T::AssetKind, T::Beneficiary> for PalletIdAsFundingSource<Id, T, C, I>
1945where
1946 Id: Get<PalletId>,
1947 T: crate::Config<I>,
1948 C: Convert<T::AccountId, T::Beneficiary>,
1949{
1950 fn try_convert(_asset_kind: T::AssetKind) -> Result<T::Beneficiary, T::AssetKind> {
1951 let account: T::AccountId = Id::get().into_account_truncating();
1952 Ok(C::convert(account))
1953 }
1954}
1955
1956pub struct BountyAccountPrefix;
1961impl Get<[u8; 3]> for BountyAccountPrefix {
1962 fn get() -> [u8; 3] {
1963 *b"mbt"
1964 }
1965}
1966
1967pub struct ChildBountyAccountPrefix;
1972impl Get<[u8; 3]> for ChildBountyAccountPrefix {
1973 fn get() -> [u8; 3] {
1974 *b"mcb"
1975 }
1976}
1977
1978pub struct BountySourceFromPalletId<Id, Prefix, T, C, I = ()>(PhantomData<(Id, Prefix, T, C, I)>);
1997impl<Id, Prefix, T, C, I> TryConvert<(BountyIndex, T::AssetKind), T::Beneficiary>
1998 for BountySourceFromPalletId<Id, Prefix, T, C, I>
1999where
2000 Id: Get<PalletId>,
2001 Prefix: Get<[u8; 3]>,
2002 T: crate::Config<I>,
2003 C: Convert<T::AccountId, T::Beneficiary>,
2004{
2005 fn try_convert(
2006 (parent_bounty_id, _asset_kind): (BountyIndex, T::AssetKind),
2007 ) -> Result<T::Beneficiary, (BountyIndex, T::AssetKind)> {
2008 let account: T::AccountId =
2009 Id::get().into_sub_account_truncating((Prefix::get(), parent_bounty_id));
2010 Ok(C::convert(account))
2011 }
2012}
2013
2014pub struct ChildBountySourceFromPalletId<Id, Prefix, T, C, I = ()>(
2034 PhantomData<(Id, Prefix, T, C, I)>,
2035);
2036impl<Id, Prefix, T, C, I> TryConvert<(BountyIndex, BountyIndex, T::AssetKind), T::Beneficiary>
2037 for ChildBountySourceFromPalletId<Id, Prefix, T, C, I>
2038where
2039 Id: Get<PalletId>,
2040 Prefix: Get<[u8; 3]>,
2041 T: crate::Config<I>,
2042 C: Convert<T::AccountId, T::Beneficiary>,
2043{
2044 fn try_convert(
2045 (parent_bounty_id, child_bounty_id, _asset_kind): (BountyIndex, BountyIndex, T::AssetKind),
2046 ) -> Result<T::Beneficiary, (BountyIndex, BountyIndex, T::AssetKind)> {
2047 let account: T::AccountId = Id::get().into_sub_account_truncating((
2050 Prefix::get(),
2051 parent_bounty_id,
2052 child_bounty_id,
2053 ));
2054 Ok(C::convert(account))
2055 }
2056}