#![allow(clippy::string_lit_as_bytes)]
#![allow(clippy::redundant_closure_call)]
#![allow(clippy::type_complexity)]
#![cfg_attr(not(feature = "std"), no_std)]
#[cfg(test)]
mod tests;
use codec::Codec;
use frame_support::{
decl_error,
decl_event,
decl_module,
decl_storage,
ensure,
traits::{
Currency,
ExistenceRequirement,
Get,
ReservableCurrency,
},
Parameter,
};
use frame_system::{
self as system,
ensure_signed,
};
use sp_runtime::{
traits::{
AtLeast32Bit,
MaybeSerializeDeserialize,
Member,
Zero,
},
DispatchError,
DispatchResult,
};
use sp_std::{
fmt::Debug,
prelude::*,
};
use util::{
bank::{
BankOrAccount,
OnChainTreasuryID,
TransferId,
},
bounty::{
ApplicationState,
BankSpend,
BountyInformation,
BountyMapID,
GrantApplication,
MilestoneStatus,
MilestoneSubmission,
},
court::ResolutionMetadata,
traits::{
ApproveGrant,
ApproveWithoutTransfer,
GenerateUniqueID,
GetVoteOutcome,
GroupMembership,
IDIsAvailable,
OpenVote,
OrganizationSupervisorPermissions,
PostBounty,
PostOrgTransfer,
PostUserTransfer,
ReturnsBountyIdentifier,
SeededGenerateUniqueID,
StartReview,
SubmitGrantApplication,
SubmitMilestone,
SuperviseGrantApplication,
},
vote::{
ThresholdConfig,
VoteOutcome,
},
};
pub type BalanceOf<T> = <<T as bank::Trait>::Currency as Currency<
<T as frame_system::Trait>::AccountId,
>>::Balance;
pub trait Trait:
frame_system::Trait + org::Trait + vote::Trait + bank::Trait
{
type Event: From<Event<Self>> + Into<<Self as frame_system::Trait>::Event>;
type BountyId: Parameter
+ Member
+ AtLeast32Bit
+ Codec
+ Default
+ Copy
+ MaybeSerializeDeserialize
+ Debug
+ PartialOrd
+ PartialEq
+ Zero;
type BountyLowerBound: Get<BalanceOf<Self>>;
}
decl_event!(
pub enum Event<T>
where
<T as frame_system::Trait>::AccountId,
<T as vote::Trait>::VoteId,
<T as bank::Trait>::BankId,
<T as Trait>::BountyId,
Balance = BalanceOf<T>,
{
BountyPosted(BountyId, AccountId, Balance),
BountyApplicationSubmitted(BountyId, BountyId, AccountId, Option<OnChainTreasuryID>, Balance),
SudoApprovedBountyApplication(AccountId, BountyId, BountyId, ApplicationState<VoteId>),
ApplicationReviewTriggered(AccountId, BountyId, BountyId, ApplicationState<VoteId>),
ApplicationPolled(AccountId, BountyId, BountyId, ApplicationState<VoteId>),
MilestoneSubmitted(AccountId, BountyId, BountyId, BountyId, Balance),
MilestoneReviewTriggered(AccountId, BountyId, BountyId, MilestoneStatus<VoteId, BankOrAccount<TransferId<BankId>, AccountId>>),
SudoApprovedMilestone(AccountId, BountyId, BountyId, MilestoneStatus<VoteId, BankOrAccount<TransferId<BankId>, AccountId>>),
MilestonePolled(AccountId, BountyId, BountyId, MilestoneStatus<VoteId, BankOrAccount<TransferId<BankId>, AccountId>>),
}
);
decl_error! {
pub enum Error for Module<T: Trait> {
CannotPostBountyIfBankReferencedDNE,
CannotPostBountyOnBehalfOfOrgWithInvalidTransferReference,
CannotPostBountyOnBehalfOfOrgWithInvalidSpendReservation,
CannotPostBountyIfAmountExceedsAmountLeftFromSpendReference,
GrantApplicationRequestExceedsBountyFundingReserved,
GrantApplicationFailsForBountyThatDNE,
CannotApplyForBountyWithOrgBankAccountThatDNE,
SubmitterNotAuthorizedToSubmitGrantAppForOrg,
CannotReviewApplicationIfBountyDNE,
CannotReviewApplicationIfApplicationDNE,
ApplicationMustBeSubmittedAwaitingResponseToTriggerReview,
CannotSudoApproveIfBountyDNE,
CannotSudoApproveAppIfNotAssignedSudo,
CannotSudoApproveIfGrantAppDNE,
AppStateCannotBeSudoApprovedForAGrantFromCurrentState,
CannotPollApplicationIfBountyDNE,
CannotPollApplicationIfApplicationDNE,
CannotSubmitMilestoneIfBaseBountyDNE,
CannotSubmitMilestoneIfApplicationDNE,
ApplicationMustBeApprovedToSubmitMilestones,
InvalidBankReferenceInApplicationThrownInMilestoneSubmission,
MilestoneSubmissionNotAuthorizedBySubmitterForBankOrgApplication,
MilestoneSubmissionNotAuthorizedBySubmitterForIndividualApplication,
CannotTriggerMilestoneReviewIfBaseBountyDNE,
CannotTriggerMilestoneReviewIfSubmissionDNE,
CannotTriggerMilestoneReviewIfSubmissionNotAwaitingResponseAkaWrongState,
CannotSudoApproveMilestoneIfBaseBountyDNE,
CallerNotAuthorizedToSudoApproveMilestone,
CannotSudoApproveMilestoneIfBaseAppDNE,
CannotSudoApproveMilestoneThatDNE,
MilestoneCannotBeSudoApprovedFromTheCurrentState,
CannotPollMilestoneThatDNE,
CannotPollMilestoneIfBaseAppDNE,
CannotPollMilestoneSubmissionIfBaseBountyDNE,
}
}
decl_storage! {
trait Store for Module<T: Trait> as Bounty {
BountyNonce get(fn bounty_nonce): T::BountyId;
BountyAssociatedNonces get(fn bounty_associated_nonces): double_map
hasher(opaque_blake2_256) T::BountyId,
hasher(opaque_blake2_256) BountyMapID => T::BountyId;
pub LiveBounties get(fn foundation_sponsored_bounties): map
hasher(opaque_blake2_256) T::BountyId => Option<
BountyInformation<
BankOrAccount<
BankSpend<TransferId<T::BankId>>,
T::AccountId
>,
T::IpfsReference,
BalanceOf<T>,
ResolutionMetadata<
T::OrgId,
ThresholdConfig<T::Signal>,
T::BlockNumber,
>,
>
>;
pub BountyApplications get(fn bounty_applications): double_map
hasher(opaque_blake2_256) T::BountyId,
hasher(opaque_blake2_256) T::BountyId => Option<
GrantApplication<
T::AccountId,
OnChainTreasuryID,
BalanceOf<T>,
T::IpfsReference,
ApplicationState<T::VoteId>,
>
>;
pub MilestoneSubmissions get(fn milestone_submissions): double_map
hasher(opaque_blake2_256) T::BountyId,
hasher(opaque_blake2_256) T::BountyId => Option<
MilestoneSubmission<
T::AccountId,
T::BountyId,
T::IpfsReference,
BalanceOf<T>,
MilestoneStatus<T::VoteId, BankOrAccount<TransferId<T::BankId>, T::AccountId>>
>
>;
}
}
decl_module! {
pub struct Module<T: Trait> for enum Call where origin: T::Origin {
type Error = Error<T>;
fn deposit_event() = default;
#[weight = 0]
fn account_posts_bounty(
origin,
description: T::IpfsReference,
amount_reserved_for_bounty: BalanceOf<T>,
acceptance_committee: ResolutionMetadata<
T::OrgId,
ThresholdConfig<T::Signal>,
T::BlockNumber,
>,
supervision_committee: Option<
ResolutionMetadata<
T::OrgId,
ThresholdConfig<T::Signal>,
T::BlockNumber,
>,
>,
) -> DispatchResult {
let poster = ensure_signed(origin)?;
let new_bounty_id = Self::post_bounty(
poster.clone(),
None,
description,
amount_reserved_for_bounty,
acceptance_committee,
supervision_committee,
)?;
Self::deposit_event(RawEvent::BountyPosted(new_bounty_id, poster, amount_reserved_for_bounty));
Ok(())
}
#[weight = 0]
fn account_posts_bounty_for_org_with_direct_transfer(
origin,
transfer_id: TransferId<T::BankId>,
description: T::IpfsReference,
amount_reserved_for_bounty: BalanceOf<T>,
acceptance_committee: ResolutionMetadata<
T::OrgId,
ThresholdConfig<T::Signal>,
T::BlockNumber,
>,
supervision_committee: Option<
ResolutionMetadata<
T::OrgId,
ThresholdConfig<T::Signal>,
T::BlockNumber,
>,
>,
) -> DispatchResult {
let poster = ensure_signed(origin)?;
let new_bounty_id = Self::post_bounty(
poster.clone(),
Some(BankSpend::Transfer(transfer_id)),
description,
amount_reserved_for_bounty,
acceptance_committee,
supervision_committee,
)?;
Self::deposit_event(RawEvent::BountyPosted(new_bounty_id, poster, amount_reserved_for_bounty));
Ok(())
}
#[weight = 0]
fn account_posts_bounty_for_org_with_reserved_spend(
origin,
reservation_id: TransferId<T::BankId>,
description: T::IpfsReference,
amount_reserved_for_bounty: BalanceOf<T>,
acceptance_committee: ResolutionMetadata<
T::OrgId,
ThresholdConfig<T::Signal>,
T::BlockNumber,
>,
supervision_committee: Option<
ResolutionMetadata<
T::OrgId,
ThresholdConfig<T::Signal>,
T::BlockNumber,
>,
>,
) -> DispatchResult {
let poster = ensure_signed(origin)?;
let new_bounty_id = Self::post_bounty(
poster.clone(),
Some(BankSpend::Reserved(reservation_id)),
description,
amount_reserved_for_bounty,
acceptance_committee,
supervision_committee,
)?;
Self::deposit_event(RawEvent::BountyPosted(new_bounty_id, poster, amount_reserved_for_bounty));
Ok(())
}
#[weight = 0]
fn account_applies_for_bounty(
origin,
bounty_id: T::BountyId,
description: T::IpfsReference,
total_amount: BalanceOf<T>,
) -> DispatchResult {
let submitter = ensure_signed(origin)?;
let new_grant_app_id = Self::submit_grant_application(
submitter.clone(),
None,
bounty_id,
description,
total_amount,
)?;
Self::deposit_event(RawEvent::BountyApplicationSubmitted(bounty_id, new_grant_app_id, submitter, None, total_amount));
Ok(())
}
#[weight = 0]
fn account_applies_for_bounty_on_org_behalf(
origin,
org_bank: OnChainTreasuryID,
bounty_id: T::BountyId,
description: T::IpfsReference,
total_amount: BalanceOf<T>,
) -> DispatchResult {
let submitter = ensure_signed(origin)?;
let new_grant_app_id = Self::submit_grant_application(
submitter.clone(),
Some(org_bank),
bounty_id,
description,
total_amount,
)?;
Self::deposit_event(RawEvent::BountyApplicationSubmitted(bounty_id, new_grant_app_id, submitter, Some(org_bank), total_amount));
Ok(())
}
#[weight = 0]
fn account_triggers_application_review(
origin,
bounty_id: T::BountyId,
application_id: T::BountyId,
) -> DispatchResult {
let trigger = ensure_signed(origin)?;
let app_state = Self::trigger_application_review(
bounty_id,
application_id,
)?;
Self::deposit_event(RawEvent::ApplicationReviewTriggered(trigger, bounty_id, application_id, app_state));
Ok(())
}
#[weight = 0]
fn account_sudo_approves_application(
origin,
bounty_id: T::BountyId,
application_id: T::BountyId,
) -> DispatchResult {
let sudo = ensure_signed(origin)?;
let app_state = Self::sudo_approve_application(
sudo.clone(),
bounty_id,
application_id,
)?;
Self::deposit_event(RawEvent::SudoApprovedBountyApplication(sudo, bounty_id, application_id, app_state));
Ok(())
}
#[weight = 0]
fn account_poll_application(
origin,
bounty_id: T::BountyId,
application_id: T::BountyId,
) -> DispatchResult {
let poller = ensure_signed(origin)?;
let app_state = Self::poll_application(
bounty_id,
application_id,
)?;
Self::deposit_event(RawEvent::ApplicationPolled(poller, bounty_id, application_id, app_state));
Ok(())
}
#[weight = 0]
fn grantee_submits_milestone(
origin,
bounty_id: T::BountyId,
application_id: T::BountyId,
submission_reference: T::IpfsReference,
amount_requested: BalanceOf<T>,
) -> DispatchResult {
let submitter = ensure_signed(origin)?;
let new_milestone_id = Self::submit_milestone(submitter.clone(), bounty_id, application_id, submission_reference, amount_requested)?;
Self::deposit_event(RawEvent::MilestoneSubmitted(submitter, bounty_id, application_id, new_milestone_id, amount_requested));
Ok(())
}
#[weight = 0]
fn account_triggers_milestone_review(
origin,
bounty_id: T::BountyId,
milestone_id: T::BountyId,
) -> DispatchResult {
let trigger = ensure_signed(origin)?;
let milestone_status = Self::trigger_milestone_review(bounty_id, milestone_id)?;
Self::deposit_event(RawEvent::MilestoneReviewTriggered(trigger, bounty_id, milestone_id, milestone_status));
Ok(())
}
#[weight = 0]
fn account_approved_milestone(
origin,
bounty_id: T::BountyId,
milestone_id: T::BountyId,
) -> DispatchResult {
let sudo = ensure_signed(origin)?;
let milestone_status = Self::sudo_approves_milestone(sudo.clone(), bounty_id, milestone_id)?;
Self::deposit_event(RawEvent::SudoApprovedMilestone(sudo, bounty_id, milestone_id, milestone_status));
Ok(())
}
#[weight = 0]
fn account_polls_milestone(
origin,
bounty_id: T::BountyId,
milestone_id: T::BountyId,
) -> DispatchResult {
let poller = ensure_signed(origin)?;
let milestone_status = Self::poll_milestone(bounty_id, milestone_id)?;
Self::deposit_event(RawEvent::MilestonePolled(poller, bounty_id, milestone_id, milestone_status));
Ok(())
}
}
}
impl<T: Trait> Module<T> {
pub fn is_bounty(id: T::BountyId) -> bool {
!Self::id_is_available(BIdWrapper::new(id))
}
pub fn transfer_milestone_payment(
sender: BankOrAccount<BankSpend<TransferId<T::BankId>>, T::AccountId>,
recipient: BankOrAccount<OnChainTreasuryID, T::AccountId>,
amount: BalanceOf<T>,
) -> Result<BankOrAccount<TransferId<T::BankId>, T::AccountId>, DispatchError>
{
match (sender, recipient) {
(
BankOrAccount::Bank(BankSpend::Transfer(full_transfer_id)),
BankOrAccount::Bank(dest_bank_id),
) => {
let result_transfer_id =
<bank::Module<T>>::direct_transfer_to_org(
full_transfer_id,
dest_bank_id,
amount,
)?;
Ok(BankOrAccount::Bank(result_transfer_id))
}
(
BankOrAccount::Bank(BankSpend::Reserved(full_reservation_id)),
BankOrAccount::Bank(_),
) => {
<bank::Module<T>>::transfer_reserved_spend(
full_reservation_id,
amount,
)
}
(
BankOrAccount::Bank(BankSpend::Transfer(full_transfer_id)),
BankOrAccount::Account(dest_acc),
) => {
<bank::Module<T>>::direct_transfer_to_account(
full_transfer_id,
dest_acc.clone(),
amount,
)?;
Ok(BankOrAccount::Account(dest_acc))
}
(
BankOrAccount::Bank(BankSpend::Reserved(full_reservation_id)),
BankOrAccount::Account(_),
) => {
<bank::Module<T>>::transfer_reserved_spend(
full_reservation_id,
amount,
)
}
(
BankOrAccount::Account(sender_acc),
BankOrAccount::Bank(dest_bank_id),
) => {
let new_transfer_id = <bank::Module<T>>::post_user_transfer(
sender_acc,
dest_bank_id,
amount,
)?;
Ok(BankOrAccount::Bank(new_transfer_id))
}
(
BankOrAccount::Account(sender_acc),
BankOrAccount::Account(dest_acc),
) => {
T::Currency::unreserve(&sender_acc, amount);
T::Currency::transfer(
&sender_acc,
&dest_acc,
amount,
ExistenceRequirement::KeepAlive,
)?;
Ok(BankOrAccount::Account(dest_acc))
}
}
}
}
pub struct BIdWrapper<T> {
pub id: T,
}
impl<T: Copy> BIdWrapper<T> {
pub fn new(id: T) -> BIdWrapper<T> {
BIdWrapper { id }
}
}
impl<T: Trait> IDIsAvailable<BIdWrapper<T::BountyId>> for Module<T> {
fn id_is_available(id: BIdWrapper<T::BountyId>) -> bool {
<LiveBounties<T>>::get(id.id).is_none()
}
}
impl<T: Trait> IDIsAvailable<(T::BountyId, BountyMapID, T::BountyId)>
for Module<T>
{
fn id_is_available(id: (T::BountyId, BountyMapID, T::BountyId)) -> bool {
match id.1 {
BountyMapID::ApplicationId => {
<BountyApplications<T>>::get(id.0, id.2).is_none()
}
BountyMapID::MilestoneId => {
<MilestoneSubmissions<T>>::get(id.0, id.2).is_none()
}
}
}
}
impl<T: Trait> SeededGenerateUniqueID<T::BountyId, (T::BountyId, BountyMapID)>
for Module<T>
{
fn seeded_generate_unique_id(
seed: (T::BountyId, BountyMapID),
) -> T::BountyId {
let mut new_id =
<BountyAssociatedNonces<T>>::get(seed.0, seed.1) + 1u32.into();
while !Self::id_is_available((seed.0, seed.1, new_id)) {
new_id += 1u32.into();
}
<BountyAssociatedNonces<T>>::insert(seed.0, seed.1, new_id);
new_id
}
}
impl<T: Trait> GenerateUniqueID<T::BountyId> for Module<T> {
fn generate_unique_id() -> T::BountyId {
let mut id_counter = <BountyNonce<T>>::get() + 1u32.into();
while !Self::id_is_available(BIdWrapper::new(id_counter)) {
id_counter += 1u32.into();
}
<BountyNonce<T>>::put(id_counter);
id_counter
}
}
impl<T: Trait> ReturnsBountyIdentifier for Module<T> {
type BountyId = T::BountyId;
}
impl<T: Trait>
PostBounty<
T::AccountId,
T::OrgId,
BankSpend<TransferId<T::BankId>>,
BalanceOf<T>,
T::IpfsReference,
ResolutionMetadata<
T::OrgId,
ThresholdConfig<T::Signal>,
T::BlockNumber,
>,
> for Module<T>
{
type BountyInfo = BountyInformation<
BankOrAccount<BankSpend<TransferId<T::BankId>>, T::AccountId>,
T::IpfsReference,
BalanceOf<T>,
ResolutionMetadata<
T::OrgId,
ThresholdConfig<T::Signal>,
T::BlockNumber,
>,
>;
fn post_bounty(
poster: T::AccountId,
on_behalf_of: Option<BankSpend<TransferId<T::BankId>>>,
description: T::IpfsReference,
amount_reserved_for_bounty: BalanceOf<T>,
acceptance_committee: ResolutionMetadata<
T::OrgId,
ThresholdConfig<T::Signal>,
T::BlockNumber,
>,
supervision_committee: Option<
ResolutionMetadata<
T::OrgId,
ThresholdConfig<T::Signal>,
T::BlockNumber,
>,
>,
) -> Result<Self::BountyId, DispatchError> {
let bounty_poster: BankOrAccount<
BankSpend<TransferId<T::BankId>>,
T::AccountId,
> = if let Some(bank_spend) = on_behalf_of {
match bank_spend {
BankSpend::Transfer(full_transfer_id) => {
ensure!(
<bank::Module<T>>::is_bank(full_transfer_id.id),
Error::<T>::CannotPostBountyIfBankReferencedDNE
);
let transfer_info = <bank::Module<T>>::transfer_info(full_transfer_id.id, full_transfer_id.sub_id).ok_or(Error::<T>::CannotPostBountyOnBehalfOfOrgWithInvalidTransferReference)?;
ensure!(transfer_info.amount_left() >= amount_reserved_for_bounty, Error::<T>::CannotPostBountyIfAmountExceedsAmountLeftFromSpendReference);
BankOrAccount::Bank(bank_spend)
}
BankSpend::Reserved(full_reservation_id) => {
ensure!(
<bank::Module<T>>::is_bank(full_reservation_id.id),
Error::<T>::CannotPostBountyIfBankReferencedDNE
);
let spend_reservation = <bank::Module<T>>::spend_reservations(full_reservation_id.id, full_reservation_id.sub_id).ok_or(Error::<T>::CannotPostBountyOnBehalfOfOrgWithInvalidSpendReservation)?;
ensure!(spend_reservation.amount_left() >= amount_reserved_for_bounty, Error::<T>::CannotPostBountyIfAmountExceedsAmountLeftFromSpendReference);
BankOrAccount::Bank(bank_spend)
}
}
} else {
T::Currency::reserve(&poster, amount_reserved_for_bounty)?;
BankOrAccount::Account(poster)
};
let new_bounty_post = BountyInformation::new(
bounty_poster,
description,
amount_reserved_for_bounty,
acceptance_committee,
supervision_committee,
);
let new_bounty_id = Self::generate_unique_id();
<LiveBounties<T>>::insert(new_bounty_id, new_bounty_post);
Ok(new_bounty_id)
}
}
impl<T: Trait>
SubmitGrantApplication<
T::AccountId,
T::VoteId,
OnChainTreasuryID,
BalanceOf<T>,
T::IpfsReference,
> for Module<T>
{
type GrantApp = GrantApplication<
T::AccountId,
OnChainTreasuryID,
BalanceOf<T>,
T::IpfsReference,
ApplicationState<T::VoteId>,
>;
fn submit_grant_application(
submitter: T::AccountId,
bank: Option<OnChainTreasuryID>,
bounty_id: Self::BountyId,
description: T::IpfsReference,
total_amount: BalanceOf<T>,
) -> Result<Self::BountyId, DispatchError> {
let bounty = <LiveBounties<T>>::get(bounty_id)
.ok_or(Error::<T>::GrantApplicationFailsForBountyThatDNE)?;
ensure!(
bounty.funding_reserved() >= total_amount,
Error::<T>::GrantApplicationRequestExceedsBountyFundingReserved
);
if let Some(treasury_id) = bank {
let the_bank = <bank::Module<T>>::bank_stores(treasury_id).ok_or(
Error::<T>::CannotApplyForBountyWithOrgBankAccountThatDNE,
)?;
let authentication = <org::Module<T>>::is_member_of_group(
the_bank.org(),
&submitter,
)
|| <org::Module<T>>::is_organization_supervisor(
the_bank.org(),
&submitter,
);
ensure!(
authentication,
Error::<T>::SubmitterNotAuthorizedToSubmitGrantAppForOrg
);
}
let new_grant_app: GrantApplication<
T::AccountId,
OnChainTreasuryID,
BalanceOf<T>,
T::IpfsReference,
ApplicationState<T::VoteId>,
> = GrantApplication::new(submitter, bank, description, total_amount);
let new_grant_id = Self::seeded_generate_unique_id((
bounty_id,
BountyMapID::ApplicationId,
));
<BountyApplications<T>>::insert(bounty_id, new_grant_id, new_grant_app);
Ok(new_grant_id)
}
}
impl<T: Trait> SuperviseGrantApplication<T::BountyId, T::AccountId>
for Module<T>
{
type AppState = ApplicationState<T::VoteId>;
fn trigger_application_review(
bounty_id: T::BountyId,
application_id: T::BountyId,
) -> Result<Self::AppState, DispatchError> {
let bounty_info = <LiveBounties<T>>::get(bounty_id)
.ok_or(Error::<T>::CannotReviewApplicationIfBountyDNE)?;
let application_to_review =
<BountyApplications<T>>::get(bounty_id, application_id)
.ok_or(Error::<T>::CannotReviewApplicationIfApplicationDNE)?;
ensure!(
application_to_review.state() == ApplicationState::SubmittedAwaitingResponse,
Error::<T>::ApplicationMustBeSubmittedAwaitingResponseToTriggerReview
);
let review_board = bounty_info.acceptance_committee();
let new_vote_id = <vote::Module<T>>::open_vote(
Some(application_to_review.submission()),
review_board.org(),
review_board.passage_threshold(),
review_board.rejection_threshold(),
review_board.duration(),
)?;
let new_application = application_to_review
.start_review(new_vote_id)
.ok_or(Error::<T>::ApplicationMustBeSubmittedAwaitingResponseToTriggerReview)?;
let app_state = new_application.state();
<BountyApplications<T>>::insert(
bounty_id,
application_id,
new_application,
);
Ok(app_state)
}
fn sudo_approve_application(
caller: T::AccountId,
bounty_id: T::BountyId,
application_id: T::BountyId,
) -> Result<Self::AppState, DispatchError> {
let bounty_info = <LiveBounties<T>>::get(bounty_id)
.ok_or(Error::<T>::CannotSudoApproveIfBountyDNE)?;
let authentication = <org::Module<T>>::is_organization_supervisor(
bounty_info.acceptance_committee().org(),
&caller,
);
ensure!(
authentication,
Error::<T>::CannotSudoApproveAppIfNotAssignedSudo
);
let app = <BountyApplications<T>>::get(bounty_id, application_id)
.ok_or(Error::<T>::CannotSudoApproveIfGrantAppDNE)?;
ensure!(
app.state().awaiting_review(),
Error::<T>::AppStateCannotBeSudoApprovedForAGrantFromCurrentState
);
let new_application = app.approve_grant();
let ret_state = new_application.state();
<BountyApplications<T>>::insert(
bounty_id,
application_id,
new_application,
);
Ok(ret_state)
}
fn poll_application(
bounty_id: T::BountyId,
application_id: T::BountyId,
) -> Result<Self::AppState, DispatchError> {
let _ = <LiveBounties<T>>::get(bounty_id)
.ok_or(Error::<T>::CannotPollApplicationIfBountyDNE)?;
let application_under_review =
<BountyApplications<T>>::get(bounty_id, application_id)
.ok_or(Error::<T>::CannotPollApplicationIfApplicationDNE)?;
match application_under_review.state() {
ApplicationState::UnderReviewByAcceptanceCommittee(vote_id) => {
let status = <vote::Module<T>>::get_vote_outcome(vote_id)?;
match status {
VoteOutcome::Approved => {
let new_application =
application_under_review.approve_grant();
let new_state = new_application.state();
<BountyApplications<T>>::insert(
bounty_id,
application_id,
new_application,
);
Ok(new_state)
}
VoteOutcome::Rejected => {
<BountyApplications<T>>::remove(
bounty_id,
application_id,
);
Ok(ApplicationState::Closed)
}
_ => Ok(application_under_review.state()),
}
}
_ => Ok(application_under_review.state()),
}
}
}
impl<T: Trait>
SubmitMilestone<
T::AccountId,
T::BountyId,
T::IpfsReference,
BalanceOf<T>,
T::VoteId,
BankOrAccount<TransferId<T::BankId>, T::AccountId>,
> for Module<T>
{
type Milestone = MilestoneSubmission<
T::AccountId,
T::BountyId,
T::IpfsReference,
BalanceOf<T>,
MilestoneStatus<
T::VoteId,
BankOrAccount<TransferId<T::BankId>, T::AccountId>,
>,
>;
type MilestoneState = MilestoneStatus<
T::VoteId,
BankOrAccount<TransferId<T::BankId>, T::AccountId>,
>;
fn submit_milestone(
submitter: T::AccountId,
bounty_id: T::BountyId,
application_id: T::BountyId,
submission_reference: T::IpfsReference,
amount_requested: BalanceOf<T>,
) -> Result<T::BountyId, DispatchError> {
ensure!(
Self::is_bounty(bounty_id),
Error::<T>::CannotSubmitMilestoneIfBaseBountyDNE
);
let application =
<BountyApplications<T>>::get(bounty_id, application_id)
.ok_or(Error::<T>::CannotSubmitMilestoneIfApplicationDNE)?;
ensure!(
application.state().approved_and_live(),
Error::<T>::ApplicationMustBeApprovedToSubmitMilestones
);
if let Some(treasury_id) = application.bank() {
let base_bank = <bank::Module<T>>::bank_stores(treasury_id).ok_or(Error::<T>::InvalidBankReferenceInApplicationThrownInMilestoneSubmission)?;
let authentication = <org::Module<T>>::is_member_of_group(
base_bank.org(),
&submitter,
)
|| <org::Module<T>>::is_organization_supervisor(
base_bank.org(),
&submitter,
);
ensure!(authentication, Error::<T>::MilestoneSubmissionNotAuthorizedBySubmitterForBankOrgApplication);
} else {
let authentication = application.is_submitter(&submitter);
ensure!(authentication, Error::<T>::MilestoneSubmissionNotAuthorizedBySubmitterForIndividualApplication);
}
let new_milestone_submission: Self::Milestone =
MilestoneSubmission::new(
submitter,
application_id,
submission_reference,
amount_requested,
);
let new_milestone_id = Self::seeded_generate_unique_id((
bounty_id,
BountyMapID::MilestoneId,
));
<MilestoneSubmissions<T>>::insert(
bounty_id,
new_milestone_id,
new_milestone_submission,
);
Ok(new_milestone_id)
}
fn trigger_milestone_review(
bounty_id: T::BountyId,
milestone_id: T::BountyId,
) -> Result<Self::MilestoneState, DispatchError> {
let bounty = <LiveBounties<T>>::get(bounty_id)
.ok_or(Error::<T>::CannotTriggerMilestoneReviewIfBaseBountyDNE)?;
let review_board = if let Some(board) = bounty.supervision_committee() {
board
} else {
bounty.acceptance_committee()
};
let milestone_submission =
<MilestoneSubmissions<T>>::get(bounty_id, milestone_id).ok_or(
Error::<T>::CannotTriggerMilestoneReviewIfSubmissionDNE,
)?;
let new_vote_id = <vote::Module<T>>::open_vote(
Some(milestone_submission.submission()),
review_board.org(),
review_board.passage_threshold(),
review_board.rejection_threshold(),
review_board.duration(),
)?;
let new_milestone_submission = milestone_submission
.start_review(new_vote_id)
.ok_or(Error::<T>::CannotTriggerMilestoneReviewIfSubmissionNotAwaitingResponseAkaWrongState)?;
let ret_state = new_milestone_submission.state();
<MilestoneSubmissions<T>>::insert(
bounty_id,
milestone_id,
new_milestone_submission,
);
Ok(ret_state)
}
fn sudo_approves_milestone(
caller: T::AccountId,
bounty_id: T::BountyId,
milestone_id: T::BountyId,
) -> Result<Self::MilestoneState, DispatchError> {
let bounty = <LiveBounties<T>>::get(bounty_id)
.ok_or(Error::<T>::CannotSudoApproveMilestoneIfBaseBountyDNE)?;
let review_board = if let Some(board) = bounty.supervision_committee() {
board
} else {
bounty.acceptance_committee()
};
ensure!(
<org::Module<T>>::is_organization_supervisor(
review_board.org(),
&caller
),
Error::<T>::CallerNotAuthorizedToSudoApproveMilestone
);
let milestone_submission =
<MilestoneSubmissions<T>>::get(bounty_id, milestone_id)
.ok_or(Error::<T>::CannotSudoApproveMilestoneThatDNE)?;
let grant_app = <BountyApplications<T>>::get(
bounty_id,
milestone_submission.referenced_application(),
)
.ok_or(Error::<T>::CannotSudoApproveMilestoneIfBaseAppDNE)?;
let grant_recipient: BankOrAccount<OnChainTreasuryID, T::AccountId> =
if let Some(bank_id) = grant_app.bank() {
BankOrAccount::Bank(bank_id)
} else {
BankOrAccount::Account(grant_app.submitter())
};
let payment_receipt = Self::transfer_milestone_payment(
bounty.poster(),
grant_recipient,
milestone_submission.amount(),
);
let new_milestone_submission = if let Ok(pay_id) = payment_receipt {
milestone_submission
.set_state(MilestoneStatus::ApprovedAndTransferExecuted(pay_id))
} else {
milestone_submission.approve_without_transfer()
};
let ret_state = new_milestone_submission.state();
<MilestoneSubmissions<T>>::insert(
bounty_id,
milestone_id,
new_milestone_submission,
);
Ok(ret_state)
}
fn poll_milestone(
bounty_id: T::BountyId,
milestone_id: T::BountyId,
) -> Result<Self::MilestoneState, DispatchError> {
let bounty = <LiveBounties<T>>::get(bounty_id)
.ok_or(Error::<T>::CannotPollMilestoneSubmissionIfBaseBountyDNE)?;
let milestone_submission =
<MilestoneSubmissions<T>>::get(bounty_id, milestone_id)
.ok_or(Error::<T>::CannotPollMilestoneThatDNE)?;
let grant_app = <BountyApplications<T>>::get(
bounty_id,
milestone_submission.referenced_application(),
)
.ok_or(Error::<T>::CannotPollMilestoneIfBaseAppDNE)?;
match milestone_submission.state() {
MilestoneStatus::SubmittedReviewStarted(live_vote_id) => {
let vote_outcome =
<vote::Module<T>>::get_vote_outcome(live_vote_id)?;
if vote_outcome == VoteOutcome::Approved {
let poster: BankOrAccount<
BankSpend<TransferId<T::BankId>>,
T::AccountId,
> = bounty.poster();
let grant_recipient: BankOrAccount<
OnChainTreasuryID,
T::AccountId,
> = if let Some(bank_id) = grant_app.bank() {
BankOrAccount::Bank(bank_id)
} else {
BankOrAccount::Account(grant_app.submitter())
};
let payment_receipt = Self::transfer_milestone_payment(
poster,
grant_recipient,
milestone_submission.amount(),
);
let new_milestone_submission =
if let Ok(pay_id) = payment_receipt {
milestone_submission.set_state(
MilestoneStatus::ApprovedAndTransferExecuted(
pay_id,
),
)
} else {
milestone_submission.approve_without_transfer()
};
let ret_state = new_milestone_submission.state();
<MilestoneSubmissions<T>>::insert(
bounty_id,
milestone_id,
new_milestone_submission,
);
Ok(ret_state)
} else {
Ok(milestone_submission.state())
}
}
_ => Ok(milestone_submission.state()),
}
}
}