near-contract-tools 0.7.2

Helpful functions and macros for developing smart contracts on NEAR Protocol.
Documentation
use near_contract_tools::{
    migrate::{MigrateExternal, MigrateHook},
    owner::Owner,
    pause::Pause,
    rbac::Rbac,
    standard::nep297::Event,
    Migrate, Owner, Pause, Rbac,
};
use near_sdk::{
    borsh::{self, BorshDeserialize, BorshSerialize},
    env, near_bindgen,
    test_utils::VMContextBuilder,
    testing_env, AccountId, BorshStorageKey,
};

mod event;
mod migrate;
mod owner;
mod pause;
mod standard;

mod my_event {
    use near_contract_tools::Nep297;
    use near_sdk::AccountId;
    use serde::Serialize;

    #[derive(Serialize, Nep297)]
    #[nep297(standard = "x-myevent", version = "1.0.0", rename = "snake_case")]
    pub struct ValueChanged {
        pub from: u32,
        pub to: u32,
    }

    #[derive(Serialize, Nep297)]
    #[nep297(standard = "x-myevent", version = "1.0.0", rename = "snake_case")]
    pub struct PermissionGranted {
        pub to: AccountId,
    }
}

#[derive(BorshSerialize, BorshStorageKey)]
enum StorageKey {
    Owner,
    Pause,
    Rbac,
}

#[derive(BorshSerialize, BorshStorageKey)]
enum Role {
    CanPause,
    CanSetValue,
}

#[derive(Owner, Pause, Rbac, BorshDeserialize, BorshSerialize)]
#[owner(storage_key = "StorageKey::Owner")]
#[pause(storage_key = "StorageKey::Pause")]
#[rbac(storage_key = "StorageKey::Rbac", roles = "Role")]
#[near_bindgen]
struct Integration {
    pub value: u32,
}

#[near_bindgen]
impl Integration {
    #[init]
    pub fn new(owner_id: AccountId) -> Self {
        let mut contract = Self { value: 0 };

        Owner::init(&mut contract, &owner_id);
        contract.add_role(owner_id.clone(), &Role::CanSetValue);
        contract.add_role(owner_id.clone(), &Role::CanPause);

        contract
    }

    pub fn add_value_setter(&mut self, account_id: AccountId) {
        Self::require_owner();

        self.add_role(account_id.clone(), &Role::CanSetValue);

        my_event::PermissionGranted { to: account_id }.emit();
    }

    pub fn set_value(&mut self, value: u32) {
        Self::require_unpaused();
        Self::require_role(&Role::CanSetValue);

        let old = self.value;

        self.value = value;

        my_event::ValueChanged {
            from: old,
            to: value,
        }
        .emit();
    }

    pub fn pause(&mut self) {
        Self::require_role(&Role::CanPause);
        Pause::pause(self);
    }

    pub fn unpause(&mut self) {
        Self::require_role(&Role::CanPause);
        Pause::unpause(self);
    }

    pub fn get_value(&self) -> u32 {
        self.value
    }
}

#[derive(Migrate, Owner, Pause, Rbac, BorshSerialize, BorshDeserialize)]
#[migrate(from = "Integration")]
#[owner(storage_key = "StorageKey::Owner")]
#[pause(storage_key = "StorageKey::Pause")]
#[rbac(storage_key = "StorageKey::Rbac", roles = "Role")]
#[near_bindgen]
struct MigrateIntegration {
    pub new_value: String,
    pub moved_value: u32,
}

impl MigrateHook for MigrateIntegration {
    fn on_migrate(old: Integration) -> Self {
        Self::require_owner();
        Self::require_unpaused();

        Self {
            new_value: "my string".to_string(),
            moved_value: old.value,
        }
    }
}

#[near_bindgen]
impl MigrateIntegration {
    pub fn add_value_setter(&mut self, account_id: AccountId) {
        Self::require_owner();

        self.add_role(account_id.clone(), &Role::CanSetValue);

        my_event::PermissionGranted { to: account_id }.emit();
    }

    pub fn set_value(&mut self, value: u32) {
        Self::require_unpaused();
        Self::require_role(&Role::CanSetValue);

        let old = self.moved_value;

        self.moved_value = value;

        my_event::ValueChanged {
            from: old,
            to: value,
        }
        .emit();
    }

    pub fn pause(&mut self) {
        Self::require_role(&Role::CanPause);
        Pause::pause(self);
    }

    pub fn unpause(&mut self) {
        Self::require_role(&Role::CanPause);
        Pause::unpause(self);
    }

    pub fn get_value(&self) -> u32 {
        self.moved_value
    }
}

#[test]
fn integration() {
    let owner: AccountId = "owner".parse().unwrap();
    let alice: AccountId = "alice".parse().unwrap();
    let context = VMContextBuilder::new()
        .predecessor_account_id(owner.clone())
        .build();

    testing_env!(context);
    let mut c = Integration::new(owner.clone());

    c.set_value(5);

    assert_eq!(c.get_value(), 5);

    c.add_value_setter(alice.clone());

    let context = VMContextBuilder::new()
        .predecessor_account_id(alice.clone())
        .build();

    testing_env!(context);

    c.set_value(15);

    assert_eq!(c.get_value(), 15);

    let context = VMContextBuilder::new()
        .predecessor_account_id(owner.clone())
        .build();

    testing_env!(context);

    Integration::pause(&mut c);
    Integration::unpause(&mut c);

    c.set_value(25);

    assert_eq!(c.get_value(), 25);

    // Perform migration
    env::state_write(&c);

    let mut migrated = <MigrateIntegration as MigrateExternal>::migrate();

    assert_eq!(migrated.moved_value, 25);
    assert_eq!(migrated.get_value(), 25);
    assert_eq!(migrated.new_value, "my string");

    let bob: AccountId = "bob_addr".parse().unwrap();

    migrated.set_value(5);

    assert_eq!(migrated.get_value(), 5);

    // make sure alice still has permission
    let context = VMContextBuilder::new()
        .predecessor_account_id(alice.clone())
        .build();

    testing_env!(context);

    migrated.set_value(256);

    assert_eq!(migrated.get_value(), 256);

    // add bob permissions
    let context = VMContextBuilder::new()
        .predecessor_account_id(owner.clone())
        .build();

    testing_env!(context);

    migrated.add_value_setter(bob.clone());

    let context = VMContextBuilder::new()
        .predecessor_account_id(bob.clone())
        .build();

    testing_env!(context);

    migrated.set_value(77);

    assert_eq!(migrated.get_value(), 77);

    let context = VMContextBuilder::new()
        .predecessor_account_id(owner.clone())
        .build();

    testing_env!(context);

    MigrateIntegration::pause(&mut migrated);
    MigrateIntegration::unpause(&mut migrated);

    migrated.set_value(8);

    assert_eq!(migrated.get_value(), 8);
}

#[test]
#[should_panic(expected = "Unauthorized")]
fn integration_fail_missing_role() {
    let owner: AccountId = "owner".parse().unwrap();
    let alice: AccountId = "alice".parse().unwrap();
    let context = VMContextBuilder::new()
        .predecessor_account_id(owner.clone())
        .build();

    testing_env!(context);
    let mut c = Integration::new(owner.clone());

    let context = VMContextBuilder::new()
        .predecessor_account_id(alice.clone())
        .build();

    testing_env!(context);

    c.set_value(15);
}

#[test]
#[should_panic(expected = "Disallowed while contract is paused")]
fn integration_fail_set_paused() {
    let owner: AccountId = "owner".parse().unwrap();
    let context = VMContextBuilder::new()
        .predecessor_account_id(owner.clone())
        .build();

    testing_env!(context);
    let mut c = Integration::new(owner.clone());

    Integration::pause(&mut c);

    c.set_value(5);
}

#[test]
#[should_panic(expected = "Owner only")]
fn integration_fail_migrate_allow() {
    let owner: AccountId = "owner".parse().unwrap();
    let alice: AccountId = "alice".parse().unwrap();
    let context = VMContextBuilder::new()
        .predecessor_account_id(owner.clone())
        .build();

    testing_env!(context);
    let c = Integration::new(owner.clone());

    env::state_write(&c);

    let context = VMContextBuilder::new()
        .predecessor_account_id(alice.clone())
        .build();

    testing_env!(context);

    <MigrateIntegration as MigrateExternal>::migrate();
}

#[test]
#[should_panic(expected = "Disallowed while contract is paused")]
fn integration_fail_migrate_paused() {
    let owner: AccountId = "owner".parse().unwrap();
    let context = VMContextBuilder::new()
        .predecessor_account_id(owner.clone())
        .build();

    testing_env!(context);
    let mut c = Integration::new(owner.clone());

    Integration::pause(&mut c);

    env::state_write(&c);

    <MigrateIntegration as MigrateExternal>::migrate();
}

#[cfg(test)]
mod pausable_fungible_token {
    use near_contract_tools::{
        pause::Pause,
        standard::nep141::{Nep141, Nep141Controller, Nep141Hook, Nep141Transfer},
        FungibleToken, Pause,
    };
    use near_sdk::{
        borsh::{self, BorshDeserialize, BorshSerialize},
        env, near_bindgen,
        test_utils::VMContextBuilder,
        testing_env, AccountId,
    };

    #[derive(FungibleToken, Pause, BorshDeserialize, BorshSerialize)]
    #[fungible_token(name = "Pausable Fungible Token", symbol = "PFT", decimals = 18)]
    #[near_bindgen]
    struct Contract {
        pub storage_usage: u64,
    }

    #[derive(Default)]
    struct HookState {
        pub storage_usage_start: u64,
    }

    impl Nep141Hook<HookState> for Contract {
        fn before_transfer(&mut self, _transfer: &Nep141Transfer) -> HookState {
            Contract::require_unpaused();
            HookState {
                storage_usage_start: env::storage_usage(),
            }
        }

        fn after_transfer(&mut self, _transfer: &Nep141Transfer, state: HookState) {
            let storage_delta = env::storage_usage() - state.storage_usage_start;
            println!("Storage delta: {storage_delta}",);

            self.storage_usage = storage_delta;
        }
    }

    #[test]
    fn hooks_modify_state() {
        let alice: AccountId = "alice".parse().unwrap();
        let bob: AccountId = "bob_account".parse().unwrap();

        let mut c = Contract { storage_usage: 0 };

        c.deposit_unchecked(&alice, 100);

        let context = VMContextBuilder::new()
            .attached_deposit(1)
            .predecessor_account_id(alice.clone())
            .build();

        testing_env!(context);

        c.ft_transfer(bob.clone(), 50.into(), None);

        assert_ne!(c.storage_usage, 0);
    }

    #[test]
    #[should_panic(expected = "Disallowed while contract is paused")]
    fn hooks_can_terminate_on_error() {
        let alice: AccountId = "alice".parse().unwrap();
        let bob: AccountId = "bob_account".parse().unwrap();

        let mut c = Contract { storage_usage: 0 };

        c.deposit_unchecked(&alice, 100);

        let context = VMContextBuilder::new()
            .attached_deposit(1)
            .predecessor_account_id(alice.clone())
            .build();

        testing_env!(context);

        Pause::pause(&mut c);

        c.ft_transfer(bob.clone(), 50.into(), None);
    }
}