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 refund = Self::calculate_payout(parent_bounty_id, child_bounty_id, value);
1778 let id = <T as Config<I>>::Paymaster::pay(&source, &beneficiary, asset_kind, refund)
1779 .map_err(|_| Error::<T, I>::RefundError)?;
1780
1781 Self::deposit_event(Event::<T, I>::Paid {
1782 index: parent_bounty_id,
1783 child_index: child_bounty_id,
1784 payment_id: id,
1785 });
1786
1787 Ok(PaymentState::Attempted { id })
1788 }
1789
1790 fn do_check_refund_payment_status(
1793 parent_bounty_id: BountyIndex,
1794 child_bounty_id: Option<BountyIndex>,
1795 payment_status: PaymentState<PaymentIdOf<T, I>>,
1796 ) -> Result<PaymentState<PaymentIdOf<T, I>>, DispatchError> {
1797 let payment_id = payment_status.get_attempt_id().ok_or(Error::<T, I>::UnexpectedStatus)?;
1798
1799 match <T as pallet::Config<I>>::Paymaster::check_payment(payment_id) {
1800 PaymentStatus::Success => {
1801 Self::deposit_event(Event::<T, I>::BountyRefundProcessed {
1802 index: parent_bounty_id,
1803 child_index: child_bounty_id,
1804 });
1805 Ok(PaymentState::Succeeded)
1806 },
1807 PaymentStatus::InProgress | PaymentStatus::Unknown =>
1808 {
1810 Err(Error::<T, I>::RefundInconclusive.into())
1811 },
1812 PaymentStatus::Failure => {
1813 Self::deposit_event(Event::<T, I>::PaymentFailed {
1815 index: parent_bounty_id,
1816 child_index: child_bounty_id,
1817 payment_id,
1818 });
1819 Ok(PaymentState::Failed)
1820 },
1821 }
1822 }
1823
1824 fn do_process_payout_payment(
1826 parent_bounty_id: BountyIndex,
1827 child_bounty_id: Option<BountyIndex>,
1828 asset_kind: T::AssetKind,
1829 value: T::Balance,
1830 beneficiary: T::Beneficiary,
1831 payment_status: Option<PaymentState<PaymentIdOf<T, I>>>,
1832 ) -> Result<PaymentState<PaymentIdOf<T, I>>, DispatchError> {
1833 if let Some(payment_status) = payment_status {
1834 ensure!(payment_status.is_pending_or_failed(), Error::<T, I>::UnexpectedStatus);
1835 }
1836
1837 let payout = Self::calculate_payout(parent_bounty_id, child_bounty_id, value);
1838
1839 let source = match child_bounty_id {
1840 None => Self::bounty_account(parent_bounty_id, asset_kind.clone())?,
1841 Some(child_bounty_id) => {
1842 Self::child_bounty_account(parent_bounty_id, child_bounty_id, asset_kind.clone())?
1843 },
1844 };
1845
1846 let id = <T as Config<I>>::Paymaster::pay(&source, &beneficiary, asset_kind, payout)
1847 .map_err(|_| Error::<T, I>::PayoutError)?;
1848
1849 Self::deposit_event(Event::<T, I>::Paid {
1850 index: parent_bounty_id,
1851 child_index: child_bounty_id,
1852 payment_id: id,
1853 });
1854
1855 Ok(PaymentState::Attempted { id })
1856 }
1857
1858 fn do_check_payout_payment_status(
1861 parent_bounty_id: BountyIndex,
1862 child_bounty_id: Option<BountyIndex>,
1863 asset_kind: T::AssetKind,
1864 value: T::Balance,
1865 beneficiary: T::Beneficiary,
1866 payment_status: PaymentState<PaymentIdOf<T, I>>,
1867 ) -> Result<PaymentState<PaymentIdOf<T, I>>, DispatchError> {
1868 let payment_id = payment_status.get_attempt_id().ok_or(Error::<T, I>::UnexpectedStatus)?;
1869
1870 match <T as pallet::Config<I>>::Paymaster::check_payment(payment_id) {
1871 PaymentStatus::Success => {
1872 let payout = Self::calculate_payout(parent_bounty_id, child_bounty_id, value);
1873
1874 Self::deposit_event(Event::<T, I>::BountyPayoutProcessed {
1875 index: parent_bounty_id,
1876 child_index: child_bounty_id,
1877 asset_kind: asset_kind.clone(),
1878 value: payout,
1879 beneficiary,
1880 });
1881
1882 Ok(PaymentState::Succeeded)
1883 },
1884 PaymentStatus::InProgress | PaymentStatus::Unknown =>
1885 {
1887 Err(Error::<T, I>::PayoutInconclusive.into())
1888 },
1889 PaymentStatus::Failure => {
1890 Self::deposit_event(Event::<T, I>::PaymentFailed {
1892 index: parent_bounty_id,
1893 child_index: child_bounty_id,
1894 payment_id,
1895 });
1896 Ok(PaymentState::Failed)
1897 },
1898 }
1899 }
1900}
1901
1902pub struct CuratorDepositAmount<Mult, Min, Max, Balance>(PhantomData<(Mult, Min, Max, Balance)>);
1907impl<Mult, Min, Max, Balance> Convert<Balance, Balance>
1908 for CuratorDepositAmount<Mult, Min, Max, Balance>
1909where
1910 Balance: frame_support::traits::tokens::Balance,
1911 Min: Get<Option<Balance>>,
1912 Max: Get<Option<Balance>>,
1913 Mult: Get<Permill>,
1914{
1915 fn convert(value: Balance) -> Balance {
1916 let mut deposit = Mult::get().mul_floor(value);
1917
1918 if let Some(min) = Min::get() {
1919 if deposit < min {
1920 deposit = min;
1921 }
1922 }
1923
1924 if let Some(max) = Max::get() {
1925 if deposit > max {
1926 deposit = max;
1927 }
1928 }
1929
1930 deposit
1931 }
1932}
1933
1934pub struct PalletIdAsFundingSource<Id, T, C, I = ()>(PhantomData<(Id, T, C, I)>);
1945impl<Id, T, C, I> TryConvert<T::AssetKind, T::Beneficiary> for PalletIdAsFundingSource<Id, T, C, I>
1946where
1947 Id: Get<PalletId>,
1948 T: crate::Config<I>,
1949 C: Convert<T::AccountId, T::Beneficiary>,
1950{
1951 fn try_convert(_asset_kind: T::AssetKind) -> Result<T::Beneficiary, T::AssetKind> {
1952 let account: T::AccountId = Id::get().into_account_truncating();
1953 Ok(C::convert(account))
1954 }
1955}
1956
1957pub struct BountyAccountPrefix;
1962impl Get<[u8; 3]> for BountyAccountPrefix {
1963 fn get() -> [u8; 3] {
1964 *b"mbt"
1965 }
1966}
1967
1968pub struct ChildBountyAccountPrefix;
1973impl Get<[u8; 3]> for ChildBountyAccountPrefix {
1974 fn get() -> [u8; 3] {
1975 *b"mcb"
1976 }
1977}
1978
1979pub struct BountySourceFromPalletId<Id, Prefix, T, C, I = ()>(PhantomData<(Id, Prefix, T, C, I)>);
1998impl<Id, Prefix, T, C, I> TryConvert<(BountyIndex, T::AssetKind), T::Beneficiary>
1999 for BountySourceFromPalletId<Id, Prefix, T, C, I>
2000where
2001 Id: Get<PalletId>,
2002 Prefix: Get<[u8; 3]>,
2003 T: crate::Config<I>,
2004 C: Convert<T::AccountId, T::Beneficiary>,
2005{
2006 fn try_convert(
2007 (parent_bounty_id, _asset_kind): (BountyIndex, T::AssetKind),
2008 ) -> Result<T::Beneficiary, (BountyIndex, T::AssetKind)> {
2009 let account: T::AccountId =
2010 Id::get().into_sub_account_truncating((Prefix::get(), parent_bounty_id));
2011 Ok(C::convert(account))
2012 }
2013}
2014
2015pub struct ChildBountySourceFromPalletId<Id, Prefix, T, C, I = ()>(
2035 PhantomData<(Id, Prefix, T, C, I)>,
2036);
2037impl<Id, Prefix, T, C, I> TryConvert<(BountyIndex, BountyIndex, T::AssetKind), T::Beneficiary>
2038 for ChildBountySourceFromPalletId<Id, Prefix, T, C, I>
2039where
2040 Id: Get<PalletId>,
2041 Prefix: Get<[u8; 3]>,
2042 T: crate::Config<I>,
2043 C: Convert<T::AccountId, T::Beneficiary>,
2044{
2045 fn try_convert(
2046 (parent_bounty_id, child_bounty_id, _asset_kind): (BountyIndex, BountyIndex, T::AssetKind),
2047 ) -> Result<T::Beneficiary, (BountyIndex, BountyIndex, T::AssetKind)> {
2048 let account: T::AccountId = Id::get().into_sub_account_truncating((
2051 Prefix::get(),
2052 parent_bounty_id,
2053 child_bounty_id,
2054 ));
2055 Ok(C::convert(account))
2056 }
2057}