Explorar el Código

Merge branch 'recurring_reward_monorepo_migration' into monorepo_external_modules
# Please enter a commit message to explain why this merge is necessary,
# especially if it merges an updated upstream into a topic branch.
#
# Lines starting with '#' will be ignored, and an empty message aborts
# the commit.

Shamil Gadelshin hace 5 años
padre
commit
7c7f4c7124

+ 57 - 0
runtime-modules/recurring-reward/Cargo.toml

@@ -0,0 +1,57 @@
+[package]
+name = 'substrate-recurring-reward-module'
+version = '1.0.1'
+authors = ['Mokhtar Naamani <mokhtar.naamani@gmail.com>']
+edition = '2018'
+
+[dependencies]
+hex-literal = '0.1.0'
+serde = { version = '1.0', optional = true }
+serde_derive = { version = '1.0', optional = true }
+rstd = { package = 'sr-std', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+runtime-primitives = { package = 'sr-primitives', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+srml-support = { package = 'srml-support', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+srml-support-procedural = { package = 'srml-support-procedural', git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+system = { package = 'srml-system', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+balances = { package = 'srml-balances', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+codec = { package = 'parity-scale-codec', version = '1.0.0', default-features = false, features = ['derive'] }
+# https://users.rust-lang.org/t/failure-derive-compilation-error/39062
+quote = '<=1.0.2'
+
+[dependencies.timestamp]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'srml-timestamp'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.runtime-io]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'sr-io'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dependencies.minting]
+default_features = false
+git = 'https://github.com/Joystream/substrate-token-minting-module/'
+package = 'substrate-token-mint-module'
+tag = 'v1.0.1'
+
+[dev-dependencies]
+runtime-io = { package = 'sr-io', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+primitives = { package = 'substrate-primitives', git = 'https://github.com/paritytech/substrate.git', rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'}
+
+[features]
+default = ['std']
+std = [
+	'serde',
+	'serde_derive',
+	'codec/std',
+	'rstd/std',
+	'runtime-io/std',
+	'runtime-primitives/std',
+	'srml-support/std',
+	'system/std',
+  	'balances/std',
+	'timestamp/std',
+	'minting/std',
+]

+ 397 - 0
runtime-modules/recurring-reward/src/lib.rs

@@ -0,0 +1,397 @@
+// Ensure we're `no_std` when compiling for Wasm.
+#![cfg_attr(not(feature = "std"), no_std)]
+use rstd::prelude::*;
+
+use codec::{Codec, Decode, Encode};
+use runtime_primitives::traits::{MaybeSerialize, Member, One, SimpleArithmetic, Zero};
+use srml_support::{decl_module, decl_storage, ensure, Parameter};
+
+use minting::{self, BalanceOf};
+use system;
+
+mod mock;
+mod tests;
+
+pub trait Trait: system::Trait + minting::Trait {
+    type PayoutStatusHandler: PayoutStatusHandler<Self>;
+
+    /// Type of identifier for recipients.
+    type RecipientId: Parameter
+        + Member
+        + SimpleArithmetic
+        + Codec
+        + Default
+        + Copy
+        + MaybeSerialize
+        + PartialEq;
+
+    /// Type for identifier for relationship representing that a recipient recieves recurring reward from a token mint
+    type RewardRelationshipId: Parameter
+        + Member
+        + SimpleArithmetic
+        + Codec
+        + Default
+        + Copy
+        + MaybeSerialize
+        + PartialEq;
+}
+
+/// Handler for aftermath of a payout attempt
+pub trait PayoutStatusHandler<T: Trait> {
+    fn payout_succeeded(
+        id: T::RewardRelationshipId,
+        destination_account: &T::AccountId,
+        amount: BalanceOf<T>,
+    );
+
+    fn payout_failed(
+        id: T::RewardRelationshipId,
+        destination_account: &T::AccountId,
+        amount: BalanceOf<T>,
+    );
+}
+
+/// Makes `()` empty tuple, a PayoutStatusHandler that does nothing.
+impl<T: Trait> PayoutStatusHandler<T> for () {
+    fn payout_succeeded(
+        _id: T::RewardRelationshipId,
+        _destination_account: &T::AccountId,
+        _amount: BalanceOf<T>,
+    ) {
+    }
+
+    fn payout_failed(
+        _id: T::RewardRelationshipId,
+        _destination_account: &T::AccountId,
+        _amount: BalanceOf<T>,
+    ) {
+    }
+}
+
+/// A recipient of recurring rewards
+#[derive(Encode, Decode, Copy, Clone, Debug, Default)]
+pub struct Recipient<Balance> {
+    // stats
+    /// Total payout received by this recipient
+    total_reward_received: Balance,
+
+    /// Total payout missed for this recipient
+    total_reward_missed: Balance,
+}
+
+#[derive(Encode, Decode, Copy, Clone, Debug, Default)]
+pub struct RewardRelationship<AccountId, Balance, BlockNumber, MintId, RecipientId> {
+    /// Identifier for receiver
+    recipient: RecipientId,
+
+    /// Identifier for reward source
+    mint_id: MintId,
+
+    /// Destination account for reward
+    account: AccountId,
+
+    /// The payout amount at the next payout
+    amount_per_payout: Balance,
+
+    /// When set, identifies block when next payout should be processed,
+    /// otherwise there is no pending payout
+    next_payment_at_block: Option<BlockNumber>,
+
+    /// When set, will be the basis for automatically setting next payment,
+    /// otherwise any upcoming payout will be a one off.
+    payout_interval: Option<BlockNumber>,
+
+    // stats
+    /// Total payout received in this relationship
+    total_reward_received: Balance,
+
+    /// Total payout failed in this relationship
+    total_reward_missed: Balance,
+}
+
+impl<AccountId: Clone, Balance: Clone, BlockNumber: Clone, MintId: Clone, RecipientId: Clone>
+    RewardRelationship<AccountId, Balance, BlockNumber, MintId, RecipientId>
+{
+    /// Verifies whether relationship is active
+    pub fn is_active(&self) -> bool {
+        self.next_payment_at_block.is_some()
+    }
+
+    /// Make clone which is activated.
+    pub fn clone_activated(&self, start_at: &BlockNumber) -> Self {
+        Self {
+            next_payment_at_block: Some((*start_at).clone()),
+            ..((*self).clone())
+        }
+    }
+
+    /// Make clone which is deactivated
+    pub fn clone_deactivated(&self) -> Self {
+        Self {
+            next_payment_at_block: None,
+            ..((*self).clone())
+        }
+    }
+}
+
+decl_storage! {
+    trait Store for Module<T: Trait> as RecurringReward {
+        Recipients get(recipients): linked_map T::RecipientId => Recipient<BalanceOf<T>>;
+
+        RecipientsCreated get(recipients_created): T::RecipientId;
+
+        RewardRelationships get(reward_relationships): linked_map T::RewardRelationshipId => RewardRelationship<T::AccountId, BalanceOf<T>, T::BlockNumber, T::MintId, T::RecipientId>;
+
+        RewardRelationshipsCreated get(reward_relationships_created): T::RewardRelationshipId;
+    }
+}
+
+decl_module! {
+    pub struct Module<T: Trait> for enum Call where origin: T::Origin {
+
+        fn on_finalize(now: T::BlockNumber) {
+            Self::do_payouts(now);
+        }
+    }
+}
+
+#[derive(Eq, PartialEq, Debug)]
+pub enum RewardsError {
+    RecipientNotFound,
+    RewardSourceNotFound,
+    NextPaymentNotInFuture,
+    RewardRelationshipNotFound,
+}
+
+impl<T: Trait> Module<T> {
+    /// Adds a new Recipient and returns new recipient identifier.
+    pub fn add_recipient() -> T::RecipientId {
+        let next_id = Self::recipients_created();
+        <RecipientsCreated<T>>::put(next_id + One::one());
+        <Recipients<T>>::insert(&next_id, Recipient::default());
+        next_id
+    }
+
+    /// Adds a new RewardRelationship, for a given source mint, recipient, account.
+    pub fn add_reward_relationship(
+        mint_id: T::MintId,
+        recipient: T::RecipientId,
+        account: T::AccountId,
+        amount_per_payout: BalanceOf<T>,
+        next_payment_at_block: T::BlockNumber,
+        payout_interval: Option<T::BlockNumber>,
+    ) -> Result<T::RewardRelationshipId, RewardsError> {
+        ensure!(
+            <minting::Module<T>>::mint_exists(mint_id),
+            RewardsError::RewardSourceNotFound
+        );
+        ensure!(
+            <Recipients<T>>::exists(recipient),
+            RewardsError::RecipientNotFound
+        );
+        ensure!(
+            next_payment_at_block > <system::Module<T>>::block_number(),
+            RewardsError::NextPaymentNotInFuture
+        );
+
+        let relationship_id = Self::reward_relationships_created();
+        <RewardRelationshipsCreated<T>>::put(relationship_id + One::one());
+        <RewardRelationships<T>>::insert(
+            relationship_id,
+            RewardRelationship {
+                mint_id,
+                recipient,
+                account,
+                amount_per_payout,
+                next_payment_at_block: Some(next_payment_at_block),
+                payout_interval,
+                total_reward_received: Zero::zero(),
+                total_reward_missed: Zero::zero(),
+            },
+        );
+        Ok(relationship_id)
+    }
+
+    /// Removes a relationship from RewardRelashionships and its recipient.
+    pub fn remove_reward_relationship(id: T::RewardRelationshipId) {
+        if <RewardRelationships<T>>::exists(&id) {
+            <Recipients<T>>::remove(<RewardRelationships<T>>::take(&id).recipient);
+        }
+    }
+
+    /// Will attempt to activat a deactivated reward relationship.
+    pub fn try_to_activate_relationship(
+        id: T::RewardRelationshipId,
+        next_payment_at_block: T::BlockNumber,
+    ) -> Result<bool, ()> {
+        // Ensure relationship exists
+        let reward_relationship = Self::ensure_reward_relationship_exists(&id)?;
+
+        let activated = if reward_relationship.is_active() {
+            // Was not activated
+            false
+        } else {
+            // Update as activated
+            let activated_relationship =
+                reward_relationship.clone_activated(&next_payment_at_block);
+
+            RewardRelationships::<T>::insert(id, activated_relationship);
+
+            // We activated
+            true
+        };
+
+        Ok(activated)
+    }
+
+    /// Will attempt to deactivat a activated reward relationship.
+    pub fn try_to_deactivate_relationship(id: T::RewardRelationshipId) -> Result<bool, ()> {
+        // Ensure relationship exists
+        let reward_relationship = Self::ensure_reward_relationship_exists(&id)?;
+
+        let deactivated = if reward_relationship.is_active() {
+            let deactivated_relationship = reward_relationship.clone_deactivated();
+
+            RewardRelationships::<T>::insert(id, deactivated_relationship);
+
+            // Was deactivated
+            true
+        } else {
+            // Was not deactivated
+            false
+        };
+
+        Ok(deactivated)
+    }
+
+    // For reward relationship found with given identifier, new values can be set for
+    // account, payout, block number when next payout will be made and the new interval after
+    // the next scheduled payout. All values are optional, but updating values are combined in this
+    // single method to ensure atomic updates.
+    pub fn set_reward_relationship(
+        id: T::RewardRelationshipId,
+        new_account: Option<T::AccountId>,
+        new_payout: Option<BalanceOf<T>>,
+        new_next_payment_at: Option<Option<T::BlockNumber>>,
+        new_payout_interval: Option<Option<T::BlockNumber>>,
+    ) -> Result<(), RewardsError> {
+        ensure!(
+            <RewardRelationships<T>>::exists(&id),
+            RewardsError::RewardRelationshipNotFound
+        );
+
+        let mut relationship = Self::reward_relationships(&id);
+
+        if let Some(account) = new_account {
+            relationship.account = account;
+        }
+        if let Some(payout) = new_payout {
+            relationship.amount_per_payout = payout;
+        }
+        if let Some(next_payout_at_block) = new_next_payment_at {
+            if let Some(blocknumber) = next_payout_at_block {
+                ensure!(
+                    blocknumber > <system::Module<T>>::block_number(),
+                    RewardsError::NextPaymentNotInFuture
+                );
+            }
+            relationship.next_payment_at_block = next_payout_at_block;
+        }
+        if let Some(payout_interval) = new_payout_interval {
+            relationship.payout_interval = payout_interval;
+        }
+
+        <RewardRelationships<T>>::insert(&id, relationship);
+        Ok(())
+    }
+
+    /*
+    For all relationships where next_payment_at_block is set and matches current block height,
+    a call to pay_reward is made for the suitable amount, recipient and source.
+    The next_payment_in_block is updated based on payout_interval.
+    If the call succeeds, total_reward_received is incremented on both
+    recipient and dependency with amount_per_payout, and a call to T::PayoutStatusHandler is made.
+    Otherwise, analogous steps for failure.
+    */
+    fn do_payouts(now: T::BlockNumber) {
+        for (relationship_id, ref mut relationship) in <RewardRelationships<T>>::enumerate() {
+            assert!(<Recipients<T>>::exists(&relationship.recipient));
+
+            let mut recipient = Self::recipients(relationship.recipient);
+
+            if let Some(next_payment_at_block) = relationship.next_payment_at_block {
+                if next_payment_at_block != now {
+                    continue;
+                }
+
+                // Add the missed payout and try to pay those in addition to scheduled payout?
+                // let payout = relationship.total_reward_missed + relationship.amount_per_payout;
+                let payout = relationship.amount_per_payout;
+
+                // try to make payment
+                if <minting::Module<T>>::transfer_tokens(
+                    relationship.mint_id,
+                    payout,
+                    &relationship.account,
+                )
+                .is_err()
+                {
+                    // add only newly scheduled payout to total missed payout
+                    relationship.total_reward_missed += relationship.amount_per_payout;
+
+                    // update recipient stats
+                    recipient.total_reward_missed += relationship.amount_per_payout;
+
+                    T::PayoutStatusHandler::payout_failed(
+                        relationship_id,
+                        &relationship.account,
+                        payout,
+                    );
+                } else {
+                    // update payout received stats
+                    relationship.total_reward_received += payout;
+                    recipient.total_reward_received += payout;
+
+                    // update missed payout stats
+                    // if relationship.total_reward_missed != Zero::zero() {
+                    //     // update recipient stats
+                    //     recipient.total_reward_missed -= relationship.total_reward_missed;
+
+                    //     // clear missed reward on relationship
+                    //     relationship.total_reward_missed = Zero::zero();
+                    // }
+                    T::PayoutStatusHandler::payout_succeeded(
+                        relationship_id,
+                        &relationship.account,
+                        payout,
+                    );
+                }
+
+                // update next payout blocknumber at interval if set
+                if let Some(payout_interval) = relationship.payout_interval {
+                    relationship.next_payment_at_block = Some(now + payout_interval);
+                } else {
+                    relationship.next_payment_at_block = None;
+                }
+
+                <Recipients<T>>::insert(relationship.recipient, recipient);
+                <RewardRelationships<T>>::insert(relationship_id, relationship);
+            }
+        }
+    }
+}
+
+impl<T: Trait> Module<T> {
+    fn ensure_reward_relationship_exists(
+        id: &T::RewardRelationshipId,
+    ) -> Result<
+        RewardRelationship<T::AccountId, BalanceOf<T>, T::BlockNumber, T::MintId, T::RecipientId>,
+        (),
+    > {
+        ensure!(RewardRelationships::<T>::exists(id), ());
+
+        let relationship = RewardRelationships::<T>::get(id);
+
+        Ok(relationship)
+    }
+}

+ 103 - 0
runtime-modules/recurring-reward/src/mock/mod.rs

@@ -0,0 +1,103 @@
+#![cfg(test)]
+
+// use crate::*;
+use crate::{Module, Trait};
+
+use primitives::H256;
+
+use balances;
+use minting;
+use runtime_primitives::{
+    testing::Header,
+    traits::{BlakeTwo256, IdentityLookup},
+    Perbill,
+};
+use srml_support::{impl_outer_origin, parameter_types};
+
+mod status_handler;
+pub use status_handler::MockStatusHandler;
+
+impl_outer_origin! {
+    pub enum Origin for Test {}
+}
+
+// Workaround for https://github.com/rust-lang/rust/issues/26925 . Remove when sorted.
+#[derive(Clone, PartialEq, Eq, Debug)]
+pub struct Test;
+parameter_types! {
+    pub const BlockHashCount: u64 = 250;
+    pub const MaximumBlockWeight: u32 = 1024;
+    pub const MaximumBlockLength: u32 = 2 * 1024;
+    pub const AvailableBlockRatio: Perbill = Perbill::one();
+    pub const MinimumPeriod: u64 = 5;
+}
+
+impl system::Trait for Test {
+    type Origin = Origin;
+    type Index = u64;
+    type BlockNumber = u64;
+    type Call = ();
+    type Hash = H256;
+    type Hashing = BlakeTwo256;
+    type AccountId = u64;
+    type Lookup = IdentityLookup<Self::AccountId>;
+    type Header = Header;
+    type Event = ();
+    type BlockHashCount = BlockHashCount;
+    type MaximumBlockWeight = MaximumBlockWeight;
+    type MaximumBlockLength = MaximumBlockLength;
+    type AvailableBlockRatio = AvailableBlockRatio;
+    type Version = ();
+}
+
+parameter_types! {
+    pub const ExistentialDeposit: u32 = 0;
+    pub const TransferFee: u32 = 0;
+    pub const CreationFee: u32 = 0;
+    pub const TransactionBaseFee: u32 = 1;
+    pub const TransactionByteFee: u32 = 0;
+    pub const InitialMembersBalance: u64 = 2000;
+}
+
+impl balances::Trait for Test {
+    /// The type for recording an account's balance.
+    type Balance = u64;
+    /// What to do if an account's free balance gets zeroed.
+    type OnFreeBalanceZero = ();
+    /// What to do if a new account is created.
+    type OnNewAccount = ();
+    /// The ubiquitous event type.
+    type Event = ();
+
+    type DustRemoval = ();
+    type TransferPayment = ();
+    type ExistentialDeposit = ExistentialDeposit;
+    type TransferFee = TransferFee;
+    type CreationFee = CreationFee;
+}
+
+impl Trait for Test {
+    type PayoutStatusHandler = MockStatusHandler;
+    type RecipientId = u64;
+    type RewardRelationshipId = u64;
+}
+
+impl minting::Trait for Test {
+    type Currency = Balances;
+    type MintId = u64;
+}
+
+pub fn build_test_externalities() -> runtime_io::TestExternalities {
+    MockStatusHandler::reset();
+
+    let t = system::GenesisConfig::default()
+        .build_storage::<Test>()
+        .unwrap();
+
+    t.into()
+}
+
+pub type System = system::Module<Test>;
+pub type Balances = balances::Module<Test>;
+pub type Rewards = Module<Test>;
+pub type Minting = minting::Module<Test>;

+ 64 - 0
runtime-modules/recurring-reward/src/mock/status_handler.rs

@@ -0,0 +1,64 @@
+#![cfg(test)]
+
+use super::Test;
+use crate::{PayoutStatusHandler, Trait};
+use std::cell::RefCell;
+
+struct StatusHandlerState<T: Trait> {
+    successes: Vec<T::RewardRelationshipId>,
+    failures: Vec<T::RewardRelationshipId>,
+}
+
+impl<T: Trait> StatusHandlerState<T> {
+    pub fn reset(&mut self) {
+        self.successes = vec![];
+        self.failures = vec![];
+    }
+}
+
+impl<T: Trait> Default for StatusHandlerState<T> {
+    fn default() -> Self {
+        Self {
+            successes: vec![],
+            failures: vec![],
+        }
+    }
+}
+
+thread_local!(static STATUS_HANDLER_STATE: RefCell<StatusHandlerState<Test>> = RefCell::new(Default::default()));
+
+pub struct MockStatusHandler {}
+impl MockStatusHandler {
+    pub fn reset() {
+        STATUS_HANDLER_STATE.with(|cell| {
+            cell.borrow_mut().reset();
+        });
+    }
+    pub fn successes() -> usize {
+        let mut value = 0;
+        STATUS_HANDLER_STATE.with(|cell| {
+            value = cell.borrow_mut().successes.len();
+        });
+        value
+    }
+    pub fn failures() -> usize {
+        let mut value = 0;
+        STATUS_HANDLER_STATE.with(|cell| {
+            value = cell.borrow_mut().failures.len();
+        });
+        value
+    }
+}
+impl PayoutStatusHandler<Test> for MockStatusHandler {
+    fn payout_succeeded(id: u64, _destination_account: &u64, _amount: u64) {
+        STATUS_HANDLER_STATE.with(|cell| {
+            cell.borrow_mut().successes.push(id);
+        });
+    }
+
+    fn payout_failed(id: u64, _destination_account: &u64, _amount: u64) {
+        STATUS_HANDLER_STATE.with(|cell| {
+            cell.borrow_mut().failures.push(id);
+        });
+    }
+}

+ 259 - 0
runtime-modules/recurring-reward/src/tests.rs

@@ -0,0 +1,259 @@
+#![cfg(test)]
+
+use super::*;
+use crate::mock::*;
+use srml_support::traits::Currency;
+
+fn create_new_mint_with_capacity(capacity: u64) -> u64 {
+    let mint_id = Minting::add_mint(capacity, None).ok().unwrap();
+    assert!(Minting::mint_exists(mint_id));
+    assert_eq!(Minting::get_mint_capacity(mint_id).ok().unwrap(), capacity);
+    mint_id
+}
+
+#[test]
+fn adding_recipients() {
+    build_test_externalities().execute_with(|| {
+        let next_id = Rewards::recipients_created();
+        assert!(!<Recipients<Test>>::exists(&next_id));
+        let recipient_id = Rewards::add_recipient();
+        assert!(<Recipients<Test>>::exists(&next_id));
+        assert_eq!(recipient_id, next_id);
+        assert_eq!(Rewards::recipients_created(), next_id + 1);
+    });
+}
+
+#[test]
+fn adding_relationships() {
+    build_test_externalities().execute_with(|| {
+        let recipient_account: u64 = 1;
+        let mint_id = create_new_mint_with_capacity(1000000);
+        let recipient_id = Rewards::add_recipient();
+        let interval: u64 = 600;
+        let next_payment_at: u64 = 2222;
+        let payout = 100;
+
+        let next_relationship_id = Rewards::reward_relationships_created();
+        let relationship = Rewards::add_reward_relationship(
+            mint_id,
+            recipient_id,
+            recipient_account,
+            payout,
+            next_payment_at,
+            Some(interval),
+        );
+        assert!(relationship.is_ok());
+        let relationship_id = relationship.ok().unwrap();
+        assert_eq!(relationship_id, next_relationship_id);
+        assert_eq!(
+            Rewards::reward_relationships_created(),
+            next_relationship_id + 1
+        );
+        assert!(<RewardRelationships<Test>>::exists(&relationship_id));
+        let relationship = Rewards::reward_relationships(&relationship_id);
+        assert_eq!(relationship.next_payment_at_block, Some(next_payment_at));
+        assert_eq!(relationship.amount_per_payout, payout);
+        assert_eq!(relationship.mint_id, mint_id);
+        assert_eq!(relationship.account, recipient_account);
+        assert_eq!(relationship.payout_interval, Some(interval));
+
+        // mint doesn't exist
+        assert_eq!(
+            Rewards::add_reward_relationship(
+                111,
+                recipient_id,
+                recipient_account,
+                100,
+                next_payment_at,
+                None,
+            )
+            .expect_err("should fail if mint doesn't exist"),
+            RewardsError::RewardSourceNotFound
+        );
+    });
+}
+
+#[test]
+fn one_off_payout() {
+    build_test_externalities().execute_with(|| {
+        System::set_block_number(10000);
+        let recipient_account: u64 = 1;
+        let _ = Balances::deposit_creating(&recipient_account, 400);
+        let mint_id = create_new_mint_with_capacity(1000000);
+        let recipient_id = Rewards::add_recipient();
+        let payout: u64 = 1000;
+        let next_payout_at: u64 = 12222;
+        let relationship = Rewards::add_reward_relationship(
+            mint_id,
+            recipient_id,
+            recipient_account,
+            payout,
+            next_payout_at,
+            None,
+        );
+        assert!(relationship.is_ok());
+        let relationship_id = relationship.ok().unwrap();
+
+        let relationship = Rewards::reward_relationships(&relationship_id);
+        assert_eq!(relationship.next_payment_at_block, Some(next_payout_at));
+
+        let starting_balance = Balances::free_balance(&recipient_account);
+
+        // try to catch 'off by one' bugs
+        Rewards::do_payouts(next_payout_at - 1);
+        assert_eq!(Balances::free_balance(&recipient_account), starting_balance);
+        Rewards::do_payouts(next_payout_at + 1);
+        assert_eq!(Balances::free_balance(&recipient_account), starting_balance);
+
+        assert_eq!(MockStatusHandler::successes(), 0);
+
+        Rewards::do_payouts(next_payout_at);
+        assert_eq!(
+            Balances::free_balance(&recipient_account),
+            starting_balance + payout
+        );
+        assert_eq!(MockStatusHandler::successes(), 1);
+
+        let relationship = Rewards::reward_relationships(&relationship_id);
+        assert_eq!(relationship.total_reward_received, payout);
+        assert_eq!(relationship.next_payment_at_block, None);
+
+        let recipient = Rewards::recipients(&recipient_id);
+        assert_eq!(recipient.total_reward_received, payout);
+    });
+}
+
+#[test]
+fn recurring_payout() {
+    build_test_externalities().execute_with(|| {
+        System::set_block_number(10000);
+        let recipient_account: u64 = 1;
+        let _ = Balances::deposit_creating(&recipient_account, 400);
+        let mint_id = create_new_mint_with_capacity(1000000);
+        let recipient_id = Rewards::add_recipient();
+        let payout: u64 = 1000;
+        let next_payout_at: u64 = 12222;
+        let interval: u64 = 600;
+        let relationship = Rewards::add_reward_relationship(
+            mint_id,
+            recipient_id,
+            recipient_account,
+            payout,
+            next_payout_at,
+            Some(interval),
+        );
+        assert!(relationship.is_ok());
+        let relationship_id = relationship.ok().unwrap();
+
+        let relationship = Rewards::reward_relationships(&relationship_id);
+        assert_eq!(relationship.next_payment_at_block, Some(next_payout_at));
+
+        let starting_balance = Balances::free_balance(&recipient_account);
+
+        let number_of_payouts = 3;
+        for i in 0..number_of_payouts {
+            Rewards::do_payouts(next_payout_at + interval * i);
+        }
+        assert_eq!(MockStatusHandler::successes(), number_of_payouts as usize);
+
+        assert_eq!(
+            Balances::free_balance(&recipient_account),
+            starting_balance + payout * number_of_payouts
+        );
+
+        let relationship = Rewards::reward_relationships(&relationship_id);
+        assert_eq!(
+            relationship.total_reward_received,
+            payout * number_of_payouts
+        );
+
+        let recipient = Rewards::recipients(&recipient_id);
+        assert_eq!(recipient.total_reward_received, payout * number_of_payouts);
+    });
+}
+
+#[test]
+fn track_missed_payouts() {
+    build_test_externalities().execute_with(|| {
+        System::set_block_number(10000);
+        let recipient_account: u64 = 1;
+        let _ = Balances::deposit_creating(&recipient_account, 400);
+        let mint_id = create_new_mint_with_capacity(0);
+        let recipient_id = Rewards::add_recipient();
+        let payout: u64 = 1000;
+        let next_payout_at: u64 = 12222;
+        let relationship = Rewards::add_reward_relationship(
+            mint_id,
+            recipient_id,
+            recipient_account,
+            payout,
+            next_payout_at,
+            None,
+        );
+        assert!(relationship.is_ok());
+        let relationship_id = relationship.ok().unwrap();
+
+        let relationship = Rewards::reward_relationships(&relationship_id);
+        assert_eq!(relationship.next_payment_at_block, Some(next_payout_at));
+
+        let starting_balance = Balances::free_balance(&recipient_account);
+
+        Rewards::do_payouts(next_payout_at);
+        assert_eq!(Balances::free_balance(&recipient_account), starting_balance);
+
+        assert_eq!(MockStatusHandler::failures(), 1);
+
+        let relationship = Rewards::reward_relationships(&relationship_id);
+        assert_eq!(relationship.total_reward_received, 0);
+        assert_eq!(relationship.total_reward_missed, payout);
+
+        let recipient = Rewards::recipients(&recipient_id);
+        assert_eq!(recipient.total_reward_received, 0);
+        assert_eq!(recipient.total_reward_missed, payout);
+    });
+}
+
+#[test]
+fn activate_and_deactivate_relationship() {
+    build_test_externalities().execute_with(|| {
+        System::set_block_number(10000);
+        let recipient_account: u64 = 1;
+        let _ = Balances::deposit_creating(&recipient_account, 400);
+        let mint_id = create_new_mint_with_capacity(0);
+        let recipient_id = Rewards::add_recipient();
+        let payout: u64 = 1000;
+        let next_payout_at: u64 = 12222;
+
+        // Add relationship
+        let relationship_id = Rewards::add_reward_relationship(
+            mint_id,
+            recipient_id,
+            recipient_account,
+            payout,
+            next_payout_at,
+            None,
+        )
+        .unwrap();
+
+        // The relationship starts out active
+        assert!(Rewards::reward_relationships(&relationship_id).is_active());
+
+        // We are able to deactivate relationship
+        assert!(Rewards::try_to_deactivate_relationship(relationship_id).unwrap());
+
+        // The relationship is no longer active
+        assert!(!Rewards::reward_relationships(&relationship_id).is_active());
+
+        // We cannot deactivate an already deactivated relationship
+        assert!(!Rewards::try_to_deactivate_relationship(relationship_id).unwrap());
+
+        // We are able to activate relationship
+        assert!(Rewards::try_to_activate_relationship(relationship_id, next_payout_at).unwrap());
+
+        // The relationship is now not active
+        assert!(Rewards::reward_relationships(&relationship_id).is_active());
+
+        // We cannot activate an already active relationship
+        assert!(!Rewards::try_to_activate_relationship(relationship_id, next_payout_at).unwrap());
+    });
+}